Skip to main content

크로스 플랫폼 디자인 시스템, 1.5년의 기록 (1,2)

About 4 minJavaScriptTypeScriptReact.jsArticle(s)blogyozm.wishket.comjsjavascripttstypescriptreactreactjs

크로스 플랫폼 디자인 시스템, 1.5년의 기록 (1,2) 관련

React.js > Article(s)

Article(s)

크로스 플랫폼 디자인 시스템, 1.5년의 기록 (1) | 요즘IT
크로스 플랫폼 디자인 시스템, 1.5년의 기록 (1)

*FEConf2023에서 발표한 <크로스 플랫폼 디자인 시스템, 1.5년의 기록>open in new window를 정리한 글입니다. 발표 내용을 2회로 나누어 발행합니다. 1회에서는 디자인 시스템과 디자인 토큰에 대해 알아봅니다. 그리고 컴포넌트를 구성요소를 파악해 디자이너와 개발자의 의사소통 문제를 해결해 봅니다. 2회에서는 1회의 내용을 바탕으로 컴포넌트를 구현하고 API를 설계해 봅니다. 본문에 삽입된 이미지의 출처는 모두 이 콘텐츠와 같은 제목의 발표 자료로, 따로 출처를 표기하지 않았습니다. 발표 자료는 FEConf2023 홈페이지open in new window에서 다운로드할 수 있습니다.

FEConf2023에서 발표된 '크로스 플랫폼 디자인 시스템, 1.5년의 기록'/하태영 당근 프론트엔드 엔지니어

안녕하세요. 저는 당근에서 디자인 시스템을 만들고 운영하고 있는 하태영입니다. 이번 글에서는 제가 1년 반 동안 디자인 시스템을 만들며 시도했던 다양한 접근 방법과 그 과정에서 함정에 빠진 경험, 그리고 이 경험을 통해 배운 것 들을 공유하고자 합니다.


1. 디자인 시스템

디자인 시스템의 목표

디자인 시스템을 만들다 보면 '디자인 시스템'이라는 단어가 굉장히 넓은 스펙트럼을 가지고 있고 이로 인해 팀원간 소통의 오류나 팀의 방향성이 흐트러지는 경험을 하게 됩니다. 이번 장에서는 이를 해결하기 위한 디자인 시스템의 방향성을 명확하게 설정하고 디자인 시스템의 설계 목표를 정확하게 파악하는 방법, 목표를 설정할 때 빠질 수 있는 함정에 대비하는 방법에 대해 알아보겠습니다.

아래 그림은 오픈소스 디자인 시스템으로 유명한 차크라와 어도비의 스펙트럼 디자인 시스템입니다. 동일한 기능을 하는 레인지 슬라이더를 구현하는데도 차이가 많이 납니다. 차크라는 모든 서브 컴포넌트를 명시해야 하는 반면에 스펙트럼은 레인지 슬라이더 한 줄만 명시하면 됩니다.

chakra vs. Adobe spectrum
chakra vs. Adobe spectrum

왜 이런 차이가 생겼을까요? 제 생각에는 두 디자인 시스템이 추구하는 가치가 다르기 때문이라고 생각합니다. 차크라는 '오픈소스를 활용하는 개발자들이 유연하게 사용할 수 있는 라이브러리'라는 목적으로 만들어졌고, 스펙트럼은 어도비 제품군에 사용될 목적으로 만들어졌습니다. 차크라는 유연성을 더 중요한 가치로 생각하고, 스펙트럼은 일관성을 더 중요한 가치로 생각하기 때문에 이런 차이가 만들어진 것입니다.

이런 이유로 저는 두 디자인 시스템을 다른 관점에서 바라보고 있고, 차크라와 같은 디자인 시스템을 '범용 디자인 시스템'이라고 부르고 스펙트럼과 같은 디자인 시스템을 '제품 언어'라고 부르며 구분하고 있습니다. 이런 제품 언어에는 스펙트럼뿐만 아니라 Material, Carbon, Fluent 등 많은 종류가 있습니다. 그런데 왜 많은 회사가 범용 디자인 시스템을 사용하여 디자인 시스템을 구축하지 않고 디자인 시스템 조직을 따로 두고 제품 언어를 만들게 될까요?

그 이유는 UI에 브랜딩, 앱의 특성 등 회사의 고유한 의사결정이 반영되어야 하기 때문입니다. 제품 언어는 일반적인 디자인 시스템의 기능 외에 '의사 결정의 집합을 관리'한다는 의미가 추가적으로 포함됩니다. 더 강조해서 말하면 제품 언어의 본질은 디자인 의사결정의 집합이고 UI 라이브러리는 그것을 구현하는 수단일 뿐이라고 할 수 있습니다.

제품 언어는 디자인 의사결정의 집합
제품 언어는 디자인 의사결정의 집합

회사에서 외부 라이브러리를 사용할 수 있는 경우는 그 라이브러리가 의사결정에 관련이 없거나 의사결정을 그대로 반영할 수 있는 경우입니다. 그러나 UI 라이브러리가 의사 결정을 반영할 수 없는 경우가 많고 이로 인해 많은 회사들이 디자인 시스템 조직을 두고 제품 언어를 만들게 됩니다.

함정: 디자인 시스템의 목표 설정

저는 디자인 시스템을 구축할 때 여러 함정에 빠졌었습니다. 먼저 목표를 명확하게 설정하지 않아서 참고할 대상을 잘 못 판단했습니다. 당근 앱에 필요한 디자인 시스템은 제품 언어여야 하고 모바일이 최우선이며 웹, IOS, 안드로이드 구현이 모두 필요합니다. 저는 팀을 구성하는 초기에 반응형 웹을 대상으로 하는 디자인 시스템을 참고했고, 이는 반응형 웹에는 맞지만 우리 제품에는 잘못된 접근을 하게 한 원인이 되었습니다.

다른 함정은 하나의 디자인 시스템으로 여러 환경과 제품을 커버하려는 욕심이었습니다. 서비스가 성장함에 따라 모바일 앱뿐만 아니라 웹사이트, 어드민 등 더 많은 제품을 지원할 필요가 생겼는데, 브랜딩은 모든 제품에 대체로 비슷하기 때문에, 잘 만든 범용 디자인 시스템 하나로 모든 제품을 커버하고자 했습니다. 그러나 이런 시도는 불가능하거나 제품 언어를 만드는 것보다 더 큰 비용이 드는 상황을 생기게 했습니다.

모든 제뭎을 커버하는<br/>범용 디자인 시스템은 불가능하다.
모든 제뭎을 커버하는
범용 디자인 시스템은 불가능하다.

이 경험을 통해 완벽한 범용 디자인 시스템을 만드는 것보다 어떤 제품 언어를 만들더라도 반복되는 방법론이 존재하고 이를 활용하는 것이 더 중요하다는 것을 알게 되었습니다. 그중 가장 중요한 것은 '디자인 의사결정의 집합을 제품 디자인과 관련된 모든 영역에 효과적으로 동기화하는 시스템'입니다. 저는 이를 농담 삼아 '디자인 시스템-시스템'이라고 부르는데요. 다음 장에서는 이러한 디자인 의사 결정을 효과적으로 관리하고 동기화하기 위한 '디자인 토큰'에 대해 알아보겠습니다.


2. 디자인 토큰

이번 장에서는 아래 내용에 대해 알아봅니다.

  1. 디자인 토큰이 가지는 의미 파악
  2. 디자인 토큰 활용 사례 알아보기
  3. 디자인 토큰 운용 시 빠질 수 있는 함정 대비

디자인 시스템을 구축하는 것은 결국 디자인 결정의 연속입니다. “버튼 컴포넌트의 높이를 40px로 할 것이다.”,”브랜드 컬러를 #ff6f0f 색상으로 할 것이다.” 등의 모든 것들이 디자인 결정에 해당합니다.

버튼 디자인
버튼 디자인

그리고 대부분의 제품이 그렇듯이 의사결정은 시간에 따라 변하기 마련입니다. 제품이 성장함에 따라 더 나은 UI가 무엇인지 새로 배우게 되기 때문입니다. 그러면 이런 의사결정의 변화를 가장 빠르게 모든 제품에 반영하는 방법은 무엇일까요?

그것은 바로 의사결정을 기계가 읽을 수 있는 방식으로 인코딩하고 제품에서 그것을 읽어 반영하는 접근 방법입니다. 이번 장의 제목이기도 한 '디자인 토큰'은 앞선 설명과 같이 디자인 의사결정을 사람과 기계 모두가 읽을 수 있는 방식으로 인코딩하는 것을 의미합니다.

이제 디자인 토큰에 대해 좀 더 자세하게 알아보겠습니다.

값을 인코딩

먼저 디자인 토큰은 값을 인코딩할 수 있습니다.

디자인 토큰은 어떤 의미를 가지고 있을까요?
디자인 토큰은 어떤 의미를 가지고 있을까요?

'이 색상의 이름은 $carrot-500이다.'라고 표현할 수 있겠죠? 이 표현도 틀린 말은 아니지만 저는 조금 더 중요한 의미를 포함하고 있다고 생각합니다. 제가 생각하는 정확한 의미는 '우리는 제품에 이 색상을 사용할 것이고 그 색상의 이름은 $carrot-500 이다.'입니다.

두 표현은 비슷해 보이고 말장난 같지만 여기에는 중요한 차이가 있습니다. 제품에 이 색상을 사용할 것이라는 것의 의미는 선언하지 않은 색상은 사용하지 않겠다는 뜻을 가지고 있습니다. 이를 통해서 디자인 시스템팀은 디자인 결정을 유연한 집합으로 유지하고 디자인 의사 결정을 효과적으로 관리할 수 있습니다. 하지만 디자인의 유연성과 자유도는 떨어질 수 있습니다.

세상에는 무한한 개념이 존재하지만 우리는 유한한 단어만으로도 충분히 소통하며 살아갈 수 있습니다. 저는 같은 맥락으로 무한한 디자인 값이 존재하지만 유한한 디자인 토큰으로 소통할 수 있다고 믿습니다.


의도를 인코딩

디자인 토큰을 좀 더 유연하게 사용하기 위해 계층을 추가할 수도 있습니다. 아래와 같이 값을 인코딩해서 유한하게 구성된 디자인 토큰에 의도를 인코딩하는 계층을 추가할 수 있습니다.

이 디자인 토큰의 표현은 '프라이머리 브랜드를 의도하는 영역에 을 사용할 것이다.'라는 의도를 인코딩합니다.
이 디자인 토큰의 표현은 '프라이머리 브랜드를 의도하는 영역에 $carrot-500을 사용할 것이다.'라는 의도를 인코딩합니다.
이렇게 계층을 추가하는 것으로 지금은 동일한 색상으로 표현되지만 앞으로 다른 색으로 표현될 수도 있다는 의사 결정을 나타낼 수 있습니다. 또한 값이 아닌 의도를 바탕으로 디자인하는 사고방식을 추구하는 것으로 제품 디자인 과정에서의 의사결정과 소통을 개선합니다.
이렇게 계층을 추가하는 것으로 지금은 동일한 색상으로 표현되지만 앞으로 다른 색으로 표현될 수도 있다는 의사 결정을 나타낼 수 있습니다. 또한 값이 아닌 의도를 바탕으로 디자인하는 사고방식을 추구하는 것으로 제품 디자인 과정에서의 의사결정과 소통을 개선합니다.

맥락

마지막으로 디자인 토큰은 맥락에 따라 다른 값을 부여할 수 있습니다.

예를 들어 같은 이라는 값에 라이트 모드와 다크 모드라는 분기를 추가할 수 있습니다.
예를 들어 같은 $carrot-500이라는 값에 라이트 모드와 다크 모드라는 분기를 추가할 수 있습니다.

앞선 세 가지를 정리하면 디자인 토큰은 다음과 같이 맥락과 계층을 통해 구성됩니다.

맥락과 계층을 활용하는 것으로 디자인 토큰은 제품 디자인에 사용하기 충분한 표현력을 가지게 됩니다.
맥락과 계층을 활용하는 것으로 디자인 토큰은 제품 디자인에 사용하기 충분한 표현력을 가지게 됩니다.

당근에서는 이 정의를 바탕으로 간단한 DSL(Domain-Specific Languages : 도메인 특화 언어)을 만들어서 디자인 토큰을 관리합니다. 이 표현식은 올해 추가된 Figma Variables와 의미적으로 일치하기 때문에 새로 디자인 토큰을 구축하는 팀이 있다면 이를 적극적으로 고려해 보는 것도 좋을 것 같습니다. 당근에서는 피그마 컴포넌트 프레임 안에 이 DSL을 넣어서 이 컴포넌트를 진실의 원천으로 사용하고 있습니다.

이 접근으로 피그마에 문서 동기화를 자동화하거나 다크 모드 프리뷰 등을 구현하고 있습니다.
이 접근으로 피그마에 문서 동기화를 자동화하거나 다크 모드 프리뷰 등을 구현하고 있습니다.
또한 컴포넌트를 배포할 때 <FontIcon icon="fas fa-globe"/>웹훅(webhook)을 추가하여 액션을 트리거 하는데, 이를 통해 피그마를 진실의 원천으로 사용하고 각 플랫폼별 코드를 생성하여 디자인 결정을 크로스 플랫폼으로 동기화하고 있습니다.
또한 컴포넌트를 배포할 때 웹훅(webhook)open in new window을 추가하여 액션을 트리거 하는데, 이를 통해 피그마를 진실의 원천으로 사용하고 각 플랫폼별 코드를 생성하여 디자인 결정을 크로스 플랫폼으로 동기화하고 있습니다.

디자인 토큰을 코드로

프론트엔드에서의 적용하는 방법을 더 자세하게 살펴보겠습니다.

데이터 어트리뷰트를 통해 맥락 분기를 구현하고 <FontIcon icon="fa-brands fa-css3-alt"/>CSS Variable로 디자인 토큰과 계층을 구현하고 있습니다.
데이터 어트리뷰트를 통해 맥락 분기를 구현하고 CSS Variable로 디자인 토큰과 계층을 구현하고 있습니다.
그리고 CSS in JS에서는 사용하기 쉽도록 <FontIcon icon="fa-brands fa-css3-alt"/>CSS Variable을 alias 하는 패키지를 함께 생성하고 있습니다.
그리고 CSS in JS에서는 사용하기 쉽도록 CSS Variable을 alias 하는 패키지를 함께 생성하고 있습니다.

이 alias 패키지는 이모션에서는 아래 첫 번째 그림과 같이 사용할 수 있고, vanilla-extract에서는 두 번째 그림과 같이 사용할 수 있습니다. 조직 별로 사용되는 기술이 다양한 당근의 특성상 특정 CSS In JS 기술만 적용할 수 없기 때문에 아래와 같이 다양한 접근으로 기술에 맞게 적용되어 있습니다.

// 사용사례: @emotion/styled
const Info = styled.p`
  ${vars.$sementic.typography.label4Regular}
  display: flex;
  align-items: center;
  color: ${vars.scale.color.gray600}
`;

// 사용사례: @vanilla-extract/recipes
// ..

  variants: {
    selected: {
      true: {
        backgroundColor: vars.$scale.color.gray800,
        border: `1px solid ${vars.$scale.color.gray800}`,
        color: vars.$scale.color.gray00
      },
      false: {
        backgroundColor: vars.$scale.color.paperDefault,
        border: `1px solid ${vars.$scale.color.divider1}`,
        color: vars.$scale.color.gray900
      }
    },
  },
});

함정: 시맨틱 토큰의 섣부른 추상화

디자인 토큰을 만들며 어려움을 겪었던 부분은 기술적인 부분보다는 운영적인 부분이었습니다. 특히 기억에 남는 잘못된 판단은 시맨틱 토큰을 성급하게 정의했던 것입니다.

만약 FAB 컴포넌트가 있다고 할 때 이 컴포넌트의 배경색이 플로팅의 의미를 가진다고 할 수 있을까요?
만약 FAB 컴포넌트가 있다고 할 때 이 컴포넌트의 배경색이 플로팅의 의미를 가진다고 할 수 있을까요?

제 경험으로 생각해 보면 이러한 섣부른 시맨틱 토큰 정의는 유용하게 사용되기보다는 더 큰 디자인 혼란을 만드는 경우가 많았습니다.

그렇다면 유용한 시맨틱 토큰을 정의하는 방법은 무엇일까요? 아래와 같이 다양한 사용 사례를 고려하여 어떤 관심사를 공유하는지 살펴보는 것입니다.

Input, Text area, Number Input 등 다양한 Input Field들이 존재하고 이들이 모두 동일한 배경 색상을 공유해야 하는 경우에는 이들의 배경 색상을 Field-bg로 정의하면 유용한 시맨틱 토큰으로 사용할 수 있습니다.
Input, Text area, Number Input 등 다양한 Input Field들이 존재하고 이들이 모두 동일한 배경 색상을 공유해야 하는 경우에는 이들의 배경 색상을 Field-bg로 정의하면 유용한 시맨틱 토큰으로 사용할 수 있습니다.

시맨틱 토큰은 잘 정의하여 사용하면 분명 강력한 도구가 될 수 있습니다. 그러나 잘못 정의된 시맨틱 토큰은 더 큰 혼란을 야기할 수 있습니다. 따라서 섣부르게 시맨틱 토큰에 의도를 부여하는 것은 위험합니다. 즉 추상화는 관찰을 통해 얻어지기 때문에, 여러 사례를 공유하는 공통의 관심사를 살펴보고 이를 통해 의도를 추출하는 접근이 더 안전합니다.

지금까지 디자인 토큰에 대해 알아봤습니다. 디자인 토큰은 디자인 시스템 조직을 운영할 때 굉장히 강력한 수단이지만 사용자가 디자인 시스템에 기대하는 것은 컴포넌트일 것입니다. 다음 장에서는 디자인 시스템에서의 컴포넌트에 대해 알아보겠습니다.


3. 컴포넌트

이번 장에서는 아래 내용에 대해 알아봅니다.

  1. Atomic Design이 혼란스러운 이유
  2. 컴포넌트의 구성요소 정확하게 이해
  3. 디자이너-개발자 간 의사소통 문제 해결

Atomic Design

아토믹 디자인은 몇 년 전까지만 해도 자주 언급되다가 언젠가부터 잘 들리지 않게 된 키워드입니다. 혹시 아토믹 디자인을 별다른 변형 없이 제품에 잘 적용하고 계신가요? 적어도 저는 아토믹 디자인을 있는 그대로 적용하기 어렵다고 느끼고 있습니다.

라디오 그룹 컴포넌트에서 어떤 부분이 아토믹한 단위일까요?
라디오 그룹 컴포넌트에서 어떤 부분이 아토믹한 단위일까요?

사실 정답은 하나로 정해지지 않습니다. 기준에 따라 정답이 달라지기 때문입니다. 형태 단위로는 노란 박스가 아톰이지만 기능 단위로는 라디오 버튼 1개로는 의미가 없기 때문에 파란 박스의 기능 단위가 아톰입니다. 그리고 접근성 단위로는 폼 요소의 레벨을 제공할 의무가 있기 때문에 빨간 박스의 접근성 단위가 아톰이 됩니다.

이렇게 기능과 형태를 나눠서 바라보는 접근법에 대해 간단하게 알아보겠습니다.

컴포넌트: 형태

이 체크박스는 선택되지 않았을 때 빈 박스의 형태를 가지고, 선택되었을 때는 체크가 되어 있는 박스로 표현됩니다. 이와 같이 현재 상태에 대응되는 스타일을 렌더링 하는 것이 컴포넌트의 형태적 측면입니다.
이 체크박스는 선택되지 않았을 때 빈 박스의 형태를 가지고, 선택되었을 때는 체크가 되어 있는 박스로 표현됩니다. 이와 같이 현재 상태에 대응되는 스타일을 렌더링 하는 것이 컴포넌트의 형태적 측면입니다.

컴포넌트: 기능

이 그림처럼 체크박스는 클릭을 통해 선택 여부를 전환할 수 있습니다. 이 경우 체크박스가 어떻게 생겼는지는 중요하지 않습니다. 체크박스의 순서가 바뀔 수도 있고, 색이 다르거나 토글스위치와 같이 체크박스가 아닌 완전히 다른 형태일 수도 있습니다.
이 그림처럼 체크박스는 클릭을 통해 선택 여부를 전환할 수 있습니다. 이 경우 체크박스가 어떻게 생겼는지는 중요하지 않습니다. 체크박스의 순서가 바뀔 수도 있고, 색이 다르거나 토글스위치와 같이 체크박스가 아닌 완전히 다른 형태일 수도 있습니다.

그래서 렌더링을 제거하고 상태만 남기면 컴포넌트의 기능적 측면이 더욱 확실해집니다. 따라서 컴포넌트의 기능적 측면은 렌더링을 제외하고 유저 입력에 대해서 컴포넌트의 상태가 어떻게 전이되는지 정의됩니다.

Atomic Design이 혼란스러운 이유

그렇다면 아토믹 디자인이 혼란스러운 이유는 무엇일까요? 결론적으로 아토믹한 기능, 아토믹한 형태는 존재할 수 있습니다. 하지만 아토믹한 컴포넌트는 하나로 정해지지 않습니다. 따라서 컴포넌트만을 분류해서 최소 단위로 정의하려고 하면 실패하기 쉽습니다. 형태적 아토믹을 제공하는 스타일 시트와 기능적 아토믹을 제공하는 헤드리스 컴포넌트를 분리해서 생각해야 무엇이 최소 단위인지에 대한 혼란을 없앨 수 있고, 팀이 앞으로 나아갈 수 있습니다. 그렇다면 이제 아토믹 디자인에서 벗어나 기능과 형태 각각에 대해서 컴포넌트의 구성 요소를 해부해 봅시다.

컴포넌트 해부해 보기: 기능

먼저 컴포넌트의 기능은 구조, 상태, 상호작용, 맥락으로 구성됩니다.
먼저 컴포넌트의 기능은 구조, 상태, 상호작용, 맥락으로 구성됩니다.

먼저 구조는 컴포넌트가 어떤 부품들로 구성되어 있고 각 부품이 유저 상호작용에 어떤 역할을 가지는지 정의합니다. 예를 들어 위 컴포넌트는 컨트롤이라는 역할을 가진 체크박스와 라벨이라는 역할을 하는 Item Label 텍스트로 구성되어 있고 이를 감싸는 Root가 있습니다.

두 번째로 상태는 유저 입력에 의해 변할 수 있는 컴포넌트의 상태를 정의합니다. 대표적으로 Pressed (active), Hover, Focused, Selected(Checked) 등이 있습니다.

세 번째로 상호작용은 상태의 변경을 유발하는 단위입니다. 체크박스를 예로 들면 클릭을 하면 Checked가 바뀌고 탭으로 포커스를 잡으면 Focused가 바뀝니다.

마지막으로 맥락은 코드에서 주입하는 옵션으로 동작에 관여합니다. 예를 들어 Disabled 옵션을 주입하면 체크박스가 비활성화되고 아무리 클릭해도 Checked가 바뀌지 않습니다. 물론 상태도 동작에 관여할 수 있지만 상태는 유저의 입력에 의해서 변경되는 것이고 맥락은 코드에 의해서만 변경된다는 중요한 차이점이 있습니다.

컴포넌트 해부해 보기: 형태

다음으로 컴포넌트의 형태에 대해 알아보겠습니다. 컴포넌트의 형태는 구조, 시각 옵션, 상태 옵션, 디자인 결정으로 구성됩니다.

먼저 구조에 대해 알아보겠습니다. 형태에서의 구조는 기능에서의 구조와 교집합이 많지만 반드시 일치하지는 않습니다. 여기서 구조는 컴포넌트가 어떤 부품들로 구성되어 있고 각 부품이 어떤 레이아웃을 가지는지 정의합니다. 예를 들어 아이콘은 기능적으로는 존재하지 않지만, 형태적으로는 존재합니다.

피그마에서는 컴포넌트의 프레임 구조를 이 것에 가능한 가깝게 구성하는 것이 좋습니다.
피그마에서는 컴포넌트의 프레임 구조를 이 것에 가능한 가깝게 구성하는 것이 좋습니다.
두 번째로 시각 옵션은 설정된 옵션에 따라 형태가 달라지는 것을 정의합니다. Size, Variant 등이 대표적인 예시입니다.
두 번째로 시각 옵션은 설정된 옵션에 따라 형태가 달라지는 것을 정의합니다. Size, Variant 등이 대표적인 예시입니다.
세 번째로 상태 옵션은 앞서 설명드린 기능에서 비롯된 상태와 맥락에 의해서 형태가 달라지는 것을 정의합니다. Hovered, Focused, Checked 등이 대표적인 예시입니다.
세 번째로 상태 옵션은 앞서 설명드린 기능에서 비롯된 상태와 맥락에 의해서 형태가 달라지는 것을 정의합니다. Hovered, Focused, Checked 등이 대표적인 예시입니다.
마지막으로 이렇게 정의된 상태 옵션과 시각 옵션의 모든 조합에 대해 각 구조 별로 어떤 속성의 어떤 디자인 값이 부여되는지를 모두 정리한 것이 곧 이 컴포넌트에 대한 디자인 결정입니다. 그리고 이 디자인 결정을 더 효과적으로 표현하는 수단으로 디자인 토큰을 사용할 수 있습니다.
마지막으로 이렇게 정의된 상태 옵션과 시각 옵션의 모든 조합에 대해 각 구조 별로 어떤 속성의 어떤 디자인 값이 부여되는지를 모두 정리한 것이 곧 이 컴포넌트에 대한 디자인 결정입니다. 그리고 이 디자인 결정을 더 효과적으로 표현하는 수단으로 디자인 토큰을 사용할 수 있습니다.

지금까지 기능과 형태 각각에 대해 어떤 구성요소를 가지는지 알아봤습니다.

정리하면, 기능의 구조와 형태의 구조는 큰 교집합을 가지지만 서로 달라질 수 있고, 기능의 상태와 맥락은 형태의 상태 옵션과 거의 정확하게 대응되지만 여기에는 아직 문제가 남아있습니다.
정리하면, 기능의 구조와 형태의 구조는 큰 교집합을 가지지만 서로 달라질 수 있고, 기능의 상태와 맥락은 형태의 상태 옵션과 거의 정확하게 대응되지만 여기에는 아직 문제가 남아있습니다.

기능과 형태 구조의 문제점

디자이너 입장에서의 상태 옵션은 왼쪽과 같이 5가지의 경우의 수만 가집니다. 그러나 개발자 입장에서의 상태와 맥락의 조합은 2의 4승으로 총 16가지의 경우의 수를 가질 수 있습니다.
디자이너 입장에서의 상태 옵션은 왼쪽과 같이 5가지의 경우의 수만 가집니다. 그러나 개발자 입장에서의 상태와 맥락의 조합은 2의 4승으로 총 16가지의 경우의 수를 가질 수 있습니다.

저는 초기에 피그마에도 이런 모든 경우의 수를 그려야 한다고 주장한 적도 있습니다. 당연히 환영받지 못했고, 실제로도 불필요한 작업이었습니다.

더 심하게 상태가 늘어나면 아래와 같이 조합적 상태 폭발이 발생해서 제어할 수 없을 정도의 경우의 수가 생기기도 합니다.
더 심하게 상태가 늘어나면 아래와 같이 조합적 상태 폭발이 발생해서 제어할 수 없을 정도의 경우의 수가 생기기도 합니다.

그러나 우리는 경험적으로 상태들 사이에 우선순위가 존재한다는 것을 알고 있습니다. 예를 들어 Disabled 일 때는 호버를 고려할 필요가 없습니다. 실제로 렌더링이 변할 필요가 있는 상태 조합은 제한적이고, 그 제한적인 상태는 곧 디자이너의 시각으로 바라본 경우의 수와 일치할 것입니다. 따라서 상태와 맥락의 조합을 조건에 따라 1차원 enum으로 정리하여 상태를 압축하면 맥락과 상태 옵션을 빠짐없이 파악하고 소통할 수 있습니다.

상태 압축

상태, 맥락 조합을 1차원 enum으로 정리

StateCondition
enabledisDisabled OFF, isHovered OFF, isFocused OFF, isPressed OFF
hoveredisDisabled OFF, isHovered ON, isPressed OFF
focusedisDisabled OFF, isFocused ON, isPressed OFF
pressedisDisabled OFF, isPressed ON
disabledisDisabled ON

관심사 분리

이렇게 상태 압축까지 마치면 기능과 형태의 공통 관심사와 고유 관심사를 파악할 수 있습니다. 구조, 상태, 맥락은 양쪽 모두 공통적으로 존재하는 관심사이고, 상호작용은 기능에만, 시각 옵션과 디자인 결정은 형태에만 존재하는 관심사입니다.

이렇게 관심사를 분리하는 것으로 어떤 이점을 얻을 수 있을까요?
이렇게 관심사를 분리하는 것으로 어떤 이점을 얻을 수 있을까요?

의사소통 순환 참조

관심사 분리는 의사소통의 순환 참조를 해결하는 키워드입니다. 아래와 같은 상황을 생각해보겠습니다.

개발자: 디자인이 없어서 아직 컴포넌트를 구현할 수 없어요.

디자이너: 컴포넌트를 디자인하려는데 어떤 Variant들이 필요하죠?

디자이너: 일단 디자인 해놨는데 개발자가 이대로 만들면 안 된대요.

개발자: 근데 이미 피그마에 배포해서 디자이너들이 쓰고 있어요.

이와 같은 상황을 의사소통의 순환 참조 문제라고 합니다. 실제로 디자인 시스템 혹은 컴포넌트 단위로 개발하는 조직들에서 발생할 수 있는 문제들입니다. 저는 이런 문제를 '디자인을 먼저 하고 컴포넌트를 구현하는 방식의 문제'라고 생각합니다. 디자인을 할때 개발에 어떤 상태들이 존재하게 될지 미리 알기 어렵기 때문에 개발에 대한 의존성이 생깁니다. 반면 개발을 할 때는 디자인이 나와야 코드를 작성할 수 있다고 생각하기 때문에 디자인에 대한 의존성이 생깁니다.

의사소통 순환 참조 해결

그렇다면 서로에게 의존적인 의사소통 순환 참조를 어떻게 해결할 수 있을까요? 개발에서는 흔히 인터페이스를 사용해서 의존 방향을 역전시키는 방법을 사용합니다. 이는 우리가 일하는 방식에도 적용할 수 있습니다. 우리는 앞에서 언급한 관심사 분리를 바탕으로 디자인과 개발의 공통 인터페이스가 무엇인지 알고 있습니다. 이를 바탕으로 제가 생각하는 디자인 시스템팀의 일하는 방식을 소개하겠습니다.

먼저 작성하려는 컴포넌트의 간단한 스케치를 디자이너와 개발자가 함께 그리고 어떤 구조와 상태 옵션을 가지게 될지 정의합니다.

해당 컴포넌트에서 사용될 공통 인터페이스입니다.
해당 컴포넌트에서 사용될 공통 인터페이스입니다.
개발자는 주어진 구조에서 사용자 접근 편의성을 고려하여 기능적으로 유의미한 구조만 남기고 유저 상호 작용에 따른 상태 전이를 파악합니다. 이것으로 헤드리스 컴포넌트를 직접 구현하거나 이미 있는 라이브러리를 가져옵니다.
개발자는 주어진 구조에서 사용자 접근 편의성을 고려하여 기능적으로 유의미한 구조만 남기고 유저 상호 작용에 따른 상태 전이를 파악합니다. 이것으로 헤드리스 컴포넌트를 직접 구현하거나 이미 있는 라이브러리를 가져옵니다.
디자이너는 상태 옵션을 바탕으로 구조에 맞게 컴포넌트를 디자인합니다.
디자이너는 상태 옵션을 바탕으로 구조에 맞게 컴포넌트를 디자인합니다.
마지막으로 개발자는 디자인된 컴포넌트를 바탕으로 스타일을 작성하고 이미 작성해둔 헤드리스 컴포넌트와 결합해 기능과 형태를 모두 구현합니다. 그리고 피그마와 동기화되어 있는 컴포넌트를 완성합니다.
마지막으로 개발자는 디자인된 컴포넌트를 바탕으로 스타일을 작성하고 이미 작성해둔 헤드리스 컴포넌트와 결합해 기능과 형태를 모두 구현합니다. 그리고 피그마와 동기화되어 있는 컴포넌트를 완성합니다.

정리하면, 앞서 말한 '디자인을 먼저 하고 컴포넌트를 구현하는 방식의 문제' 즉, 피그마에서 디자인한 내용이 개발로 이어지는 방식으로 일하는 것이 아니라, 구조나 상태와 같은 공통 인터페이스를 먼저 정의하고 이 내용이 피그마와 개발로 이어지는 방식으로 바꾸어 의사소통의 순환 참조를 해결할 수 있습니다.

이 과정을 통해 우리가 배울 수 있는 점이 있습니다. 결국 피그마도 개발과 동일하게 환경으로 바라보고 개발과 동일하게 구현체로 취급하는 것이 실제 일하는 방식에 더 알맞다는 것입니다.
이 과정을 통해 우리가 배울 수 있는 점이 있습니다. 결국 피그마도 개발과 동일하게 환경으로 바라보고 개발과 동일하게 구현체로 취급하는 것이 실제 일하는 방식에 더 알맞다는 것입니다.

크로스 플랫폼 디자인 시스템, 1.5년의 기록 (2) | 요즘IT

크로스 플랫폼 디자인 시스템, 1.5년의 기록 (2)

유연성 VS 일관성

앞서 살펴본 차크라와 스펙트럼의 API를 다시 한번 살펴보겠습니다. 제품 언어를 만드는 입장에서 일관성을 최우선으로 추구하여 오른쪽의 API처럼 간결하게 제공하고 싶은 것은 당연한 생각입니다. 그러나 일관성을 추구하는 형태로 제공함으로써 발생할 수 있는 모든 케이스의 90%를 커버할 수 있다고 하더라도, 사용자 입장에서는 커버하지 못하는 10%의 케이스로 인해 개발이 지연될 수 있습니다.

유연성 vs 일관성
유연성 vs 일관성
저는 일관성을 제공하는 기본 패키지와 유연성을 제공하는 합성 가능한 패키지를 분리하는 것으로 이 문제에 접근하고 있습니다.
저는 일관성을 제공하는 기본 패키지와 유연성을 제공하는 합성 가능한 패키지를 분리하는 것으로 이 문제에 접근하고 있습니다.

실제 구현에서는 아래와 같이 패키지를 Core, Composable, Pre-Composed라는 세 가지 계층으로 분리합니다. 사전 조합(Pre-Composed) 된 컴포넌트를 이용해 일관성을 기본값으로 제공합니다. 그리고 Styled와 유사한 방식의 스타일링 프로퍼티를 제공합니다. 단, margin 등의 레이아웃에만 한정해서 제공합니다. 그 이상의 유연성이 필요한 경우에는 Composable 패키지를 사용하고, 그것을 사용하는 방식이 사전 조합된 컴포넌트를 사용하는 것에 비해 어렵지 않게 합니다.

컴포넌트 구현

컴포넌트 기능의 코어에 해당하는 로직은 상태 차트와 Dom Binding으로 구성됩니다.
컴포넌트 기능의 코어에 해당하는 로직은 상태 차트와 Dom Binding으로 구성됩니다.

이번 글에서는 상태 차트의 활용은 다루지 않고 Dom Binding에 대해서만 살펴보겠습니다.

기능: Dom Binding

먼저 아래와 같은 요구 사항을 가진 컴포넌트가 필요하다고 가정하겠습니다. 4개의 구조를 가지고, 체크 여부를 나타내는 상태를 가집니다. 그리고 클릭을 하면 그 체크 여부가 전환이 되며, Dissabled가 주입되면 전환이 발생하지 않아야 하는 컴포넌트입니다.

  1. 구조 - Root, Control, Input, Label
  2. 상태 - isSelected : boolean
  3. 상호작용 - click
  4. 맥락 - isDisabled : boolean

Dom Binding을 구현하기 위해 먼저 구조에 대해 표현해 보겠습니다. 앞서 말한 4개의 구조를 아래와 같이 선언할 수 있습니다.

export function connect() {
  return {
    rootProps: { /* ... */ },
    controlProps: { /* ... */ },
    inputProps: { /* ... */ },
    labelProps: { /* ... */ },
  }
}


 
 
 
 


이 프로퍼티들은 최종적으로 JSX에 아래와 같이 스프레드 되어서 기능을 제공합니다.

const api = useCheckbox(otherProps);

return {
  <div {...api.rootProps}>
    <div {...api.controlProps}>
      <input type="checkbox" {...api.inputProps} />
    </div>
    <div {...api.labelProps}>{label}</div>
  </div>
}

상태를 Dom에 적용하는 것은 상태 차트로부터 전달받은 상태를 바탕으로 엘리먼트의 프로퍼티를 결정하는 것입니다. 아래의 경우에는 isSelectedinputPropschecked에 바인딩하는 것으로 구현할 수 있습니다.

export function connect(state) {
  return {
    rootProps: { /* ... */ },
    controlProps: { /* ... */ },
    inputProps: {
      checked: state.isselected
    },
    labelProps: { /* ... */ },
  }
}
 




 




상호작용은 이벤트 핸들러가 상태 차트에 이벤트를 전달하는 것으로 구현합니다. 아래의 경우에는 inputPropsonChange가 발생하면 상태 차트에 'TOGGLE'이라는 이벤트를 전달합니다. 상태 차트는 'TOGGLE'이라는 이벤트와 isDisabled라는 맥락을 바탕으로 현재 상태가 바뀌어야 하는지 아닌지를 판단하여 갱신된 상태를 Dom Binding에 전달하게 됩니다.

export function connect(state, send) {
  return {
    rootProps: { /* ... */ },
    controlProps: { /* ... */ },
    inputProps: {
      checked: state.isselected,
      onChange: (e) => {
        send("TOGGLE", { event: e });
      },
    },
    labelProps: { /* ... */ },
  }
}
 





 
 
 




맥락을 적용하는 것도 상태 적용과 마찬가지로 엘리먼트의 프로퍼티를 결정하는 것으로 구현됩니다. 둘의 차이는 상태 차트에서 제공하는 상태가 아닌 외부에서 주입한 프로퍼티를 그대로 사용한다는 것입니다. 아래 경우에는 inputPropsdisabled 프로퍼티에 외주에서 주입된 ctxisDisabled를 바인딩 하는 것으로 표현되고 있습니다.

export function connect(state, send, ctx) {
  return {
    rootProps: { /* ... */ },
    controlProps: { /* ... */ },
    inputProps: {
      disabled: ctx.isDisabled,
      checked: state.isselected,
      onChange: (e) => {
        send("TOGGLE", { event: e });
      },
    },
    labelProps: { /* ... */ },
  }
}
 




 








이렇게 작성된 로직은 리액트 의존성이 없기 때문에 얇은 래퍼(thin-wrapper) 작성을 통해 리액트에 통합됩니다.
이렇게 작성된 로직은 리액트 의존성이 없기 때문에 얇은 래퍼(thin-wrapper) 작성을 통해 리액트에 통합됩니다.

상태와 맥락에 대한 인터페이스를 각각 선언한 다음 이것을 extends 해서 상태 변화에 대한 모든 콜백을 추가할 수 있습니다. 이를 통해 헤드리스 체크박스 컴포넌트가 요구하는 모든 프로퍼티를 선언할 수 있습니다.

interface State {
  isSelected: boolean;
}

interface Context {
  isDisabled: boolean;
}

export interface CheckboxBehaviorProps extends State, Context {
  onSelectedChange: {isSelected: boolean } => void;
}

그리고 아래와 같이 이 프로퍼티를 받아서 상태 차트에 전달하고 그 결과를 다시 Dom Binding에 전달하고 그 결과를 바로 리턴하는 것만으로 리액트 래퍼를 구현할 수 있습니다. 실제 코드에서는 리액트와의 상태 동기화를 위해 useSyncExternalStoreuseEffect와 같은 몇 가지 기법들이 더 포함되어야 하지만 아래에서는 생략하여 표현하였습니다.

export function useCheckbox(props: CheckboxBehaviorProps) {
  const [state, send] = useMachine(machine);
  return connect(state, send, props);
}

형태

지금까지 기능 영역의 컴포넌트 구현에 대해 알아봤습니다. 이번에는 형태의 경우를 살펴보겠습니다. CSS와 자바스크립트 언어는 다르기 때문에 Hook처럼 래퍼를 작성하기는 어렵습니다. 자바스크립트에서 CSS를 바로 사용할 수는 없기 때문입니다.

대신 단일 스키마에서 CSS와 CSS in JS를 함께 생성하는 방식으로 무결성을 유지하고 있습니다.
대신 단일 스키마에서 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과 유사하게 구조를 표현하는 것으로 시작합니다.

const checkbox = defineReceipt({
  name: 'checkbox',
  slots: ["root", "control", "icon", "label"],
  base: {
    root: { /* ... */ },
    control: { /* ... */ },
    icon: { /* ... */ },
    label: { /* ... */ },
  },
  variants: { /* ... */ },
});


 

 
 
 
 



그리고 아래와 같이 Variants 표현식을 사용하여 시각 옵션을 표현할 수 있습니다. 아래의 경우에는 large 일 때 root의 height32px이 되고, medium 일 때 height24px이 된다고 표현하고 있습니다.

const checkbox = defineReceipt({
  name: 'checkbox',
  slots: ["root", "control", "icon", "label"],
  base: {
    root: { /* ... */ },
    control: { /* ... */ },
    icon: { /* ... */ },
    label: { /* ... */ },
  },
  variants: {
    size: {
      large: {
        root: { height: "32px" },
      },
      medium: {
        root: { height: "24px" },
      }
    }
  },
});










 
 
 
 
 
 
 
 


다음으로 상태 옵션은 데이터 어트리뷰트를 선택자로 사용하는 것으로 표현할 수 있습니다. 예를 들어서 아래와 같이 data-selected라는 데이터 어트리뷰트가 존재하면 컨트롤의 배경 색상(background)을 primary로 바꾸는 방식으로 구현할 수 있습니다. 그런데 이 data-selected라는 선택자는 HTML이 알아서 추가해 주지 않습니다. 그렇기 때문에 Dom binding에도 관련 코드를 다시 추가해야 합니다.

const checkbox = defineReceipt({
  name: 'checkbox',
  slots: ["root", "control", "icon", "label"],
  base: {
    root: { /* ... */ },
    control: {
      "&[data-selected]": {
        background: $semantic.color.primary,.
      }
    },
    icon: { /* ... */ },
    label: { /* ... */ },
  },
  variants: { /* ... */ },
});






 
 
 






아래와 같이 Dom binding의 controlPropsselected 여부를 데이터 어트리뷰트로 전달하는 로직이 추가됩니다. 이것은 상태 옵션이 공통 관심사였던 것을 생각하면 당연히 추가되어야 한다는 것을 알 수 있습니다.

export function connect(state, send, ctx) {
  return {
    rootProps: { /* ... */ },
    controlProps: {
      "date-selected": state.isSelected,
    },
    inputProps: {
      checked: state.isselected,
      onChange: (e) => {
        send("TOGGLE", { event: e });
      },
    },
    labelProps: { /* ... */ },
  }
}



 
 
 









이렇게 작성된 스키마를 바탕으로 아래와 같이 선택자로 사용될 클래스 명은 구조와 시각 옵션을 바탕으로 규칙에 따라 생성할 수 있습니다.
이렇게 작성된 스키마를 바탕으로 아래와 같이 선택자로 사용될 클래스 명은 구조와 시각 옵션을 바탕으로 규칙에 따라 생성할 수 있습니다.
그리고 상태 옵션도 CSS의 네스팅 문법을 활용해 선택자로 생성할 수 있습니다.
그리고 상태 옵션도 CSS의 네스팅 문법을 활용해 선택자로 생성할 수 있습니다.

마지막으로 생성된 CSS 코드에 대응되는 CSS in JS 코드를 함께 생성합니다. 아래 코드는 두 가지 파트로 구성되어 있습니다. 먼저 시각 옵션을 나타내는 인터페이스인 checkboxVariantProps와 시각 옵션을 Slot 별 클래스 네임으로 변환하는 함수를 함께 제공합니다. 예를 들어서 size가 large로 전달되면 시각 옵션 예제에서 작성한 checkbox__root–size_large와 같은 클래스 명을 반환하는 방식입니다.

type CheckboxSlots = "root" | "control" | "icon" | "label";

export interface CheckboxVariantProps {
  size: "large" | "medium" | "small";
}

export function checkbox(
  props: CheckboxVariantProps
): Record<CheckboxSlots, string> {
  // props에 따른 slot별, className 반환
}

최종적으로 이렇게 만들어진 기능과 형태 인터페이스를 다시 extends 하고 필요한 추가 프로퍼티를 받아서 체크박스의 컴포넌트 인터페이스를 완성합니다.

export interface CheckboxBehaviorProps extends State, Context {
  onSelectedChange: (isSelected: boolean) => void;
}

export interface CheckboxVariantProps {
  size: "large" | "medium" | "small";
}

export interface CheckboxProps extends CheckboxBehaviorProps, Checkbox VariantProps {
  label: string;
}

그리고 아래와 같이 합쳐진 프로퍼티를 통해 기능의 Hooks를 호출하고 형태의 클래스 네임 생성 함수를 각각 호출할 수 있습니다. 그리고 return 문 아래의 JSX를 살펴보면, 기능의 Hooks에서 얻어온 API와 형태에서 얻어온 클래스 네임들을 각각 JSX에 스프레드하고 클래스 네임에 바인딩 해서 기능과 형태를 합성합니다. 이 코드는 매우 단순하고 반복적이기 때문에 사용자가 Composable 패키지를 사용해서 다시 구현하는데 부담이 적습니다.

const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
  (props, ref) => {
    const { label, size, ...otherProps} = props;
    const api = useCheckbox(otherProps); // 기능
    const classNames = checkbox({ size }); // 형태

    return (
      <div className={classNames.root} {...api.rootProps}>
        <div className={classNames.control} {...api.controlProps}>
          <div className={classNames.icon} />
          <input ref={ref} type="checkbox" {...api.inputProps} />
        </div>
      </div>
      <div className={classNames.label} {...api.labelProps}>
        {label}
      </div>
    );
  }
);
이렇게 본래 목표였던 사전 조합된 컴포넌트와 조합 가능한 패키지를 분리해서 일관성과 유연성을 모두 제공하는 API를 구성할 수 있습니다.
이렇게 본래 목표였던 사전 조합된 컴포넌트와 조합 가능한 패키지를 분리해서 일관성과 유연성을 모두 제공하는 API를 구성할 수 있습니다.
그리고 리액트 의존적인 코드는 모두 단순 바인딩으로 작성되었기 때문에 다른 프레임워크에서의 재사용성이 높아졌습니다.
그리고 리액트 의존적인 코드는 모두 단순 바인딩으로 작성되었기 때문에 다른 프레임워크에서의 재사용성이 높아졌습니다.
더 나아가서는 컴포넌트를 정의한 방법에 따라서 컴포넌트가 렌더링 될 수 있는 모든 경우의 수를 사전에 계산할 수 있습니다. 이를 바탕으로 스냅샷 테스팅 및 QA 자동화도 가능합니다.
더 나아가서는 컴포넌트를 정의한 방법에 따라서 컴포넌트가 렌더링 될 수 있는 모든 경우의 수를 사전에 계산할 수 있습니다. 이를 바탕으로 스냅샷 테스팅 및 QA 자동화도 가능합니다.
그리고 당근 팀에서는 아래와 같이 Figma variables를 사용해서 컴포넌트 스펙 문서를 피그마와 웹에 자동으로 생성하고 이를 컴포넌트 코드에 전부 동기화하는 방법도 실험하고 있습니다.
그리고 당근 팀에서는 아래와 같이 Figma variables를 사용해서 컴포넌트 스펙 문서를 피그마와 웹에 자동으로 생성하고 이를 컴포넌트 코드에 전부 동기화하는 방법도 실험하고 있습니다.

제가 지금까지 설명한 컴포넌트에 대한 접근과 구현은 모두 거인의 어깨 위에 서서 바라보며 얻어진 것들입니다. 특히 어도비 스펙트럼, Zag.js, Class Variance Authority의 영향을 크게 받았습니다. 혹시 디자인 시스템을 구축하려는 팀이나 깊게 이해하려고 하는 분들은 이 라이브러리들을 살펴보는 것을 추천드립니다.


마치며

오늘 다룬 주제들을 정리해 보면 다음과 같습니다.

  1. 디자인 시스템의 목표 설정
  2. 디자인 토큰의 정의와 활용
  3. 아토믹 디자인이 혼란스러운 이유
  4. 컴포넌트 구성 요소 해부
  5. 디자이너와 개발자의 의사소통 문제 해결
  6. 컴포넌트의 구현과 API 설계

이 내용을 바탕으로 제가 배운 점은 다음과 같습니다. 이 내용은 제가 새로 디자인 시스템을 만들 때 꼭 신경 쓸 부분이기 때문에 기억해 주시면 좋을 것 같습니다.

  1. 디자인 시스템의 목표와 벤치마킹 대상 명확하게 설정하기
  2. 디자인 의도를 인코딩할 때 섣부른 추상화 경계하기
  3. 컴포넌트의 기능/형태를 분리해서 최소 단위 정의하기
  4. 상태 압축으로 의사소통을 개선하고 상태 폭발 제거하기
  5. 관심사 분리를 통한 의사소통의 순환 참조 제거하기
  6. 유저가 컴포넌트를 직접 조립하기 쉬운 환경 제공하기

궁극적으로 위 내용을 바탕으로 일관성과 유연성을 동시에 달성하는 것이 제가 지금까지 디자인 시스템을 만들며 배운 것들입니다. 이 글을 통해서 확신을 가지고 행복하게 디자인 시스템을 만드는 개발자가 더 많아지길 바라며 글을 마칩니다. 감사합니다.


이찬희 (MarkiiimarK)
Never Stop Learning.