디자인 시스템 재설계

달리는 서비스에 컴포넌트 갈아끼우기

by 나우naw

시작하며

SaaS 프로젝트에서 2년동안 고수하던 디자인시스템의 체계를 재설계하는 내용을 담았습니다.

회사마다 프로젝트마다 도메인이 다르고 개발 방식이 다르기 때문에 정보를 찾아봐도 저희 디자인시스템에 꼭 맞는 것이 없었습니다. 수많은 논의와 시도 끝에 아래 방법대로 시도해보고 있습니다.


+ 최근 Figma에서 진행한 Design System 웨비나에서 Slot 기능이 3~4월 중에 업데이트된다는 소식을 들었습니다. FE개발자와 이야기를 나누며 우리만의 고민이 아니었구나 생각했죠. 추후 Slot 기능을 활용해 개선을 이어나갈 예정입니다.





왜 재설계하는가?


문제는 컴포넌트로 커버하지 못하는 예외 케이스들이 생겨난다는 것입니다.

서비스에서는 더 고도화된 기능들이 생겨나게 되고,

그러면서 기존 컴포넌트 규칙을 벗어난 예외 케이스가 생깁니다.

Property나 Variant로 나누기엔 애매한 분기가 많아져 괴물 컴포넌트가 됩니다,

그렇다고 새로운 컴포넌트로 추가하기에는 비슷한 것이 여러개 생겨나버립니다.

이 같은 결정들은 모두 관리 포인트가 늘어나 생산성, 일관성을 해칠 수 있습니다.

결국 인스턴스를 detach하거나 우회적인 방법을 써야 했습니다.

이같은 고민은 반복되기만 하고 시원하게 해결되지 않았습니다.


우리에게 필요한 디자인시스템은

관리 포인트가 중앙화되어 일관성이 유지되어야 하고,

예외가 있어도 시스템 안에서 제어될 수 있어야 합니다.






기반이 되는 컴포넌트는 순수성을 유지하고,

예외를 부담없이 추가할 수 있는 구조로.


원자의 순수성

우선, 기반이 되는 컴포넌트는 그 자체로 순수성을 유지해야 하는 것이 핵심입니다.

UI의 최소 단위이자, 탑 다운으로 상속되는 위계에서 가장 상위에 위치합니다.

우리는 이를 Base라고 정의했습니다.

Base는 예외가 있다고 해서 쉽게 추가될 수 없습니다.

예외는 반드시 그 아래에서 처리되어야 합니다.

기반이 되는 Base라는 틀에서 언제든 예외 케이스를 대응하려면

어떤 게 들어갈 지 나중에 결정할 수 있는 [ Blank ]가 필요했습니다.


Child 패턴을 이용

이를 위해 코드의 'Child' 패턴을 이용하기로 했습니다.

Base 컴포넌트에 Slot을 비워두고, 그 안에서 A,B,C...무엇이든 넣을 수 있게 됩니다.

단, 도구상 한계가 있습니다.

코드에서는 [ Blank ] 안에 A,B,C...무엇이든, A+B+C...몇개든 넣을 수 있거든요.

그런데 Figma에서는 Slot에 다른 컴포넌트를 1:1 대치(Swap)하는 것만 가능합니다.

그래서 A,B,C를 미리 BaseTemplate이라는 컴포넌트로 만들고, Base의 Slot에 1:1 Swap 하는 형태로 위계를 구성했습니다.

*여기에서 Figma 도구의 한계 때문에 이 방식을 선택했는데, 다음 업데이트에서 Slot이라는 기능을 이용하면 Swap이 아닌 진정한 Child의 형태로 뭐든, 몇개든 추가하는 빈 공간으로 사용할 수 있습니다. 심지어 요소끼리 Auto Layout도 된대요!!! 추후 이 기능으로 찐 Child형태로 개선하면 코드 - 디자인 구조가 거의 일치될 것 같아요.








컴포넌트 설계와 폴더구조


설계

1. Base UI의 최소 단위 (원자 컴포넌트)

내부에서 다른 컴포넌트를 직접 import하지 않는 '순수성'을 유지합니다.

Plat 패턴 단순 완제품이 필요할 때 (슬롯이나 컴파운드 없이 그대로)

2. Basetemplate Base를 조립하여 만드는 복합 컴포넌트.

내용, 구조적 변형 등 상황에 맞게 패턴을 선택합니다.

Slot 패턴 특정 컴포넌트를 1:1로 대치. 내용적 변형이 필요할 때 사용

Compound 패턴 독립적인 하위 조각이며, 자유롭게 쌓아 조합. 구조적 변형이 필요할 때 사용

Combination 패턴 하나의 컨텍스트를 가진 최종 조합 컴포넌트. 새로운 state 부여 가능


*Local (Domain, Product Level) 에서는..

Combination 패턴의 BaseTemplate 사용할 것을 첫번째로 권장합니다.

Base를 직접 가져와 조립하여 사용할 수 있습니다.

비즈니스 로직을 부여한 로컬 컴포넌트로 가공하여 사용할 수 있습니다. (유기체, 템플릿 등)



폴더구조

원자별, 그룹별 등 다양한 방법들이 나왔습니다.

핵심 기준은 context를 어디에 둘 것인가 였습니다.

핵심 원칙인 base의 순수성을 유지할 수 있어야 하고,

DX를 해치지 않도록 쉬운 사용을 위해 3번 방식을 선택했스습니다.

원자는 Base 그룹 / 분자들은 Basetemplate 그룹으로 나누면서도,

재료들과 조합이 한눈에 가장 잘 들어오는 구조였습니다.



1. Base / Template / Combination 따로따로 두는 구조

: 큰 카테고리로 묶어서 볼 수 없어서 쓰기 불편하다

/base베이스

/A

/basetemplate템플릿

/A

/slot슬롯

/a1

/a2

/combination조합

/A

/Aa1

/Aa2


2. Base에 원자별로 Slot을 두고, Basetamplate에는 Combination

: base는 다른 컴포넌트를 import 하면 안되는데, slot이 base들의 조합이라 순수성이 깨진다.

/base베이스

/A

/slot슬롯

/a1

/a2


/basetemplate템플릿

/A

/combination조합

/Aa1

/Aa2


3. Base에는 원자만, Basetemplate에는 분자만

: Base 폴더를 깔끔하게 유지하고, BaseTemplate에서 큰 맥락으로 묶어 (slot)재료와 (combination)완성본을 함께 보는 것이 편리하다고 판단했다.

/base베이스

/A


/basetemplate템플릿

/A

/slot슬롯

/a1

/a2

/combination조합

/Aa1

/Aa2


3번 예시

1. Slot 패턴을 사용한 컴포넌트

/base (순수한 기본상태)

/ListItem

/basetemplate(레시피)

/ListItem

/slot(재료)

/Menu

/Tree

/combination(완성!)

/MenuListItem --------*ListItem의 Slot에 Menu를 Swap

/TreeListItem --------*ListItem의 Slot에 Tree를 Swap


2. Compaound 패턴을 사용한 컴포넌트

/basetemplate

/Form

/combination

/TextField --------*label + input + helpertext 조각을 조합

/ButtonField --------*label + button 조각을 조합

/Autocomplete







실제 Figma 에서 만들어보며

실제로 저 구조를 만들어보니 여러 에피소드들이 있었어요,

이에 대해 이야기해보겠습니다.



01 Slot에 아무거나? 선호하는 것들만? Swap

맥락 슬롯, 자유 슬롯에 대해서


ListItem에 slot을 비워두고,

nav, menu 같은 미리 정해진 변형을 넣고 싶었습니다만

어떤 Base든 자유롭게 넣을 수 있을 수도 있어야 했습니다.


Figma의 선호하는 인스턴스 기능을 이용해서 선호를 지정해놓되,

다른 컴포넌트로 스왑해서 사용할 수 있게 했습니다.

즉, 아무거나 넣을 수도 있고, 맥락을 정해둘 수도 있었습니다.


Figma │ 자유 슬롯만 표현, 맥락 슬롯을 쓰고 싶으면 Preference (선호)기능을 사용해두기

폴더 구조 │ 맥락별 조합을 검색 가능하게 정리

가이드 문서 │ 어떤 맥락에서 어떤 조합을 쓰는지 기록




02 Nested Props가 전부 열려버리는 문제

Figma 기능상 한계에 대해서


Combination 컴포넌트에서 icon만 변경 가능하게 열어주고 싶었는데,

Figma에서 nested를 열면 child compoent의 모든 props가 한꺼번에 노출됩니다.

부분적으로만 열 수가 없거든요.

토큰(텍스트 스타일 등)으로 해결할 수 있는 것은 컴포넌트로 감싸지 않는 편이 안전합니다.

그래도 컴포넌트를 여러번 감싸 컴포넌트로 다시 만든다면,

그리고 감싼 안쪽의 컴포넌트의 설정을 표면에 열어줘야 한다면

다 열린 채로 쓸수 밖에 없죠..

단, 컴포넌트 설명에 건들지 말아야 할 프롭스에 대해 명시해주는 것이 중요합니다.




03 TextField를 만들 때는 Compound 패턴이 딱임

TextField의 구조를 결정할 차례였습니다.

라벨 + 인풋 + 헬퍼텍스트가 기본 구조인데,

헬퍼텍스트 자리에 컬러 선택창이나 체크박스가 들어갈 수도 있었습니다.

슬롯 베이스 방식으로는 구조가 고정되어 이런 변형에 대응할 수 없었거든요.


결론은 Compound 패턴이었습니다.

compound1 + compound2 + compound3 형태로 자유롭게 재조합이 가능한 구조입니다.

이렇게 베이스 컴파운드들을 조합하는 방식을 Combination이라고 이름 붙였습니다.




04 "Slot"이라는 이름을 "Swap"으로

용어가 코드와 충돌


"slot"이라는 이름이 Web Components의 <slot> 프롭스와 충돌할 수 있었거든요.

swap, override, inject, render, custom, as — 여러 후보 중 swap을 선택했습니다.

"이 자리에 다른 컴포넌트를 바꿔 넣는다"는 의미가 직관적이고,

HTML 표준과 충돌도 없습니다.


<TextField

swaps={{ input: CustomInput, label: CustomLabel }}

swapProps={{ input: { ... } }}

/>





위에서 설명한 것 외에도 여러 가지가 달라졌습니다.

- 위계 : 3단계 (Base → BaseTemplate → Context) ==> 2단계 (Base → BaseTemplate)

- 패턴 : 4가지 (Flat, Slot, Compound, Render Props) ==> 5가지 (+Combination 추가)

- Context의 역할 : 별도 위계 ==> BaseTemplate의 /combination으로 흡수

- Slot 용어 : "slot" ==> "swap" (Web Components 충돌 회피)


처음의 이론적 구조가 Figma 실습과 실제 사용 시나리오를 거치면서

더 단순하고 실용적인 형태로 수렴한 과정이었습니다.






돌아보며


반복적으로 나타난 패턴이 있습니다.

"Figma에서 안 되는 것"을 만날 때마다, Figma 바깥에서 해결하는 방법을 찾았습니다.

폴더 구조로, 문서로, 코드로.


Figma는 디자인 도구이지 디자인 시스템의 전부가 아닙니다.

도구의 한계를 시스템의 한계로 만들지 않는 것 —

이것이 이번 여정에서 얻은 가장 큰 교훈입니다.



***

3월 5일 드디어 figma 에 slot 기능이 업데이트됩니다.

Slot 기능이 도입되면 이 문제들이 상당 부분 해소될 것으로 기대합니다.

- 과도한 Variant 문제 — 슬롯 영역 콘텐츠 조합을 모두 Variant로 만들면 수십 개가 됩니다

- 디자인-코드 구조 불일치 — 코드에서는 이미 children, startAdornment, endAdornment 등 슬롯 패턴을 쓰는데 Figma에서 표현할 방법이 없습니다

- 인스턴스 detach 문제 — 디자이너가 컴포넌트를 조합할 때 인스턴스를 깨뜨려야 하는 상황이 발생합니다

작가의 이전글UX 디자이너에게 '결과'란 무엇일까?