요즘 스노로즈 백오피스 개발을 하면서 shadcn/ui 컴포넌트를 자주 사용하고 있어요. shadcn/ui 자체도 컴파운드 컴포넌트 형태로 구성되어 있지만, 프로젝트 구조상 src/components/ui에서 다시 export하는 과정이 있다 보니 import가 빠르게 늘어나고 구조도 점점 복잡해지고 있었습니다.
그러던 중, 컴포넌트를 객체 형태로 묶어 한 엔트리로 관리하는 방식을 소개한 아티클을 보게 되었고, 이 형태가 우리 구조와 잘 맞겠다고 느껴 Tooltip부터 방식 개선을 시작해보았어요. 이번 글에서는 왜 이런 선택을 했는지, 적용 후 어떤 변화가 있었는지 기록해보려고 합니다.
컴파운드 컴포넌트 패턴은 하나의 기능을 여러 구성 요소가 함께 완성하도록 만든 구조예요. 부모가 상태와 동작을 관리하고, 하위 요소들은 역할만 담당해 조립식으로 UI를 구성할 수 있습니다.
이 방식은 다음과 같은 UI에서 특히 유용합니다.
백오피스 프로젝트는 아직 초기 단계임에도 import 줄 수가 빠르게 늘어났어요. 23개의 컴포넌트를 가져오는 데 104줄이 필요했을 정도니, 앞으로 확장될수록 관리 비용은 더 커질 수밖에 없는 구조였죠.
또한
이런 점들이 계속 부담으로 느껴졌습니다.
그래서 컴포넌트별 롤을 하나로 묶는 방식이 지금 구조와 잘 맞겠다고 판단해 Tooltip부터 방식을 바꿔보기로 했어요.
개선하고 싶은 코드

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 기반으로 다시 재구성해주었습니다.
shadcn/ui에서 제공하는 컴포넌트들은 기본적으로 각각 별도 export 형태를 가지고 있어요. Tooltip처럼 여러 구성 요소가 함께 동작하는 UI는 루트 컴포넌트를 중심으로 한 엔트리로 묶어두면 구조가 더 명확해지고, import하는 사람도 훨씬 편하게 사용할 수 있어요.
정리 방식은 아래와 같아요.
Tooltip 루트 컴포넌트는 그대로 유지하되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 };프로젝트에서는 shadcn/ui 컴포넌트들을 src/shared/components/ui 폴더에서 한 번 더 export하고 있어요.
이 레이어에서 각각의 Tooltip 구성 요소를 모두 다시 export하면 import 라인이 계속 늘어나기 때문에, Tooltip 객체 하나만 내보내도록 구조를 단순화했습니다.
적용 전
export { Tooltip, TooltipProvider, TooltipTrigger, TooltipContent }from'./tooltip';적용 후
export { Tooltip }from'./tooltip';비교

적용 전: 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 기반으로 재구성하면서 전체 구조가 훨씬 안정적으로 정리되었고 팀원들 모두가 더 예측 가능한 방식으로 컴포넌트를 사용할 수 있게 되었습니다.
이 작업을 진행하면서 “컴포넌트를 어떻게 설계해야 유지보수와 확장이 쉬울까?”라는 고민을 다시 정리해볼 수 있었어요. 앞으로 기능이 더 많아질 뿐 아니라 협업 과정에서도 이런 구조적 정리가 얼마나 중요한지 점점 더 실감하게 되는 것 같습니다. 😊