안녕하세요 👋🏻 인프랩 프론트엔드 개발자 홍시입니다.

24년 3월 현재 인프랩은 프론트엔드 개발자 10명, 프로덕트 디자이너 5명이 있는 작은 제품 조직입니다.

그래서 플랫폼 팀은 따로 없지만, 가지고 있는 자원으로 생산성과 제품의 완성도를 높이는 것을 목적으로 22년 4월부터 약 2년 가까이 오픈 소스를 이용해 디자인 시스템을 구축하고 운영하고 있습니다.

이번 글에서는 인프랩에서 디자인 시스템이 어떻게 만들어졌고, 어떻게 운영되고 있는지에 대해 소개해 드리려고 합니다.

read time만 보면 글이 길어 보이지만, 사실은 코드 블럭과 이미지 때문에 길어 보이는 것일 수도 있습니다 😏

디자인 시스템이 필요한 이유

먼저, 디자인 시스템은 왜 필요할까요?

디자인 시스템을 개발하고 유지보수 하는 데에는 여러분도 아시다시피 매우 많은 리소스가 필요합니다.
큰 조직에서 전담 팀을 두고 디자인 시스템을 운영하는 것도 그 때문이라고 생각합니다.
단순히 개발자 뿐만 아니라 디자이너, 더 나아가 PM을 포함한 사내 모든 제품 조직이 쓰게 될 제품이기 때문에 다양한 요구사항을 조율하고 반영하는 운영 또한 쉽지 않습니다.

반면, 디자인 시스템이 성공적으로 정착했을 때 얻을 수 있는 이점이 매우 큽니다.
바로 생산성입니다.

태초에 3개의 디자인 시스템이 있었다

인프랩은 라이프타임 커리어 플랫폼 인프런랠릿을 운영하고 있는데요. 인프런을 이용해 보신 분들이라면 아마 공통적으로 이런 생각을 한 번쯤은 해보셨을 거라고 생각합니다.

“인프런 UI는 왜 이렇게 제각각이지?”

실제로 UI의 미적 영역에 대한 피드백을 유저분들로부터 많이 받다 보니 ‘우리도 UI 예쁘게 만들 수 있다!’ 라는 마음으로 만든 게 랠릿이기도 했지요.

1

할 수 있다

인프런의 UI가 제각각인 이유는 3개의 디자인 시스템이 섞여있기 때문입니다.
인프런은 여러분이 생각하시는 것보다 오래된 서비스인데요.
<앤트맨>이라고 불리는 태초 인프런 프로젝트의 역사를 잠깐 살펴보면, 이전 버전이 완전히 사라지지 않은 상태에서 새로운 디자인 시스템이 세워지길 반복했습니다.
그리고 이는 생산성 관점에서 여러 문제를 만들었습니다.

생산성 문제

  1. 어떤 컴포넌트들이 어떤 모습으로 만들어져 있는지 확인하기 어렵습니다.
    1. 스토리북과 피그마가 없고 모든 히스토리는 사람의 기억, 지난 슬랙 메세지, 코드를 통해 확인해야 합니다.
    2. 코드로 컴포넌트의 모습을 확인하는 것이 가장 확실하지만, <앤트맨>의 핫리로드 속도가 개발자를 고통스럽게(?) 했으며 무엇보다 디자이너가 개발자에게 의존하지 않고 직접 확인하기 어렵습니다.
  2. 컴포넌트를 사용하기 어렵습니다.
    1. 바닐라 자바스크립트로 만들어진 <앤트맨>의 코드들은 interface나 type을 통해 어떤 인자를 전달할 수 있는지 확인하기 어렵습니다. jsdoc도 없습니다.
    2. 코드 포맷팅도 되어있지 않고, 알아볼 수 없는 축약어로 이루어진 매개변수들로 되어있어 파악하기 어렵습니다.
  3. 없는 컴포넌트가 많습니다.
    1. 없는 컴포넌트를 직접 개발하는 비용을 스프린트 진행 시 고려해야 했고, 디자인 시스템이 있을 때에 비해 많은 시간이 소모됩니다.
  4. 디자인 작업이 지연됩니다.
    1. 피그마 컴포넌트 에셋이 없어 서비스를 캡쳐 해서 디자인 작업을 진행하기도 했습니다. 그마저도 어려운 경우엔 피그마로 컴포넌트를 하나하나 만들어야 하기 때문에 많은 시간이 필요했습니다.
    2. 어느 상황에서 어느 기준으로 컴포넌트를 활용하는지 기준을 알기 어려워 디자인할 때마다 어려움을 겪었습니다.
    3. 시스템 적으로 사소한 부분을 보장해주지 않으니, 디자인 QA에서 사소한 부분까지 확인해야 하고 이는 비용 증가로 이어집니다.

이러한 생산성 문제를 해결하기 위해 디자인 시스템을 다시 구축하기로 결정했습니다.

다만, 이미 <앤트맨>의 역사에서 3번의 구축 시도가 실패로 끝난 데다가 저희 규모에서 목적 조직을 뒤로 하고 디자인 시스템만 전담할 팀을 만드는 데 한계가 있다는 것에 팀원 모두가 공감했습니다.

그래서 0부터 직접 구축하기보다 잘 만들어진 UI 오픈 소스를 사용하기로 했습니다.
디자인 시스템 수준으로 구축된 UI 오픈 소스를 활용하는 것에는 여러 장점이 있다고 생각했습니다.

물론 단점도 뚜렷합니다.

  1. 프론트엔드 개발자는 비즈니스 로직에, 프로덕트 디자이너는 UX에 보다 집중할 수 있습니다.
  2. 많이 사용되는 오픈 소스를 써보며 왜 사랑받는지, 어떻게 설계되었는지, 어떻게 유지보수 되는지 등등 그 경험치를 흡수할 수 있습니다.
  3. 문서화 비용이 현저히 줄어듭니다.
  4. 해당 오픈 소스에만 집중하여 논의하고 개발하는 메인테이너와 컨트리뷰터를 포함한 커뮤니티 (전문가 집단이라고 부르고 싶습니다)가 따로 있고 이들로부터 일정 퀄리티가 보장됩니다.

디자인 시스템 구축 사전 작업

이후 C레벨을 포함한 (당시 기준) 15명이 참여하는 많은 회의를 거치며 디자인 시스템을 어떻게 구축할 것인지에 대해 논의했는데요.

그 과정에 대해 자세히 소개해 드리겠습니다.

1. 목표 정의하기

저희는 어떤 기준으로 오픈 소스를 선정해야 했을까요?
디자인 시스템을 통해 얻고자 하는 바를 먼저 함께 정의해 보았습니다.

  1. UI 개발보다, 만들어진 UI를 어떤 기준과 원칙에 의거해 사용할 것인지 고민하는데 집중합니다.
  2. 프론트엔드 개발자와 디자이너 사이의 의사소통 비용을 줄입니다.
    ex. font-size 16px / line-height 1.5 / font-weight 500 → Text 컴포넌트 sm 사이즈 사용해주세요
  3. QA 비용을 줄입니다.
    디자인 시스템의 각 컴포넌트는 이미 검증이 끝난 상태인 데다가 개발자는 디자이너가 피그마에 명시한 옵션을 그대로 사용했을 것이므로, 전처럼 스타일 하나하나 확인하지 않아도 될 것으로 기대했습니다.
  4. 문서화 비용을 줄입니다.
    대부분의 내용은 UI 오픈 소스 공식문서를 참고하고 그 외에 정의한 부분만 문서화를 진행합니다.

2. UI 오픈 소스 선정

이 모든 장점을 가져다줄 오픈 소스를 선정하는 과정이 꽤나 험난했습니다.
약 두 달간, 많은 회의를 거쳐 다양한 오픈 소스를 검토했습니다.

tired

절대 지치지 않았습니다

이렇게 오래 걸렸던 이유는 디자인 파트와 프론트엔드 파트가 최우선으로 생각하는 가치가 서로 달랐기 때문입니다.

디자인 파트에서는 공식적으로 관리, 제공되는 피그마 에셋이 얼마나 풍부한지를 중요하게 생각했고,
프론트엔드 파트에서는 커스터마이징에 얼마나 열려있는지를 중요하게 생각했습니다.

안타깝게도 이 둘 모두를 만족하는 오픈 소스는 존재하지 않았습니다.
저희 팀은 이 갈등을 다음과 같이 해결해나갔습니다.

1. 서로를 이해하기

프론트엔드 개발자는 공식 피그마 에셋 존재 유무가 왜 그토록 중요한지, 디자이너는 커스터마이징에 얼마나 열려있는지가 왜 그토록 중요한지 단번에 이해하기 어렵습니다.

모든 사람은 모두 다르기 때문에, 서로 같은 언어로 같은 현상을 이해하기 위해서는 많은 노력이 필요하다고 생각합니다.

뇌 동기화를 진행합니다…

저희는 이 간극을 메우는 것을 먼저 시작했습니다.

이 글을 읽고 계시는 여러분도 한 번 상상해 보시면 좋을 것 같습니다.
만약 왜 그런지 잘 알고 계시다면, 다음 섹션으로 넘어가셔도 좋습니다 😉

디자이너는 컴포넌트에 대해 피그마 에셋을 만들 때, 그 컴포넌트의 옵션 수만큼 만들어야 합니다.
이게 무슨 말일까요?

예를 들어, 버튼 컴포넌트에 color prop이 있고 6가지의 컬러셋이 있습니다.
그리고 스타일을 결정하는 variant는 5가지가 있다고 해봅시다.
그럼 피그마로 6*5=30가지의 컴포넌트를 하나하나 다 만들어주어야 합니다.
그뿐일까요? UI 오픈 소스를 써보셨다면 아시겠지만 color, variant외에도 여러 가지 중요한 prop이 존재하죠.
loading 중인 상태, disabled인 상태, padding과 font size 등이 달라지는 사이즈 등등…
주요한 prop만 꼽아보아도 500개는 가볍게 넘어갑니다.

4명의 디자이너로 하나의 컴포넌트당 500개의 케이스를 다 만들어야 한다?
이건 불가능하다는 생각이 들었습니다.
물론 물리적으로 시간을 들인다면 가능할 것입니다.
하지만 저희 팀이 집중하고자 하는 문제는 500개의 케이스를 만드는 것이 아니었기 때문에, 다른 해결방법이 필요했습니다.

이번에는 프론트엔드 개발자의 입장을 이해해 볼까요?
꼭 UI 오픈 소스가 아니더라도, 다른 사람이 만든 컴포넌트나 패키지를 사용할 때에 뭐가 가장 힘드셨나요?
여러 가지가 있겠지만 꼭 빠지지 않는 것이 “외부에서 내부에 접근할 수 없다”일 것입니다.

버튼의 focus ring 컬러를 변경하고 싶다고 해봅시다.
오픈 소스 측에서 순수하게 css만으로 이를 제어하고 있다면, 제공되는 인터페이스가 없어도 커스터마이징 하는 것이 그리 어렵지 않을 것입니다.
하지만 만약 다른 옵션과 연계해서 js 로직으로 제어하고 있다면 어떨까요? 심지어 inline style이나 !important를 사용하고 있다면요?

덮어씌울 방법이 없지는 않겠지만, 매우 고통스럽겠죠.
불필요한 선택자를 중첩하여 우선순위를 억지로 높인다거나, 최악의 경우 오픈 소스 코드를 그대로 복사, 붙여넣기 해서 수정이 필요한 부분만 수정하고 빌드하여 사용할 수 있을 것입니다.
이런 방법은 생산성을 향상하는 데 도움이 된다고 할 수 있을까요?

2. 방법 찾기

여기까지 이야기를 들었을 때 아마도 양측의 입장이 팽팽하게 느껴지셨을 것 같습니다.
실제로 생산성과 직결되는 문제였기 때문에 어떤 것 하나도 쉽게 무시할 수 없는 사안이었습니다.

다행히도 이런 고민은 저희만 하는 것이 아니었습니다.
피그마 에셋을 자동으로 만들어 줄 방법이 없는지 찾아보다, Anima라는 피그마 플러그인을 발견했습니다.
이 플러그인은 당시 스토리북의 컴포넌트를 자동으로 피그마 컴포넌트로 만들어내는 새로운 기능을 베타로 서비스하고 있었습니다.
플러그인을 만드는 팀과 디스코드로 소통하며 버그도 제보하고 기능 제안도 해가며, 필요한 컴포넌트들에 대해 피그마 에셋을 자동으로 만들어줄 수 있었습니다.

Anima를 이용해 만든 버튼 컴포넌트 피그마 에셋

3

다만, 플러그인이 완벽하지는 않았기 때문에 부족한 부분을 보완하고 정리하는 것은 수작업으로 진행해야 합니다.
플러그인 사용법을 문서로 정리해 프론트엔드 팀에 공유하고, 처음 PoC한 사람이 아니더라도 누구나 사용할 수 있게 했습니다.

4

현재는 Anima 대신 https://story.to.design/ 를 이용해 오픈 소스의 메이저 업데이트에서 새로 추가된 컴포넌트에 대한 피그마 에셋을 생성해보려고 하고있습니다.

선정하기

피그마 에셋 문제가 해결이 되었기 때문에, “커스터마이징이 쉬운지”만을 고려하면 됩니다.
그렇게 여러 가지 오픈 소스 각각을 직접 사용해보며 검토한 결과 Mantine이 선정되었습니다.

diff

22년 기준 비교

당시 Mantine을 선정한 데에는 여러 이유가 있지만 가장 결정적인 것은 다른 오픈 소스에 비해 세세한 부분까지 쉽게 스타일을 커스터마이징 할 수 있었기 때문입니다.

각 오픈 소스를 깊게 이해하기엔 PoC 기간이 충분하지 않았던 점은 아쉬운 점으로 남습니다.

3. 디자인 시스템 원칙 합의하기

사용할 오픈 소스 선정이 완료되었으니 이제 이 오픈 소스를 사용하여 본격적으로 구축할 차례입니다.
그전에 생각해 볼 문제가 있습니다.

저희 팀에게 디자인 시스템은 왜 성공하기 어려웠을까요?

다양한 이유가 있겠지만, 저희가 생각했을 때 가장 큰 이유는 시스템을 시스템으로 사용하지 않는다는 것이었습니다.

새로운 제품을 기획하다 보면 디자인 시스템의 룰을 따르는 것만으로는 제품을 만들기 어려울 때가 있습니다.
그럴 때에는 다양한 커스터마이징 요구가 생겨날 수밖에 없습니다. 당연한 일입니다.
그러나 이를 모두 수용한다면, 그것은 더 이상 시스템이라고 보기 어려울 것입니다.
어렵지만 시스템의 룰을 지키는 것과 커스터마이징 사이 밸런스를 적절하게 유지하는 것이 중요했습니다.

시스템을 무너뜨릴 수 있는 요인을 제거하기 위해서 아래 3가지 내용을 원칙으로 세웠습니다.

  1. 디자인 시스템은 일종의 제약이며 룰을 최대한 존중하기
  2. 오픈 소스에 끌려가는 것이 아니라, 오픈 소스를 이용해 우리의 디자인 시스템을 구축하는 것임을 명확하게 하기
  3. 오픈 소스에서 제공하는 기능을 충분히 활용하되, 커스터마이징은 꼭 필요한 경우에만 하기:
    “꼭 필요한 경우”를 객관적으로 명문화하기엔 어렵지만 운영하다 보면 감을 잡을 수 있을 거라 기대했습니다.

4. 오픈 소스 학습하기

또한 선정한 오픈 소스에 대해 학습하는 시간을 가졌습니다.
오픈 소스가 어떤 원칙을 가지고 있고, 어떤 컴포넌트를 제공하고, 어떤 방식으로 활용할 수 있는지, 인프런 서비스에 맞게 녹여내려면 최소한 어떤 부분을 어떻게 커스터마이징 해야 하는지를 확인했습니다.

새롭게 진행하는 프로젝트에 오픈 소스를 직접 써보며 적응하는 시간을 가지기도 했습니다.

5

학습의 과정에서 디자이너가 문서로 확인하기 어려운 부분은 프론트엔드 개발자가 적극적으로 코드를 통해 확인했습니다.

아무래도 UI 오픈 소스는 디자인 팀이 없는 프론트엔드 개발자를 위해 개발된 경우가 많기 때문에, 그 공식문서가 디자이너에게도 친절한 경우는 많지 않습니다. 디자인 파트에서 프론트엔드 파트에게 주로 요청 주시는 부분도 공식 문서에 없는 내용과 관련된 부분이 대다수였습니다.

예를 들면, Mantine의 Button 컴포넌트는 variant에 따라 각기 다른 스타일을 보여주는데요. variant에 따라 어떤 스타일링 로직이 있는지 (폰트 사이즈와 굵기, 패딩, 컬러 쉐이드 등) 확인하는 것은 디자이너가 개발자 도구로 하나하나 확인하기보다 개발자가 코드를 통해 확인하는 것이 효율적입니다.
중앙에서 제어하는 foundation 개념의 spacing과 size 체계가 컴포넌트별로 어디까지 적용되고 있는지 확인하는 것도 마찬가지입니다.

그러한 점들을 파악하다 보면 자연스럽게 공식 문서뿐만 아니라 Mantine의 코드 내부와 디스코드 (Mantine의 커뮤니티는 깃허브보다 디스코드에서 활성화되어 있습니다)를 들여다보며 오픈 소스를 학습하게 됩니다.

디자인 시스템 구축하기

사전 작업이 완료되었으니 이제 실질적으로 디자인 시스템을 어떻게 구축했는지 알아보겠습니다.

1. 컴포넌트 단위 패키징

먼저 디자인 시스템을 구성하는 각 컴포넌트가 개별 패키지로 개발, 배포되는 것을 의도했습니다.
개발자는 각 패키지를 개별적으로 설치하여 사용할 수도 있고, @inflean/ds-react 이라는 통합 패키지만을 설치하여 사용할 수도 있는 형태인 것이죠.

왜 통합 패키지만으로 구성하지 않고 각 컴포넌트별로 패키지를 구성했는지 궁금하실 수 있을 것 같습니다.

  1. 가까운 미래에 직접 개발한 컴포넌트로 디자인 시스템을 교체하게 될 줄 알았습니다.
    초기에는 가까운 미래에 디자인 시스템 컴포넌트를 직접 개발하게 될 줄 알고 각 컴포넌트를 패키지로 관리하기로 했습니다. 그럼 그 컴포넌트만 단독으로 버전관리를 할 수 있으므로 통합 패키지만으로 구성하는 것보다 유연하게 직접 만든 컴포넌트로 마이그레이션 할 수 있을 거라고 생각했기 때문입니다. 하지만 디자인 시스템을 운영하다 보니 Mantine을 걷어내고 직접 개발하는 일도 아주 먼 미래가 되어, 현재는 유효하지 않은 이유가 됐습니다.

  2. 모든 컴포넌트를 사용하는 것이 아니라 특정 컴포넌트만을 사용하게 될 일이 있을 줄 알았습니다.
    인프콘 랜딩 페이지 등을 개발할 때와 같이 일부 컴포넌트만 선택적으로 설치하여 사용할 일이 있을 줄 알았습니다만 실질적으로 그럴 일은 거의 없었습니다.

  3. 버전 관리를 컴포넌트 단위로 단독적으로 할 수 있습니다.
    컴포넌트 별로 버전 관리를 단독으로 할 수 있다면 커스터마이징 로직 적용 등을 좀 더 독립적으로 할 수 있습니다.

현시점에서 마지막으로 남은 이점인 3번에 대해 좀 더 자세히 알아보겠습니다.
만약 통합 패키지로만 관리를 했다면, 다음과 같은 시나리오가 발생할 수 있습니다.

  1. ds-react내 button 디렉토리에서 button 컴포넌트에 대해 커스터마이징 로직을 작성합니다.
  2. ds-react를 minor 업데이트합니다. (ex. 1.1.0)
  3. ds-react내 text 디렉토리에서 text 컴포넌트에 대해 커스터마이징 로직을 작성합니다.
  4. ds-react를 minor 업데이트합니다. (ex. 1.2.0)
  5. 1.1.0 업데이트 내역으로 인해 button에 버그가 발생했습니다. 사용처에서 ds-react 버전을 1.1.0 전으로 롤백하고자 하지만, 1.2.0 업데이트에 포함된 text 컴포넌트 커스터마이징 로직은 반드시 유지되어야 합니다.

컴포넌트 별로 버전을 관리했더라면 위와 같은 곤란한 상황을 피할 수 있습니다.

  1. ds-button에서 button 컴포넌트에 대해 커스터마이징 로직을 작성하고 1.0.0에서 1.1.0으로 minor 업데이트합니다.
  2. ds-text에서 text 컴포넌트에 대해 커스터마이징 로직을 작성하고 minor 업데이트합니다. (ex. 1.2.0)
  3. ds-button 1.1.0 업데이트 내역으로 인해 button에 버그가 발생했습니다. 사용처에서 ds-button의 버전을 1.0.0 으로 고정하여 다시 설치합니다. ds-text 는 1.2.0 버전으로 유지되고, ds-button의 버전만 1.1.0 이전으로 되돌릴 수 있습니다.
    1. 단, ds-react에서 ds-button의 범위가 ^1.1.0 으로 설정되어 있지 않아야 합니다. 만약 그렇게 설정되어 있다면 1.0.0을 허용하도록 업데이트가 필요합니다.

컴포넌트 단위로 패키징을 한다는 것은 선정된 오픈 소스를 그대로 사용하지 않고 디자인 시스템 레포지토리를 두고 그곳에서 한 차례 랩핑해서 쓰기로 한 것이기도 합니다.

// ❌
import { Button } from '@mantine/core';
// ✅
import { Button } from '@inflearn/ds-react';

이는 우리 서비스와 외부 오픈 소스 사이에 벽을 하나 두는 셈인데요.
이렇게 랩핑해서 쓰면 분명 귀찮아지긴 하지만, 코드 히스토리와 커스터마이징 로직을 한 곳에서만 공통으로 관리할 수 있다는 장점이 있습니다.

오픈 소스에 Breaking Change와 같은 큰 변경사항이 발생했을 때, 수정의 영향 범위를 최대한 디자인 시스템 레벨에서 끊어내고 각 서비스 코드에는 영향을 최소화하는 것도 랩핑의 장점입니다.

처음엔 이것을 의도했지만, 운영하다 보니 오픈 소스 공식문서와의 간극이 발생하는 문제가 있었습니다.

문서와 실제 코드의 연동이 더 중요하다고 판단하여, Major 업데이트를 디자인 시스템에서 끊어내는 것이 아니라 각 서비스 코드에서 처리하도록 하여 오픈 소스 공식문서와의 연동을 유지하는 것으로 방향을 변경했습니다.

랩핑하는 방법은 여러 가지가 있을 수 있겠지만 앞서 설명드린 이유로 저희는 컴포넌트 별로 패키지를 생성하는 방법을 선택했습니다.
컴포넌트 별로 패키지를 생성하는 것은 많은 보일러 플레이트 코드를 양산하기 때문에, Hygen을 이용해서 간편하게 패키지를 생성할 수 있도록 설정해두었습니다.

hygen을 이용해 새 패키지를 생성하는 모습

2. 패키지 사이 의존 관계 관리

통합 패키지는 각 컴포넌트별 패키지를 의존성으로 가집니다.
각 컴포넌트별 패키지도 다른 컴포넌트 패키지를 의존성으로 가질 수 있습니다.
예를 들어, @inflearn/ds-modal@inflearn/ds-button을 사용하기 때문에 이를 의존성으로 가지고 있습니다.

이러한 의존 관계를 설정할 때는 순환 참조가 되지 않도록 유의하고, 각각을 번들에 포함시키지 않고 peer dependency로 설정했습니다.
peer dependency로 설정한 이유는 사용하는 쪽에서 해당 패키지의 버전을 선택하여 직접 설치할 가능성이 높기 때문입니다. 그럼 서로 다른 버전이 여러 개 설치될 우려가 있습니다.
그런 우려를 방지하기 위해 peer dependency로 관계를 설정해 버전 범위를 벗어나지 않는다면 사용하는 쪽에서 직접 하나의 버전으로 통일해 설치할 수 있도록 했습니다.

3. 빌드

이 당시 사용하던 Vite 버전은 3버전이었습니다.

디자인 시스템 레포지토리 안에서 여러 패키지를 관리할 수 있어야 하기 때문에 자연스럽게 모노레포가 만들어졌으며, 강력한 캐싱 기능과 모노레포 운영에 다양한 편의성을 제공하는 Turborepo를 사용했습니다.

컴포넌트를 빌드하는 도구는 ViteTsup을 사용하고 있습니다.
Turborepo의 파이프라인에 의해 의존관계에 따라 수십 개의 패키지가 병렬로 빌드 되는데, Vite와 Tsup을 비교해 보니 Tsup의 설정이 더 간편하고 (type을 생성하기 위한 별도의 플러그인 설치 불필요) 속도가 약간 더 빨랐기 때문입니다.
단, Tsup은 css 파일 생성에 이슈가 있어 css를 내보내야 하는 패키지는 Vite를 이용해 빌드하고 있습니다.

4. 버전 관리

Changesets를 사용해 각 패키지의 버전을 관리하고 있습니다.
수많은 컴포넌트 패키지의 버전을 수동으로 관리하기보다 SemVer에 의거해 의존 관계를 자동으로 계산하여 안정성을 높이고 개발자들의 관리 비용을 줄이기 위함입니다.

Changesets에서 안내하는 바처럼 마크다운으로 각 패키지의 변경 사항을 패치노트 작성하듯 작성하면 (Changelog라고 부릅니다), 특정 조건 아래에서 Changesets에서 제공하는 Github Action이 Changelog에 명시된 컴포넌트 패키지의 버전을 계산해 새로운 버전을 지정하고 Github Registry를 통해 npm에 배포합니다.

Changesets에서 기본으로 제공하는 Changelog 포맷이 있으나, 그것만으로는 패키지 별 변경내역을 파악하는 데 가독성이 안 좋다고 판단하여 아래 이미지와 같이 저희에게 맞는 방식으로 변형하여 이용하고 있습니다.

7

Changelog를 편집하기 위해선 포맷팅 함수를 작성하고 설정 파일에서 changelog key의 value로 해당 파일의 경로를 지정해 주면 됩니다.

{
  ...
  "changelog": "./changelogFormat.js",
}

코드는 Changesets에서 제공하는 기본적인 포맷팅 파일 파일을 참고해서 작성했습니다.

8

로컬테스트가 번거롭기 때문에 테스트코드를 작성하며 개발했습니다

작성된 Changelog를 활용하는 방법은 후술할 스토리북 섹션에서 소개해 드리겠습니다.

Changesets를 이용하며 만났던 한 가지 이슈는 peer dependency의 버전 업데이트 내역이 patch에 불과한데, 해당 peer dependency에 의존하는 패키지의 major 버전이 의도치 않게 올라간다는 점이었습니다.
다음과 같이 두 패키지 ab가 있다고 가정해 보겠습니다. ab를 peer dependency로 갖고 있는 상황입니다.

// package A
{
  "name": "a",
  "version": "1.2.0",
  "peerDependencies": {
    "b": "^1.1.0",
    ...
  }
  ...
}
// package B
{
  "name": "b",
  "version": "1.2.1",
  ...
}

여기서 b 패키지의 버전이 1.2.2로 patch 업데이트가 되었다고 가정해 보겠습니다.
SemVer에 의하면 patch는 breaking change를 발생시키지 않는 업데이트를 의미합니다.
그리고 a 의 pakcage.json을 보면 ^1.1.01.2.2 버전을 포함하는 범위입니다.
그렇기 때문에 b에 의존하고 있는 a의 버전은 변하지 않을 것이라고 기대했습니다.
하지만 실제로 Changesets이 업데이트해준 버전을 보면 a 의 버전이 patch도 아니고 major가 올라가버립니다.

major 업데이트는 Breaking Change를 의미하기 때문에 많은 주의가 필요하며 신중해야 하는데, 왜 이렇게 동작하는 걸까요?
찾아보니 이는 의도된 동작이었습니다. Changesests의 메인테이너의 근거를 간단히 요약하자면 다음과 같았는데요.
패키지를 설치할 때 peer dependency까지 함께 설치되지 않으므로 peer dependency의 업데이트가 항상 반영된다는 보장이 없고, Changesests는 안전한 버전관리를 위한 도구이므로 peer dependency의 버전에 변경이 있을 시 major 업데이트로 강력하게 알려준다는 것입니다.

이에 대한 여러 이슈 리포팅과 토론 덕분에 다행히(?) onlyUpdatePeerDependentsWhenOutOfRange 옵션이 실험적 기능으로 제공되고 있습니다.
이 옵션을 활성화하여 peer dependency로 명시된 버전 범위를 벗어나지 않는 선에서 major 업데이트가 발생하지 않게 설정할 수 있었습니다.

5. 커스터마이징

처음 디자인 시스템을 준비하며 우려했던 것과 달리, 커스터마이징이 필요한 경우는 생각보다 많지 않았습니다. 만약 필요한 경우 관련 업무를 진행 중인 디자이너와 프론트엔드 개발자 각 1인이 논의를 시작했으며, 논의 결과는 반드시 슬랙을 통해 모든 팀원에게 공유되었습니다.

11

단순 스타일 수정의 경우

12

기능 수정의 경우

Mantine에서 커스터마이징은 여러 가지 방법으로 이루어질 수 있습니다. 저희는 그중 두 가지 방법을 주로 이용했는데요, 자세히 소개해 드리겠습니다.

Mantine v6를 기준으로 합니다.

1. ThemeProvider를 통해 전역으로 커스터마이징 로직 적용하기

저희 디자인 시스템에서 가장 권장하는 방법입니다.

Mantine은 원자 수준의 컴포넌트를 합성하여 새로운 컴포넌트를 만들어내는 경우가 잦습니다.
그런 점을 응용해 Mantine의 컴포넌트를 이용해 직접 개발한 컴포넌트도 있는데요.
만약 Button 컴포넌트를 사용하는 컴포넌트가 있다고 했을 때, Button에 대한 커스터마이징 로직은 ThemeProvider를 통해 주입되어야만 Button 컴포넌트를 사용하는 모든 컴포넌트에 반영될 수 있습니다.

Mantine의 ThemeProvider에서는 component prop으로 각 컴포넌트를 커스터마이징 할 수 있는 API를 제공합니다. 예를 들어 아래와 같이 사이즈 별로 스타일을 수정할 수도 있고, 새로운 사이즈를 추가할 수도 있습니다.

Chip: {
      sizes: {
        xs: () => ({
          root: {
            height: 24,
          },
        }),
        sm: () => ({
          root: {
            height: 28,
          },
        }),
        md: () => ({
          root: {
            height: 32,
          },
        }),
        lg: () => ({
          root: {
            height: 36,
          },
        }),
        // 새로운 사이즈 추가
        xl: () => ({
          root: {
            height: 40,
          },
        }),
      },
    },

그리고 각 컴포넌트의 size, variant 등의 prop을 두 번째 인자로 가지고 있는 styles 콜백 함수를 통해 prop의 값에 따라 스타일을 수정할 수도 있습니다.

Notification: {
      // withTitle prop을 이용한 스타일 커스텀
      styles: (theme, { withTitle }: NotificationStylesParams) => ({
        ...
        description: {
          // withTitle prop을 이용한 스타일 커스텀
          color: withTitle ? theme.colors.gray[4] : theme.colors.gray[0],
        },
      }),
    },

그러나 Mantine에서 제공하는 API로 접근할 수 없는 케이스라면 ThemeProvider를 통해 커스터마이징 로직을 주입할 수 없습니다. 이런 경우 어쩔 수 없이 각 컴포넌트에 직접 접근해 커스터마이징 해야 합니다.

2. 각 컴포넌트 패키지에서 직접 커스터마이징

예를 들어, Notification 컴포넌트에 다음과 같은 커스터마이징을 적용하고자 한다면 1번으로 처리가 불가능합니다.

  • message 단독 사용 불가
  • title 단독 사용 가능
  • title & message 함께 사용 가능

이런 경우 Notification 패키지에서 직접 커스터마이징을 진행했으며, 그 의도를 타입으로 전달하려 노력했습니다.

export type EditedNotificationProps =
  | ArgumentRequired<MakeNever<DefaultEditedNotificationProps, 'message'>, 'title'>
  | ArgumentRequired<DefaultEditedNotificationProps, 'title' | 'message'>;

또한 커스터마이징 로직에 대해서는 되도록 테스트코드를 작성하는 것을 지향하고 있습니다.
안정성을 확보할 수 있을 뿐만 아니라, 처음 코드를 읽는 사람이 테스트코드를 통해 정책을 이해하는데 도움을 줄 수 있습니다.

13

저희 팀은 새로운 컴포넌트를 만드는 것 또한 커스터마이징의 넓은 범위에 속한다고 보았는데요. 새 컴포넌트 UI가 디자인 시스템의 일관성을 해치지 않도록 개발 시 오픈 소스의 정책을 최대한 이용하려고 했습니다.

6. 스토리북 구성하기

앞서 기본적으로 문서는 오픈 소스에서 제공하는 공식 문서를 이용하기로 했었습니다.
그럼에도 자체적인 스토리북이 필요한 이유는 다음과 같습니다.

  1. 프론트엔드 개발자뿐만 아니라 디자이너가 실제 컴포넌트의 모습과 동작을 확인할 수 있는 플레이그라운드가 필요합니다:
    특히 공식 문서에서 확인하기 어려운 prop의 조작, 커스터마이징 로직이 반영된 결과물, 새로 만든 컴포넌트를 확인할 수 있어야 합니다.

    14

  2. 디자인 시스템 로컬 개발 환경이 필요합니다.

  3. README, Changelog를 스토리로 제공하여 Github에 접근하지 않고도 디자이너가 확인할 수 있어야 합니다.

스토리북은 기능이 워낙 다양하고, 필요하면 직접 플러그인을 개발할 수도 있기 때문에 활용 범위가 방대한데요. 한 번에 모든 것을 구성하려 하기보다 그때그때 필요한 것들을 천천히 추가해 나갔습니다.

Control

저희가 스토리북에서 가장 잘 사용하는 것은 Control 패널입니다. Control 패널을 통해 코드 작성 없이 손쉽게 컴포넌트의 prop을 설정하며 컴포넌트를 눈으로 직접 확인할 수 있습니다.
그렇기 때문에 디자이너 분들이 가장 많이 사용하시는 기능인데요. 기본적으로는 다음과 같이 argTypes을 직접 선언해서 Control을 만들 수 있습니다.

export default {
  title: 'Foundation/Icon',
  component: IIcon,
  argTypes: {
    // 폰트어썸 아이콘을 직접 넣어볼 수 있게끔 class name을 string으로 입력받습니다.
    icon: {
      type: 'text',
      description: 'fa-regular fa-house와 같은 폰트어썸 class명을 입력해 주세요',
    },
    // 배열을 옵션으로 제공하여 select로 size를 수정해 볼 수 있습니다.
    size: {
      control: {
        type: 'select',
        options: ICON_SIZE,
      },
    },
  },
};

만약 모든 컴포넌트에 공통적으로 쓰이는 Control이 있다면 각 스토리마다 하나하나 작성해주어야 할까요? 그것보다는 preview.js에서 Control을 전역적으로 설정하는 방법을 추천드립니다.

// preview.js
const argTypes = {
  color: {
    defaultValue: 'infgreen',
    description: '숫자가 안 붙으면 6번째 색상을 의미합니다.',
    table: {
      defaultValue: { summary: 'infgreen' },
    },
    options: COLOR_CONTROL_OPTIONS,
    control: {
      type: 'select',
    },
  },
};

/** @type { import('@storybook/react').Preview } */
const preview = {
  argTypes,
  ...
};

export default preview;

만약 전역으로 설정된 Control이 필요 없는 스토리라면, 다음과 같이 스토리의 parameters 에서 해당 컨트롤을 제외할 수 있습니다.

<%= name %>.parameters = { controls: { exclude: ['color'] } };

컴포넌트의 Prop을 하나하나 Control로 작성하는 것은 매우 귀찮을 수 있습니다. 스토리북은 react-docgen-typescriptreact-docgen-typescript-plugin를 사용하여 컴포넌트의 Prop 타입을 Control로 자동 생성해 줄 수 있습니다. (공식 문서)

// main.js
  typescript: {
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      // union prop type을 select 컨트롤로 생성합니다.
      shouldExtractLiteralValuesFromEnum: true,
      // string,boolean type을 inputs 혹은 switches 컨트롤로 생성합니다.
      shouldRemoveUndefinedFromOptional: true,
      propFilter: (prop) =>
        prop.parent ? !/node_modules\/(?!@mantine)/.test(prop.parent.fileName) : true,
    },

Global Types

Control 패널 외에도 디자이너가 디자인 시스템을 확인하는 데 도움이 되는 요소를 제공할 수 있습니다.

개발자도구를 열지 않고 각 뷰포트마다 어떻게 스타일이 달라지는지 확인할 수도 있고, globalTypes 를 이용해서 ThemeProvider 를 조작해 primary color, primary shade, color scheme 등을 변경하는 UI를 제공할 수도 있습니다. 아래는 decoratorglobalTypes 을 사용한 예시입니다.

// preview.js

const withTheme = (Story, context) => {
  // control을 통해 폰트어썸 아이콘을 설정할 수 있도록 폰트어썸 아이콘들을 셋업해줍니다.
  library.add(fas, far, fal, fat, fad, fass, fasr, fasl);
  
  return (
    // context를 통해 globalType 값에 접근할 수 있습니다. 
    // Mantine은 primaryColor, colorScheme, primaryShade등을 ThemeProvider를 통해 주입받으므로 이 값을 넘겨 설정해줍니다.
    <ThemeProvider theme={{ primaryColor: context.globals.primaryColor, colorScheme: context.globals.colorScheme, primaryShade: context.globals.primaryShade }}>
      <ModalsProvider>
        <NotificationsProvider />
        <Story />
      </ModalsProvider>
    </ThemeProvider>

  );
};

// 데코레이터는 모든 스토리에 적용됩니다.
const decorators = [withTheme];

const globalTypes = {
  primaryShade: {
    name: 'PrimaryShade',
    description: 'Global primary shade for components',
    defaultValue: 6,
    toolbar: {
      title: 'PrimaryShade',
      icon: 'cog',
      items: [
        // 이런 값이 컬러 인덱스인 0~9 에 대해 제공됩니다
        { value: 3, title: '3' },
      ],
      showName: true,
    },
  },
  primaryColor: {
    name: 'PrimaryColor',
    description: 'Global theme for components',
    defaultValue: 'infgreen',
    toolbar: {
      title: 'PrimaryColor',
      icon: 'cog',
      items: [
        // 인프런과 랠릿의 primary 컬러를 선택 적용할 수 있게 합니다.
        { value: 'infgreen', title: 'infgreen' },
        { value: 'rallit', title: 'rallit' },
      ],
      showName: true,
    },
  },
  colorScheme: {
    name: 'ColorScheme',
    description: 'select light/dark mode',
    defaultValue: 'light',
    toolbar: {
      title: 'ColorScheme',
      icon: 'lightning',
      items: [
        // light, dark 모드를 적용할 수 있게 합니다.
        { value: 'light', title: 'light' },
        { value: 'dark', title: 'dark' },
      ],
      showName: true,
    },
  },
};

/** @type { import('@storybook/react').Preview } */
const preview = {
  decorators,
  globalTypes,
  ...
};

export default preview;

Markdown

마크다운 문서를 스토리북을 통해 서빙할 수도 있습니다. 저희가 직접 만든 컴포넌트는 당연하게도 Mantine의 공식 문서에서 사용법 등을 확인할 수 없기 때문에, 다음과 같이 README를 필수로 작성하고 있습니다.

15

스토리북의 Markdown 컴포넌트를 이용하면 마크다운 파일을 손쉽게 스토리북의 일부로 제공할 수 있습니다. 문서의 내용을 수정할 때에는 마크다운 파일만 편집하면 되기 때문에 이중으로 관리하지 않아도 됩니다. README를 스토리북으로도 서빙하는 이유는 필요시 디자이너도 확인할 수 있게 하기 위함입니다.
디자인 시스템은 프론트엔드 개발자만의 것이 아니니까요.

// 스토리북 7버전을 사용하고 있습니다.

import { Meta, Markdown } from '@storybook/blocks';
import README from '../README.md?raw';

<Meta title="V6/Overlays/Modal/README" />

<Markdown>{README}</Markdown>

마찬가지로, Changelog 또한 스토리북을 통해 서빙하고 있습니다.
Changelog는 패키지가 릴리즈될 때에 Github에서 릴리즈 노트로도 같은 내용이 발행되지만, Github이 익숙하지 않은 분들은 릴리즈 노트를 확인하기 어려울 수 있기 때문입니다.

MDX

스토리북은 MDX를 지원합니다.
일반 마크다운 문서나 Story와 다른 점은 마크다운 문법으로 간편하게 글을 작성하면서도 글 사이사이에 스토리북의 Canvas (컴포넌트를 직접 사용해볼 수 있습니다) 등 스토리북의 블록을 삽입할 수 있다는 점입니다.
주로 컴포넌트를 직접 조작하면서 볼 수 있어야 하는 문서를 작성하는 경우 MDX를 사용했습니다.

17
모달 MDX 예시
18
에디터 MDX 예시

단, 코드를 통해 작성해야 하기 때문에 디자이너가 직접 UX 정책 등을 작성하거나 수정하기 어렵다는 단점이 있습니다.
이 때문에 스토리북에서는 Canvas만 생성하고, 문서화 도구로는 스토리북 대신 Zeroheight를 이용해보려는 시도를 하고 있습니다.
Zeroheight를 스토리북과 연동하면 Canvas를 가지고 디자이너가 글을 작성하기도 편리합니다.

7. 배포 파이프라인

인프랩의 모든 프로젝트들은 기본적으로 Devops 팀에서 Jenkins를 통해 배포 파이프라인을 관리해 주시는데요.
디자인 시스템은 Github Action을 통해 프론트엔드 개발자가 배포 파이프라인을 작성하고 Devops 팀에서 최적화를 도와주시는 방향으로 진행되었습니다.

먼저 디자인 시스템 배포 파이프라인의 핵심은 Changesets Github Actions를 이용해 빌드된 결과물에 대한 버전 업데이트 PR (Release PR)을 생성하고, 해당 PR이 병합되면 npm 배포를 진행하는 것입니다.

19

여기에 추가적인 편의성을 더했습니다.
패키지가 배포 완료되기까지 약간의 시간이 필요하고, 배포를 트리거한 사람은 바쁘기 때문에 배포 완료 사실을 알림으로 알려줄 필요가 있었습니다.
그리고 버저닝 특성상, 이전에 쌓인 커밋이 늦게 배포될 경우도 있기 때문에 관련 작업을 한 사람에게 배포가 완료되었다고 알려줄 필요도 있었습니다.

이를 위해 다음과 같은 정보를 담은 슬랙 메세지를 발송하는 job을 추가했습니다.

  • 배포된 패키지 명과 새로운 버전 정보
  • 배포된 버전의 릴리즈 노트 링크
  • 이번 배포에 포함된 커밋의 작성자 슬랙 멘션 태깅

20

디자인 시스템 운영하기

이번에는 구축된 디자인 시스템을 어떻게 운영하고 있는지, 문화적인 측면에서 소개해 드리겠습니다.

1. 적극적인 기여 유도

인프랩은 단 한 명의 스타에게 의존하기보다 다 함께 성장하는 팀플레이를 가장 중요하게 생각합니다.
디자인 시스템 또한 마찬가지입니다.
다른 팀원보다 많은 관심을 갖는 사람이 있을 수는 있지만, 기본적으로 누구나 주도할 수 있고 기여할 수 있는 프로젝트를 추구합니다.
특히 디자인 시스템은 모든 서비스에 영향을 주는 제품이기 때문에 누구 한 명의 퇴사로 흔들리지 않을 수 있는 환경이어야 합니다.

그러려면 디자인 시스템 기여의 진입 장벽이 낮아야 하는데요. 쉬운 기여 환경을 만들기 위해 여러 가지 문서를 제공하고 있습니다.

먼저 Contribution 가이드는 어떻게 기여하면 되는지에 대한 내용을 담고 있습니다.
이 문서는 프로젝트의 구성, 실행 방법, 컴포넌트 및 패키지 개발 방법, PR 작성 방법, 배포 방법 등등에 대해 설명합니다.

21

다음 항목에서 설명할 Jira 보드를 사용하는 방법에 대한 문서도 제공됩니다.
인프랩은 Jira를 적극적으로 사용하는 조직으로, 디자인 시스템 또한 Jira로 작업 상황을 관리하여 필수적이기 때문입니다.

22

SemVer가 무엇인지 설명하는 문서는 Semantic Versioning에 익숙하지 않을 수 있는 팀원을 위해 제공됩니다.
SemVer는 디자인 시스템 소통 수단의 일부이기 때문에 프론트엔드 개발자만 알아서는 안되고 디자이너도 똑같이 이해할 필요가 있습니다.
따라서 문서를 작성할 때 두 파트가 함께 읽을 수 있게 신경 써서 작성되었습니다.

Changelog를 작성하는 방법에 대한 문서도 제공되는데요. 주로 Changelog 내용의 지향점에 대해 설명합니다.
그리고 버전관리를 위해 사용하는 도구인 Changesets에 의해 생성되기 때문에 Changesets가 낯선 팀원들에게 Changesets의 팁을 함께 담아 작성되었습니다.

23

이렇게 작성된 문서 및 기여할 때 주요하게 알아야 하는 정책들은 PR 템플릿에도 포함되어 안내되고 있습니다.

<!-- 새로운 패키지를 개발하는 경우 반드시 'new package' 라벨을 달아주세요. 새로운 패키지는 아직 npm에 배포되기 전이라 CI수행시 install 단계에서 에러가 발생합니다. 'new package'라벨이 달린 PR은 CI 수행시 문제가 발생할 수 있는 job을 실행하지 않습니다. -->

<!-- 급히 배포되어야 하는 경우, 라벨 D-0 설정해주세요 -->

디자인 시스템 기여가 처음이시라면, 먼저 [Contribution Guide](../CONTRIBUTING.md)를 읽고 작업 부탁드립니다.

✅ PR 작성 전, 아래 체크리스트를 확인해주세요.

- [ ] changelog를 [작성 가이드](link)대로 작성했나요?
- [ ] 수정한 버전은 SemVer를 바탕으로 결정되었나요?
- [ ] 디자인 시스템 지라 보드에 이슈가 연결되어있나요?

---

## 요약

<!-- 작업하신 내용을 요약하여 간략히 적어주세요. 커밋 내용(제목 및 상세 내용)을 그대로 적어주셔도 됩니다. -->

## 이슈

<!-- 현재 작업에 해결되지 않은 이슈가 있다면 적어주시고 없다면 지워주세요. -->

## 스크린샷

<!-- 현재 작업이 ui와 관련된 작업이라면 스크린샷을 첨부해주세요. 없다면 지워주세요. -->

마지막으로 디자인 시스템 작업 중 마주한 이슈가 있다면 다른 사람이 같은 이슈로 고생하지 않도록 어떤 문제가 있었고 원인은 무엇이었으며 어떻게 해결했는지에 대해 글로 남기고 있습니다.

24

2. 코드 리뷰

디자인 시스템은 모든 서비스에 영향을 주는 제품이다 보니 코드 리뷰를 통한 품질 관리가 필수적입니다. 플랫폼 팀은 없지만 효율적인 운영을 위해 Github의 Code Owner 기능을 활용하여 디자인 시스템을 주로 담당하는 코드 리뷰어를 두어 운영하고 있는데요, 디자인 시스템 Code Owner는 다음과 같은 역할을 합니다.

  1. 가능한 모든 히스토리를 기억하고 PR의 품질을 함께 검증합니다.

    25

    • 사람이 모든 히스토리를 기억하는 것은 불가능하기 때문에 히스토리 관리를 위한 여러 장치가 있지만, 그럼에도 때로는 구두 소통이 검색보다 빠를 때가 있습니다.
    • 버저닝을 하고 있어 유사시 이전 버전으로 롤백하면 되긴 하지만 모든 서비스에 영향을 주는 제품이다 보니 QA가 필요합니다.
  2. 버전이 정확하게 계산되었는지, Changelog는 잘 작성되었는지 확인합니다.

    • Changesets과 Changesets의 Github Action을 이용해 자동으로 버전을 관리하고 있지만 최종 단계인 Release PR 검토는 사람이 해야 하기 때문입니다.

코드리뷰는 주로 다음의 관점에서 이루어집니다.

  1. 컴포넌트의 인터페이스가 적절한지?
    디자인 시스템의 목적이 생산성을 높이는 데 있고 많은 개발자들이 사용하는 만큼, 사용자의 인지부하를 줄이고 일관성을 유지하는 것이 생산성을 높일 수 있다고 생각했습니다.
    이때 특히 중요한 것이 컴포넌트의 인터페이스라고 생각했는데요.
    인터페이스가 기존의 컴포넌트와 크게 다르지 않아 학습된 경험을 바탕으로 사용하기에 어려움이 없는지, 웹 표준을 준수하는 이름이어서 직관적으로 이해하기 어려움이 없는지 등의 관점에서 PR을 리뷰합니다.

    26

  2. 문서가 충분히 제공되는지?
    직접 만든 컴포넌트는 오픈 소스 공식문서에서 확인할 수 없는 컴포넌트이기 때문에, 공식문서처럼 직접 UI를 눈으로 확인할 수 있는 스토리북과 사용방법이 담긴 README 등이 충분하게 작성되었는지 검토합니다.

  3. 컴포넌트에 대한 정책이 코드에서 잘 드러나는지?
    컴포넌트에 대한 정책이 컴포넌트의 옵션이나 내부 로직에도 반영되어 있어야 정확하게 구현할 수 있으며 사용하기에도 편리하고 코드를 읽는 것으로 히스토리를 파악하기 용이합니다.
    테스트코드 작성이 필요하다고 생각되는 복잡도의 로직이라면 테스트코드 작성을 부탁드리기도 합니다.

    27

    27 2

디자인 시스템 QA

QA가 중요한 이유는 한 번 배포된 버전은 되돌릴 수 없기 때문입니다.
신중하게 배포하고자 처음엔 Chromatic을 활용해 디자이너와 함께 QA를 진행하는 것을 권장했었습니다.
Chromatic으로 UI 스냅샷을 찍어 변경사항을 비교할 수 있으며, 피그마처럼 UI 위에 코멘트를 남겨 편리하게 디자이너와 소통할 수 있습니다.
무엇보다 실제 스토리북 배포나 로컬 프로젝트 실행 없이 직접 확인할 수 있는 형태로 작업사항을 디자이너에게 공유할 수 있습니다.

다만 Chromatic 무료 요금제에서 제공하는 배포 횟수가 저희 팀에게는 부족했고, 정적인 UI 스냅샷 비교가 크게 의미 있지 않아 현재는 PR마다 스토리북 프리뷰를 html로 직접 생성하는 Github Action을 Devops 팀에서 만들어주셔서 이용 중입니다. pr preview

코드리뷰가 병목이 되지 않게 하기 위한 노력

적은 인원으로 효율적으로 코드를 리뷰하기 위해 Github Action을 적극적으로 활용하고 있습니다.

예를 들어 디자인 시스템에 버그가 있어 급하게 수정하여 배포되어야 하는 경우, 코드 리뷰가 블로커가 될 수 있습니다.
그럴 때면 Code Owner에게 빨리 확인해달라고 요청하는 슬랙 메세지를 작성하게 되는데, 이 번거로움을 줄일 수 있도록 PR에 특정 라벨이 달리면 슬랙 메세지를 발송하는 Github Action을 작성하여 운영하고 있습니다.

name: Send slack message about D-0 PR

on:
  pull_request:
    branches:
      - main
    types: [labeled]

jobs:
  send-slack-message-about-d0-pr:
    runs-on: ubuntu-latest
    steps:
      - name: Send slack message about D-0 PR
        if: ${{ contains(github.event.pull_request.labels.*.name, 'D-0') }}
        uses: slackapi/slack-github-action@v1.23.0
        with:
          payload: |
            {
              "pr": "${{ github.event.pull_request.html_url }}",
              "author": "${{ github.event.pull_request.user.login }}"
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL_D0 }}

슬랙 메세지 예시

3. 히스토리 관리 및 작업 진행 상황 트래킹

히스토리 관리는 왜 중요할까요?

  1. 정보를 투명하게 공유할 수 있습니다.
    모두가 함께 만드는 시스템이기 때문에 정보가 독점되지 않는 것은 중요합니다. 특정인에게 의존하는 것을 최대한 줄이고, 누구나 언제 어디서든 쉽게 정보에 접근할 수 있어야 합니다.

  2. 컴포넌트 파편화를 방지할 수 있습니다.
    디자인 시스템의 각 부분이 어떻게 개발되었는지, 이렇게 개발되었는지, 어떻게 사용할 수 있는지 기록되지 않는다면 (아마도) 가까운 미래에 이해하기 어렵다는 이유로 이전에 개발된 컴포넌트를 deprecated 시키고 새로운 컴포넌트를 개발하고 싶을 수 있습니다.
    그럼 컴포넌트는 파편화되고 더 이상 시스템으로서 역할하기 어려워진다고 생각했습니다.

때문에 목적 조직 안에서 제품을 개발하다가 나온 논의일지라도, 그 대상이 디자인 시스템이라면 반드시 디자인 시스템 채널을 통해 전체 공유되도록 강력하게 권장되었습니다.
구두로 논의한 사항도 마찬가지입니다. 구두로 나눈 이야기는 당사자들의 기억에만 의존하기에 휘발되기 쉽습니다.

인프랩은 디자인 시스템의 히스토리를 관리하기 위해 코드와 슬랙 메세지 외에도 여러 장치를 운영하고 있습니다.

Jira

인프랩에서는 모든 일의 단위를 Jira 이슈로 생성하는 등 Jira를 적극적으로 활용하고 있습니다.
디자인 시스템에 있어서도 예외는 아닌데요.
디자인 시스템에 작업해야 할 일이 생겼다면, 먼저 디자인 시스템 Jira 보드에서 이슈가 생성됩니다.
Jira 보드의 사용 방법은 컨플루언스 문서를 작성해 안내됩니다.

29

Jira 이슈 생성 시, 대부분 히스토리는 슬랙 채널에서 기록되기 때문에 Jira 이슈에는 슬랙 링크 혹은 간략화된 요구사항만이 기록됩니다.

30

이렇게 생성된 이슈를 기반으로 프로덕트 디자이너는 피그마에서 작업하고 프론트엔드 개발자는 코드를 작업합니다.
디자이너 작업을 위한 하위 이슈, 프론트엔드 작업을 위한 하위 이슈와 상태를 각각 생성하여 관리합니다.

31
Jira 디자인 시스템 보드
32
Jira 워크플로우

이렇게 Jira를 통해 관리하면 다음과 같은 장점이 있습니다.

  • 커밋에 이슈가 연결되어 커밋만으로도 히스토리를 찾아가기 쉽습니다.
  • 개발자, 디자이너 상관없이 Jira 보드를 보는 것만으로도 현재 어떤 작업이 진행 중인지 가시적으로 확인할 수 있습니다.

33

Confluence

Jira는 문서 작성 툴이 아니기 때문에 Jira만으로는 한계가 있습니다.
Confluence에서 대부분의 문서를 작성해 기록을 남기고 있으며 주로 회의록을 상세하게 작성합니다.

34

4. 이슈 제보

디자인 시스템에 대한 이슈는 슬랙을 통해서도 제보되지만 레포지토리의 이슈 탭을 통해서도 제보될 수 있습니다.
하지만 앞서 설명드렸듯 인프랩의 모든 일은 Jira로 관리되기 때문에 이를 Jira로 일원화하기 위해, 이슈 탭에 작성된 것은 Github Action을 이용해 Jira issue로 이관합니다.

5. 오픈 소스 버전 업데이트

저희가 사용하는 Mantine은 업데이트 빈도가 정기적이고 꽤 잦은 편이었습니다.

Breaking change가 없는 마이너, 패치 업데이트는 큰 비용을 들이지 않고 업데이트가 가능했습니다.
다만, Mantine에서 제공하는 패치 노트를 요약해서 팀에 전달하는 작업이 추가되었습니다.
특히 오픈 소스 자체가 아무래도 개발자를 타겟으로 하는 것이다 보니 디자이너 분들이 이해하기 어려운 서술도 존재했는데요.
디자인 파트에서 꼭 아셔야 하는 부분은 별도로 요약 및 발췌하여 전달하여 업데이트를 진행했습니다.

35

2년 동안 3개의 메이저 버전이 공개되었으니, 메이저 업데이트도 꽤 잦은 편이라고 할 수 있을 것 같습니다.
Mantine은 메이저 업데이트가 공식적으로 릴리즈되기 전부터 어떤 작업들이 진행되고 있는지 깃허브와 디스코드 채널을 통해 확인할 수 있습니다.
이를 통해 변경점들을 미리 살펴보고, 리소스를 들여서라도 따라가야 할 정도로 판단되면 우리의 디자인 시스템에 대해서도 메이저 업데이트를 준비했습니다.
모든 메이저 업데이트를 다 따라갈 필요는 없지만, 지금까지는 항상 따라갈 필요가 있는 업데이트들만 이루어졌습니다.

36

메이저 버전 업데이트를 진행하기 앞서 먼저 업데이트 계획을 세우고, 이에 대해 Frontend 파트에서 먼저 의견을 수렴한 후 수렴된 의견을 바탕으로 디자인 파트와 논의합니다.

37

또한 새로운 메이저 버전이 공개되면 마이그레이션 가이드가 제공되기 마련입니다.
다만 대부분의 내용은 디자인 시스템 레포지토리 안에서 업데이트를 준비하는 사람만 알아도 되는 부분이었기 때문에, 실제로 시스템을 사용하는 개발자들이 알아야만 하는 것을 따로 정리하여 문서로 제공했습니다.

38

앞으로 남은 작업들

디자인 시스템이 성공적으로 자리를 잡았으니 다음으로는 고도화할 차례입니다.
크게 다음과 같은 일들을 계획하고 있습니다.

혹시 함께하고 싶으시다면… 클릭해 보시겠어요? 😉

  1. Emotion 제거
  • React가 주도하는 서버 패러다임에 좀 더 수월하게 대응하기 위해 런타임 비용이 발생하는 Emotion 제거를 준비중입니다.
  • 우연이지만, 저희가 사용하는 Mantine 또한 Emotion에 대한 의존성을 제거하고 css module 혹은 Zero-runtime 도구를 적용하는 메이저 업데이트를 진행했기 때문에 작업하기 좀 더 수월해졌습니다.
  1. 디자인 토큰 적용
  • 디자인 시스템을 이용하는 개발자와 디자이너의 경험은 지속적으로 개선되어야 합니다.
  • 피그마에 업데이트된 variables 라는 기능을 활용하여 원활하게 소통하기 위해, 다크모드 지원을 준비하기 위해 토큰 작업을 준비중입니다.
  • 디자인 토큰을 기반으로 하는 새로운 사이즈 추가 등의 변화가 있을 예정입니다.
  1. 반응형 개선
  • Mantine v4~v6 버전을 이용하면서 가장 아쉬웠던 부분은 Chakra처럼 컴포넌트의 property들을 브레이크 포인트별로 적용할 문법이 없었다는 점입니다.
    // 이런 식으로 말이죠!
    <Text fontSize={{ base: "24px", md: "40px", lg: "56px" }}>
      This is responsive text
    </Text>
    그래서 직접 공통 프로퍼티로 관리, 추가할 계획을 가지고 있었는데요. 이제 Mantine v7에서 해당 기능이 지원되어, 메이저 업데이트를 하는 것으로 작업의 크기가 축소되었습니다.

마무리

이렇게 오픈 소스를 이용해 디자인 시스템을 만들고 운영하면서 저희 팀은 무엇을 배웠을까요? 개인적으로 생각했을 땐 이론으로만 알고 있던 것들을 직접 몸으로 부딪히면서 체득할 수 있었다는 점이 가장 큰 것 같습니다.

오픈 소스를 이용해서 디자인 시스템을 운영한다는 것의 빛과 어둠, 피그마를 통해 소통하는 방법, 서로 다르게 생각하는 10명 이상의 사람들의 생각을 맞추고 같은 방향을 바라보는 연습, 패키지 버전과 의존성에 대한 이해, 모듈 시스템과 트리셰이킹에 대한 이해, 사내 라이브러리지만 오픈 소스를 운영하는 경험 등등…

자세히 이야기하려면 글이 너무 길어질 것 같아 간단히만 언급하는 점 양해 부탁드립니다.

디자인 시스템 구축에 참여한 이후로 문제가 생기면 그냥 냅다 코드를 까보는 습관이 생겼으며, 오픈 소스 커뮤니티에 참여하는 경험도 쌓을 수 있었습니다. 어떻게 하면 이슈를 빠르게 검색해 볼 수 있는지 노하우도 생겼죠.

회사적 성과로는 디자인 시스템을 통해 프로젝트 개발 속도가 유의미하게 향상되고, 디자인 시스템이 제공하는 컴포넌트나 Foundation에 대한 QA 비용이 감소하여 프로덕트 파트가 보다 더 중요한 것에 집중할 수 있게 되었습니다.
물론 오픈 소스를 사용하는 것에 대한 답답함이 완전히 없는 것은 아니지만, 디자인 시스템에 대한 디자이너와 프론트엔드 개발자의 만족도도 높은 편입니다.

2년의 경험을 압축적으로 글로 담아내려다 보니 추리고 추렸음에도 많이 긴 글이 되었는데요, 긴 글임에도 불구하고 읽어주셔서 감사합니다 :)

더 좋은 방법이나 제안에 대한 코멘트는 언제나 환영입니다! 😊