처음 디자인 시스템을 만들 때는 비교적 단순한 문제에서 출발했습니다.
프론트엔드 개발자는 4명이었고, 동시에 만들어야 하는 제품은 카플랫 웹, 카플랫 매니저, 카플랫 앱이었습니다. 웹과 매니저는 화면 구조와 UI가 꽤 비슷했고, 앱에서도 가능한 한 같은 컴포넌트를 활용하고 싶었습니다.
처음에는 목표가 분명했습니다. UI를 일관되게 만들고, 반복 구현을 줄이는 것이었습니다.
2년이 지난 지금은 카플랫 웹, 카플랫 매니저, 카플랫 앱, 개발자센터, 투루카 웹, biz 멤버십 관리페이지, 커뮤니티 멤버십 관리페이지까지 총 7개 프로젝트에서 디자인 시스템을 사용하고 있습니다. 주요 컴포넌트도 50개를 넘었습니다.
처음 만든 컴포넌트는 Button, Input, Select, Dialog, DataTable, FormField 같은 것들이었습니다. 당시에는 이 정도만 잘 만들어도 많은 반복을 줄일 수 있을 거라고 생각했습니다.
그 생각은 절반 정도 맞았습니다. 반복 구현은 확실히 줄었습니다. 하지만 시간이 지나면서 더 큰 문제는 "컴포넌트를 얼마나 많이 만들었는가"가 아니라 "팀이 이 시스템을 얼마나 오래 운영할 수 있는가"에 가까워졌습니다.
이 글에서 다루려는 변화도 결국 이 질문으로 모입니다.
- 반복되는 UI 결정을 어디까지 컴포넌트로 감쌀 것인가
- 사용처에서 바꿔야 하는 스타일을 어디까지 허용할 것인가
- 좋은 라이브러리보다 팀이 익숙한 API가 더 중요해지는 순간은 언제인가
- 문서를 사람뿐 아니라 AI 에이전트가 읽게 하려면 무엇이 달라져야 하는가
디자인 시스템은 컴포넌트를 많이 만드는 일이 아니라, 팀이 계속 쓸 수 있는 구조로 바꿔가는 일이었습니다.
처음에는 React Aria를 선택했습니다
초기 Headless UI 라이브러리로는 React Aria를 선택했습니다.
당시 Radix UI와 비교했을 때 React Aria를 선택한 이유는 대략 이랬습니다.
- GitHub issue가 상대적으로 적어 보였습니다.
- Reddit이나 Stack Overflow 같은 커뮤니티에서 접근성 측면의 평가가 좋았습니다.
- Adobe가 관리한다는 점이 신뢰에 영향을 줬습니다.
- 업데이트도 꾸준히 이루어지고 있었습니다.
접근성을 직접 챙기는 비용은 생각보다 큽니다. 키보드 탐색, 포커스 관리, 스크린 리더 대응, ARIA 속성 같은 요소는 구현 자체보다 계속 놓치지 않는 것이 어렵습니다. 작은 팀에서는 이런 부분을 라이브러리에 위임하는 것이 합리적인 선택이라고 봤습니다.
다만 form은 React Aria의 내장 form 흐름을 그대로 사용하지 않고 React Hook Form을 선택했습니다.
커뮤니티가 크고, 팀원들이 이미 익숙했으며, FormProvider를 중심으로 복잡한 폼을 유연하게 구성하기 좋았습니다. 디자인 시스템은 이상적인 API보다 팀이 실제로 유지할 수 있는 API가 더 중요했습니다.
React Hook Form은 지금도 선택한 걸 후회하지 않습니다. 특히 여러 프로젝트에서 같은 폼 패턴을 반복해야 하는 상황에서는 React Hook Form을 기준으로 컴포넌트를 설계한 것이 생산성에 도움이 됐습니다.
나중에 Radix UI로 넘어갈 때에도 React Hook Form을 사용한 덕분에 수고를 덜었습니다.
FormField가 가장 많은 중복을 줄였습니다
디자인 시스템을 운영하면서 가장 효과가 컸던 컴포넌트 중 하나는 FormField였습니다.
처음에는 Input, Select 같은 입력 컴포넌트를 공통화하면 폼 개발이 많이 줄어들 것이라고 생각했습니다. 하지만 실제로 반복되는 코드는 입력 컴포넌트 자체보다 그 주변에 더 많았습니다.
화면마다 반복되는 결정은 이런 것들이었습니다.
- label을 어디에 둘지
- description을 어떻게 보여줄지
invalid message와success message를 어떤 우선순위로 표시할지form-control영역을 어떻게 묶을지- 입력 컴포넌트와 에러 메시지 사이의 접근성 속성을 어떻게 연결할지
그래서 FormField는 label, form-control, description, success message, invalid message를 모두 표현할 수 있게 만들었습니다. form-control에는 Input뿐 아니라 Select, Checkbox 그룹, 커스텀 컴포넌트까지 넣을 수 있도록 했습니다.
이 구조를 만든 뒤에는 폼 화면을 만들 때 고민해야 하는 범위가 줄었습니다. "이 필드가 어떤 입력을 받는가"에 더 집중할 수 있었고, 에러 메시지나 설명 문구의 배치 방식은 컴포넌트가 책임졌습니다.
작은 팀에서 디자인 시스템이 가장 먼저 줄여야 하는 것은 화려한 UI 구현 시간이 아니라, 매번 비슷한 결정을 반복하는 시간이었습니다.
컴포넌트가 늘자 빌드가 먼저 버티지 못했습니다
초기 빌드 구조는 tsup 기반이었습니다. 컴포넌트별 진입점을 나누고, 사용하는 쪽에서 필요한 컴포넌트만 가져갈 수 있도록 구성했습니다.
처음에는 문제가 없었습니다. 컴포넌트 수가 적을 때는 빌드도 빠르고 설정도 단순했습니다. 하지만 컴포넌트가 늘어나면서 빌드 시간이 점점 길어졌고, 어느 순간 OOM이 발생하기 시작했습니다.
디자인 시스템은 한 번 배포되면 여러 프로젝트의 개발 속도에 영향을 줍니다. 빌드가 느려지면 단순히 라이브러리 개발자만 불편한 것이 아니라, 컴포넌트를 수정하고 검증하는 전체 피드백 루프가 느려집니다.
결국 빌드 도구를 Vite로 전환했습니다. 전환 후 빌드 시간은 약 2분에서 10초 내외로 줄었습니다.
정리하면 변화는 단순했습니다.
- 이전: tsup 기반, 컴포넌트별 진입점이 늘수록 빌드 시간이 길어짐
- 문제: 컴포넌트 증가 후 OOM 발생
- 이후: Vite 전환 후 빌드 시간이 2분에서 10초 내외로 감소
이 변화는 단순한 성능 개선 이상의 의미가 있었습니다. 컴포넌트를 고치고, 빌드하고, 사용하는 프로젝트에서 확인하는 흐름이 훨씬 가벼워졌습니다. 디자인 시스템은 패키지인 동시에, 계속 고쳐 쓰는 개발 도구에 가까웠습니다.
초기에는 minor 버전도 자주 올라갔습니다. 컴포넌트가 계속 추가되고 API도 바뀌었기 때문입니다. 요즘은 큰 변화보다 patch 배포가 대부분입니다. 어느 정도 안정화된 뒤에는 새 기능을 계속 넣기보다, 사용 중 발견되는 작은 문제를 빠르게 고치는 쪽에 가까워졌습니다.
운영 과정에서 빌드 속도는 DX 체감에 큰 영역이었습니다.
!important가 늘어나기 시작했을 때
스타일 구조도 처음부터 완성되어 있지 않았습니다.
초기에는 CSS specificity를 중심으로 스타일을 설계했습니다. 컴포넌트 내부 스타일을 어느 정도 강하게 잡아두면 일관성을 지킬 수 있을 거라고 생각했습니다.
하지만 실제 사용처에서는 override가 필요한 상황이 계속 생겼습니다. 어떤 화면에서는 여백을 조금 바꿔야 했고, 어떤 화면에서는 상태별 색상을 다르게 보여줘야 했습니다. 그런데 컴포넌트 스타일의 우선순위가 높으면 사용하는 쪽에서 원하는 스타일을 적용하기 어려웠습니다.
결과적으로 !important가 늘어났습니다.
!important가 한두 번 등장할 때는 큰 문제가 아닌 것처럼 보입니다. 하지만 반복되기 시작하면 스타일을 예측하기 어려워집니다. 어떤 스타일이 왜 적용되는지 설명하기 어려워지고, 새로 들어온 팀원은 CSS를 수정하기 전에 기존 우선순위부터 추적해야 합니다.
이 문제를 줄이기 위해 CSS layer를 도입했습니다.
우선순위는 아래 순서로 정리했습니다.
resetcomponentsutilities
컴포넌트 기본 스타일은 components layer에 두고, 사용처에서 필요한 조정은 Tailwind utility를 통해 자연스럽게 덮을 수 있도록 했습니다.
이 변경 이후 스타일 override를 위해 불필요하게 specificity를 높이는 일이 줄었습니다. 컴포넌트는 기본값을 제공하고, 사용처는 필요한 만큼 조정할 수 있게 되었습니다.
일관성을 지키기 위해 스타일을 강하게 잠그는 방식은 초반에는 편하지만, 프로젝트가 늘어날수록 비용이 커졌습니다.
API가 낯설면 좋은 컴포넌트도 잘 쓰이지 않았습니다
React Aria를 사용하면서 시간이 지나며 다른 종류의 문제가 생겼습니다. 접근성이나 안정성보다는, 팀이 API를 얼마나 익숙하게 쓸 수 있느냐의 문제였습니다.
팀원들은 shadcn이나 Radix UI 경험이 더 많았습니다. 그런데 React Aria는 이벤트 이름부터 낯설었습니다. 예를 들어 일반적인 onClick 대신 onPress를 사용하는 식입니다. prop 네이밍이나 컴포넌트 조합 방식도 익숙한 패턴과 달랐습니다.
Dialog 구조에서도 차이가 컸습니다.
React Aria의 Modal은 DialogTrigger가 전체를 감싸고, 내부에 Modal, Dialog가 들어가는 구조입니다.
반면 shadcn이나 Radix에 익숙한 개발자는 보통 아래와 같은 구조를 기대합니다.
이 낯설음은 사람에게만 생긴 문제가 아니었습니다. AI 코드 생성 도구도 대체로 shadcn/Radix 스타일의 마크업을 먼저 제안했습니다. React Aria 기반 컴포넌트를 쓰고 있는데도, AI가 Radix처럼 Dialog 구조를 작성하는 일이 반복됐습니다.
이때부터는 "기술적으로 더 적합한가"뿐 아니라 "팀원과 AI 도구가 별다른 설명 없이 쓸 수 있는가"도 중요한 기준이 됐습니다.
결국 일부 컴포넌트는 점진적으로 Radix UI 기반으로 전환했습니다.
Radix UI로 전환하며 생긴 비용
하지만 이 선택에도 비용이 있었습니다.
React Aria에서 제공하던 컴포넌트 범위가 줄어들면서 몇몇 컴포넌트는 직접 구현해야 했습니다.
- Switch
- Toggle, ToggleGroup
- CheckboxGroup, RadioGroup
- Dropzone
- FileTrigger
- Multiselect
직접 구현한 컴포넌트는 처음에는 빠르게 만들 수 있지만, 적극적으로 유지보수하지 않으면 시간이 지나며 부담이 됩니다.
지금 돌아보면 Radix 전환은 당시 팀의 생산성을 위해 필요한 결정이었습니다. 다만 Radix Primitive가 이슈 대응이 느리다는 평도 있어서 Base UI 같은 선택지도 다시 검토해야 할 수 있다고 보고 있습니다.
디자인 시스템의 기술 선택은 한 번 정하면 끝나는 것이 아니라, 팀의 숙련도와 생태계 변화에 따라 계속 다시 평가해야 하는 영역이었습니다.
팔레트 대신 역할에 이름을 붙이기 시작했습니다
초기에는 색상 팔레트를 직접 사용하는 방식이었습니다.
예를 들어 base-500, destructive-800, primary-400 같은 팔레트 값을 컴포넌트와 화면에서 직접 사용했습니다. 처음에는 빠르고 직관적이었습니다. 어떤 색을 쓰는지 눈으로 바로 알 수 있었고, 디자이너와 개발자 모두 큰 불편 없이 사용할 수 있었습니다.
하지만 다크모드 요구가 생기고, 신규 디자이너와 협업하면서 문제가 드러났습니다. 같은 base 계열이라도 배경인지, 보더인지, 비활성 텍스트인지, 보조 텍스트인지 맥락이 필요했습니다.
팔레트는 색상값은 알려주지만, 그 색을 어디에 써야 하는지는 알려주지 못했습니다.
그래서 팔레트 토큰을 직접 쓰기보다, 사용 의도가 드러나는 시맨틱 토큰으로 옮기자고 제안했습니다.
목표는 토큰 이름에 역할이 보이게 만드는 것이었습니다.
text-default: 기본 텍스트 색상surface-elevated: 기본 배경보다 한 단계 떠 있는 표면primary-strong: primary 의도를 가진 강한 강조 색상
이 과정에서는 디자이너와 논의를 많이 했습니다.
개발자 입장에서는 가능한 한 규칙이 있는 시스템을 만들고 싶었습니다. 시맨틱 토큰을 컴포넌트별로 너무 잘게 나누기보다, 여러 곳에서 재사용할 수 있는 구조로 유지하고 싶었습니다.
반면 디자이너 입장에서는 표현할 수 있는 디자인의 폭이 줄어들지 않는 것도 중요했습니다. 토큰 수를 너무 줄이면 화면마다 필요한 미묘한 차이를 표현하기 어려워질 수 있기 때문입니다.
처음에는 디자이너가 시맨틱 토큰을 약 100개 정도로 제안했습니다. 이후 두 기준으로 다시 정리했습니다.
- intent별 value 강도:
muted < subtle < accent < default < strong < intense - key의 표면 성격:
background < surface < elevated
그 결과 지금은 약 60개 정도의 토큰으로 관리하고 있습니다.
이곳에 Figma Variable에서 팔레트 토큰이 시맨틱 토큰으로 바뀐 화면 캡처 첨부 예정
이 규칙을 도입하면서 색상을 고르는 기준이 조금 바뀌었습니다. "어떤 파란색을 쓸까?"보다 "이 UI는 어떤 의미와 강도를 가져야 할까?"를 먼저 생각하게 됐습니다.
토큰을 줄이는 것 자체가 목적은 아니었습니다. 중요한 것은 적은 수의 토큰이 아니라, 팀이 같은 의미로 이해하고 계속 관리할 수 있는 수의 토큰이었습니다.
Storybook 문서는 충분하다고 생각했지만 아니었습니다
컴포넌트를 만들고 Storybook에 예제를 추가하면 문서화도 어느 정도 됐다고 생각했습니다.
하지만 사용하는 입장에서는 부족했습니다. 페이지를 개발하다 보면 Storybook을 계속 오가야 했고, 어떤 컴포넌트를 써야 하는지, 어떤 prop 조합이 권장되는지, 실제 마크업은 어떻게 구성해야 하는지 한 번에 파악하기 어려웠습니다.
작성자인 저는 "스토리가 있으니 충분하다"고 느꼈지만, 사용자는 "지금 이 화면을 만들기 위해 무엇을 봐야 하는지"가 더 중요했습니다.
그래서 Storybook 문서의 목차와 읽기 흐름을 다시 정리했습니다.
문서는 크게 두 단계로 나눴습니다.
SKILL.md: 컴포넌트를 찾기 위한 목차와 짧은 설명turucar-components/comp/{COMPONENT}.md: props, 마크업 방법, 엣지케이스를 담은 상세 문서
MDX 문서의 제목과 1~2줄 설명은 SKILL.md의 목차처럼 사용할 수 있는 진입점으로 추출했습니다. 상세 문서는 컴포넌트별로 나누어 props, 마크업 방법, 엣지케이스를 한곳에서 볼 수 있게 했습니다. 이 과정에는 node:fs 기반의 파일 읽기/쓰기를 사용해 문서를 생성하고 갱신했습니다.
AI 에이전트도 문서를 읽는 사용자가 됐습니다
평소 Claude Code, Codex에게 프롬프트로 지시할 때 컴포넌트 문서를 활용하게 하여 정확도를 높이고 싶었습니다.
효과는 꽤 컸습니다. Figma MCP로 화면 의도를 전달했을 때, 반복되는 관리자 화면에서는 체감상 90% 정도는 의도한 컴포넌트와 마크업 구조로 구현됐습니다. 예전에는 AI가 shadcn이나 Radix 기준으로 코드를 생성하는 경우가 많았는데, 이제는 우리 컴포넌트 문서를 참고해 더 가까운 결과를 만들 수 있었습니다.
문서는 사람이 읽는 설명서일 뿐 아니라, 이제는 도구에 팀의 맥락을 알려주는 인터페이스이기도 했습니다.
이곳에 turucar-components tool 사용 횟수 이미지 첨부 예정
아직 풀지 못한 문제도 남아 있습니다
디자인 시스템은 개발 생산성을 높였습니다. 반복 구현은 줄었고, 여러 프로젝트에서 일관된 UI를 유지하기 쉬워졌습니다.
하지만 모든 문제가 해결된 것은 아닙니다.
디자인 시스템이 디자이너에게는 조립식 UI처럼 느껴질 수 있습니다. 다양한 디자인 실험을 하고 싶은 욕구와, 관리 가능한 컴포넌트 범위를 유지해야 하는 현실 사이에는 계속 긴장이 있습니다.
컴포넌트와 variant를 늘리면 표현력은 좋아집니다. 하지만 작은 팀에서는 그 모든 조합을 설계하고, 구현하고, 문서화하고, 유지보수하는 비용을 감당하기 어렵습니다. 반대로 범위를 너무 좁게 유지하면 새로운 화면을 만들 때 디자인이 답답하게 느껴질 수 있습니다.
지금 팀 규모에서는 variant와 컴포넌트를 무작정 늘리기 어렵다고 판단하고 있습니다. 대신 자주 반복되는 패턴은 시스템으로 끌어오고, 실험이 필요한 영역은 너무 빨리 고정하지 않으려 합니다.
아직은 아래 기준 사이에서 계속 조정하고 있습니다.
- 한두 화면에서만 필요한 UI는 너무 빨리 공통으로 만들지 않습니다.
- variant를 늘릴 때는 구현 비용보다 유지보수 비용을 먼저 봅니다.
- 디자이너의 시인성/가독성 제안을 존중합니다.
일관성과 표현 자유도의 균형은 아직 진행 중인 과제입니다.
디자인 시스템은 계속 변해야 했습니다
처음에는 디자인 시스템을 공통 컴포넌트 라이브러리에 가깝게 생각했습니다. Button, Input, Dialog 같은 컴포넌트를 잘 만들고 배포하면 많은 문제가 해결될 것이라고 봤습니다.
2년 동안 운영해보니 실제로 바뀐 것은 컴포넌트만이 아니었습니다.
빌드 도구를 바꿨고, CSS 우선순위를 다시 설계했고, Headless UI 선택을 다시 검토했습니다. 색상 팔레트는 시맨틱 토큰으로 옮겨갔고, Storybook 문서는 사람뿐 아니라 AI 에이전트가 읽을 수 있는 형태로 확장됐습니다.
디자인 시스템은 한 번 잘 만들면 끝나는 라이브러리가 아니었습니다. 팀의 규모, 제품의 수, 도구의 변화, 디자이너와 개발자의 협업 방식에 맞춰 계속 바뀌는 운영 구조에 가까웠습니다.
지금 중요하게 보는 기준은 컴포넌트 개수가 아닙니다.
지금은 아래 질문을 더 중요하게 봅니다.
- 팀이 오래 운영할 수 있는 구조인가
- 새로 합류한 사람이 이해할 수 있는가
- 사용하는 프로젝트에서 자연스럽게 확장할 수 있는가
- 변경 비용을 감당할 수 있는가
소규모 팀에서 디자인 시스템을 만든다는 것은 완벽한 체계를 한 번에 세우는 일이 아니었습니다. 반복되는 비용을 발견하고, 그 비용이 충분히 커졌을 때 시스템의 일부로 옮기는 과정에 가까웠습니다.
앞으로도 이 시스템은 계속 바뀔 것입니다. 다만 이제는 무엇을 더 만들지보다, 무엇을 오래 유지할 수 있을지를 먼저 보려고 합니다.