React Header · HeaderSub · FilterIcon

재사용 가능한 헤더 컴포넌트 구조와 props, 조건부 렌더링, SVG 아이콘 전달 방식을 한 페이지로 정리

Header메인 헤더 · logo / brand / title / right
HeaderSub서브 헤더 · left / title / action / right
FilterIconsize / stroke / fill을 받는 SVG 컴포넌트

1. 전체 구조부터 보기

App ├─ Header │ ├─ logo → 없으면 brand + title │ └─ right → 없으면 placeholder │ ├─ HeaderSub │ ├─ left → 없으면 placeholder │ ├─ title → 있을 때만 렌더링 │ ├─ action → 전달하면 렌더링 │ └─ right → 없으면 placeholder × 2 │ └─ FilterIcon └─ Header / HeaderSub의 각 영역에 JSX로 전달 가능
핵심: Header는 레이아웃을 담당하고, 실제로 들어갈 버튼·아이콘·로고 같은 UI는 부모가 props로 갈아끼울 수 있게 만든 구조다.

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>}
/>
meritzONE

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>
  )
}
┌──────────────────────────────────────────┐ │ [left] title action [right] │ └──────────────────────────────────────────┘

title && JSX 조건부 렌더링

{title && <strong>{title}</strong>}

title이 truthy일 때만 strong을 렌더링한다.

{isLogin && <button>로그아웃</button>}
{isAdmin && <AdminMenu />}
{error && <p>에러가 발생했습니다.</p>}

??와 && 비교

문법의미이 코드의 예
A ?? BA가 null/undefined면 Bleft ?? 기본아이콘
A && BA가 참이면 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역할기본값
sizeSVG width / height32
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를 사용한다.

JSX SVG 속성: React에서는 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>}
/>
meritzONE
FilterIcon
상품검색
FilterIcon

여러 요소를 한 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 ?? 기본UIlogo가 null/undefined면 기본 UI
title && <strong>title이 truthy일 때만 렌더링
<>...</>추가 DOM 태그 없이 JSX 여러 개 묶기
right={<FilterIcon />}컴포넌트 JSX를 props로 전달
size = 32SVG 크기 기본값
stroke ?? '#222'stroke가 없으면 기본 선 색상
fill ?? '#fff'fill이 없으면 기본 채움 색상
한 줄 결론: Header/HeaderSub는 공통 레이아웃을 만들고, logo·left·action·right 같은 슬롯에 부모가 원하는 JSX를 넣게 함으로써 재사용성을 높인 컴포넌트다. FilterIcon은 그 슬롯에 넣어 재사용할 수 있는 SVG 아이콘 컴포넌트다.