회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
AWS 이용 중이라면 최대 700만 원 지원받으세요
*FEConf2023에서 발표한 <크로스 플랫폼 디자인 시스템, 1.5년의 기록>를 정리한 글입니다. 발표 내용을 2회로 나누어 발행합니다. 1회에서는 디자인 시스템과 디자인 토큰에 대해 알아봅니다. 그리고 컴포넌트를 구성요소를 파악해 디자이너와 개발자의 의사소통 문제를 해결해 봅니다. 2회에서는 1회의 내용을 바탕으로 컴포넌트를 구현하고 API를 설계해 봅니다. 본문에 삽입된 이미지의 출처는 모두 이 콘텐츠와 같은 제목의 발표 자료로, 따로 출처를 표기하지 않았습니다. 발표 자료는 FEConf2023 홈페이지에서 다운로드할 수 있습니다.
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
*FEConf2023에서 발표한 <크로스 플랫폼 디자인 시스템, 1.5년의 기록>를 정리한 글입니다. 발표 내용을 2회로 나누어 발행합니다. 1회에서는 디자인 시스템과 디자인 토큰에 대해 알아봅니다. 그리고 컴포넌트를 구성요소를 파악해 디자이너와 개발자의 의사소통 문제를 해결해 봅니다. 2회에서는 1회의 내용을 바탕으로 컴포넌트를 구현하고 API를 설계해 봅니다. 본문에 삽입된 이미지의 출처는 모두 이 콘텐츠와 같은 제목의 발표 자료로, 따로 출처를 표기하지 않았습니다. 발표 자료는 FEConf2023 홈페이지에서 다운로드할 수 있습니다.
▶2024년 FEConf 라이트닝 토크 발표자 모집 중
FEConf2023에서 발표된 ‘크로스 플랫폼 디자인 시스템, 1.5년의 기록’/하태영 당근 프론트엔드 엔지니어
이번 글에서는 앞서 발행된 ‘크로스 플랫폼 디자인 시스템, 1.5년의 기록(1)’에서 살펴본 정의를 실제 컴포넌트 구현에 어떻게 적용할 수 있는지 알아보겠습니다. 이번 장의 목표는 아래와 같습니다.
1. 일관성과 유연성 모두 챙기는 API 구성
2. 크로스 플랫폼 지향적 패키지 설계
3. 컴포넌트 스펙을 코드에 반영하는 패턴 이해
앞서 살펴본 차크라와 스펙트럼의 API를 다시 한번 살펴보겠습니다. 제품 언어를 만드는 입장에서 일관성을 최우선으로 추구하여 오른쪽의 API처럼 간결하게 제공하고 싶은 것은 당연한 생각입니다. 그러나 일관성을 추구하는 형태로 제공함으로써 발생할 수 있는 모든 케이스의 90%를 커버할 수 있다고 하더라도, 사용자 입장에서는 커버하지 못하는 10%의 케이스로 인해 개발이 지연될 수 있습니다.
저는 일관성을 제공하는 기본 패키지와 유연성을 제공하는 합성 가능한 패키지를 분리하는 것으로 이 문제에 접근하고 있습니다.
실제 구현에서는 아래와 같이 패키지를 Core, Composable, Pre-Composed라는 세 가지 계층으로 분리합니다. 사전 조합(Pre-Composed) 된 컴포넌트를 이용해 일관성을 기본값으로 제공합니다. 그리고 Styled와 유사한 방식의 스타일링 프로퍼티를 제공합니다. 단, margin 등의 레이아웃에만 한정해서 제공합니다. 그 이상의 유연성이 필요한 경우에는 Composable 패키지를 사용하고, 그것을 사용하는 방식이 사전 조합된 컴포넌트를 사용하는 것에 비해 어렵지 않게 합니다.
위 그림에서 컴포넌트 기능의 코어에 해당하는 로직은 상태 차트와 Dom Binding으로 구성됩니다. 이번 글에서는 상태 차트의 활용은 다루지 않고 Dom Binding에 대해서만 살펴보겠습니다.
먼저 아래와 같은 요구 사항을 가진 컴포넌트가 필요하다고 가정하겠습니다. 4개의 구조를 가지고, 체크 여부를 나타내는 상태를 가집니다. 그리고 클릭을 하면 그 체크 여부가 전환이 되며, Dissabled가 주입되면 전환이 발생하지 않아야 하는 컴포넌트입니다.
1. 구조 - Root, Control, Input, Label
2. 상태 - isSelected : boolean
3. 상호작용 - click
4. 맥락 - isDisabled : boolean
Dom Binding을 구현하기 위해 먼저 구조에 대해 표현해 보겠습니다. 앞서 말한 4개의 구조를 아래와 같이 선언할 수 있습니다.
이 프로퍼티들은 최종적으로 JSX에 아래와 같이 스프레드 되어서 기능을 제공합니다.
상태를 Dom에 적용하는 것은 상태 차트로부터 전달받은 상태를 바탕으로 엘리먼트의 프로퍼티를 결정하는 것입니다. 아래의 경우에는 isSelected를 inputProps의 checked에 바인딩하는 것으로 구현할 수 있습니다.
상호작용은 이벤트 핸들러가 상태 차트에 이벤트를 전달하는 것으로 구현합니다. 아래의 경우에는 inputProps의 onChange가 발생하면 상태 차트에 ‘TOGGLE’이라는 이벤트를 전달합니다. 상태 차트는 ‘TOGGLE’이라는 이벤트와 isDisabled라는 맥락을 바탕으로 현재 상태가 바뀌어야 하는지 아닌지를 판단하여 갱신된 상태를 Dom Binding에 전달하게 됩니다.
맥락을 적용하는 것도 상태 적용과 마찬가지로 엘리먼트의 프로퍼티를 결정하는 것으로 구현됩니다. 둘의 차이는 상태 차트에서 제공하는 상태가 아닌 외부에서 주입한 프로퍼티를 그대로 사용한다는 것입니다. 아래 경우에는 inputProps의 disabled 프로퍼티에 외주에서 주입된 ctx의 isDisabled를 바인딩 하는 것으로 표현되고 있습니다.
이렇게 작성된 로직은 리액트 의존성이 없기 때문에 얇은 래퍼(thin-wrapper) 작성을 통해 리액트에 통합됩니다.
상태와 맥락에 대한 인터페이스를 각각 선언한 다음 이것을 extends 해서 상태 변화에 대한 모든 콜백을 추가할 수 있습니다. 이를 통해 헤드리스 체크박스 컴포넌트가 요구하는 모든 프로퍼티를 선언할 수 있습니다.
그리고 아래와 같이 이 프로퍼티를 받아서 상태 차트에 전달하고 그 결과를 다시 Dom Binding에 전달하고 그 결과를 바로 리턴하는 것만으로 리액트 래퍼를 구현할 수 있습니다. 실제 코드에서는 리액트와의 상태 동기화를 위해 useSyncExternalStore나 useEffect와 같은 몇 가지 기법들이 더 포함되어야 하지만 아래에서는 생략하여 표현하였습니다.
지금까지 기능 영역의 컴포넌트 구현에 대해 알아봤습니다. 이번에는 형태의 경우를 살펴보겠습니다. CSS와 자바스크립트 언어는 다르기 때문에 Hook처럼 래퍼를 작성하기는 어렵습니다. 자바스크립트에서 CSS를 바로 사용할 수는 없기 때문입니다. 대신 단일 스키마에서 CSS와 CSS in JS를 함께 생성하는 방식으로 무결성을 유지하고 있습니다.
이번에는 아래와 같은 요구사항의 컴포넌트가 있다고 가정하겠습니다.
1. 구조 - Root, Control, Icon, Label
2. 시각 옵션 - size = large, medium
3. 상태 옵션 - selected
4. 디자인 결정 - large일때, root height = 32px / medium일때, root height = 24px / selected일때, control 배경= primary
먼저 구조에 대해 표현해 보겠습니다. Dom binding과 유사하게 구조를 표현하는 것으로 시작합니다.
그리고 아래와 같이 Variants 표현식을 사용하여 시각 옵션을 표현할 수 있습니다. 아래의 경우에는 large 일 때 root의 height가 32px이 되고, medium 일 때 height는 24px이 된다고 표현하고 있습니다.
다음으로 상태 옵션은 데이터 어트리뷰트를 선택자로 사용하는 것으로 표현할 수 있습니다. 예를 들어서 아래와 같이 data-selected라는 데이터 어트리뷰트가 존재하면 컨트롤의 배경 색상(background)을 primary로 바꾸는 방식으로 구현할 수 있습니다. 그런데 이 data-selected라는 선택자는 HTML이 알아서 추가해 주지 않습니다. 그렇기 때문에 Dom binding에도 관련 코드를 다시 추가해야 합니다.
아래와 같이 Dom binding의 controlProps에 selected 여부를 데이터 어트리뷰트로 전달하는 로직이 추가됩니다. 이것은 상태 옵션이 공통 관심사였던 것을 생각하면 당연히 추가되어야 한다는 것을 알 수 있습니다.
이렇게 작성된 스키마를 바탕으로 아래와 같이 선택자로 사용될 클래스 명은 구조와 시각 옵션을 바탕으로 규칙에 따라 생성할 수 있습니다.
그리고 상태 옵션도 CSS의 네스팅 문법을 활용해 선택자로 생성할 수 있습니다.
마지막으로 생성된 CSS 코드에 대응되는 CSS in JS 코드를 함께 생성합니다. 아래 코드는 두 가지 파트로 구성되어 있습니다. 먼저 시각 옵션을 나타내는 인터페이스인 checkboxVariantProps와 시각 옵션을 Slot 별 클래스 네임으로 변환하는 함수를 함께 제공합니다. 예를 들어서 size가 large로 전달되면 시각 옵션 예제에서 작성한 ‘checkbox__root–size_large’와 같은 클래스 명을 반환하는 방식입니다.
최종적으로 이렇게 만들어진 기능과 형태 인터페이스를 다시 extends 하고 필요한 추가 프로퍼티를 받아서 체크박스의 컴포넌트 인터페이스를 완성합니다.
그리고 아래와 같이 합쳐진 프로퍼티를 통해 기능의 Hooks를 호출하고 형태의 클래스 네임 생성 함수를 각각 호출할 수 있습니다. 그리고 return 문 아래의 JSX를 살펴보면, 기능의 Hooks에서 얻어온 API와 형태에서 얻어온 클래스 네임들을 각각 JSX에 스프레드하고 클래스 네임에 바인딩 해서 기능과 형태를 합성합니다. 이 코드는 매우 단순하고 반복적이기 때문에 사용자가 Composable 패키지를 사용해서 다시 구현하는데 부담이 적습니다.
이렇게 본래 목표였던 사전 조합된 컴포넌트와 조합 가능한 패키지를 분리해서 일관성과 유연성을 모두 제공하는 API를 구성할 수 있습니다.
그리고 리액트 의존적인 코드는 모두 단순 바인딩으로 작성되었기 때문에 다른 프레임워크에서의 재사용성이 높아졌습니다.
더 나아가서는 컴포넌트를 정의한 방법에 따라서 컴포넌트가 렌더링 될 수 있는 모든 경우의 수를 사전에 계산할 수 있습니다. 이를 바탕으로 스냅샷 테스팅 및 QA 자동화도 가능합니다.
그리고 당근 팀에서는 아래와 같이 Figma variables를 사용해서 컴포넌트 스펙 문서를 피그마와 웹에 자동으로 생성하고 이를 컴포넌트 코드에 전부 동기화하는 방법도 실험하고 있습니다.
제가 지금까지 설명한 컴포넌트에 대한 접근과 구현은 모두 거인의 어깨 위에 서서 바라보며 얻어진 것들입니다. 특히 어도비 스펙트럼, Zag.js, Class Variance Authority의 영향을 크게 받았습니다. 혹시 디자인 시스템을 구축하려는 팀이나 깊게 이해하려고 하는 분들은 이 라이브러리들을 살펴보는 것을 추천드립니다.
오늘 다룬 주제들을 정리해 보면 다음과 같습니다.
1. 디자인 시스템의 목표 설정
2. 디자인 토큰의 정의와 활용
3. 아토믹 디자인이 혼란스러운 이유
4. 컴포넌트 구성 요소 해부
5. 디자이너와 개발자의 의사소통 문제 해결
6. 컴포넌트의 구현과 API 설계
이 내용을 바탕으로 제가 배운 점은 다음과 같습니다. 이 내용은 제가 새로 디자인 시스템을 만들 때 꼭 신경 쓸 부분이기 때문에 기억해 주시면 좋을 것 같습니다.
1. 디자인 시스템의 목표와 벤치마킹 대상 명확하게 설정하기
2. 디자인 의도를 인코딩할 때 섣부른 추상화 경계하기
3. 컴포넌트의 기능/형태를 분리해서 최소 단위 정의하기
4. 상태 압축으로 의사소통을 개선하고 상태 폭발 제거하기
5. 관심사 분리를 통한 의사소통의 순환 참조 제거하기
6. 유저가 컴포넌트를 직접 조립하기 쉬운 환경 제공하기
궁극적으로 위 내용을 바탕으로 일관성과 유연성을 동시에 달성하는 것이 제가 지금까지 디자인 시스템을 만들며 배운 것들입니다. 이 글을 통해서 확신을 가지고 행복하게 디자인 시스템을 만드는 개발자가 더 많아지길 바라며 글을 마칩니다. 감사합니다.
요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.