Testlify는 4개의 별도 웹 애플리케이션으로 구성된 제품군으로 운영된다. 이전에는 각 제품 팀이 공통 UI 컴포넌트를 독립적으로 개발하고 스타일링해서, 중복된 엔지니어링 작업과 제품 간 시각적 불일치를 초래했다. 나는 UI 개발을 표준화하기 위한 공유 Vue 3 컴포넌트 라이브러리를 제안하고 그것을 만든 팀을 이끌었다.
내가 만든 것
- 아키텍처적 통합: 공통 UI 프리미티브를 중앙화된 라이브러리로 통합하는 이니셔티브를 주도해, 팀 간 일관된 디자인 토큰과 컴포넌트 계약을 확립했다.
- 상태 없는 컴포넌트 설계: 라이브러리 컴포넌트를 엄격하게 상태 없는(stateless) 프레젠테이션 형태로 유지해, 비즈니스 로직을 분리한 채로 4개 애플리케이션 어디에든 깔끔하게 통합될 수 있게 했다.
- 프라이빗 패키지 배포: 라이브러리를 GitHub Packages에 버전 관리된 패키지로 게시하고 유지보수해, 팀들이 코드 중복 없이 표준 의존성 업데이트를 통해 버그 수정과 새 컴포넌트를 도입할 수 있게 했다.
왜 중요했는가
UI 개발을 하나의 공유 패키지로 중앙화함으로써 4개 애플리케이션 전반의 중복 코드를 없앴고, 기능 배포 주기를 가속화했으며, Testlify 전체 제품군에 걸쳐 일관된 디자인 시스템을 보장했다.
주도했지만, 혼자는 아니었다
나는 라이브러리를 제안하고 아키텍처를 주도했지만, 이것은 한 사람의 작업이 아니었다: 2026년 5월 이후 7명의 기여자로부터 86건의 커밋이 있었고, 내 커밋이 전체의 약 3분의 2를 차지한다. 4개 제품 팀에 걸쳐 공유 디자인 시스템을 채택시키는 데는 컴포넌트를 작성하는 것 이상이 필요했다. 채택을 저위험으로 만드는 릴리스 프로세스가 필요했고, 그것이 진짜 어려운 문제가 되었다.
모든 PR에 게시하지 않고 릴리스되지 않은 컴포넌트 변경을 테스트하기
공유 라이브러리의 가장 큰 채택 장벽은 컴포넌트를 작성하는 것이 아니라, "내가 뭔가를 바꿨다"와 "소비하는 팀이 실제로 그것을 써볼 수 있다" 사이의 간극이다. 매 PR마다 임시 버전을 게시하는 것은 대부분의 PR이 어디에도 설치되지 않는 패키지에 대해 실질적이고 반복적인 CI 비용을 발생시키므로, 이는 선택적이다: 소비자가 실제로 테스트해야 할 때만 PR에 needs-snapshot 라벨이 붙고, 이것이 임시 @pr<번호> 태그가 붙은 패키지를 게시하는 snapshot.yml GitHub Action을 트리거한다. 짝을 이루는 cleanup-snapshot.yml이 PR이 닫히면 그 태그를 제거한다. 정기 릴리스는 Changesets를 통해 두 채널로 나가는데, main에서 @latest, 스테이징 브랜치에서 @beta이며, 릴리스 워크플로우는 CI가 통과하면 자체 생성한 "Version Packages" PR을 자동으로 머지한다.
자체 의존성 방향을 강제하는 모노레포
라이브러리는 하나의 평평한 패키지가 아니라 여러 계층으로 나뉘어 있다: tokens, icons, styles는 내부 의존성이 전혀 없는 최하위 계층에 있고, core는 그 위에 구축되며, ds는 core 위에, ds-patterns는 ds, core, styles 위에 구축된다. 각 계층은 위로가 아니라 아래로만 의존할 수 있어서, 저수준 프리미티브가 조용히 고수준 패턴 컴포넌트에 대한 의존성을 키울 수 없다. ds-patterns도 단순한 범용 프리미티브가 아니다: 실제 Testlify 도메인 위젯(예를 들어 CandidateCard)을 포함하며, 나머지 모든 것이 쓰는 것과 같은 프리미티브 계층 위에 구축되어 있다. 접근성 애드온이 활성화된 Storybook이 모든 컴포넌트를 문서화하고 공개적으로 배포되어, 소비 팀(또는 외부 리뷰어)이 저장소를 가져오지 않고도 컴포넌트의 상태와 접근성 결과를 확인할 수 있다. 모든 컴포넌트는 또한 수동 검사를 위한 Storybook 애드온에 그치지 않고, CI에서 실제 axe-core 검사를 실행하는 전용 .a11y.spec.ts 파일을 갖고 있다.
데이터 모델과 규모
- 6개의 게시된 패키지(
ds,-core,-icons,-patterns,-styles,-tokens), 각각 독립적으로 버전 관리됨. - 라이브러리 자체를 직접 검증하는 243개의 스펙 파일(Vitest + Vue Test Utils), 소비 앱만이 아니다.
- CI, 프리뷰 및 프로덕션 배포, Storybook 배포, 스테이징 및 프로덕션 릴리스, PR 스냅샷, 스냅샷 정리를 다루는 9개의 GitHub Actions 워크플로우.
- pnpm 워크스페이스와 Turborepo가 주어진 변경에 실제로 영향받는 패키지로 빌드 작업 범위를 캐싱하고 좁힌다. 테스트는 Turbo의 태스크 그래프를 거치지 않고 워크스페이스 전체에서 Vitest를 통해 직접 실행된다.
채택은 강제된 일괄 전환일이 아니라 팀별로 단계적으로 이루어졌다: 리크루터/관리자 앱은 오늘 6개 게시된 패키지 모두를 사용하는 반면, 다른 제품 팀들은 4개 애플리케이션 전체에 걸친 단일 조율된 전환이 아니라 각자의 릴리스 일정에 맞춰 개별 컴포넌트를 도입하고 있다.
Build vs. Buy
수작업 버전 범핑 스크립트 없음: Changesets가 PR당 하나의 changeset 파일로부터 6개 패키지 전체의 버전 관리와 체인지로그를 관리한다. 자체 호스팅 프라이빗 레지스트리 없음: GitHub Packages는 Verdaccio 같은 별도의 레지스트리를 실행하고 보안을 유지하는 대신, 이미 저장소 접근을 관리하는 동일한 GitHub 인증을 재사용한다.
Stack
Vue 3, TypeScript, Vite, Turborepo와 함께하는 pnpm 워크스페이스, 접근성 애드온이 포함된 Storybook, 버전 관리를 위한 Changesets, Vitest와 Vue Test Utils, GitHub Packages를 통해 게시.