React Header · HeaderSub · FilterIcon
재사용 가능한 헤더 컴포넌트 구조와 props, 조건부 렌더링, SVG 아이콘 전달 방식을 한 페이지로 정리
1. 전체 구조부터 보기
2. Header 컴포넌트
import { cn } from '@/utils/cn'
import styles from './Header.module.css'
export default function Header({
logo,
title = 'ONE',
brand = 'meritz',
right,
className,
brandClassName,
titleClassName,
...props
}) {
return (
<header className={cn(styles.header, className)} {...props}>
<div className={styles.logoArea}>
{logo ?? (
<>
<span className={cn(styles.brand, brandClassName)}>{brand}</span>
<span className={cn(styles.title, titleClassName)}>{title}</span>
</>
)}
</div>
<div className={styles.right}>
{right ?? <span className={styles.iconPlaceholder} aria-hidden="true" />}
</div>
</header>
)
}
props 기본값
title = 'ONE'
brand = 'meritz'
<Header />처럼 아무것도 전달하지 않아도 기본적으로 meritz ONE을 사용할 수 있다.
cn(styles.header, className)
CSS Module의 기본 클래스와 부모가 전달한 추가 클래스를 합친다.
<Header className="mainHeader" />
// 개념적으로
<header class="Header_header_xxx mainHeader">
...props
구조분해에서 따로 꺼내지 않은 나머지 props를 실제 <header>에 전달한다.
<Header
id="mainHeader"
data-type="main"
onClick={() => console.log('클릭')}
/>
// id, data-type, onClick은 ...props에 들어가
// 실제 header 태그로 전달된다.
3. logo ?? 기본 UI
{logo ?? (
<>
<span>{brand}</span>
<span>{title}</span>
</>
)}
??는 왼쪽 값이 null 또는 undefined일 때 오른쪽 값을 사용한다.
| 사용 | 결과 |
|---|---|
<Header /> | logo가 없으므로 기본 meritz ONE |
<Header logo={<img ... />} /> | 기본 텍스트 대신 전달한 이미지 사용 |
<Header
logo={<img src="/logo.svg" alt="메리츠" />}
/>
A ?? B → A가 null/undefined면 B. 이 코드에서는 “사용자가 직접 UI를 넣지 않았으면 기본 UI를 사용한다”는 용도로 많이 쓰인다.Fragment <>...</>
여러 JSX 요소를 묶되 실제 DOM에는 불필요한 div를 추가하지 않는다.
4. Header의 right 영역
<div className={styles.right}>
{right ?? (
<span className={styles.iconPlaceholder} aria-hidden="true" />
)}
</div>
right를 전달하면 그 JSX가 나오고, 전달하지 않으면 placeholder가 나온다.
<Header
right={<button>로그인</button>}
/>
5. HeaderSub 컴포넌트
export default function HeaderSub({
title,
left,
action,
right,
className,
titleClassName,
...props
}) {
return (
<header className={cn(styles.header, className)} {...props}>
<div className={styles.left}>
{left ?? <span className={styles.iconPlaceholder} aria-hidden="true" />}
{title && (
<strong className={cn(styles.title, titleClassName)}>
{title}
</strong>
)}
</div>
<div className={styles.right}>
{action}
{right ?? (
<>
<span className={styles.iconPlaceholder} aria-hidden="true" />
<span className={styles.iconPlaceholder} aria-hidden="true" />
</>
)}
</div>
</header>
)
}
title && JSX 조건부 렌더링
{title && <strong>{title}</strong>}
title이 truthy일 때만 strong을 렌더링한다.
{isLogin && <button>로그아웃</button>}
{isAdmin && <AdminMenu />}
{error && <p>에러가 발생했습니다.</p>}
??와 && 비교
| 문법 | 의미 | 이 코드의 예 |
|---|---|---|
A ?? B | A가 null/undefined면 B | left ?? 기본아이콘 |
A && B | A가 참이면 B 렌더링 | title && <strong>... |
right 영역
{action}은 전달되면 그대로 렌더링되고, 없으면 아무것도 나오지 않는다. 반면 right는 없으면 placeholder 2개가 기본으로 나온다.
6. FilterIcon 컴포넌트
export default function FilterIcon({ size = 32, stroke, fill }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
...
</svg>
)
}
| prop | 역할 | 기본값 |
|---|---|---|
size | SVG width / height | 32 |
stroke | 선 색상 | 대부분 #222 |
fill | 원의 내부 색상 | #fff |
size
<FilterIcon /> // 32 × 32
<FilterIcon size={24} /> // 24 × 24
<FilterIcon size={16} /> // 16 × 16
viewBox="0 0 32 32"는 내부 좌표 체계를 유지하고, width/height를 바꿔 아이콘 전체 크기를 조절한다.
stroke / fill의 기본값
stroke={stroke ?? '#222'}
fill={fill ?? '#fff'}
stroke나 fill props가 있으면 전달값을 사용하고, 없으면 기본 색상을 사용한다.
가운데 원은 기본 stroke가 다름
stroke={stroke ?? '#EF3B24'}
가운데 원만 기본 빨간색이다. 하지만 <FilterIcon stroke="blue" />처럼 stroke를 전달하면 가운데 원을 포함해 해당 stroke 표현식들이 모두 blue를 사용한다.
stroke-width, stroke-linecap, stroke-linejoin보다 strokeWidth, strokeLinecap, strokeLinejoin처럼 camelCase로 작성하는 것이 좋다.7. 컴포넌트를 props로 전달하기
이 코드에서 가장 중요한 설계 포인트 중 하나다. right, left, action, logo는 단순 문자열뿐 아니라 JSX 자체를 받을 수 있다.
<Header right={<FilterIcon />} />
<HeaderSub
title="상품검색"
left={<button>←</button>}
action={<FilterIcon size={24} />}
right={<button>메뉴</button>}
/>
여러 요소를 한 prop에 전달하고 싶으면 Fragment로 묶을 수도 있다.
<Header
right={
<>
<button>검색</button>
<FilterIcon />
</>
}
/>
8. 실제 조립 예제
import Header from './Header'
import HeaderSub from './HeaderSub'
import FilterIcon from './FilterIcon'
function App() {
return (
<>
<Header
title="ONE"
brand="meritz"
right={<FilterIcon size={24} />}
/>
<HeaderSub
title="상품검색"
left={<button type="button">←</button>}
action={<FilterIcon size={24} />}
/>
</>
)
}
9. 핵심 문법 최종 정리
| 코드 | 의미 |
|---|---|
title = 'ONE' | props 기본값 |
...props | 나머지 props를 모아 실제 DOM 요소에 전달 |
cn(styles.header, className) | 기본 CSS Module 클래스 + 외부 클래스 결합 |
logo ?? 기본UI | logo가 null/undefined면 기본 UI |
title && <strong> | title이 truthy일 때만 렌더링 |
<>...</> | 추가 DOM 태그 없이 JSX 여러 개 묶기 |
right={<FilterIcon />} | 컴포넌트 JSX를 props로 전달 |
size = 32 | SVG 크기 기본값 |
stroke ?? '#222' | stroke가 없으면 기본 선 색상 |
fill ?? '#fff' | fill이 없으면 기본 채움 색상 |