요즘 회사에서 디자이너가 피그마에 만들어 둔 디자인 시스템을 공통 UI 패키지의 컴포넌트 라이브러리로 옮기는 일을 하고 있다.
처음엔 "그냥 피그마 보고 똑같이 만들면 되는 거 아냐?" 싶었는데, 막상 해보니 그게 함정이었다. 디자이너가 도구(피그마) 안에서 표현한 구조를 그대로 코드로 박제하면 오히려 쓰기 나쁜 컴포넌트가 나온다. 이 과정에서 아토믹 디자인이라는 개념, 디자이너가 쓰는 언어, 그리고 그 둘을 코드로 번역하는 감각을 꽤 배웠다.
공부도 할 겸, 배운 걸 세 가지 관점으로 정리해 본다.
- 디자이너의 언어 — 아토믹 디자인과 디자인 시스템, 그리고 피그마 용어들
- 개발자의 번역 — 디자인 시스템을 컴포넌트로 옮길 때 주의할 점
- 둘 사이의 약속 — id로 소통하는 방법
아토믹 디자인이 뭔데?
아토믹 디자인(Atomic Design)은 Brad Frost가 정리한 UI 설계 방법론이다. 화학에서 원자(atom)가 모여 분자(molecule)가 되고, 분자가 모여 유기체(organism)가 되는 것처럼, 작은 UI 조각부터 조합해서 큰 화면을 만든다는 사고방식이다. (위 그림이 그 흐름이다.)
단계는 다섯 개다.
- Atoms(원자) — 더 쪼갤 수 없는 최소 단위. 버튼, 인풋, 라벨, 아이콘 같은 것들.
- Molecules(분자) — 원자 몇 개를 묶은 작은 기능 단위. 예를 들어
라벨 + 인풋 + 버튼을 묶은 검색바. - Organisms(유기체) — 분자와 원자가 모인, 화면에서 독립적으로 의미를 갖는 덩어리. 헤더, 상품 카드 리스트, 폼 전체 같은 것들.
- Templates(템플릿) — 데이터 없이 유기체들을 배치한 화면의 뼈대(레이아웃).
- Pages(페이지) — 템플릿에 실제 데이터를 채운 최종 화면.
핵심은 재사용이다. 원자를 잘 만들어 두면 분자·유기체를 조립하는 비용이 줄고, 전체 UI가 일관돼진다.
사실 나도 예전에 개인 프로젝트에서 atoms / molecules / organisms 폴더를 나눠서 "아토믹 디자인 해봤다" 한 적이 있다. 그런데 그건 그냥 폴더 이름만 아토믹이었다. 진짜 어려운 건 폴더를 나누는 게 아니라, 디자이너가 이 언어로 설계한 결과물을 코드로 옮기는 순간에 있었다.
1. 디자이너의 언어를 이해하기
디자이너는 아토믹 디자인을 머릿속 개념으로만 두지 않는다. 피그마 안에서 실제 디자인 시스템으로 구축한다. 그래서 개발자가 협업하려면, 디자이너가 피그마에서 쓰는 용어를 알아야 한다. 다행히 대부분 우리가 아는 코드 개념과 1:1로 대응된다.
컴포넌트(Component)와 인스턴스(Instance)
피그마의 메인 컴포넌트(◆) 는 React로 치면 컴포넌트 정의다. 그리고 그걸 화면에 가져다 쓴 인스턴스(◇) 는 컴포넌트를 렌더한 사용처다.
메인 컴포넌트를 수정하면 모든 인스턴스에 변경이 전파된다. React 컴포넌트를 고치면 그걸 쓰는 모든 곳이 바뀌는 것과 똑같다.
피그마의 메인 컴포넌트 = 코드의 컴포넌트 정의, 인스턴스 = 사용처. 이 대응을 기억하면 디자이너와 "이건 컴포넌트로 뺄까요, 그냥 한 번 쓰고 말까요" 같은 대화를 같은 언어로 할 수 있다.
Variant(변형)
하나의 컴포넌트가 갖는 상태나 종류다. 버튼이라면 filled / outline / ghost, sm / md / lg 같은 것들. 코드의 props와 정확히 대응한다. 피그마의 variant 축이 곧 컴포넌트의 prop 설계 힌트가 된다.
오토 레이아웃(Auto Layout)
이건 사실상 Flexbox다. 방향(가로/세로), 간격(gap), 안쪽 여백(padding), 정렬(align/justify)을 지정하는데, CSS flex와 거의 그대로 매핑된다. 디자이너가 "여기 오토 레이아웃 걸어놨어요" 하면 개발자 머릿속에선 display: flex가 켜지면 된다.
플레이그라운드(Playground)와 Dev Mode
피그마의 개발자 모드에는 컴포넌트 플레이그라운드가 있다. variant를 토글하면서 컴포넌트가 어떤 상태·조합을 가질 수 있는지 코드를 짜기 전에 미리 확인할 수 있다. 어떤 prop 조합이 존재하는지 여기서 먼저 파악하면 헛구현을 줄일 수 있다.
한 가지 더, 디자이너가 정해 둔 ready for dev(개발 준비 완료) 표시도 중요하다. 피그마 파일에는 아이디어 스케치와 확정된 스펙이 섞여 있어서, "화면에 그려진 모든 UI가 최신 스펙"이 아니다. 개발이 믿어야 하는 건 ready for dev로 표시된 영역뿐이다.
정리하면, 이런 매핑이 머릿속에 있으면 협업이 훨씬 매끄러워진다.
| 피그마(디자이너) | 코드(개발자) |
|---|---|
| 메인 컴포넌트(◆) | 컴포넌트 정의 |
| 인스턴스(◇) | 컴포넌트 사용처 |
| Variant | props |
| Auto Layout | Flexbox |
| Foundation(토큰) | 시맨틱/스케일 토큰 |
| ready for dev | "이게 진짜 스펙" 신호 |
2. 개발자는 이걸 곧이곧대로 옮기면 안 된다
여기가 이 글에서 제일 하고 싶은 얘기다.
피그마의 구조는 디자인 도구의 사정이지 코드의 사정이 아니다. 피그마는 "조합"을 표현할 수단이 컴포넌트와 variant밖에 없어서, 코드였다면 굳이 안 나눴을 것들을 어쩔 수 없이 variant나 별도 요소로 만들어 둔다. 그걸 그대로 코드로 박제하면 오히려 이상한 API가 나온다.
예: variant를 그대로 옮기면 안 된다
디자이너가 헤더의 오른쪽 액션 개수를 표현하려고 피그마에 count=1 / count=2 / count=3 variant를 만들어 뒀다고 하자. 이걸 그대로 옮기면 이렇게 된다.
// ❌ 피그마 variant를 그대로 박제한 코드
<Header actionType="count-2" />
<Header actionType="count-3" />하지만 코드에서 "개수"는 variant로 만들 이유가 없다. 그냥 children의 개수다.
// ✅ 코드다운 표현 — 개수는 그냥 children
<Header title="프로필" actions={[<IconButton />, <IconButton />]} />예: Organism의 기본형을 그대로 박제하지 않는다
Organism(예: Header)은 피그마에 action-item, slot 같은 세부 파트(Parts)로 쪼개져 있고, 딱 정해진 "기본형" 모양이 있다. 그렇다고 그 파트들을 전부 독립 컴포넌트로 export 하면 안 된다. 그것들은 Header 내부의 합성 요소일 뿐이고, 개발자에게는 조립된 <Header /> 하나만 노출하는 게 맞다.
디자인 가이드에 적힌 규칙(예: "우측 액션은 아이콘 최대 3개 / 버튼 최대 2개")도 마찬가지다. 문서로만 두면 아무도 안 지킨다. 이건 TypeScript 타입이나 개발 모드 경고로 인코딩해서 코드가 규칙을 강제하게 만들어야 한다.
피그마는 "무엇을 보여줄지"를 그리는 도구고, 코드는 "어떻게 조합할지"를 정의하는 곳이다. 피그마의 표현 수단(variant, Parts)을 구조까지 그대로 옮기면, 디자인 도구의 한계를 코드가 그대로 물려받는다.
DS 컴포넌트를 어떻게 끼워 넣을까 — 주입 vs 키워드
이게 개인적으로 제일 어려운 설계 지점이었다. 디자이너는 디자인 시스템에 버튼을 종류별로 잔뜩 만들어 둔다. 그리고 카드·헤더 같은 조합 컴포넌트는 그 안에 이 DS 버튼을 품는다. 문제는 "어떻게 품게 할지"를 개발자가 API로 정해야 한다는 거다. 크게 두 갈래가 있다.
① 컴포넌트 자체를 주입받기 (slot / children)
// 사용처가 원하는 버튼을 직접 넣는다
<ProfileHeaderCard action={<Button intent="brand">팔로우</Button>} />- 장점: 유연하다. 어떤 버튼이든, 몇 개든, 필요하면 커스텀까지 넣을 수 있다.
- 단점: 통제가 안 된다. 사용처마다 제각각 버튼을 넣어서 "이 자리엔 브랜드 버튼 하나"라는 디자인 규칙이 쉽게 깨진다.
② 약속된 키워드를 받아 내부에서 매핑하기
// 정해진 키워드만 받고, 어떤 버튼을 렌더할지는 컴포넌트가 안다
<ProfileHeaderCard action="follow" />- 장점: 통제와 일관성. 디자인 규칙을 컴포넌트가 강제하고, 사용처는 단순해진다.
- 단점: 경직된다. 새 케이스가 생길 때마다 컴포넌트를 열어 키워드를 추가해야 한다.
내가 세운 기준은 이렇다.
- 그 자리의 변화 축이 **"정해진 몇 가지 상태"**라면 → 키워드(또는 variant)로. 디자인이 통제하고 싶은 자리다.
- 그 자리의 변화 축이 **"자유로운 조합"**이라면 → 주입(slot/children)으로. 사용처가 주도해야 하는 자리다.
- 절충안으로 제한된 슬롯도 있다. children을 받되 타입으로 허용 컴포넌트를 좁히거나, 개발 모드 경고로 "여기엔 Button만" 같은 규칙을 강제하는 방식이다.
"컴포넌트를 넣을까, 키워드를 넣을까?"보다 먼저 물어야 할 건 "이 자리에서 실제로 바뀌는 축이 유한한 상태인가, 자유로운 조합인가?"다. 유연성과 통제는 트레이드오프고, 한번 정하면 수많은 사용처에 영향을 줘서 바꾸기 비싸다. 그래서 코드를 짜기 전에 변화 축부터 정의하는 게 핵심이다.
그럼 뭘 따라가고 뭘 안 따라가나
내가 정리한 기준은 이거다.
- 분류와 네이밍은 1:1로 따라간다. Storybook 제목을
Organisms/Header로, variant 이름과 토큰 이름을 디자이너가 쓰는 그대로 맞춘다. 그래야 디자이너와 같은 지도를 보고 대화할 수 있다. - 폴더 구조와 내부 구현은 코드 사정에 맞춘다. 피그마 파트를 그대로 파일로 나누지 않고, 재사용·타입 안정성·API 편의성 기준으로 설계한다.
한 문장으로 줄이면, 분류는 디자이너의 언어로, 구조는 코드의 언어로다.
컴포넌트보다 토큰 검증이 먼저
하나 더. 컴포넌트를 만들기 전에 Foundation 토큰(색상·타이포·간격·라운딩)부터 코드와 대조하는 게 좋다. 나는 실제로 컴포넌트 작업 전에 피그마 토큰을 뽑아 코드의 값과 전수 비교했는데, 그 과정에서 불일치가 여러 건 나왔다. 만약 검증 없이 컴포넌트부터 만들었다면, 잘못된 행간 하나가 수백 개 파일로 전파됐을 것이다.
그리고 이때 색은 hex를 하드코딩하지 않고 전부 시맨틱 토큰으로 처리한다.
// variant × intent × size를 피그마 variant 축과 1:1로 맞추고,
// 색은 전부 시맨틱 토큰 클래스로 (하드코딩 hex 0개)
const button = cva("...base...", {
variants: {
variant: { filled: "...", outline: "...", ghost: "..." },
intent: { brand: "bg-surface-brand text-content-onBrand", danger: "..." },
size: { sm: "...", md: "...", lg: "..." },
},
});피그마 변수(토큰)를 추출해 코드와 자동으로 대조하면, 사람이 눈으로 비교하는 것보다 훨씬 빠르고 정확하다. "Foundation은 최신이니 믿으세요"라는 말도, 믿되 검증하고 나서 신뢰하는 게 낫다.
3. 디자이너와 개발자의 약속: id로 소통하기
마지막은 소통 규약이다. 이게 실제로 협업을 굴러가게 만든 핵심이었다.
흐름은 이렇다.
디자이너가 DS 컴포넌트를 조합해 id를 부여하고, 개발자가 그 id를 컴포넌트 이름으로 삼아 DS 컴포넌트로 조립하는 흐름
- 디자이너는 디자인 시스템 컴포넌트들(Atoms/Molecules/Organisms)을 조합해서 특정 컴포넌트를 만들고, 거기에 id(이름) 를 부여한다.
- 개발자는 그 id를 그대로 컴포넌트 이름으로 삼아, 디자인 시스템 컴포넌트들로 조립한다.
즉 디자이너가 만든 "조합" 자체가 코드 컴포넌트의 명세가 된다. id가 곧 계약인 셈이다.
예를 들어 디자이너가 ProfileHeaderCard라는 id로 Avatar + Text + Button(전부 DS 컴포넌트)을 조합해 두면, 개발자는 같은 이름의 컴포넌트를 만들어 안에서 DS 컴포넌트들을 조립한다.
// 디자이너가 만든 조합 컴포넌트 id: "ProfileHeaderCard"
// → 같은 이름의 코드 컴포넌트로, DS 컴포넌트들을 조립한다
function ProfileHeaderCard({ user }: Props) {
return (
<Card>
<Avatar src={user.avatar} />
<Text variant="title">{user.name}</Text>
<Button intent="brand">팔로우</Button>
</Card>
);
}이 약속이 있으면 디자이너와 개발자가 같은 이름으로 같은 것을 가리키게 된다. "그 프로필 카드요" 한마디로 서로 무엇을 말하는지 안다. 변경 추적도 쉬워진다. 디자이너가 ProfileHeaderCard를 바꾸면 개발자는 어떤 코드 컴포넌트를 손봐야 하는지 바로 안다.
id를 코드 컴포넌트 이름으로 쓰되, 그 내부 구현은 여전히 2번 원칙을 지킨다. 디자이너가 피그마에서 조립한 모양을 곧이곧대로 박제하는 게 아니라, DS 컴포넌트를 코드답게 조립하는 것이다. 이름은 계약, 구현은 코드의 몫.
그럼 id는 어느 레벨에 붙나?
여기서 헷갈리기 쉬운 게 하나 있다. 아토믹 레벨(원자·분자·유기체·템플릿·페이지)과 id는 서로 다른 축이라는 점이다. id는 "재사용할 조합"에 붙는 이름이지, "데이터가 들어갔는지"와는 상관이 없다. 그리고 데이터를 넣는 건 그 컴포넌트를 새로 만드는 게 아니라 "사용"하는 것(props로 흘려보내는 인스턴스화)이다.
- 유기체(Organism): 그 자체가 이미 이름(id)을 가진 재사용 컴포넌트다.
ProfileHeaderCard에user데이터를 넣는 건 새 컴포넌트를 만드는 게 아니라 그냥 사용하는 것이다. 데이터를 넣는다고 id가 새로 생기지 않는다. - 페이지(Page): 보통 특정 라우트에 실제 데이터를 꽂은 일회성 화면이다. 그래서 재사용 컴포넌트로서의 id를 따로 붙이지 않는다 — 라우트(경로)가 곧 정체성이기 때문이다. 페이지는 id를 부여받는 대상이라기보다, 여러 유기체를 배치하고 진짜 데이터를 흘려보내는 조립 장소에 가깝다.
- 템플릿(Template): 데이터 없는 뼈대라, 그 뼈대가 여러 페이지에서 반복된다면 그때는 컴포넌트(id)가 될 수 있다.
정리하면, id는 "이 조합을 재사용하겠다"는 선언이고, 주로 분자·유기체(가끔 템플릿)처럼 반복되는 단위에 붙는다. "유기체에 데이터를 넣으면 새 컴포넌트인가?"에 대한 답은 아니오 — 이미 id를 가진 유기체를 그냥 쓰는 것이고, "페이지에 id를 붙이나?"도 대체로 아니오 — 페이지의 정체성은 라우트다.
여기서 한 가지 함정도 있었다. 디자이너가 실사용 예시를 인스턴스가 아니라 손으로 그린 프레임으로 만들어 두면, 나중에 메인 컴포넌트를 수정해도 그 예시엔 반영되지 않는다. 실제로 패딩 값이 어긋난 걸 발견하고 디자이너에게 "이건 인스턴스로 만들어 주세요"라고 제안했던 적이 있다. 피그마 인스턴스 = 코드 컴포넌트 재사용이라는 개념이 여기서도 똑같이 통했다.
마치며
이 일을 하기 전엔 아토믹 디자인이 그냥 "폴더 나누는 규칙" 정도인 줄 알았다. 그런데 실제로는 디자이너와 개발자가 공유하는 공통 언어에 가까웠다.
배운 걸 세 줄로 요약하면,
- 디자이너의 언어(아토믹 디자인, 피그마 용어)를 이해하면 협업 비용이 확 준다. 대부분 코드 개념과 1:1로 대응된다.
- 그렇다고 피그마 구조를 곧이곧대로 코드로 옮기면 안 된다. 분류·네이밍은 따라가되, 구조는 코드답게.
- 디자이너가 조합에 부여한 id를 계약으로 삼으면, 같은 이름으로 같은 것을 가리키며 소통할 수 있다.
제일 크게 남은 배움은 이거다. 디자이너의 일하는 방식을 배우되, 그 방식을 전부 코드화하지는 않는다. 좋은 컴포넌트는 디자이너의 의도를 존중하면서도, 코드로서 쓰기 좋아야 한다.
