창립이래 최초의 디자인시스템 구축하기(feat. 30년만에!)의 2편인 이번에는 시스템 구축을 위한 설계 과정, 사용법 그리고 시스템의 확장에 대한 부분에 대해 소개한다.
시스템 설계에 있어서 가장 기반이 되는 Foundation을 지정하게 되는데, 그중 디자인 토큰은 디자인 시스템 구축에 있어서 기반이 되고 공수가 많이 드는 작업 중 하나다. 다음으로는 설계한 토큰을 실제 컴포넌트에 어떻게 적용하고 피그마에도 활용할 수 있는지 확인하고 마지막으로 컴포넌트의 다형성(Polymorphism)을 위해 Type-Safe한 인터페이스를 만들어 더 확장성 있는 디자인 시스템을 구축하는 단계를 살펴보자.
Foundation
디자인 토큰 설계 시 아래 4가지를 고민하여 설계한다.
- 브랜드 컨셉을 따르는 컬러와 폰트에 대한 대략적인
네이밍 구조설계 - Object Styles, Color 및 Typography에 대한 토큰
네이밍의 의미를 정의 - 토큰값을 서로
참조할 수 있는 구조 설계 테마적용에도 유연하게 반영할 수 있는 구조 설계
먼저 브랜드 디자인 컨셉의 기본이 되는 Foundation을 선정한다. 이는 디자인 시스템의 발판이 되는 구조인데 이 단계에서는 일반적으로 Color, Typography, Size, Border, Spacing, Layout... 등 시스템화할 수 있도록 견고하게 구조화하고 디자인 정책을 확립하는 단계이다.
디자인 토큰
먼저 컬러에 대한 의미를 명확하게 하기 위해 디자인 토큰에 대한 네이밍을 지어준다. 이때 명칭의 기준을 라인 LDSG를 많이 참고했다.

우리는 다소 간소하게 사용하기 위해 Visual Element, Category, Value만을 사용해서 PirmitiveColor값을 선별했다.
const PrimitiveColor = {
'--color-transparent': '#ffffff00',
'--color-black-100': '#000000',
'--color-credit-100': '#fffbc5',
'--color-primary-100': '#e5e4fd',
...
}이처럼 의미에 맞는 위치에 네이밍함으로써 컬러의 hex 값을 일일이 찾아볼 필요가 없다. 그리고 테마 컬러 작업 시에도 PirmitiveColor에 있는 값을 가지고 변경하도록 세팅해 두면 테마 변경에도 문제가 없다.
const LightColorTokenX = {
'--effect-shadow-e0': PrimitiveColor['--color-transparent'],
'--outline-black': PrimitiveColor['--color-black-100'],
'--outline-warning-low-em': PrimitiveColor['--color-credit-100'],
'--outline-primary-low-em': PrimitiveColor['--color-primary-100'],
...
};
const DarkColorTokenX = {
'--effect-shadow-e0': PrimitiveColor['--color-transparent'],
'--outline-black': PrimitiveColor['--color-black-100'],
'--outline-warning-low-em': PrimitiveColor['--color-credit-900'],
'--outline-primary-low-em': PrimitiveColor['--color-primary-800'],
...
};실제 사용하는 코드에선 위에 정의한 컬러 토큰값을 각 테마에 집어넣어서 사용하게 되면 type intelligence를 통해 토큰값을 불러올 수 있게 되고, 개발할 때 의미에 맞는 컬러 값을 직관적인 네이밍으로 인해 개발할 수 있게 된다.
const Wrapper = styled.div`
background-color: ${({ theme }) => theme.coloTokenX["--effect-shadow-e0"]};
`
const CheckBox = styled.button`
&:focus {
outline: ${(props) => props.theme.coloTokenX["--outline-primary-low-em"]} solid 2px;
}
`그리고 사전에, 피그마에서 컬러 Foundation을 지정하여, 각 디자인 토큰을 기반으로 주로 사용되는 컬러에 대한 정보를 더욱 직관적으로 네이밍 해서 관리했다.

이제 피그마에선 Foundation에 정의한 컬러 정보에 그대로 대응할 수 있게 되어 실제 디자인 시스템에서 작성된 컴포넌트나 서비스별 디자인 시안에서 사용되는 컬러값은 토큰을 기준으로 작성되기 때문에 디자인과 개발 간의 괴리감이 없어지게 된다.
디자인 시스템의 모든 부분은 컬러를 지정한 방식과 동일하다. 모두 작성하기에는 너무 많으니, 정책을 잘 수립한 회사의 문서를 참고해 보길 바란다.
컴포넌트
이제 토큰을 정했으니 실제 UI 컴포넌트에 적용할 단계이다. 가장 많이 사용되는 컴포넌트인 Button을 가지고 확인해 보자.
default 색상은 surface/primary, hover 색상은 surface/primiary_accent_3, disabled 색상은 surface/primiary_accent_2_transparent와 같이 시스템화시킬 수 있다.

모달에 적용되는 버튼을 확인해 보면, size, type, status, fill 정보를 선택만 하게 되면 디자인 시안을 작성할 때에도, 개발을 할 때에도 더 이상 화면마다 다른 코드와 디자인을 작성할 필요가 없게 된다.

확장성
그러면 이제 확장해서 사용해 볼 수 있다. 아래는 안내창의 디자인에서 상용되는 버튼이다. 디자인 요구 사항에 따라 좌우에 아이콘을 넣는 니즈가 있어 leftIcon, rightIcon을 확장시켜서 넣을 수 있다. 또 버튼에 로딩을 넣고 싶은 니즈가 있어 isLoading을 확장시킬 수 있다. 만약 아이콘 하나만 필요한 버튼이라면? leftIcon, rightIcon 중 하나만 작성하고 내부 text에 대한 정보를, 옵션을 받으면 된다.

디자인 시스템은 적재적소에 맞는 요구사항에 대응하기 위해 기존 컴포넌트의 확장을 위해 옵션을 줄 수 있도록 설계하고 구성해야 한다. 이제 코드로 구성을 해보자.
사용하기
Button에 대한 구조는 props를 그대로 내려받아 style 파일로 던져지는 단순한 구조로 구성된다.
// Button.tsx
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({
label,
color = 'primary',
size = 'md',
leftIcon,
rightIcon,
disabled = false,
isLoading,
...props
}, ref
) => {
...
return (
<ButtonContainer
ref={ref}
disabled={isLoading || disabled}
$size={size}
$color={color}
{...props}
...
>
{isLoading ?
(<Loading size="xs" />) :
(<>
{leftIcon}
{label}
{rightIcon}
</>)}
</ButtonContainer>
)}
);style 파일의 primary, secondary 색상만 확인해 보자. color, background-color에 대한 정보, hover와 disabled시 정보를 지정하면 된다.
// Button.style.ts
const ButtonColorStyles = {
primary: css`
color: ${(props) => props.theme.coloTokenX['--text-white']};
background-color: ${(props) => props.theme.coloTokenX['--surface-primary']};
@media (hover: hover) and (pointer: fine) {
&:hover {
background-color: ${(props) =>
props.theme.coloTokenX['--surface-primary-accent-3-transparent']};
}
}
&:disabled {
color: ${(props) => props.theme.coloTokenX['--text-disabled-transparent']};
background-color: ${(props) =>
props.theme.coloTokenX['--surface-primary-accent-2-transparent']};
}
`,
secondary: css`
color: ${(props) => props.theme.coloTokenX['--text-high-em']};
background-color: ${(props) => props.theme.coloTokenX['--surface-surface-2']};
@media (hover: hover) and (pointer: fine) {
&:hover {
background-color: ${(props) => props.theme.coloTokenX['--surface-surface-3']};
}
}
&:disabled {
color: ${(props) => props.theme.coloTokenX['--text-disabled']};
background-color: ${(props) => props.theme.coloTokenX['--surface-surface-2']};
}
`,
...
}이제 사용하는 곳에서는 HTMLButtonElement타입을 그대로 받을 수 있게 하여 button 태그의 속성, 이벤트 핸들러, aria 등 모두 적용이 되도록 하고, props로 내려받는 color, size 등에 따라 내부에서 지정한 컬러와 크기, 아이콘 등을 규격화해서 사용이 가능해진다.
<Button label="Button" color="primary" />
<Button label="Button" size="xs" isLoading />
<Button label="Button" size="xs" leftIcon={/* 아이콘 */} rightIcon={/* 아이콘 */ />마무리
컴포넌트의 다형성을 고려한 설계와 적용 방법에 대한 주제로 Type-Safe하게 다형성(Polymorphism) 지원하기는 별도 Tech 분류로 나뉘어 작성했으니 참고해보면 좋을 것 같다.
이번 글에서는 디자인 시스템 구축을 위해 참고한 자료를 통해 현업에서는 어떻게 구성했는지, 실제 코드 구성을 어떻게 짰지, 시스템 확장을 위해 어떤 고민과 니즈를 해결했는지에 대해 전달했다.
현업에서는 시스템을 구축하기 위해 많은 리소스가 필요하다. 그 때문에 대기업이나 디자이너, 개발자에게 따로 리소스를 할당해 주는 회사는 많지 않다. 따라서 모든 것을 완벽하게 설계하고 구축하는 것은 어려운 환경이 대부분이다.
따라서 회사 상황에 맞게 최소한의 시스템을 만들면서 점차 시스템을 확장해 나가는 수밖에 없다. 그리고 서로의 니즈가 충족될 수 있도록 지속적으로 팔로업하면서 리드를 해야 한다. 나는 이 일이 결국 나와 팀, 회사를 위한 투자이며, 성숙한 서비스를 개발할 수 있는 단계 중 하나라고 생각하며 시작했다. 서비스를 성숙함은 단순히 기능 개발에만 그치지 않는다. 그 과정을 어떤 식으로 해결해 나가고, 어떻게 하면 불필요한 리소스를 덜어내면서 좋은 서비스를 개발할지에 대해 고민하고 직접 적용하는 것도 중요하다.