달리는 서비스에 컴포넌트 갈아끼우기
시작하며
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
실제로 저 구조를 만들어보니 여러 에피소드들이 있었어요,
이에 대해 이야기해보겠습니다.
맥락 슬롯, 자유 슬롯에 대해서
ListItem에 slot을 비워두고,
nav, menu 같은 미리 정해진 변형을 넣고 싶었습니다만
어떤 Base든 자유롭게 넣을 수 있을 수도 있어야 했습니다.
Figma의 선호하는 인스턴스 기능을 이용해서 선호를 지정해놓되,
다른 컴포넌트로 스왑해서 사용할 수 있게 했습니다.
즉, 아무거나 넣을 수도 있고, 맥락을 정해둘 수도 있었습니다.
Figma │ 자유 슬롯만 표현, 맥락 슬롯을 쓰고 싶으면 Preference (선호)기능을 사용해두기
폴더 구조 │ 맥락별 조합을 검색 가능하게 정리
가이드 문서 │ 어떤 맥락에서 어떤 조합을 쓰는지 기록
Figma 기능상 한계에 대해서
Combination 컴포넌트에서 icon만 변경 가능하게 열어주고 싶었는데,
Figma에서 nested를 열면 child compoent의 모든 props가 한꺼번에 노출됩니다.
부분적으로만 열 수가 없거든요.
토큰(텍스트 스타일 등)으로 해결할 수 있는 것은 컴포넌트로 감싸지 않는 편이 안전합니다.
그래도 컴포넌트를 여러번 감싸 컴포넌트로 다시 만든다면,
그리고 감싼 안쪽의 컴포넌트의 설정을 표면에 열어줘야 한다면
다 열린 채로 쓸수 밖에 없죠..
단, 컴포넌트 설명에 건들지 말아야 할 프롭스에 대해 명시해주는 것이 중요합니다.
TextField의 구조를 결정할 차례였습니다.
라벨 + 인풋 + 헬퍼텍스트가 기본 구조인데,
헬퍼텍스트 자리에 컬러 선택창이나 체크박스가 들어갈 수도 있었습니다.
슬롯 베이스 방식으로는 구조가 고정되어 이런 변형에 대응할 수 없었거든요.
결론은 Compound 패턴이었습니다.
compound1 + compound2 + compound3 형태로 자유롭게 재조합이 가능한 구조입니다.
이렇게 베이스 컴파운드들을 조합하는 방식을 Combination이라고 이름 붙였습니다.
"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 문제 — 디자이너가 컴포넌트를 조합할 때 인스턴스를 깨뜨려야 하는 상황이 발생합니다