미숑이의 블로그
블로그 홈소개

© 2025 Ryu Mi Sung. All rights reserved.

목차

들어가기 전에
Compound Component Pattern이란 무엇인가?
왜 기존 구조가 불편해졌을까?
적용하기
1. tooltip.tsx
2. index.ts
3. 컴포넌트 적용해보기
구조를 재구성하고 나서, 무엇이 달라졌을까?
구조를 재구성하고 나서 느낀 장점
이 방식의 단점은 무엇일까?
마치며
참고
스노로즈

shadcn/ui 컴포넌트를 객체 형태로 묶어서 사용하기

류미성
2026년 1월 29일

들어가기 전에

요즘 스노로즈 백오피스 개발을 하면서 shadcn/ui 컴포넌트를 자주 사용하고 있어요. shadcn/ui 자체도 컴파운드 컴포넌트 형태로 구성되어 있지만, 프로젝트 구조상 src/components/ui에서 다시 export하는 과정이 있다 보니 import가 빠르게 늘어나고 구조도 점점 복잡해지고 있었습니다.

그러던 중, 컴포넌트를 객체 형태로 묶어 한 엔트리로 관리하는 방식을 소개한 아티클을 보게 되었고, 이 형태가 우리 구조와 잘 맞겠다고 느껴 Tooltip부터 방식 개선을 시작해보았어요. 이번 글에서는 왜 이런 선택을 했는지, 적용 후 어떤 변화가 있었는지 기록해보려고 합니다.


Compound Component Pattern이란 무엇인가?

컴파운드 컴포넌트 패턴은 하나의 기능을 여러 구성 요소가 함께 완성하도록 만든 구조예요. 부모가 상태와 동작을 관리하고, 하위 요소들은 역할만 담당해 조립식으로 UI를 구성할 수 있습니다.

이 방식은 다음과 같은 UI에서 특히 유용합니다.

  • UI의 내부 구조가 여러 컴포넌트로 나뉘어 있지만 하나의 기능으로 묶여 동작해야 할 때
  • 셀렉트, 모달, 아코디언, 드롭다운처럼 구조는 복잡하지만 라이프사이클이 서로 긴밀하게 연결되어 있을 때
  • 사용자가 필요에 따라 UI 구조를 자유롭게 조합할 수 있도록 하고 싶을 때

왜 기존 구조가 불편해졌을까?

백오피스 프로젝트는 아직 초기 단계임에도 import 줄 수가 빠르게 늘어났어요. 23개의 컴포넌트를 가져오는 데 104줄이 필요했을 정도니, 앞으로 확장될수록 관리 비용은 더 커질 수밖에 없는 구조였죠.

또한

  • 어떤 파일에서 어떤 shadcn/ui 컴포넌트를 사용하는지 한눈에 파악하기 어렵고
  • Sidebar처럼 구조가 큰 컴포넌트는 20개 이상을 개별 export해야 하고
  • import 또한 늘어날수록 가독성과 유지보수성이 떨어지는 문제가 있었어요.

이런 점들이 계속 부담으로 느껴졌습니다.

그래서 컴포넌트별 롤을 하나로 묶는 방식이 지금 구조와 잘 맞겠다고 판단해 Tooltip부터 방식을 바꿔보기로 했어요.

  • 개선하고 싶은 코드

    index.ts 코드

    export { Button } from './button';
    export { Input } from './input';
    ... 생략 ...
    export {
      Sidebar,
      SidebarContent,
      SidebarFooter,
      SidebarGroup,
      SidebarGroupAction,
      SidebarGroupContent,
      SidebarGroupLabel,
      SidebarHeader,
      SidebarInput,
      SidebarInset,
      SidebarMenu,
      SidebarMenuAction,
      SidebarMenuBadge,
      SidebarMenuButton,
      SidebarMenuItem,
      SidebarMenuSkeleton,
      SidebarMenuSub,
      SidebarMenuSubButton,
      SidebarMenuSubItem,
      SidebarProvider,
      SidebarRail,
      SidebarSeparator,
      SidebarTrigger,
      useSidebar,
    } from './sidebar';
     
    ... 생략 ...

적용하기

Tooltip 구조를 정리하기 위해 tooltip.tsx 내부에서 루트 컴포넌트를 기준으로 관련 기능들을 하나의 묶음으로 다시 구성했어요. shadcn/ui가 제공하는 Tooltip 컴포넌트들은 이미 컴파운드 컴포넌트 패턴으로 구성되어 있지만, 우리 프로젝트에서는 이들을 다시 export하는 레이어가 한 번 더 있기 때문에 사용 방식이 점점 복잡해지고 있었어요. 그래서 Tooltip의 하위 요소들을 정적 속성(Static Property)으로 연결해 Tooltip.Provider, Tooltip.Trigger처럼 하나의 네임스페이스로 다룰 수 있도록 Dot Notation 기반으로 다시 재구성해주었습니다.

1. tooltip.tsx

shadcn/ui에서 제공하는 컴포넌트들은 기본적으로 각각 별도 export 형태를 가지고 있어요. Tooltip처럼 여러 구성 요소가 함께 동작하는 UI는 루트 컴포넌트를 중심으로 한 엔트리로 묶어두면 구조가 더 명확해지고, import하는 사람도 훨씬 편하게 사용할 수 있어요.

정리 방식은 아래와 같아요.

  1. shadcn/ui에서 제공하는 Tooltip 관련 컴포넌트를 가져오고,
  2. Tooltip 루트 컴포넌트는 그대로 유지하되
  3. 기존의 TooltipProvider, TooltipTrigger, TooltipContent를Tooltip.Provider, Tooltip.Trigger, Tooltip.Content처럼Dot Notation 형태로 다시 연결합니다.
  • 적용 전 (하위 요소들이 각각 독립된 컴포넌트로 존재)

    function TooltipProvider(props) {
      return <TooltipPrimitive.Provider delayDuration={0} {...props} />;
    }
  • 적용 후 (Tooltip 객체에 정적 속성으로 연결)

    Tooltip.Provider = function TooltipProvider(props) {
      return <TooltipPrimitive.Provider delayDuration={0} {...props} />;
    };
  • Tooltip 전체 구조 예시

    // 루트 컴포넌트는 그대로 유지
    function Tooltip(props) {
      return <TooltipPrimitive.Root {...props} />;
    }
     
    // dot notation으로 하위 역할 추가
    Tooltip.Provider = function TooltipProvider(props) {
      return <TooltipPrimitive.Provider delayDuration={0} {...props} />;
    };
     
    Tooltip.Trigger = function TooltipTrigger(props) {
      return <TooltipPrimitive.Trigger {...props} />;
    };
     
    Tooltip.Content = function TooltipContent({ className, ...props }) {
      return (
    		... 생략 ...
      );
    };
     
    export { Tooltip };

2. index.ts

프로젝트에서는 shadcn/ui 컴포넌트들을 src/shared/components/ui 폴더에서 한 번 더 export하고 있어요.

이 레이어에서 각각의 Tooltip 구성 요소를 모두 다시 export하면 import 라인이 계속 늘어나기 때문에, Tooltip 객체 하나만 내보내도록 구조를 단순화했습니다.

  • 적용 전

    export { Tooltip, TooltipProvider, TooltipTrigger, TooltipContent }from'./tooltip';
  • 적용 후

    export { Tooltip }from'./tooltip';
  • 비교

    index.ts 코드 개선

3. 컴포넌트 적용해보기

  • 적용 전: Tooltip 기능을 사용하려면 여러 컴포넌트를 직접 import해야 했어요.

    import { 
    	Tooltip, 
    	TooltipProvider, 
    	TooltipTrigger, 
    	TooltipContent 
    } from '@/shared/components/ui/tooltip';
     
    <TooltipProvider delayDuration={0}>
    	...
    </TooltipProvider>
     
    <Tooltip>
    	<TooltipTrigger asChild>{button}</TooltipTrigger>
      <TooltipContent side="right" ... />
    </Tooltip>
  • 적용 후: Tooltip 하나만 import하면 되고, 하위 역할은 Tooltip.xxx로 접근할 수 있어요.

    import { Tooltip } from '@/shared/components/ui/tooltip';
     
    <Tooltip.Provider delayDuration={0}>
      ...
    </Tooltip.Provider>
     
    <Tooltip>
      <Tooltip.Trigger asChild>{button}</Tooltip.Trigger>
      <Tooltip.Content side="right" ... />
    </Tooltip>

구조를 재구성하고 나서, 무엇이 달라졌을까?

기존에는 TooltipRoot, TooltipContent, TooltipTrigger를 각각 별도로 import해야 했어요. 하지만 Dot Notation 기반으로 구조를 재구성한 뒤에는 Tooltip 하나만 import하면 되고, 필요한 역할은 Tooltip.xxx 형태로 바로 사용할 수 있게 되었습니다.

이 방식으로 바꾸고 나니 컴포넌트 구조가 자연스럽게 정돈됐어요. “이 컴포넌트에서 어떤 역할을 쓸 수 있는지”가 한눈에 보이고, 무엇보다 import 라인이 줄어들면서 코드 전체가 훨씬 깔끔해졌습니다.

구조를 재구성하고 나서 느낀 장점

이번 작업은 단순히 import 개수를 줄이는 것에 그치지 않고, Tooltip처럼 여러 역할이 함께 움직이는 UI 구조를 더 명확하게 만드는 데 도움이 되었어요.

1) import 구문이 깔끔해져요.

기존에는 Tooltip 관련 요소들을 3~4개씩 따로 import해야 해서 파일 상단이 금방 복잡해졌어요. 지금은 import { Tooltip } ... 한 줄이면 필요한 기능을 모두 사용할 수 있어서 훨씬 읽기 편해졌습니다.

2) 컴포넌트 간 관계가 명확해져요.

Tooltip은 Trigger·Content와 함께 사용해야 의미가 완성되는 UI인데, Dot Notation으로 묶이니 이 관계가 코드에서도 그대로 드러나요. “Tooltip 아래에 어떤 역할이 있는지”가 Tooltip 객체에 모여 있어서 API가 더 직관적으로 느껴졌어요.

3) 유지보수와 확장도 더 수월해져요.

Tooltip에 새로운 기능을 추가해야 할 때도 Tooltip.SomeFeature처럼 기존 네임스페이스 안에서 자연스럽게 확장할 수 있어요. 하위 컴포넌트를 별도로 export·import 관리하지 않아도 되니, 컴포넌트가 늘어나도 부담이 적습니다.

이 방식의 단점은 무엇일까?

Dot Notation으로 다시 재구성해서 쓰는 방식은 편리하지만, 사용하면서 고려해야 할 점도 있어요.

1) 객체가 커질수록 구조가 복잡해질 수 있어요.

여러 하위 요소들을 한 객체에 모아두다 보면, 컴포넌트 종류가 많아질수록 내부 구조가 길어지고 한눈에 파악하기 어려워질 가능성이 있어요.

2) 컴포넌트끼리 공유하는 내부 상태가 많아질 수 있어요.

컴파운드 구조 특성상 여러 요소가 동일한 상태나 context를 공유하는데, 규모가 커질수록 렌더링 흐름과 의존 관계를 조금 더 세심하게 관리해야 해요.

3) shadcn/ui 컴포넌트를 바로 쓰지 못하고, 한 번 더 재구성해야 해요.

shadcn/ui 기본 형태 대신 Dot Notation 구조를 사용하려면, 루트 컴포넌트를 기준으로 하위 요소들을 정리해주는 초기 작업이 필요해요. 큰 부담은 아니지만, 프로젝트 초반에는 이 추가 작업을 고려해야 합니다.


마치며

프로젝트가 커질수록 import 구조가 복잡해지고 관리 비용이 커지는 문제가 눈에 띄기 시작했는데, 이번에 컴포넌트 구조를 Dot Notation 기반으로 재구성하면서 전체 구조가 훨씬 안정적으로 정리되었고 팀원들 모두가 더 예측 가능한 방식으로 컴포넌트를 사용할 수 있게 되었습니다.

이 작업을 진행하면서 “컴포넌트를 어떻게 설계해야 유지보수와 확장이 쉬울까?”라는 고민을 다시 정리해볼 수 있었어요. 앞으로 기능이 더 많아질 뿐 아니라 협업 과정에서도 이런 구조적 정리가 얼마나 중요한지 점점 더 실감하게 되는 것 같습니다. 😊


참고

  • [Vercel] Understanding Compound Components
  • [10분 테코톡] 해삐의 컴포넌트 합성과 Compound Component 패턴
  • Compound 패턴