React: Compound Components 설계 패턴
React에서 컴포넌트 재사용성을 높이는 compound component 패턴의 개념과 구현 예제, 장단점과 접근성 및 성능 고려사항 설명
목차
개요
컴포넌트 재사용은 규모가 커지는 애플리케이션에서 필수 과제다. compound component 패턴은 관련된 여러 하위 컴포넌트를 하나의 상위 컴포넌트 API로 묶어 사용성을 개선한다. 이 글에서는 개념, 구현 방법, 예제, 장단점과 접근성 고려사항을 다룬다.
왜 compound components를 사용하는가
단일 책임과 재사용성 두 가지를 동시에 만족시키기 어렵지 않게 만든다. 상위 컴포넌트가 상태와 로직을 관리하고, 하위 컴포넌트가 UI 역할만 담당하면 사용자는 선언적으로 조합해 쓸 수 있다. 또한 props drilling을 줄이고, 명확한 사용 API를 제공한다.
핵심 아이디어
핵심은 다음 세 가지이다.
- 상태와 로직은 상위 컴포넌트에서 관리
- 하위 컴포넌트는 상위 컴포넌트의 상태를 소비
- 하위 컴포넌트는 상위의 컨텍스트나 클론된 엘리먼트로 연결
구현 전략
1) React Context 사용
가장 흔한 방법은 Context를 만들어 상위에서 값을 제공하고 하위에서 소비하는 방식이다. 이 방식은 명확하고 성능 최적화도 가능하다.
2) React.cloneElement 사용
상위 컴포넌트가 자식 엘리먼트를 순회하며 props를 주입하는 방법이다. 간단하지만 중첩 구조나 동적 렌더링에서는 유연성이 떨어질 수 있다.
3) Render props 대체
render props 패턴과 결합해 상태와 API를 전달할 수도 있다. 다만 API가 복잡해지면 가독성이 낮아질 수 있다.
실제 예제: Tabs 구현
아래 예제는 Context 기반의 compound components 예제다. 상위는 Tabs, 하위는 Tabs.List, Tabs.Tab, Tabs.Panel로 구성된다. JSX는 < >를 이스케이프하여 표기한다.
const TabsContext = React.createContext(null);
function Tabs({ children, defaultIndex = 0 }) {
const [index, setIndex] = React.useState(defaultIndex);
const value = { index, setIndex };
return (
<TabsContext.Provider value={value}>
{children}
</TabsContext.Provider>
);
}
function TabsList({ children }) {
return <div role="tablist">{children}</div>
}
function Tab({ children, tabIndex }) {
const ctx = React.useContext(TabsContext);
const selected = ctx.index === tabIndex;
return (
<button
role="tab"
aria-selected={selected}
onClick={() => ctx.setIndex(tabIndex)}
>
{children}
</button>
);
}
function Panel({ children, panelIndex }) {
const ctx = React.useContext(TabsContext);
return ctx.index === panelIndex ? <div role="tabpanel">{children}</div> : null;
}
// 사용 예시
function Example() {
return (
<Tabs defaultIndex={0}>
<TabsList>
<Tab tabIndex={0}>첫 번째</Tab>
<Tab tabIndex={1}>두 번째</Tab>
</TabsList>
<Panel panelIndex={0}>첫 번째 내용</Panel>
<Panel panelIndex={1}>두 번째 내용</Panel>
</Tabs>
);
}
설명
위 구조는 상위가 상태를 보관하고 하위는 컨텍스트로 상태를 읽는다. 이렇게 하면 사용자가 API를 선언적으로 조합할 수 있다. 또한 하위 컴포넌트는 역할에 집중하므로 테스트와 유지보수가 쉬워진다.
장단점
- 장점: 명확한 API, 재사용성 증대, props drilling 감소
- 단점: Context 남발 시 리렌더링 이슈, 구조가 복잡해질 수 있음
성능 및 접근성 고려사항
Context를 쓸 때는 제공하는 값의 변경 단위를 최소화하는 것이 중요하다. 예를 들어 상태와 업데이트 함수를 별도 객체로 묶지 않거나, useMemo/useCallback으로 참조를 안정화하면 불필요한 리렌더링을 줄일 수 있다.
접근성 측면에서는 ARIA 속성과 키보드 네비게이션을 신경 써야 한다. Tabs 예제에서는 role, aria-selected, role="tabpanel" 등을 적용했고, 키보드 이벤트로 좌우 화살표 접근성을 추가하면 좋다.
베스트 프랙티스
- API를 단순하게 유지하고 사용 예시를 문서화
- 하위 컴포넌트는 최대한 프레젠테이션 역할만 하도록 설계
- Context 값은 변경 빈도가 낮은 값과 업데이트 함수로 분리
- 필요 시 cloneElement와 Context를 혼합해 유연성 확보
대체 패턴과 비교
Render props나 HOC와 비교하면 compound components는 JSX 조합이라는 자연스러운 사용성을 제공한다. 반면 작은 유틸성 컴포넌트에는 오히려 과한 설계가 될 수 있으므로 적절한 균형이 필요하다.
마무리
compound components 패턴은 리액트 컴포넌트 재사용을 위한 강력한 도구다. 적절한 Context 사용과 명확한 하위 컴포넌트 분리, 접근성 고려를 통해 유지보수성과 확장성을 확보할 수 있다. 실제 프로젝트에서는 코드 복잡도와 성능을 함께 고려하며 적용하는 것이 중요하다.