잘 쓰이는 디자인 시스템을 위한 여정

서지
29CM TEAM
Published in
18 min readApr 17, 2024

안녕하세요, 29CM 프론트엔드 플랫폼 개발자 서진혁입니다.

29CM는 작년 하반기부터 디자인 시스템 Ruler를 개발하기 시작했습니다. 50% 이상 완성된 지금, 저희가 어떤 디자인 시스템을 만들고자 했는지, 그리고 그 과정에서 어떤 고민과 실행들을 했는지 공유해보려고 해요.

29CM 서비스의 상황

29CM의 프론트엔드는 Mono-Repo 구조로 MFA(Micro Frontend Architecture)를 지향하고 있습니다.
이는 여러 목적 조직이 서로 다른 도메인에서 제품을 개발하기 때문인데요.

예를 들어, 검색 기능을 배포했는데 주문 시 문제가 발생한다면 유저분들은 구매를 할 수 없을테고 서비스는 그에 대한 손실을 보게 됩니다

즉, 실시간적으로 결제가 이뤄지는 거대한 서비스이므로 연관되어 있는 도메인이 아니라면 문제가 발생해도 사용자가 불편함을 겪으면 안 되기 때문에 저희는 MFA를 선택하게 되었습니다.

하지만 여러 도메인에서 병렬적으로 진행되다 보니 UI 파편화는 피할 수 없는 문제였는데요.

위 그림을 보시면 버튼이 사용자에게 다양한 형태로 나타나고 있습니다.
29CM 앱을 조금만 살펴보면 이런 UI 파편화 문제를 쉽게 확인할 수 있어요.

UI 파편화는 사용자에게 혼란을 주고 불필요한 재학습을 요구합니다.

또힌 내부적으로는 지속적인 의사소통과 개발 비용을 높여 생산성 저하를 초래합니다.
이러한 이유로 UI가 계속 생겨나는 것은 단기/장기적으로도 전혀 좋지 않습니다.

디자인 시스템

이러한 문제를 해결하기 위해 많은 기업과 서비스들이 디자인 시스템을 도입하기 시작했습니다.

디자인 시스템에 대해서 더 알고 싶다면? 링크

디자인 시스템이란, 서비스의 디자인 결정을 담은 UI 구성 요소를 포함하여 디자이너와 개발자가 협업하여 제품을 만들 때 사용하는 공통의 도구입니다. 여기에는 디자인 가이드와 컴포넌트가 모두 포함되어 있습니다.

29CM의 디자인 플랫폼 팀에서는 앞서 언급한 문제들을 디자인 시스템을 통해 개발 시점부터 해결할 수 있다고 판단했습니다.

그렇다면 어떻게 디자인 시스템을 시작해야 할까요?
디자인 플랫폼 팀과 저는 우선 컴포넌트를 빠르게 제공하는 것이 중요하다고 생각했습니다.
이는 프론트엔드 개발자들이 가장 많은 시간을 소모하는 부분이 UI 구현이기 때문입니다.

29CM의 디자인 시스템, Ruler

이렇게 29CM의 디자인 시스템 Ruler의 개발이 시작되었습니다.
먼저, 왜 이름을 Ruler로 지었을까요?
이름을 고민할 당시 29mm, Ruler, Moyang(모양) 등 여러 후보가 있었는데, 29CM라는 서비스 이름과 길이를 재는 도구인 자가 잘 어울려 Ruler로 결정되었습니다.

사실 29CM의 서비스가 Angular 기반(Angular to React에 대한 내용은 여기)이었을 때 공용 UI 라이브러리의 이름이 Ruler였어서 반감을 가지진 않을까 걱정도 했지만, 다행히 지금은 Ruler의 정체성을 다시 잡는 데 성공했습니다.

디자인 시스템을 만드는 규칙과 마음가짐

본격적인 시작에 앞서,
서비스 전반에 걸쳐 시각적 일관성을 보장하고 반복되는 작업을 줄이는 디자인 시스템의 본질을 잃지 않으면서, 최대한 빠르게 구성원들에게 제공하고 잘 활용될 수 있도록 몇 가지 규칙을 정하기로 했습니다.
소개하기 부끄럽지만, 강한 의지로 디자인 시스템을 개발하는 과정에서 가장 큰 힘이 되었던 것은 이 규칙이었어요.

첫 번째, 코드레벨의 유연성을 지킬 것이다.
디자인 가이드가 확실히 지켜지면 개발자는 의도를 따라가게 됩니다. 의도를 벗어날 수 있는 부분만 제약된 인터페이스를 제공하고, 유연한 인터페이스를 기본으로 합니다.

두 번째, 이해관계자를 최소화한다.
초기 단계의 시스템에서는 규칙을 강하게 지켜야 합니다. 이해관계자를 줄이고 팀의 싱크 레벨을 유지해야 합니다. 리소스가 부족하다면 제가 더 열심히 하면 됩니다.

세 번째, 디자인 시스템은 하나의 제품이다.
디자인 시스템도 지속 가능한 제품으로 보고, 그 가치를 지속적으로 측정하고 개선할 수 있는 체계를 마련해야 합니다.

그럼 어떻게 잘 쓰이는 디자인 시스템을 빠르게 만들었는지 이야기해볼게요.

컴포넌트 구현에 들어가는 시간을 최소화하기

Ruler 팀에서 컴포넌트를 구현하는 과정은 다음과 같습니다

개발할 컴포넌트 논의 -> 컴포넌트 본질에 대한 논의 -> 디자인 가이드 제작 -> 리뷰 -> 컴포넌트 구현 -> 배포와 적용 -> 측정과 개선

컴포넌트 구현은 전체 프로세스 중 짧은 과정이며,
이 부분을 최소화하면 시스템과 컴포넌트 자체에 대한 논의와 설계, 측정과 개선에 더 많은 리소스를 투입할 수 있습니다.
그렇다면 필수적으로 UI/UX에 고민이 집중되어야 했습니다.

저희는 이러한 Logical한 기능의 집중을 피하기 위해 Headless UI를 도입했습니다.

Headless UI가 무엇인지 제 친구 ChatGPT가 친절하게 답해주고 있는데요

간단히 설명드리자면 스타일이 없는 UI 라이브러리입니다.
내부적으로 상태가 관리되고 동작도 하지만 스타일이 HTML 브라우저의 default 스타일이라서 원하는 스타일을 주입하면 깔끔하게 동작하면서 접근성도 고려된 컴포넌트를 만들 수 있습니다.

Ruler는 디자인 시스템을 위한 Headless UI인 ark-ui를 사용하게 되었습니다.

왼쪽은 스타일이 없는 Headless UI의 모습이고, 오른쪽은 Headless UI에 스타일만 주입한 모습입니다.
이렇게 Headless UI를 활용하여 상태 관리와 동작 구현에 대한 리소스를 줄였습니다.

이제 스타일 관리를 해야 합니다.모든 디자인 시스템이 그렇듯, Ruler도 하나의 컴포넌트에 여러 가지 스타일이 존재합니다.

Ruler 버튼을 일부분 모아둔 피그마 내 사진입니다.

위에서부터 정보의 위계를 결정하는 priority와 크기를 결정하는 size에 따라 여러 스타일의 버튼이 존재합니다.

이런 식으로 각 컴포넌트 내부에서 prioritysize를 매번 직접 구현할 수도 있겠지만…

이는 컴포넌트를 개발할 때마다 매번 작성해야 하며,
새로운 prioritysize가 추가될 때마다 구현체를 찾아 수정해야 하므로 비효율적입니다.

29CM의 높은 기준으로, 확실하게 리소스를 줄이는 방법이 아니라고 생각했습니다.

따라서, Ruler의 구조에 맞게 매번 재사용할 수 있는 체계를 만들었습니다.

{
large: {
iconSize: 18,
spinnerSize: 'medium',
typography: 'text-l-bold',
minWidth: '88px',
height: '52px',
minHeight: '52px',
padding: {
default: `0 ${vars.$scale.dimension.dimension250}`,
hasPrefixIcon: `0 ${vars.$scale.dimension.dimension250} 0 ${vars.$scale.dimension.dimension200}`,
hasActionIcon: `0 ${vars.$scale.dimension.dimension150} 0 ${vars.$scale.dimension.dimension250}`,
},
borderRadius: '4px',
},
medium: {
iconSize: 16,
spinnerSize: 'medium',
typography: 'text-l-bold',
minWidth: '84px',
height: '44px',
minHeight: '44px',
padding: {
default: `0 ${vars.$scale.dimension.dimension225}`,
hasPrefixIcon: `0 ${vars.$scale.dimension.dimension225} 0 ${vars.$scale.dimension.dimension150}`,
hasActionIcon: `0 ${vars.$scale.dimension.dimension150} 0 ${vars.$scale.dimension.dimension225}`,
},
borderRadius: '4px',
},
// ....
}
{
primary: {
enabled: {
iconColor: 'onColor',
typographyColor: 'onColor',
backgroundColor: vars.$semantic.color.fill.primary,
borderColor: 'transparent',
spinnerColor: 'white',
},
hover: {
iconColor: 'onColor',
typographyColor: 'onColor',
backgroundColor: vars.$semantic.color.fill.primaryHover,
borderColor: 'transparent',
spinnerColor: 'white',
},
pressed: {
iconColor: 'onColor',
typographyColor: 'onColor',
backgroundColor: vars.$semantic.color.fill.primaryPressed,
borderColor: 'transparent',
spinnerColor: 'white',
},
disabled: {
iconColor: 'disabled',
typographyColor: 'disabled',
backgroundColor: vars.$semantic.color.fill.disabled,
borderColor: 'transparent',
spinnerColor: 'white',
},
},
// ...
}

이렇게 size priority 에 필요한 스타일 속성들을 상수로 모아 관리하고,

${({ size }) => getSizeStyle({ size })};

${({ priority }) => getPriorityStyleByState(priority, 'enabled')};

&:hover {
${({ variant }) => getPriorityStyleByState(priority, 'hover')};
}

&:active {
${({ priority }) => getPriorityStyleByState(priority, 'pressed')};
}

&:disabled {
${({ priority }) => getPriorityStyleByState(priority, 'disabled')};
}

구현체에서는 함수를 통해 스타일을 생성하여 주입합니다.

그러면 새로운 sizepriority가 추가되면 상수 파일만 조금 수정하면 바로 대응할 수 있는 구조가 됩니다.

상상해볼까요? Button에 새로운 priority인 information이 추가되었다고 가정해보겠습니다.

상수 파일에 information 속성에 해당하는 스타일을 추가하면, 새로운 버튼이 즉시 대응됩니다.
(gif 속도가 느려서 3배로 올렸더니 마우스 커서가 좀 빠르게 움직이네요)

이렇게 상태 관리와 입력 해석 같은 어려운 구현은 Headless UI에 위임하고, 관리 체계를 통해 시각 해석의 과정도 크게 줄였습니다.

컴포넌트 구현에 들어가는 시간을 줄이는 데 성공했는데, 이렇게 절약된 리소스를 어떻게 활용했을까요?

측정 도구 만들기

사용되고 있는 컴포넌트와 쓰면 좋은 컴포넌트를 알고 있지만 실제로 어디에서 얼마나 쓰이는지,
그리고 일관성을 유지하고 반복 작업을 줄이고 있는지 확인할 방법이 필요했습니다.

피그마는 라이브러리 사용을 집계해주는 툴을 제공하지만
각 컴포넌트가 몇 번이나 재사용되었는지를 집계하는 데 그칩니다.

페이지 하나의 디자인이라도 여러 유저 플로우를 각각 그려야 하기 때문에
하나의 실행에서도 같은 컴포넌트가 여러 번 사용될 수 있습니다.

팀에서는 하나의 컴포넌트가 몇 번의 실행에서 사용되었는지를 알고 싶었기 때문에 측정 도구를 직접 만들어야 했습니다.

다행히 피그마에서 REST API를 제공하여 이를 활용하기로 했습니다.

스크립트 순서를 간단하게 적어볼게요.
1. 피그마 파일을 읽고 측정에 필요한 페이지를 가져옵니다.
2. 페이지 내부의 프레임을 모두 읽어 사용된 컴포넌트를 측정합니다.

생각보다 쉽죠? 하지만 몇 가지 문제가 있었고
이 문제들을 해결해야 정확한 측정이 가능해집니다.

🤔 읽어야 할 파일을 어떻게 구분하지?
목적 조직과 관련 없는 파일까지 읽으면 정확한 측정이 어렵습니다.

🤔 페이지는 어떻게 측정하지?
실행 후 롤백한 디자인, 아이디에이션 페이지 등 다양한 페이지가 포함되어 있어 필요한 페이지를 정확히 구분해야 합니다.

저희는 피그마의 구조를 개선하는 방법으로 이 문제를 해결했습니다.

🤩 파일은 이미 구분되고 있었네?
각 목적 조직은 한 분기당 하나의 피그마 파일을 만들고 있습니다
그렇다면 해당 파일의 ID를 스크립트 내부에서 관리할 수 있습니다.

const squads: Squad[] = [
{squad: "Search", file: ""},
{squad: "Content", file: ''},
{squad: "Order", file: ""},
{squad: "Activation", file: ""},
{squad: "Recommendation", file: ""}
// .....
];

🤩 측정 페이지에 접두사(Prefix)를 사용하면 어떨까?
페이지 이름에 접두사를 붙여 상태를 명확히 구분합니다.
기존에는 페이지 이름에 실행의 이름만 포함되었지만 이제는 상태를 나타내는 접두사를 추가해 측정에 필요한 페이지만 필터링할 수 있도록 했습니다.

또한 메인 디자인들을 섹션으로 묶어 동일한 방식으로 접두사를 붙였습니다.

왼쪽 (구조 개선 전) 오른쪽 (구조 개선 후)

기존에는 페이지 이름이 실행의 이름뿐이었고, 각 실험의 상태를 빈 페이지로 구분했습니다.

이제는 페이지 이름에 상태에 대한 접두사(prefix)를 붙여 측정에 필요한 페이지만 필터링할 수 있도록 개선했습니다.
또한, 아이디에이션을 제외한 메인 디자인들은 섹션으로 묶어 동일한 방식으로 접두사를 붙였습니다.

이렇게 스크립트를 작성할 준비를 마쳤고 Node.js 기반의 스크립트를 작성했습니다.
이 스크립트를 통해 보다 체계적으로 데이터를 관리하고 분석할 수 있게 되었습니다.

function getTargetPages(file: string) {
const { data } = await axios.get(`https://api.figma.com/v1/files/${file}?depth=1`, { headers: { "X-FIGMA-TOKEN": "" } });

return data.document.children.filter(page => TARGET_FLAG.some(flag => page.name.includes(flag))).map(page => page)
}

먼저 파일에서 페이지를 가져온 후 접두사 기반으로 필터링합니다.

async function getSectionIds(pageIds: string[], file: string) {
const { data } = await axios.get(`https://api.figma.com/v1/files/${file}/nodes?ids=${pageIds.join(",")}&depth=1`, { headers: { "X-FIGMA-TOKEN": "" } });

const children = pageIds.reduce((prev, curr) => { return [...prev, ...data.nodes[curr].document.children] }, [])

return children.filter(node => TARGET_FLAG.some(flag => node.type === "SECTION" && node.name.includes(flag))).map(section => section.id)
}

필터링한 페이지의 ID를 이용해 접두사가 붙은 섹션을 찾습니다.

async function getComponentSets(sectionIds: string[], file: string) {
const { data } = await axios.get(`https://api.figma.com/v1/files/${file}/nodes?ids=${sectionIds.join(",")}`, { headers: { "X-FIGMA-TOKEN": "" } });

const componentSets = sectionIds.reduce((prev, curr) => { return {...prev, ...data.nodes[curr].componentSets} }, {})

return Object.values(componentSets);
}

섹션에서 사용된 컴포넌트를 찾아 데이터를 가공하여 저장합니다.

측정된 데이터는 Notion API를 통해 Notion 테이블에 적재합니다.

이렇게 만들어진 측정 도구를 통해 Ruler 컴포넌트가 몇 번의 실행에서 얼마나 사용되고 있는지 추적할 수 있게 되었습니다.

앞으로의 과제

이제 하나의 실행에서 평균 몇 개의 컴포넌트를 활용하는지는 측정이 가능하지만, Ruler가 실제로 화면의 몇 %를 커버하는지는 측정이 불가능합니다.

이 문제는 프레임 단위로 커버리지를 측정할 수 있는 피그마 플러그인을 개발하여 해결할 예정입니다.
아직 갈 길이 머네요 😥

번외: Figma Codegen API를 통한 코드 자동 완성

Figma에서 devMode를 공개하면서 Codegen API를 지원하기 시작했습니다. 이를 활용하면 다양한 방식으로 코드 자동 완성을 지원할 수 있어요

button 컴포넌트의 코드가 자동 완성되는 모습

저희는 아직 모든 컴포넌트에 적용되지는 않았지만,
이런 방법도 있다는 것을 공유하고자 합니다.
점차 적용 컴포넌트를 늘려가면서 UI 개발 비용을 0에 가깝게 만드는 것이 목표입니다.

마치며

지금까지 29CM의 디자인 시스템 Ruler를 개발하며 겪었던 고민과 실행들을 공유드렸습니다.
본질을 잃지 않으며 잘 활용될 수 있도록 한 노력들이 이 글을 읽는 분들께 도움이 되기를 바랍니다.

마지막으로 Ruler를 함께 만들어가는
플랫폼 디자이너 윤민희님 (곧 나올 민희님의 포스팅도 기대해주세요)
iOS 개발자 박형석님
안드로이드 개발자 김평수님

그리고 항상 개선을 위해 좋은 의견을 주시는 프론트엔드, 프로덕트 디자인 팀 모두에게 감사의 인사를 전합니다.

감사합니다!

29CM CAREER

함께할 동료를 찾습니다.

29CM는 ‘고객의 더 나은 선택을 돕는다’라는 미션으로 출발했습니다. 우리는 우리만의 방식으로 콘텐츠를 제공하며, 브랜드와 고객 모두에게 대체 불가능한 커머스 플랫폼을 만들어가고 있습니다. 이 미션을 이루기 위해 우리는 흥미로우면서도 복잡한 문제들을 해결하고 있습니다. 만약 우리와 함께 이 문제들을 해결해 보고 싶다면, 주저하지 말고 29CM에 합류하세요!

🚀 29CM 채용 페이지 : https://www.29cmcareers.co.kr/

--

--