| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 | 30 |
- frontend
- useEffect
- WebP
- JWT
- 프론트엔드개발자
- 프론트엔드성능
- JavaScript
- reactquery
- 리팩토링
- TanStackQuery
- lighthouse
- 프론트엔드개발
- Next
- Refactoring
- 최적화
- 성능최적화
- frontenddevelopment
- RTR
- 비동기처리
- NextJs
- react
- TechBlog
- 리액트
- fe
- 프론트엔드
- 웹개발
- refresh token
- 데이터캐싱
- react성능최적화
- 이미지최적화
- Today
- Total
juncci 님의 블로그
[디자인 시스템] TypeScript + Tailwind CSS 4.0 환경에서 구축하기 본문
개발자들이 가장 비효율적이라고 느끼는 순간은 언제일까?

개인적으로 반복되는 작업을 수행할 때라고 생각한다.
처음 프로젝트를 시작하고 여러 버튼을 생성할 때
비슷한 버튼을 만들고, 또 만들고, 또 만든 경험이 있다….
비슷한 인풋을 만들고, 모달을 만들고, 약간씩 다른 스타일을 계속 복붙하면서 구현했었다.
처음에는 빠르게 개발할 수 있어서 좋아 보였지만, 시간이 지나면서 문제가 쌓이기 시작했다.
- 버튼 색상이 화면마다 조금씩 다르다
- 간격과 라운드 값이 일관되지 않다
- 비슷한 컴포넌트가 계속 복제된다
- 수정이 필요할 때 어디를 고쳐야 할지 모른다
이때 자연스럽게 이런 질문이 생긴다.
“이걸 계속 이렇게 만들어도 괜찮은 걸까?”
“이 반복을 줄일 수 있는 방법은 없을까?”
이 질문을 따라가다 보니 디자인 시스템이라는 개념을 알게 되었고,
단순히 UI를 만드는 방식이 아니라 제품을 만드는 구조 자체를 바꾸는 접근 방식이라는 걸 이해하게 되었다.
🥨 디자인 시스템이란?
디자인 시스템(Design System)은
디지털 제품을 일관되고 효율적으로 설계하고 개발하기 위해 디자인 원칙, 시각 요소, UI 컴포넌트, 코드, 문서를 통합해 관리하는 체계이다.

버튼을 어떤 기준으로 만들고, 어떤 상황에서 사용하고, 어떤 상태를 가져야 하며, 변경이 생겼을 때 어디를 수정해야 하는지를 포함한 제품 UI의 운영 체계에 가깝다.
이런 디자인 시스템을 아래와 같이 구성해 보았다.
| Design Principles | 제품이 추구하는 UX/UI 방향성 |
| Design Tokens | 색상, 폰트, 간격, 라운드, 그림자 같은 최소 시각 단위 |
| Components | Button, Input, Modal, BottomSheet 같은 재사용 UI |
| Patterns | 로그인, 폼, 리스트, 피드백 플로우 같은 조합 규칙 |
| Documentation | 사용법, 상태, 예시, 금지 케이스 문서 |
| Quality Gate | Storybook, interaction test, a11y, CI 검증 |
이 프로젝트에서는 특히 Design Token → Tailwind Theme → Shared UI → Storybook → CI 흐름을 중심으로 디자인 시스템을 설계했다.
🥨 디자인 토큰이란?
디자인 토큰은 디자인 시스템의 가장 작은 단위다.
색상, 폰트 크기, 여백, 라운드, 그림자, 높이, 애니메이션 같은 값을 그냥 코드에 직접 쓰지 않고 의미 있는 이름으로 추상화한 것이다.
예를 들어 이렇게 값을 직접 사용하면
<buttonclassName="bg-[#35558b] rounded-[14px] text-white">
확인
</button>
이 방식은 당장은 빠르지만 프로젝트가 커지면 문제가 된다.
#35558b가 어떤 의미인지 알기 어렵다.
브랜드 메인 컬러인지, 버튼 전용 컬러인지, 선택 상태 컬러인지 알 수 없다.
나중에 색상이 변경되면 프로젝트 전체에서 해당 값을 찾아 바꿔야 한다.
그래서 디자인 토큰을 사용한다.
<buttonclassName="bg-primary-main rounded-md text-white">
확인
</button>
이렇게 작성하면 값이 아니라 의도가 드러난다.
bg-primary-main은 “Primary 계열의 메인 배경색”이라는 의미를 가진다.
rounded-md는 “중간 단계의 라운드 값”이라는 의미를 가진다.
디자인 토큰은 단순한 변수명이 아니라,
디자인 의사결정을 코드에서 재사용할 수 있게 만든 계약서다.
🥨 직접 설계해 본 디자인 시스템 구조
이번 프로젝트에서 내가 만들고자 한 흐름은 다음과 같다.
핵심은 UI를 만드는 과정을 아래처럼 체계화하는 것이었다.
- 디자인 토큰을 먼저 정의한다.
- Tailwind theme에 의미 기반 클래스로 매핑한다.
- shared UI 컴포넌트가 토큰을 소비한다.
- feature/widget 레이어에서는 shared UI를 조합해서 사용한다.
- Storybook으로 상태와 사용법을 문서화한다.
- interaction test와 a11y 검증을 CI에 연결한다.
정의된 기준을 shared UI가 소비하고 제품 레이어는 조합만 하는 구조를 만들고자 했다.
🥨 디자인 토큰 설계
CSS 변수로 원천값 정의
먼저 globals.css에서 디자인 토큰의 원천값을 정의했다.

여기서 중요한 점은 토큰을 단순히 값으로 나열하지 않고 역할별로 분리했다는 점이다.
🥨 토큰 카테고리
1️⃣ Layout Token

--app-header-height: 36px;
--app-footer-height: 72px;
헤더와 푸터 높이를 토큰화했다.
이렇게 하면 layout 관련 컴포넌트에서 동일한 기준을 사용할 수 있다.
예를 들어 화면마다 36px, 72px를 직접 쓰면 나중에 앱 프레임이 바뀔 때 모든 화면을 찾아야 한다.
하지만 토큰으로 관리하면 기준값만 변경하면 된다.
2️⃣ Primary Color Token

--color-primary-main:#35558b;
--color-primary-sub:#65778c;
--color-primary-active:#d2deea;
--color-primary-disabled:#d2deea;
Primary 계열은 브랜드나 주요 액션을 표현하는 색상이다.
여기서 main, sub, active, disabled를 분리한 이유는 버튼, 탭, 선택 상태, 비활성 상태 등에서 같은 Primary 계열을 일관되게 사용하기 위해서다.
3️⃣ Text Token

--color-text-title:#000000;
--color-text-body:#2c2c2c;
--color-text-caption-strong:#555555;
--color-text-caption:#808080;
--color-text-hint-main:#aeaeae;
--color-text-hint-sub:#bcbcbc;
텍스트는 단순히 “검정/회색”이 아니라 정보의 위계가 있다.
- title: 가장 중요한 제목
- body: 본문
- caption-strong: 강조 캡션
- caption: 보조 정보
- hint-main: 입력 힌트
- hint-sub: 더 약한 보조 힌트
4️⃣ Radius Token

--radius-light: 10px;
--radius-sm: 12px;
--radius-md: 14px;
--radius-lg: 20px;
--radius-xl: 24px;
--radius-round: 9999px;
라운드 값은 제품의 인상을 크게 좌우한다.
어떤 화면은 10px, 어떤 화면은 13px, 어떤 화면은 16px처럼 제각각 쓰기 시작하면 UI가 금방 어긋난다.
그래서 라운드도 스케일로 정의했다.
- light
- sm
- md
- lg
- xl
- round
이렇게 하면 컴포넌트는 임의값을 쓰지 않고 정해진 스케일 안에서 선택하게 된다.
5️⃣ Shadow / Effect Token

--shadow-light: 0 0 30pxrgba(212, 212, 212, 1);
--shadow-blur-strong: 0 0 14px rgba(255, 255, 255, 1);
--shadow-blur-basic: 0 0 4pxrgba(255, 255, 255, 1);
그림자와 blur 효과도 토큰화했다.
shadow는 작은 차이만 있어도 화면의 깊이감이 달라진다.
그래서 컴포넌트마다 임의의 shadow를 만들기보다, 시스템에서 제공하는 shadow만 사용하도록 했다.
🥨 Tailwind Theme로 매핑하기

CSS 변수로 원천값을 정의한 뒤 tailwind.config.ts에서 Tailwind theme로 매핑했다.
이 매핑을 통해 컴포넌트에서는 CSS 변수명을 직접 알 필요가 없어진다.
className="bg-primary-main text-text-title rounded-md shadow-light"
컴포넌트 입장에서는 Tailwind utility class만 사용하면 된다.
이 구조의 장점은 두 가지다.
첫째, CSS 변수 기반이라 런타임 테마 전환 가능성이 있다.
예를 들어 나중에 다크 모드가 필요하면 :root[data-theme='dark']에서 변수값만 바꾸면 된다.
둘째, Tailwind utility 기반이라 개발 생산성이 좋다.
개발자는 별도 CSS 파일을 계속 만들지 않고도 의미 기반 스타일을 빠르게 적용할 수 있다.
🥨 Shared UI 컴포넌트 전략
토큰을 정의하는 것만으로는 디자인 시스템이 완성되지 않는다.
토큰은 실제 컴포넌트에서 사용되어야 한다.
그래서 src/shared/ui/* 아래에 공통 UI 컴포넌트를 만들었다.
(여러 컴포넌트 중 Button을 예로 설명해보겠습니다.)
Button
typeButtonVariant='primary'|'secondary'|'ghost';
Button은 variant 기반으로 스타일을 분리했다.
constvariants:Record<ButtonVariant,string>= {
primary:`
bg-primary-main/80
border border-white/20
text-white
hover:bg-primary-main/70
active:bg-primary-main/90
disabled:bg-gray-300 disabled:text-gray-500
`,
secondary:`
bg-gray-200 text-black
hover:bg-gray-300
active:bg-gray-400
disabled:bg-gray-200 disabled:text-gray-400
`,
ghost:`
bg-transparent text-primary-main
hover:bg-primary-main/10
disabled:text-gray-400
`,
};
이 설계에서 핵심은 Button이 primary.main 토큰을 사용한다는 점이다.
Button은 브랜드 주요 액션을 표현하는 컴포넌트이기 때문에 bg-primary-main, text-primary-main 같은 semantic token을 소비한다.
다만 개선할 점도 보인다.
현재 secondary나 disabled 상태에는 gray-200, gray-300, gray-500 같은 Tailwind 기본 색상이 직접 들어가 있다.
이것도 나중에는 아래처럼 토큰화할 수 있다.
--color-button-secondary-bg: ...
--color-button-secondary-hover: ...
--color-button-disabled-bg: ...
--color-button-disabled-text: ...
현재는 global token 기반이고, 다음 단계에서는 component token으로 확장할 수 있다.
🥨 Storybook 문서화 전략
디자인 시스템에서 Storybook은 단순한 미리보기 도구가 아니다.
이번 프로젝트에서는 Storybook을 다음 목적으로 사용했다.
- shared UI를 독립적으로 확인한다.
- variant와 state를 문서화한다.
- interaction test를 작성한다.
- 접근성 검증을 CI와 연결한다.
- 컴포넌트 누락 여부를 audit script로 검사한다.
운영 기준은 다음과 같이 정의했다.
- src/shared/ui의 모든 컴포넌트 슬라이스는 .stories.tsx를 가진다.
- 각 스토리 파일은 최소 1개 이상의 play 상호작용 검증을 가진다.
- 주요 상태를 분리해서 스토리로 명시한다.
- 주요 컴포넌트는 argTypes를 정의해 Controls playground를 제공한다.
- parameters.docs.description.component에 사용 맥락을 작성한다.
- 접근성 검증은 a11y.test = error로 CI 실패 조건으로 관리한다.
운영 기준을 기반으로 하여 품질 게이트의 일부로 사용하였다.
🥨 storybook-audit 스크립트 전략
디자인 시스템은 만들고 끝나는 것이 아니라 계속 유지되어야 한다.
그래서 storybook-audit.mjs를 통해 src/shared/ui의 상태를 검사했다.
검사 항목은 다음과 같다.
- shared/ui slice 개수
- story 파일 보유 여부
- story export 개수
- play block 개수
- story가 없는 컴포넌트
- play 검증이 없는 컴포넌트
핵심 로직은 다음과 같다.
constmissingStorySlices=metrics
.filter((m) =>m.componentFiles.length>0&&m.storyFiles.length===0)
.map((m) =>m.slice);
constslicesWithoutPlay=metrics
.filter((m) =>m.storyFiles.length>0)
.filter((m) =>m.storySummaries.every((item) =>item.playBlocks===0))
.map((m) =>m.slice);
그리고 문제가 있으면 CI에서 실패하도록 했다.
constfailed=missingStorySlices.length>0||slicesWithoutPlay.length>0;
if (failed) {
process.exitCode=1;
console.log('Result: FAILED');
return;
}
이 전략의 장점은 명확하다.
컴포넌트를 추가했는데 Storybook 문서를 만들지 않으면 CI에서 잡힌다.
스토리는 있지만 interaction test가 없으면 CI에서 잡힌다.
디자인 시스템의 문서화와 검증을 개발자의 선의에만 맡기지 않고 자동화했다.
(추가) Tailwind CSS 4.0에서 주의할 점
이번 프로젝트의 개발 환경은 TypeScript + Tailwind CSS 4.0이다.
Tailwind CSS 4.0은 이전 버전과 꽤 다르다.
특히 디자인 시스템을 구축할 때는 이 차이를 정확히 이해해야 한다.
1️⃣ Tailwind CSS 4.0은 CSS-first configuration을 강조한다
Tailwind CSS v4.0의 가장 큰 변화 중 하나는 CSS-first configuration이다.
공식 블로그에서도 v4는 CSS에서 디자인 토큰, 커스텀 유틸리티, variant 등을 설정할 수 있는 CSS-first 방식을 강조한다.
이전 버전에서는 보통 tailwind.config.ts가 설정의 중심이었다.
// Tailwind v3 스타일
exportdefault {
theme: {
extend: {
colors: {
primary:'#35558b',
},
},
},
};
하지만 Tailwind v4에서는 CSS의 @theme를 통해 theme variable을 정의하는 방식이 중요해졌다.
공식 문서에 따르면 @theme로 정의한 theme variables는 프로젝트에서 어떤 utility class가 생성되는지에 영향을 준다. 예를 들어 --color-mint-500을 정의하면 bg-mint-500, text-mint-500 같은 클래스가 사용 가능해진다.
@import"tailwindcss";
@theme {
--color-primary-main: #35558b;
--radius-md:14px;
}
이제 Tailwind는 CSS 변수와 더 강하게 연결된다.
2️⃣ tailwind.config.ts 중심 사고에서 벗어나야 한다
v3까지 Tailwind를 사용하던 사람들은 대부분 이렇게 생각한다.
커스텀 색상? tailwind.config.ts에 넣자.
커스텀 spacing? tailwind.config.ts에 넣자.
커스텀 radius? tailwind.config.ts에 넣자.
하지만 v4에서는 CSS-first가 기본 방향이다.
물론 @config를 통해 기존 config 파일을 불러오는 방식도 사용할 수 있다.
@import'tailwindcss';
@config"../../tailwind.config.ts";
이 방식은 마이그레이션이나 TypeScript config를 유지해야 하는 상황에서는 유용하다.
다만 장기적으로는 @theme 기반으로 토큰을 옮기는 것도 고려할 수 있다.
현재 구조
:root {
--color-primary-main: #35558b;
}
colors: {
primary: {
main:"var(--color-primary-main)",
},
}
v4 친화적인 구조
@import"tailwindcss";
@theme {
--color-primary-main: #35558b;
--color-text-title: #000000;
--radius-md:14px;
--shadow-light:0030pxrgba(212,212,212,1);
}
이렇게 하면 Tailwind의 theme variable 시스템과 더 직접적으로 연결된다.
3️⃣ 브라우저 지원 범위를 확인해야 한다
Tailwind CSS v4.0은 최신 브라우저를 대상으로 설계되었다.
공식 upgrade guide에 따르면 v4는 Safari 16.4+, Chrome 111+, Firefox 128+를 대상으로 하며, 더 오래된 브라우저를 지원해야 한다면 v3.4를 유지하라고 안내한다.
이건 생각보다 중요하다.
특히 기업용 서비스나 구형 WebView를 지원해야 하는 프로젝트라면 Tailwind v4 도입 전에 브라우저 정책을 확인해야 한다.
체크해야 할 항목은 다음과 같다.
- 서비스가 지원해야 하는 최소 Safari 버전
- Android WebView 버전
- iOS WebView 버전
- 사내 브라우저 정책
- 고객사가 구형 브라우저를 사용하는지 여부
4️⃣ @theme와 일반 :root CSS 변수의 차이를 이해해야 한다

Tailwind v4에서 헷갈리기 쉬운 점이 있다.
:root {
--color-primary-main: #35558b;
}
이건 일반 CSS 변수다.
반면 아래는 Tailwind theme variable이다.
@theme {
--color-primary-main: #35558b;
}
둘 다 CSS 변수처럼 보이지만 의미가 다르다.
구분 설명
| :root CSS 변수 | 런타임에서 CSS 값으로 사용할 수 있는 일반 변수 |
| @theme 변수 | Tailwind utility class 생성과 연결되는 theme variable |
:root에만 변수를 선언하면 CSS에서는 사용할 수 있지만, Tailwind가 그 변수를 기준으로 utility class를 자동 생성한다고 보장할 수 없다.
반대로 @theme에 선언하면 Tailwind가 해당 토큰을 인식하고 utility class 생성에 활용한다.
5️⃣ @import 'tailwindcss' 방식이 달라졌다
이전 Tailwind에서는 보통 다음처럼 작성했다.
@tailwindbase;
@tailwindcomponents;
@tailwindutilities;
Tailwind v4에서는 보통 아래처럼 사용한다.
@import"tailwindcss";
다만 주의할 점은 @config를 사용할 때 경로가 정확해야 한다는 것이다.
특히 Next.js App Router 구조에서는 globals.css 위치 기준으로 상대 경로를 잘 맞춰야 한다.
마치며
이번 작업을 통해 디자인 시스템은 단순히 UI를 정리하는 작업이 아니라, 디자이너와 프론트엔드 개발자 사이의 협업 방식을 정리하는 과정이라는 것을 느꼈다.
디자인 토큰과 컴포넌트를 기준으로 공통 언어를 만들면, 불필요한 커뮤니케이션 비용을 줄이고 더 빠르고 일관된 결과를 만들어낼 수 있다.
특히 최근에는 Figma MCP와 같은 도구들이 등장하면서 디자인과 개발 사이의 간극을 더 자연스럽게 연결할 수 있는 환경이 만들어지고 있다.
앞으로는 이런 도구들을 함께 활용해 디자인 시스템을 단순한 구조가 아닌, 실제 협업을 더 잘 작동하게 만드는 방향으로 확장해보고자 한다.

'[FE]' 카테고리의 다른 글
| 🚀 Next.js 이미지 최적화: LCP 개선을 위한 전체 설계 (0) | 2026.03.31 |
|---|---|
| [A/B 테스트] “감”이 아니라 “증명”으로 만드는 방법 (0) | 2026.02.24 |
| [Storybook] 도입과 운영 기록 (0) | 2026.02.09 |
| FSD 아키텍처 적용기: 코드 위치를 설계하다 (0) | 2026.02.08 |
| Next.js App Router에서 BFF 레이어를 구성한 방법과 고민 (0) | 2026.01.25 |