개인 프로젝트 디자인 시스템 만들기: 시안에서 React 컴포넌트까지
개인 프로젝트가 늘면서 비슷한 화면을 여러 번 만들고 있었습니다. 메뉴와 검색창을 조금씩 고치다 보니 같은 버튼인데 높이가 다르고, 어떤 화면은 둥글고 어떤 화면은 각져 있었습니다. 라이트·다크 전환도 프로젝트마다 따로 손보고 있었습니다.
처음에는 공통 CSS를 몇 개 복사하면 될 것 같았습니다. 그런데 서비스가 늘어날수록 무엇을 공통으로 쓸지부터 정해야 했습니다. Company Lens의 가입자 수와 PogoAtlas의 포켓몬 타입은 같은 방식으로 색을 정할 수 없었고, 홈페이지와 데이터 검색 화면도 필요한 밀도가 달랐습니다.
그래서 먼저 화면 시안을 비교하고, Figma에서 색과 형태·컴포넌트를 정리한 뒤 코드로 옮겼습니다. 이후 실제 서비스에 적용하면서 메뉴와 데이터 표시, 차트 테마를 보완했습니다. 이 글은 그 과정과 지금 사용할 수 있는 React 패키지를 함께 정리한 기록입니다.
소스는 ersione-design-system 에 공개했습니다. 아래 API와 팔레트는 v0.5.1 기준입니다. 공개 쇼케이스 에서도 구성 요소를 볼 수 있습니다.
Figma 전에 비교한 HTML 시안
처음부터 Figma의 모든 컴포넌트를 만들지는 않았습니다. 몇 가지 HTML 시안을 놓고 글꼴과 색, 카드 모서리, 테두리와 그림자, 화면 밀도를 먼저 비교했습니다. 실제 크기의 글과 버튼이 들어간 화면을 봐야 마음에 드는 방향을 고르기 편했습니다.
여기서 확인한 것은 색 하나가 아니었습니다. 같은 색을 써도 제목이 너무 크거나 입력창의 모서리가 각져 있으면 다른 화면처럼 보였습니다. 반대로 모든 내용을 둥근 카드로 감싸면 구분은 쉬워져도 표와 본문을 읽는 공간이 줄었습니다.
최종 구성에는 Noto Sans KR과 블루·오렌지 브랜드, 라이트·다크 테마를 사용했습니다. 색뿐 아니라 간격과 컨트롤 높이, 모서리와 글자 크기를 함께 정리했습니다. 예를 들어 버튼은 S/M/L 높이를 32·40·48px로 두고, 간격은 4px 단위로 맞췄습니다. 모든 요소에 억지로 세 크기를 만드는 대신 실제로 크기 선택이 필요한 컨트롤에 적용했습니다.
디자인 토큰을 원본으로 관리
Figma와 CSS에서 같은 색을 각자 고치기 시작하면 다시 맞추는 일이 생깁니다. 코드 쪽 기준은 tokens/tokens.json에 두고, 여기서 CSS 변수를 생성하도록 했습니다.
tokens/tokens.json # 테마와 크기·색상 토큰 원본
tokens/tokens.css # 생성한 CSS 변수
styles.css # 공통 eds-* 클래스
src/react/ # React 컴포넌트와 타입
lib/react/ # 패키지 빌드 결과
showcase/ # 정적 UI 예제
examples/react/ # 실제 React 조합 예제
dist/ # 공개 쇼케이스 산출물같은 블루 브랜드에서도 밝은 화면과 어두운 화면에 같은 버튼색을 그대로 쓰지는 않았습니다. 심볼의 브랜드 원색과 흰 글자가 올라가는 버튼의 액션색도 용도가 달랐습니다.
| 테마 | 배경 | 본문 | 주요 동작 |
|---|---|---|---|
| blue-light | #f6f8fc | #182235 | #2563eb |
| orange-light | #f6f8fc | #182235 | #b84300 |
| blue-dark | #101620 | #edf2fa | #93b4ff |
| orange-dark | #101620 | #edf2fa | #ffb580 |
표의 값은 저장소의 토큰에서 가져왔습니다. 브랜드 색과 밝은·어두운 테마를 각각 선택할 수 있어 모두 네 가지 조합을 사용할 수 있습니다.
<div class="eds-theme" data-brand="blue" data-dark="false">
<!-- 이 안의 공통 UI가 같은 테마 변수를 사용합니다. -->
</div>이 구조에서는 개별 버튼이 다크 모드를 판단할 필요가 없습니다. 컴포넌트는 var(--accent), var(--surface) 같은 역할을 사용하고, 실제 값은 테마 루트가 제공합니다.
팔레트와 상태색의 구분
데이터를 보여주는 색은 blue·teal·orange·purple·rose·gray 여섯 가지로 만들었습니다. 각 색에는 기본색과 tint, 더 옅은 subtle을 뒀습니다. 큰 영역에 진한 색을 칠하기보다 아주 연한 배경 안에 연한 배지와 기본색 숫자를 조합하기 위한 구분입니다.
아래는 저장소의 토큰과 실제 React 컴포넌트로 만든 예제입니다. 브랜드와 다크 테마를 바꾸면 팔레트와 메뉴·버튼·수치 표시가 함께 바뀝니다. 표시한 숫자는 사용법을 설명하기 위한 가상 값입니다.
색을 공통으로 제공한다고 모든 서비스의 지표 의미까지 정하지는 않았습니다. Company Lens에서 가입자 수에 어떤 색을 쓸지는 앱이 정하고, PogoAtlas의 포켓몬 타입 고유색도 유지했습니다. 증가가 항상 좋은 상태인 것도 아니므로 양수·음수를 성공·오류 색과 같은 의미로 묶지 않았습니다.
삭제 버튼과 오류 메시지는 danger·error 같은 UI 역할을 사용합니다. 반면 데이터의 분류와 강조에는 팔레트를 선택합니다. 사용자가 값을 이해하는 데 필요한 단위와 라벨도 색과 함께 표시해야 합니다.
Figma에서 정리한 설계
Figma 파일은 Foundations, Components, Patterns & Applications로 나눴습니다. 첫 페이지에서 색과 타이포그래피·간격을 보고, 컴포넌트 페이지에서는 버튼과 입력·표·모달을 확인하고, 응용 페이지에서는 Company Lens의 검색과 결과·상세 화면에 조합했습니다.
단일 버튼만 볼 때 괜찮아 보여도 실제 검색 결과에서는 긴 한국어와 숫자, 단위가 겹칠 수 있었습니다. 실제로 큰 글자가 잘리거나 지표의 값과 보조 설명이 겹치는 문제가 남았습니다. 이 부분은 컴포넌트와 상위 레이아웃을 함께 고쳐야 하는 문제로 기록했습니다.
코드를 만들었다고 Figma 파일도 자동으로 최신이 되지는 않습니다. v0.5.1의 새 팔레트와 데이터 컴포넌트를 Figma에 반영하는 작업도 별도로 남아 있습니다. Figma 갱신 계획 에 그 차이와 수정 대상을 적어 두었습니다. 설계 파일, 코드 릴리스, 서비스 반영의 완료 상태를 각각 관리하는 이유입니다.
공통 CSS에서 React 컴포넌트로
초기 v0.1은 토큰과 명세, HTML 쇼케이스 중심이었습니다. 이후 공통 CSS를 eds-* 클래스에 두고, v0.2부터 React 컴포넌트와 타입을 제공했습니다. 색과 형태를 React 안에 다시 정의하는 대신 같은 CSS를 감싸는 구성입니다.
버튼은 네이티브 button을 사용합니다. loading일 때 비활성화하고 aria-busy를 설정하되, 어떤 요청을 보내고 어떤 문구를 보여줄지는 사용하는 앱이 정합니다. 기본 type은 button이어서 폼 제출 버튼에는 type="submit"을 명시합니다.
아래는 Button 소스 의 상태 처리 부분입니다.
export function Button({
variant = "primary", size = "md", loading = false,
disabled, type = "button", className, children, ...props
}: ButtonProps) {
return <button
{...props}
type={type}
disabled={disabled || loading}
aria-busy={loading || props["aria-busy"]}
className={cx("eds-button", `eds-button--${variant}`, `eds-control--${size}`, className)}
>{children}</button>;
}타입과 클래스 결합 함수는 원본 모듈에 있습니다. 여기서는 로딩·비활성 상태와 제출 방식이 어디에서 결정되는지 볼 수 있습니다. 버튼이 로딩 중인 이유와 실패 메시지까지 패키지에 넣지는 않았습니다.
입력과 선택도 네이티브 폼 속성을 전달하고, 표는 table·th·caption을 유지합니다. Tabs·Dialog·Toast처럼 상태와 브라우저 API가 필요한 컴포넌트는 Client Component로 분리했습니다. 기본 컴포넌트를 가져왔다는 이유로 패키지 전체가 클라이언트 코드가 되도록 만들지는 않았습니다.
설치와 컴포넌트 조합
v0.5.1은 npm 레지스트리에 게시한 버전이 아니라 GitHub 태그로 설치하는 패키지입니다. React 컴포넌트는 React 19를 대상으로 하고, 토큰과 CSS만 쓰는 경우에는 React 런타임이 필요하지 않습니다.
npm install github:ersione/ersione-design-system#v0.5.1Node.js 20.11 이상과 npm·Git이 필요합니다. Git 설치 시 prepare가 JavaScript와 타입 선언을 빌드합니다. 설치한 태그와 lockfile을 함께 고정해 서비스가 검토하지 않은 변경을 갑자기 받지 않게 했습니다.
다음은 공통 CSS와 메뉴, 데이터 표시를 조합하는 예입니다. 값과 지표의 뜻은 앱에서 전달합니다.
import '@ersione/design-system/styles.css';
import {
Navigation, NavigationLink,
DataPanel, DataBadge, DataHighlight,
} from '@ersione/design-system/react';
export function Overview() {
return (
<div className="eds-theme" data-brand="blue" data-dark="false">
<Navigation aria-label="주 메뉴">
<NavigationLink href="/" active>개요</NavigationLink>
<NavigationLink href="/settings">설정</NavigationLink>
</Navigation>
<DataPanel tone="purple">
<DataBadge tone="purple" label="가입자">
<DataHighlight tone="purple" size="lg" unit="명" secondary="+31.7%">
83
</DataHighlight>
</DataBadge>
</DataPanel>
</div>
);
}예제의 83명과 변화율은 가상 값입니다. 실제 앱에서는 기준월과 원본 값을 전달합니다. 글꼴 로딩과 전역 reset도 앱이 맡습니다. 이벤트 핸들러를 전달하거나 테마 선택 상태를 관리한다면 사용하는 화면을 Client Component로 구성해야 합니다.
서비스에 적용하면서 바꾼 부분
Company Lens에서는 필터를 바꿀 때마다 요청을 보내는 대신 버튼이나 Enter로 검색을 제출하도록 정리했습니다. 로딩 중 버튼 크기가 달라지는 부분과 중복 메뉴도 수정했습니다. 패키지의 버튼이 있어도 검색 시점은 앱의 사용 흐름에 맞춰 정해야 했습니다.
PogoAtlas에는 React 전환과 함께 공통 컴포넌트를 적용했습니다. 메뉴와 폼, 데이터 패널은 공유하지만 포켓몬 타입의 고유색과 지표 의미는 해당 서비스에 남겼습니다. 공통 UI를 적용하는 작업이 기존 데이터 해석까지 바꾸는 작업이 되지 않도록 했습니다.
차트는 라이브러리 자체를 패키지에 넣는 대신 readChartTheme·observeChartTheme로 색과 폰트, 선과 범례 설정을 읽게 했습니다. 차트의 축 단위와 숫자 포맷, 자료가 없는 구간을 어떻게 표시할지는 앱에서 결정합니다.
import { readChartTheme, observeChartTheme } from '@ersione/design-system/charts';
const root = document.querySelector('.eds-theme');
if (root) {
const initialTheme = readChartTheme(root);
console.log(initialTheme.palette.blue, initialTheme.series);
const stop = observeChartTheme(root, nextTheme => {
// 사용 중인 차트 라이브러리에 새 색상과 폰트 설정을 전달합니다.
console.log(nextTheme.palette, nextTheme.font);
});
// 이 화면을 해제하는 시점에 호출합니다.
// stop();
}초기 감시 로직은 Canvas 크기처럼 색과 관계없는 style 변경에도 반응할 수 있었습니다. 테마를 읽은 결과가 이전과 같은지 비교해 같은 설정으로 차트를 반복 갱신하는 일을 줄였습니다. 단순히 공통 코드를 설치하는 것 외에 앱에서 발생시키는 변경도 확인해야 했습니다.
릴리스와 서비스 반영
메뉴와 수치 표시, 팔레트 보완을 거쳐 첫 GitHub 릴리스를 v0.5.1로 정리했습니다. 이 버전에는 회색과 subtle 단계, DataPanel·DataBadge 조합도 포함했습니다. 변경 기록은 CHANGELOG 에 남겼습니다.
패키지를 수정한 뒤에는 사용하는 서비스에서 버전을 갱신하고 다시 빌드·검증·배포해야 합니다. GitHub 릴리스가 생성됐다는 것과 실제 운영 화면에 적용됐다는 것은 별도 작업입니다.
| 단계 | 확인할 내용 |
|---|---|
| 패키지 | 토큰 생성, 타입과 컴포넌트 명세, 테마별 화면 |
| 설치 | 지정한 태그와 lockfile, export 경로 |
| 서비스 | 폼 제출·라우팅·로딩·차트의 기존 동작 |
| 배포 | 빌드한 버전이 실제 서비스에 반영됐는지 |
| Figma | 코드와 다른 토큰·컴포넌트·레이아웃 수정 |
패키지에서 제공하는 것은 반복되는 UI의 기준과 구현입니다. Vue 전용 래퍼와 별도 바닐라 JavaScript 컨트롤러는 만들지 않았고, CSS만 가져왔다고 Tabs나 Dialog의 React 동작이 자동으로 붙지는 않습니다.
처음 시안을 비교할 때는 화면을 비슷하게 만드는 일이 중심이었습니다. 실제로 패키지를 적용하고 나서는 공유해도 되는 표현과 앱에 남겨야 하는 판단을 나누는 일이 더 많았습니다. 검색 시점과 데이터 의미까지 공통화하기보다, 같은 UI를 고칠 때 여러 프로젝트를 따로 수정하지 않도록 만드는 데 범위를 뒀습니다.
소스와 실행
git clone https://github.com/ersione/ersione-design-system.git
cd ersione-design-system
npm ci
npm run build:site
python3 -m http.server 8766 --bind 127.0.0.1 --directory disthttp://127.0.0.1:8766에서 쇼케이스를 열고, /examples/react/와 /docs/palette.html을 확인할 수 있습니다. 위 명령은 저장소 개발용 미리보기입니다. 사용하는 앱에 패키지를 설치하는 명령과는 목적이 다릅니다.