React tabs를 컴파운드패턴으로 만들어보기

김도현·2024년 5월 27일

3차 프로젝트

목록 보기
10/11
post-thumbnail

컴포넌트 설계를 하면서, 여러 컴포넌트들이 상호작용하는 복잡한 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)이 서로 떨어져 있어, 새로운 탭을 추가하거나 수정할 때 각각의 배열 요소와 내용 컴포넌트를 일일이 관리해야 한다

  • 탭의 순서를 바꾸거나 내용을 추가할 때 실수가 발생할 가능성이 있다. 왜냐하면, 탭 제목과 내용이 배열의 인덱스로만 연결되기 때문에 이 인덱스를 실수로 맞추지 않으면 예상치 못한 동작을 할 수 있다.

확장성 문제

  • 만약 탭에 추가적인 속성이나 동작(예: 비활성화 상태나 아이콘 추가 등)을 적용하려고 한다면, tabLabels 배열에 추가적인 데이터를 넣고 그 데이터를 처리하는 코드를 추가하면 된다.
  • 컴포넌트 간의 결합도가 높아져, 나중에 탭 기능이 확장되거나 변경될 경우 유지보수가 어려워질 수 있다.

컴파운드 컴포넌트 패턴으로의 리팩토링

이러한 문제들을 해결하기 위해, 컴파운드 컴포넌트 패턴을 도입하면 훨씬 더 유연하고 확장성 있는 구조를 만들 수 있다. 아래는 컴파운드 컴포넌트 패턴을 적용한 방식이다.


//탭스
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;

컴파운드 컴포넌트 패턴의 장점

컴파운드 컴포넌트 패턴은 각각의 역할을 명확하게 분리하고, 컴포넌트 간의 관계를 보다 직관적으로 구성할 수 있습니다. 이를 통해 얻을 수 있는 장점들은 다음과 같습니다:

더 직관적인 구조

  • 이제 탭 제목과 탭 내용은 각각의 하위 컴포넌트로 명확하게 나누어져 있습니다. 즉, TabTabContent가 각각 탭의 제목과 내용을 담당한다. 이로 인해 코드의 가독성이 높아지고, 새로운 탭을 추가하거나 제거할 때 실수할 가능성이 줄어든다.
  • 탭의 제목과 내용이 바로 옆에 있어, 하나의 논리적 단위로 관리되므로, 확장이나 수정 시 훨씬 더 쉽게 작업할 수 있다.
<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>

높은 재사용성

  • 컴파운드 컴포넌트 패턴에서는 각 탭과 탭의 내용을 독립된 컴포넌트로 만들어서 다른 곳에서도 재사용할 수 있다. 예를 들어, Tab 컴포넌트를 단순히 다른 프로젝트에서도 사용할 수 있으며, 하위 컴포넌트를 원하는 대로 재배열하거나 다양한 스타일을 추가할 수 있다.

확장성 높은 설계

  • 기존 방식에서는 단순히 탭 제목을 tabLabels 배열로 관리했지만, 컴파운드 컴포넌트 패턴을 사용하면 탭에 더 많은 속성을 쉽게 추가할 수 있다. 예를 들어, 비활성화된 탭이나 아이콘이 포함된 탭 등을 추가하고 싶다면, Tab 컴포넌트에 간단히 추가 속성을 넣으면 된다.
<Tabs>
  <Tabs.Tab index={0} disabled>
    Disabled Tab
  </Tabs.Tab>
  <Tabs.TabContent index={0}>This tab is disabled</Tabs.TabContent>
</Tabs>

유지보수 용이성

컴파운드 컴포넌트 패턴은 코드의 가독성과 유지보수성을 크게 향상시킨다. 기존의 배열 기반 접근 방식에서는 각각의 탭이 배열의 인덱스로만 연결되기 때문에, 배열의 순서를 변경하거나 새로운 기능을 추가할 때 실수가 발생하기 쉬웠다. 하지만 컴파운드 컴포넌트 패턴에서는 탭과 그 내용이 한 쌍으로 함께 관리되므로, 이러한 문제를 쉽게 피할 수 있다.

느낀 점

이 컴포넌트 설계 과정을 통해 컴파운드 컴포넌트 패턴이 얼마나 유용한지 체감할 수 있었다.

초기에는 단순한 배열과 children을 이용해 탭을 관리하는 방식이 직관적이라 느꼈지만, 점점 더 복잡한 기능이나 다양한 요구사항을 추가하면서 한계에 부딪혔다..

이러한 문제를 해결하기 위해 컴파운드 컴포넌트 패턴을 도입하면서, 코드 구조가 훨씬 명확해졌고, 유지보수와 확장성 측면에서 많은 이점을 얻을 수 있었다.

특히, 각각의 탭과 그에 해당하는 내용을 독립적인 하위 컴포넌트로 분리하면서 코드의 가독성이 높아졌고, 새로운 탭을 추가하거나 변경하는 작업이 매우 간편해졌다.

또한, 재사용성과 확장성 면에서도 큰 개선이 있었다.
예를 들어, 탭에 추가적인 속성을 쉽게 추가할 수 있고, 필요에 따라 다른 프로젝트에서도 같은 컴포넌트를 유연하게 사용할 수 있었다.

이번 경험을 통해, 컴포넌트 간의 결합도를 낮추고 유연성을 높이는 것이 얼마나 중요한지 알게 되었고, 더 나은 코드 설계를 위해 앞으로도 이런 패턴을 적극적으로 활용할 계획이다.

참고 : https://www.patterns.dev/react/compound-pattern

profile
두니코딩

0개의 댓글