
컴포넌트 설계를 하면서, 여러 컴포넌트들이 상호작용하는 복잡한 UI를 더 효율적이고 유연하게 관리할 수 있는 방법을 찾는 것은 매우 중요하다. 이번 글에서는, 기존의 단순한 방식에서 컴파운드 컴포넌트 패턴으로 리팩토링하면서 어떤 점이 개선되었는지 설명하고자 한다.
재사용을 고려하면서 만든 탭 컴포넌트이다..
//tabs컴포넌트
import React, { useState } from 'react';
import Tab from './Tab';
import useTabStore from '../../store/useTabStore';
const Tabs = ({ tabLabels, children }) => {
const { activeTab, setActiveTab } = useTabStore();
const handleTabClick = (index) => {
setActiveTab(index);
};
return (
<div className="w-80%">
<div className="flex space-x-16 mb-4">
{tabLabels.map((label, index) => (
<Tab
key={index}
label={label}
onClick={() => handleTabClick(index)}
isActive={activeTab === index}
/>
))}
</div>
<div className="w-full">
{React.Children.map(children, (child, index) => {
return index === activeTab ? child : null;
})}
</div>
</div>
);
};
export default Tabs;
//페이지에서 쓰는경우
<Tabs
tabLabels={[
'나의 여행 루트',
'찜한 여행 루트',
'나의 여행 후기',
'찜한 여행 후기',
]}
>
<MyRoot />
<LoverRoot />
<MyReview />
<LoverReview />
</Tabs>
위에 라벨명만 정하면 리스트갯수만큼 생기고 밑에 children을 tab내용으로 사용하는 형식이다.
기존 방식에서는 tabLabels라는 배열과 children을 활용하여 탭과 탭 내용을 관리하고 있었다. 이 방식은 간단하고 직관적이긴 하지만, 몇 가지 한계점이 있다
탭 제목(tabLabels)과 그에 해당하는 내용(children)이 서로 떨어져 있어, 새로운 탭을 추가하거나 수정할 때 각각의 배열 요소와 내용 컴포넌트를 일일이 관리해야 한다
탭의 순서를 바꾸거나 내용을 추가할 때 실수가 발생할 가능성이 있다. 왜냐하면, 탭 제목과 내용이 배열의 인덱스로만 연결되기 때문에 이 인덱스를 실수로 맞추지 않으면 예상치 못한 동작을 할 수 있다.
이러한 문제들을 해결하기 위해, 컴파운드 컴포넌트 패턴을 도입하면 훨씬 더 유연하고 확장성 있는 구조를 만들 수 있다. 아래는 컴파운드 컴포넌트 패턴을 적용한 방식이다.
//탭스
const Tabs = ({ children }) => {
const validChildren = React.Children.toArray(children).filter((child) => {
return child.type === Tab || child.type === TabContent;
});
return <div className="tabs">{validChildren}</div>;
};
//탭스안에 탭
const Tab = ({ index, children }) => {
const { activeIndex, setActiveIndex } = useTabStore();
return (
<button
className={`text-lg font-medium py-3 px-4 text-gray-600 hover:text-gray-900 ${
activeIndex === index ? 'border-b-2 border-prime' : ''
}`}
onClick={() => setActiveIndex(index)}
>
{children}
</button>
);
};
//탭에 내용들
const TabContent = ({ index, children }) => {
const { activeIndex } = useTabStore();
return activeIndex === index ? (
<div className="tab-content">{children}</div>
) : null;
};
Tabs.Tab = Tab;
Tabs.TabContent = TabContent;
export default Tabs;
컴파운드 컴포넌트 패턴은 각각의 역할을 명확하게 분리하고, 컴포넌트 간의 관계를 보다 직관적으로 구성할 수 있습니다. 이를 통해 얻을 수 있는 장점들은 다음과 같습니다:
Tab과 TabContent가 각각 탭의 제목과 내용을 담당한다. 이로 인해 코드의 가독성이 높아지고, 새로운 탭을 추가하거나 제거할 때 실수할 가능성이 줄어든다.<Tabs>
<Tabs.Tab index={0}>Tab 1</Tabs.Tab>
<Tabs.TabContent index={0}>Content 1</Tabs.TabContent>
<Tabs.Tab index={1}>Tab 2</Tabs.Tab>
<Tabs.TabContent index={1}>Content 2</Tabs.TabContent>
</Tabs>
<Tabs>
<Tabs.Tab index={0} disabled>
Disabled Tab
</Tabs.Tab>
<Tabs.TabContent index={0}>This tab is disabled</Tabs.TabContent>
</Tabs>
컴파운드 컴포넌트 패턴은 코드의 가독성과 유지보수성을 크게 향상시킨다. 기존의 배열 기반 접근 방식에서는 각각의 탭이 배열의 인덱스로만 연결되기 때문에, 배열의 순서를 변경하거나 새로운 기능을 추가할 때 실수가 발생하기 쉬웠다. 하지만 컴파운드 컴포넌트 패턴에서는 탭과 그 내용이 한 쌍으로 함께 관리되므로, 이러한 문제를 쉽게 피할 수 있다.
이 컴포넌트 설계 과정을 통해 컴파운드 컴포넌트 패턴이 얼마나 유용한지 체감할 수 있었다.
초기에는 단순한 배열과 children을 이용해 탭을 관리하는 방식이 직관적이라 느꼈지만, 점점 더 복잡한 기능이나 다양한 요구사항을 추가하면서 한계에 부딪혔다..
이러한 문제를 해결하기 위해 컴파운드 컴포넌트 패턴을 도입하면서, 코드 구조가 훨씬 명확해졌고, 유지보수와 확장성 측면에서 많은 이점을 얻을 수 있었다.
특히, 각각의 탭과 그에 해당하는 내용을 독립적인 하위 컴포넌트로 분리하면서 코드의 가독성이 높아졌고, 새로운 탭을 추가하거나 변경하는 작업이 매우 간편해졌다.
또한, 재사용성과 확장성 면에서도 큰 개선이 있었다.
예를 들어, 탭에 추가적인 속성을 쉽게 추가할 수 있고, 필요에 따라 다른 프로젝트에서도 같은 컴포넌트를 유연하게 사용할 수 있었다.
이번 경험을 통해, 컴포넌트 간의 결합도를 낮추고 유연성을 높이는 것이 얼마나 중요한지 알게 되었고, 더 나은 코드 설계를 위해 앞으로도 이런 패턴을 적극적으로 활용할 계획이다.