React에서 여러 기능을 가진 UI를 만들 때, 각각의 역할을 분리하여 조립 가능한 형태로 구성하는 것이 중요합니다. 이때 유용하게 쓸 수 있는 방법이 바로 Compound Component 패턴입니다.
이번 글에서는 공통 컴포넌트를 더 유연하고 확장 가능하게 만들기 위해 Compound Component 패턴을 선택한 이유와 장점을 소개하고, 이를 Select 컴포넌트 예시로 구체적으로 풀어보겠습니다.
1. 왜 Compound Component 패턴을 선택했을까?
기능과 UI를 하나의 컴포넌트에 모두 포함시키면 코드가 비대해지고, 작은 수정이나 새로운 기능 추가에도 전체 구조를 건드려야 해서 유지보수와 확장이 어렵습니다.
반면, Compound Component 패턴을 사용하면 다음과 같은 장점이 있습니다.
- 유연성: 필요한 컴포넌트만 골라 자유롭게 조합할 수 있습니다.
- 책임 분리: 로직은 Provider에, UI는 각각의 컴포넌트에 나누어 관리할 수 있습니다.
- 재사용성: 버튼, 옵션 등 다양한 파트를 원하는 대로 커스터마이징할 수 있습니다.
- 변경 대응력: UI 구조나 디자인이 변경되더라도, 핵심 로직은 유지되기 때문에 수정이 간편합니다.
특히, 추가 요구사항이나 디자인 변경이 생겨도 일부 컴포넌트만 수정하면 되기 때문에 빠르게 대응할 수 있습니다.
2. 예시로 살펴보기: Select 컴포넌트 만들기
Compound Component 패턴을 적용해 Select 컴포넌트를 만들어보겠습니다.
기본 구조
<Select>
<Select.Trigger>Choose an option</Select.Trigger>
<Select.Options>
<Select.Option value="apple">Apple</Select.Option>
<Select.Option value="banana">Banana</Select.Option>
</Select.Options>
</Select>
이런 구조 덕분에 사용자는 <Select> 내부에 Trigger, Options, Option을 자유롭게 배치할 수 있어, 다양한 레이아웃과 스타일을 쉽게 적용할 수 있습니다.
✨ 여기서 Compound Component 패턴의 진짜 장점은?
- 기본적인 Select UI를 구현하는 것에 그치지 않고,
- 나중에 옵션 항목마다 아이콘이나 추가 설명 텍스트를 붙인다든지,
- 옵션을 그룹화하여 Select.Group 형태로 확장한다든지,
- 트리거 버튼을 다른 스타일이나 동작으로 바꾼다든지 하는 다양한 요구사항이 생겨도
핵심 로직은 그대로 유지한 채, 필요한 컴포넌트만 수정하거나 추가하면 대응할 수 있다는 점입니다.
즉, UI나 인터랙션에 새로운 변화가 들어와도, 전체를 다시 짜지 않고 각 역할별 컴포넌트만 교체하거나 조합하는 방식으로 빠르게 대응할 수 있습니다.
이처럼 Compound Component 패턴은 "변경에 유연하고, 유지보수가 쉬운 구조"를 자연스럽게 만들어줍니다.
예를 들어, 옵션에 아이콘을 추가하는 확장도 쉽게 가능합니다.
<Select>
<Select.Trigger>Select a fruit</Select.Trigger>
<Select.Options>
<Select.Option value="apple">
🍎 Apple
</Select.Option>
<Select.Option value="banana">
🍌 Banana
</Select.Option>
</Select.Options>
</Select>
또는 옵션을 그룹화하고 싶을 때도 별도의 Select.Group 컴포넌트를 추가하는 식으로 유연하게 확장할 수 있습니다.
<Select>
<Select.Trigger>Choose an item</Select.Trigger>
<Select.Options>
<Select.Group label="Fruits">
<Select.Option value="apple">Apple</Select.Option>
<Select.Option value="banana">Banana</Select.Option>
</Select.Group>
<Select.Group label="Vegetables">
<Select.Option value="carrot">Carrot</Select.Option>
<Select.Option value="broccoli">Broccoli</Select.Option>
</Select.Group>
</Select.Options>
</Select>
이처럼 구조를 미리 잘 나눠두면, 다양한 변경이나 확장 요구가 들어와도 부담 없이 대응할 수 있습니다.
이런 구조 덕분에 사용자는 <Select> 내부에 Trigger, Options, Option을 자유롭게 배치할 수 있어, 다양한 레이아웃과 스타일을 쉽게 적용할 수 있습니다.
3. SelectContext로 상태 공유하기
Select 컴포넌트는 열림 여부(isOpen), 현재 선택된 값(value), 토글 함수(toggle), 선택 함수(select) 같은 상태와 동작을 공유합니다.
Context를 이용하면 prop drilling 없이 필요한 컴포넌트끼리 상태를 자연스럽게 공유할 수 있어 확장성과 유지보수성이 높아집니다.
const SelectContext = createContext<SelectContextProps | undefined>(undefined);
export const useSelectContext = () => {
const context = useContext(SelectContext);
if (!context) {
throw new Error('useSelectContext must be used within a SelectProvider');
}
return context;
};
4. SelectProvider로 상태 관리하기
Provider 컴포넌트에서는 열림/닫힘, 선택 기능 같은 상태를 집중 관리합니다.
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (localRef.current && !localRef.current.contains(event.target as Node)) {
close();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [close]);
상태를 하나의 Provider에서 관리하면, 다양한 하위 컴포넌트들이 쉽게 동기화되고, 추가 로직이 생겨도 확장하기 쉽습니다.
5. 세부 컴포넌트 설계하기
(1) SelectTrigger
const SelectTrigger = ({ children, disabled, ...props }: TriggerProps) => {
const { toggle } = useSelectContext();
return (
<button onClick={toggle} disabled={disabled} {...props}>
{children}
</button>
);
};
- 버튼 UI를 독립적으로 관리할 수 있어, 나중에 스타일이나 기능을 쉽게 교체할 수 있습니다.
(2) SelectOptions
const SelectOptions = ({ children, ...props }: OptionsProps) => {
const { isOpen } = useSelectContext();
if (!isOpen) {
return null;
}
return <div {...props}>{children}</div>;
};
- 드롭다운 리스트를 별도로 분리해 필요에 따라 애니메이션이나 다양한 효과를 추가할 수 있습니다.
(3) SelectOption
const SelectOption = ({ value, children, ...props }: OptionProps) => {
const { select } = useSelectContext();
return (
<button onClick={() => select(value)} {...props}>
{children}
</button>
);
};
- 옵션 하나하나를 독립적으로 다룰 수 있어, 아이콘 추가, 비활성화 옵션 같은 확장이 쉬워집니다.
6. 마무리: Compound Component 패턴의 진짜 가치
공통 컴포넌트를 설계할 때 가장 중요한 것은 변화에 유연하게 대응할 수 있는 구조를 만드는 것입니다.
Compound Component 패턴은 이를 완벽하게 지원합니다.
- 상태와 로직을 하나의 Provider에서 집중 관리하고
- 각각의 UI 파트를 독립적인 컴포넌트로 분리하며
- Context를 통해 컴포넌트 간 자연스럽고 안전한 연결을 유지합니다.
이렇게 설계하면,
- UI 수정이나 추가 기능 반영이 쉽고
- 새로운 요구사항에도 안정적으로 대응할 수 있으며
- 복잡한 공통 컴포넌트도 깔끔하게 관리할 수 있습니다.
이번 글에서는 Select 컴포넌트를 예시로 살펴봤지만, Dropdown, Modal, Tabs, Accordion 등 다양한 컴포넌트에도 똑같은 원리를 적용할 수 있습니다.
결국, 변화에 강하고, 확장성과 유지보수가 뛰어난 공통 컴포넌트를 만들기 위해서는 Compound Component 패턴이 가장 강력한 선택지 중 하나입니다.
'React' 카테고리의 다른 글
Portal과 Compound 패턴으로 Modal 만들기 (0) | 2025.04.29 |
---|---|
[React] 렌더링 성능 개선하기 (0) | 2022.04.19 |
[Back&Stock] 피드백 반영하기 (0) | 2022.04.10 |
[Back&Stock] 컴포넌트는 무엇일까? (0) | 2022.04.07 |
[React: code] 리덕스는 어떻게 사용할까? (0) | 2022.03.01 |
댓글