bandal.dev

주니어 개발자의 디자인 시스템 구축 회고

이직 후 처음 맡은 디자인 시스템 구축 프로젝트의 중간 과정에서 느낀 점들을 공유합니다.

21
디자인 시스템
프론트엔드

이직하고 이제 막 3개월이 지난 3년차 프론트엔드 개발자입니다.
이직 후 첫 과제로 회사의 디자인 시스템 구축을 맡게 되었는데요, 처음엔 낯선 환경에서 중요한 프로젝트를 맡게 되어 기대도 되고 걱정도 많이 있었지만,
지금은 프로젝트가 어느 정도 진행된 것을 보고 저에게 정말 값진 자산이 되었음을 느끼고 있습니다.
이번에 디자인 시스템을 구축하면서 마주했던 고민과 경험을 공유하고 싶어 이렇게 글을 쓰게 되었습니다.

왜 디자인 시스템을 구축하게 되었나요?

우리 회사는 여러 제품을 개발하고 있었는데, 각 제품들이 독립적으로 UI 컴포넌트를 가지고 있었습니다.
같은 컴포넌트를 구현한 거였지만 제품마다 다른 소스코드로 구성되어 있었고, 새로운 개발자는 각 프로젝트에 투입되기 위해선 새로운 컴포넌트 학습이 필요할 수 밖에 없었습니다.

이런 문제에서 탈피하고자 디자인 시스템을 통해 여러 가지 이점들을 얻고자 했습니다.

  1. 제품은 필요한 컴포넌트를 꺼내쓰기만 하면 되도록 컴포넌트 라이브러리를 제공합니다.
  2. 디자인이 변하더라도 토큰을 사용하여 UI를 쉽게 변경할 수 있도록 합니다.
  3. Storybook을 활용하여 사용하는 개발자의 이해를 돕고, 사용 중 발생할 수 있는 오류를 미리 방지합니다.
  4. 컴포넌트의 일관성을 유지하고, 중복 작업을 줄여 생산성을 높입니다.

디자인 시스템 구축 전 고려사항

디자인 시스템을 구축하게 되면서 가장 중요히게 생각한건 지속 가능성이었습니다.
규칙을 정한 뒤 문서로 남기고, 이를 기반으로 컴포넌트를 구현함으로써 지속 가능한 디자인 시스템을 구축하고자 했습니다.
또한 복잡하고 유지보수 어려운 컴포넌트 개발을 방지하여, 다른 팀원들이 쉽게 사용하면서 누구나 Contribute할 수 있는 환경을 만들고 싶었습니다.

이러한 목표를 달성하기 위해 아래의 핵심 원칙들을 정하고 이를 기반으로 컴포넌트를 구현하고 있습니다.

  1. 예측 가능한 컴포넌트 인터페이스를 제공합니다.
    • 컴포넌트의 인터페이스는 직관적이고 일관되어야 합니다.
    • 시맨틱하고 일관된 Prop 네이밍을 모든 컴포넌트가 동일한 패턴을 따르도록 설계했습니다.
  2. 단순하고 명확한 컴포넌트를 설계합니다.
    • 복잡한 코드는 사용하는 개발자가 코드를 이해하기 어렵게 만듭니다.
    • 컴포넌트의 기능을 최소화하고, 필요한 경우에만 확장할 수 있도록 설계했습니다.
  3. 디자이너와 간극을 줄입니다.
    • 디자인 의도와 기술적 제약사항을 상호 이해하고 소통합니다.
    • Storybook을 활용하여 디자인-개발 간 피드백 루프를 짧게 유지합니다.
    • 디자인 QA를 통해 높은 퀄리티를 유지합니다.
  4. 단계적으로 접근합니다.
    • 처음부터 완벽한 디자인 시스템을 구축하기보다는 점진적으로 개선하고 확장합니다.
    • 핵심 컴포넌트부터 시작하여 요구사항이 생길 때마다 확장하고 개선합니다.

디자인 시스템 구축하기

디자인 시스템을 처음부터 설계하고 구축하는걸 맡게된 저에게 기술 스택 선정은 어려운 고민과 선택의 연속이었습니다.
여러 프로젝트에 적용에 어려움이 없어여 하고, 다른 팀원들의 러닝커브를 최소화하기 위해 아래의 선택과 집중이 필요했습니다.

Styling

SCSS Module

  • CSSOM 기반으로 높은 성능을 보장하며, IDE 자동완성 지원으로 개발 생산성도 뛰어났습니다.
  • SCSS의 연산자와 중첩 구조 같은 강력한 기능들이 CSS의 한계를 보완해주어 복잡한 스타일 로직을 효과적으로 관리할 수 있었습니다.
  • 다만 SCSS 고유 기능(@function, @mixin 등)을 사용할 때 모듈간 의존성 관리가 복잡해질 수 있다는 점을 고려해야 했습니다.

Tailwind CSS

  • 설정 기반의 유틸리티 클래스 자동 생성으로 빠른 개발이 가능하고, 컴포넌트 분리를 통해 높은 생산성을 기대할 수 있었습니다.
  • 디자인 토큰 기반의 일관된 디자인 시스템 구축에 매우 적합하다는 장점이 있었습니다.
  • 하지만 팀원들의 Tailwind 학습 곡선과 더불어, hover, focus 같은 상태값이나 before 같은 가상 클래스를 다룰 때 className이 지나치게 길어져 코드 가독성이 크게 저하된다는 단점이 있었습니다.
  • 디자인 시스템을 도입하려는 모든 제품에 Tailwind 설치가 강제된다는 점도 부담이었습니다.
  • CSS Splitting이 지원되지 않아 전체 Tailwind 클래스가 단일 CSS 파일로 번들링되는 것도 큰 단점으로 작용했습니다.

Styled, Emotion

  • 친숙한 CSS 문법과 뛰어난 TypeScript 지원으로 개발자 경험이 탁월했습니다.
  • 그러나 런타임에서 CSS를 동적으로 생성하는 방식이 CSSOM 구조보다 성능이 떨어졌고, 특히 RSC를 지원하지 않는다는 점이 치명적이었습니다. Next.js 기반 제품을 개발하는 우리 팀에겐 이 제약이 매우 큰 단점으로 작용했습니다.

결론적으로 SCSS Module을 선택했습니다. CSSOM기반으로 높은 성능을 보장하면서도, SCSS의 강력한 기능들을 활용하여 효율적인 개발이 가능했기 때문입니다.
우려되었던 단점은 SCSS Variable이나 @use 대신 CSS Variable만을 사용하여 SCSS 간 의존성을 제거하는 방식으로 진행하였습니다.
또한, 빌드 이후엔 번들러가 컴포넌트별로 CSS를 분리하고, minified된 CSS 파일을 생성하게 되어 성능에 대한 우려도 해소할 수 있었습니다.

SCSS Module로 만든 Variants Class의 Storybook 문서
Variants 규칙 문서

Component

직접 구현

  • 컴포넌트 라이브러리를 직접 구현하는 방법도 고려해봤습니다.
  • 대부분의 요구사항에 대응이 가능할 것이고, 직접 구현하면서 컴포넌트의 동작 원리를 이해할 수 있어 개발자 경험을 향상시킬 수 있을 것이라고 생각했습니다.
  • 다만, 시간이 많이 소요될 것이고, 테스트 및 문서화에 더 많은 공수가 들어갈 것이 예상되어 제외했습니다.

Radix-ui

  • UI 로직이 미리 정의된 Headless UI 라이브러리로, Shadcn-ui의 인기에 힘입어 커뮤니티가 매우 활성화되어 있습니다.
  • 필요한 컴포넌트의 의존성만 설치할 수 있고, 단일 책임 원칙하에 컴포넌트 요소마다 분리되어 있어 커스텀이 용이합니다.
  • 하지만 컴포넌트 요소별로 지나치게 분리되어 있어 JSX 구조가 복잡해질 수 있고, 컴포넌트 조합 시 개발자들이 혼란을 겪는 경우가 있는 듯 보였습니다.
    https://github.com/radix-ui/primitives/issues/1836

React-Aria-Components

  • Adobe에서 관리하는 만큼 활발한 업데이트와 안정적인 유지보수가 이루어지고 있습니다.
  • 다양한 입력 방식(터치, 키보드, 스크린리더)에 대한 세밀한 인터랙션 지원이 돋보였습니다
    • dialog 내에서 textfield에 포커싱될 경우
      • radix는 화면 확대 및 키보드에 TextField가 가려집니다.
      • aria는 dialog가 키보드 영역 위로 올라갑니다.
  • 컴포넌트 외에, 기능만을 위한 Hook 또한 제공하고 있어서, 커스텀이 용이하다는 점이 매우 큰 장점으로 작용했습니다.

결론적으로 React-Aria-Components를 선택하게 되었습니다. 다양한 인터랙션에 대한 포괄적인 지원, Adobe의 안정적인 유지보수, 뛰어난 접근성과 성능이 주된 이유였습니다.
직접 구현 대비 개발 시간을 크게 단축할 수 있었고, 활성화된 커뮤니티와 높은 코드 품질 덕분에 장기적인 유지보수도 용이할 것으로 판단했습니다.

React-Aria + Variants Class를 적용한 Button 컴포넌트
Button 컴포넌트

Build Tool

tsup

  • ESBuild 기반의 빠른 빌드 속도와 작은 번들 사이즈가 매력적이어서 처음에는 tsup을 도입했고 실제로 운용했었습니다.
  • 그러나 몇 가지 심각한 문제점들이 발견되었습니다:
    • SCSS 지원이 미흡하여 별도의 번들링 설정이 필요했습니다.
    • 컴포넌트별 진입점(index.ts)을 생성하는 구조에서 프로젝트 규모가 커질수록 타입 정의 파일(.d.ts) 생성 시간이 기하급수적으로 증가했습니다.
    • 결국 Out of Memory(OOM)문제가 발생하였고, 이는 tsup의 오래된 Issue#920과 연관된 것으로 확인되었습니다.
  • 이러한 확장성 문제로 인해 다른 대안을 찾아야 했습니다.

Vite

  • Vue.js 팀에서 만든 빠른 빌드 속도와 더불어, React 지원이 강화되면서 React 프로젝트에도 적용이 가능해졌습니다.
  • tsup보다도 간단하게 설정이 가능하고, SCSS 지원도 뛰어나며, 번들 파일을 체계적으로 관리할 수 있었습니다.
  • 또한, 한글로 문서가 번역되어 있어서 빠르게 적용할 수 있었고, 빠른 빌드 속도와 더불어 더 나은 개발 경험을 제공해주었습니다.

Vite로 변경한 후 빌드속도는 tsup를 사용할 때 3~40초 이상(혹은 OOM)에서 vite로 변경 후 10초 이내로 줄어들었고, 번들 파일 또한 tsup보다 더욱 관리가 용이해졌습니다.
회사 Ci 환경에서는 5초도 안걸리는데, 집 데스크탑으로 하니 7초가 나오는네요. 바꿀 때가 된걸까요..
241104-221437

사용성 개선하기

컴포넌트를 개발할 때에도 많은 고민들이 있었지만, 실제 사용자가 편하게 사용할 수 있는가, 성능을 보장하는가를 고민하는 것이 가장 큰 핵심이었습니다.
그 중 특히 기억에 남는 Form 관련 관련 컴포넌트를 개발하면서 겪은 문제와 해결 과정을 공유하고자 합니다.

문제사항

처음에는 shadcn-ui의 Form 구현 방식을 참고했습니다. 하지만 실제 적용 과정에서 사용이 불편하다는 피드백과 몇 가지 심각한 문제점들이 발견되었습니다.

1. 성능 이슈

  • Input이 Controlled Component로 구현되어 모든 입력마다 불필요한 리렌더링이 발생했습니다.
  • 500개의 입력 필드 테스트 결과
    • 메모리 사용량: heap 44MB, tab 450~600MB
    • 각 Input의 최초 Focus 및 입력 시 프리징 현상 발생

2. 복잡한 코드 구조

<FormField
  control={control}
  name={"field1"}
  rules={{ required: "Field 1 is required" }}
  render={({ field }) => (
	<Box display={"flex"} align={"center"} gap={"sm"}>
	  <FormLabel isRequired>Field 1</FormLabel>
	  <div>
		<Input {...field} />
		<FieldError />
	  </div>
	</Box>
  )}
/>
  • 단순한 입력 필드를 위해 과도하게 많은 컴포넌트 중첩
  • 반복되는 보일러플레이트 코드
  • 낮은 코드 가독성

개선사항

이러한 문제를 해결하기 위해 고차 컴포넌트(HOC)를 활용하였습니다.

1. 구조 단순화

  • 구성이 복잡한 레이아웃 배치는 디자인 시스템의 규칙에 따라 prop으로 제어하도록 하였습니다.
  • 접근성 속성(htmlFor, aria-invalid 등)을 HOC 내부에서 자동 주입되도록 구현하였습니다.
  • 핵심 기능만 남긴 컴포넌트에 HOC를 적용하는 방식으로 단순화 하였습니다.

2. 사용성 개선

// design-system/Input.tsx
function Input ({}: HTMLInputElement) {}
const InputField = withFormField(Input); // 접근성, 레이아웃, Invalid 등 자동 처리
 
// production/page.tsx
<InputField
  label="field1"
  isRequired
  error={errors.username}
  {...register("username", {
    required: "사용자 이름을 입력해주세요",
  })}
/>

이러한 개선을 통해:

  • 동일한 500개의 입력 필드 테스트 결과
    • heap: 16mb / tab: 170mb 로 메모리 사용량이 크게 감소하였습니다.
    • 최초 Focus 및 입력 시 발생하던 프리징 현상이 더 발생하지 않았습니다.
  • 코드의 가독성과 유지보수성을 높일 수 있었습니다.
  • 불필요한 리렌더링 감소로 성능이 개선되었습니다.
  • JSX 구조 단순화로 사용이 편리해졌습니다.

이 후 계획

지금까지 만들어진 디자인 시스템을 기반으로, 기존의 제품들을 리뉴얼하고 새로운 제품을 개발하는 과정에서 디자인 시스템을 활용하고 있습니다.
디자인 시스템이 자리를 잡아가고 있으니, 이제 더 안정적으로 운용하기 위해 다음과 같은 계획을 세우고 있습니다.

  1. Changesets을 활용한 버전 관리
  • Changeset을 활용하여 커밋 단위로 버전을 관리하고, 변경 사항을 추적할 수 있도록 할 예정입니다.
  • 버전 관리를 통해 다른 팀원 및 기획, 디자이너와의 협업을 더욱 원활하게 할 수 있을 것이라 기대합니다.
  1. 디자인 토큰 자동화
  • 지금은 디자이너분께서 Figma를 통해 제공하는 토큰을 JSON 파일에 수동으로 입력하고 있습니다.
  • 이를 자동화하여 Figma에서 변경된 토큰을 실시간으로 반영할 수 있도록 구현해볼 계획입니다.

마치며

디자인 시스템을 구축하는 동안 마주한 다양한 도전과 시행착오들은 값진 학습 경험이 되었습니다.
특히 React-Aria를 활용하면서 얻은 인사이트가 많았는데요,

  • 직접 구현과 검증된 라이브러리 사용의 트레이드오프를 실제로 경험해볼 수 있었습니다.
  • 문제 해결 과정에서 라이브러리의 내부 구조를 분석하며 오픈소스에 대한 이해도를 높일 수 있었습니다.
  • Github Issue를 통해 라이브러리의 현재 상태와 잠재적 문제점들을 미리 파악하는 것이 중요하다는 것을 배웠습니다.
    전 회사에서도 지적받았던 적이 있지만.. 사용자가 많고, 유명한 기업이 사용하더라도 라이브러리 이슈 파악은 도입 전 필수적인 과정이라는 것을 다시 한번 깨달았습니다.

앞으로도 이 디자인 시스템을 지속적으로 발전시키며, 이를 통해 더 나은 사용자 경험을 제공하도록 노력하겠습니다.
아직 년차가 얼마 되지 않은 주니어이지만 이런 중요한 프로젝트를 믿고 맡겨주신 팀에 감사드리며 글을 마치겠습니다.