합성 컴포넌트 패턴으로 확장성있는 Header 컴포넌트 설계
Header 구조가 복잡해지면서 적용하게된 Compound Component 패턴에 대해 알아보자
2025.04.14
20분 소요
글을 시작하며
최근 프로젝트에서 다양한 UI의 Header가 필요했습니다.
다양한 Header UI
다양한 형태의 Header를 구현하면서 느꼈던 문제점과 이를 해결하기 위해 도입한 Compound Component 패턴에 대해 알아보고자 합니다.
문제 상황
왼쪽에는 아이콘이 있을 수도 있고, 가운데에는 텍스트가, 오른쪽에는 알림/검색 버튼이 존재하는 등 조합이 계속 바뀌는 구조였습니다.
처음에는 아래와 같이 props로 left, center, right를 전달 받아서 처리했었습니다.
<Header
left={<BackIcon />}
center="프로필"
right={<BellIcon />}
/>
이 방식은 간단해 보이지만, 다음과 같은 문제점이 있었습니다.
- Header의 요구사항이 복잡해지면서 그에 따른 props도 늘어났습니다.
- UI 구조가 복잡해질수록 JSX만 보고 구조를 이해하기 어려워졌습니다.
- 하나의 컴포넌트에서 모든 경우를 분기하다 보니 조건문이 많아지고 분기처리가 많아졌습니다.
- 어떻게 렌더링할지 컴포넌트 내부에 의존하게 됩니다.
이런 문제를 해결하기 위해 Compound Component 패턴을 도입하게 되었습니다.
Compound Component 패턴 도입
합성 컴포넌트 패턴은 하나의 컴포넌트를 여러 가지 집합체로 분리한 뒤, 각 컴포넌트를 사용하는 쪽에서 조합해 사용하는 컴포넌트 패턴을 의미합니다.
간단한 예시로 html의 select를 볼 수 있는데, select는 <select>와 <option> 태그의 조합으로 이루어집니다. <select>와 <option>은 각각 독립적으로는 큰 의미가 없지만 사용하는 곳에서 이를 조합해 사용함으로써 화면에 의미 있는 요소가 됩니다.
<select>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>
이처럼 사용하는 곳에서 컴포넌트의 조합을 활용할 수 있다면 높은 재사용성을 만족하면서 다양한 상황에 대응할 수 있다는 생각이 들어 도입해 보게 되었습니다.
Wrapper 컴포넌트 구현
서브 컴포넌트들을 묶어서 화면에 적절하게 보이도록 하는 컴포넌트입니다.
import { ComponentProps, ReactNode } from 'react';
import { cn } from '@/shared/util';
interface Props extends ComponentProps<'header'> {
children: ReactNode;
}
const HeaderWrapper = ({ children, className, ...rest }: Props) => {
return (
<header
className={cn(
'fixed inset-x-0 top-0 z-10 mx-auto flex h-[5.5rem] w-full items-center bg-white px-[1.5rem] border border-gray-11',
className,
)}
{...rest}
>
{children}
</header>
);
};
서브 컴포넌트 구현
각각의 서브 컴포넌트는 별개로는 큰 의미를 갖지 못하지만 Wrapper 컴포넌트와 조합하여 사용함으로써 의미를 갖게 됩니다. Left, Center, Right, RideInfo, Search 등 Header의 구성 요소를 서브 컴포넌트로 구현합니다.
const HeaderLeft = ({ children, className, ...rest }: Props) => {
return (
<div
className={cn('mr-auto flex items-center gap-3', className)}
{...rest}
>
{children}
</div>
);
};
const HeaderCenter = ({ children }: { children: ReactNode }) => {
return (
<Text
variant="subtitle1"
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
>
{children}
</Text>
);
};
const HeaderRight = ({ children, className, ...rest }: Props) => {
return (
<div
className={cn('ml-auto flex items-center gap-3', className)}
{...rest}
>
{children}
</div>
);
};
const HeaderRideInfo = ({ from, to, participants, maxParticipants = 4, className, ...rest }: Props) => {
return (
<div
className={cn('px-2', className)}
{...rest}
>
<FromToText
from={from}
to={to}
variant="body3"
/>
<Text
className="text-[1rem]"
color="hint"
align="left"
>
참여자 {participants}/{maxParticipants}명
</Text>
</div>
);
};
const HeaderSearch = () => {
const [keyword, setKeyword] = useState('');
return (
<div className="ml-2 flex w-full items-center rounded-xl bg-gray-100 px-3 py-[0.8rem]">
<Icon
name="search"
size={16}
color="#B9B9B9"
/>
<input
type="text"
placeholder="글 제목, 내용"
className="ml-2 w-full bg-transparent text-[1.4rem] outline-none placeholder:text-gray-6"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
</div>
);
};
메인 & 서브 컴포넌트 export
이렇게 구현한 컴포넌트들을 묶어서 export 해줍니다. 각각의 컴포넌트가 Header의 서브 컴포넌트임을 확실하게 알 수 있어 가독성에 도움을 줄 수 있습니다.
const Header = Object.assign(HeaderWrapper, {
Left: HeaderLeft,
Center: HeaderCenter,
Right: HeaderRight,
RideInfo: HeaderRideInfo,
Search: HeaderSearch,
});
export default Header;
합성 컴포넌트를 사용한 화면 구현
완성된 합성 컴포넌트를 활용하여 다양한 요구사항에 대응할 수 있게 되었습니다. 또 다른 요구사항이 생기더라도, 서브 컴포넌트들의 조합으로 손쉽게 구현할 수 있습니다.
// profile header
<Header>
<Header.Center>프로필</Header.Center>
<Header.Right>
<Icon name='bell' />
</Header.Right>
</Header>
// ride info header
<Header>
<Header.Left>
<Icon name='arrow-left' />
<Header.RideInfo from='서울역' to='잠실역' participants={3} />
</Header.Left>
<Header.Right>
<Icon name='header-list' />
</Header.Right>
</Header>
/// search header
<Header>
<Header.Left>
<Icon name='caret-left' size={25} />
</Header.Left>
<Header.Search />
</Header>
결론
이렇게 Compound Component 패턴을 활용해 구현한 Header는 선언형으로 작성되어 각 영역의 역할이 명확해졌고, 다양한 조합에도 일관된 구조와 가독성을 유지할 수 있게 되었습니다. 또한 자식 컴포넌트를 일일히 import 하지 않아도 된다는 점도 좋았습니다.
요구사항이 복잡하고, 조금 더 다양한 상황을 고려해야 할 때 합성 컴포넌트 패턴은 좋은 대안이 될 거라 생각합니다.
참고자료