Skip to content

VRT로 디자인 시스템 마이그레이션하기

Web | 2026년 7월 15일


들어가며

회사에서 레거시 UI를 신규 디자인 시스템으로 마이그레이션하며, 타입 검사와 시각 회귀 검증으로 변경의 안전성을 확인했습니다. 당시 서비스에는 두 라이브러리의 컴포넌트가 함께 사용되고 있어, 레거시 의존성을 점진적으로 줄여야 했습니다.

하지만 컴포넌트마다 API가 달라 import만 바꿀 수 없었고, 타입 검사를 통과해도 레이아웃이 달라질 수 있었습니다. 그래서 파일 단위로 컴포넌트를 전환한 뒤, 타입 검사와 시각 회귀 검증을 차례로 수행했습니다.

이 글에서는 그 과정에서 마주한 문제와 검증 방식을 소개합니다. 회사 내부 구현과 구분하기 위해 기존 컴포넌트는 Legacy*로 표기하고, 일부 코드와 명칭은 핵심 의미를 해치지 않는 범위에서 단순화했습니다.

이름이 비슷해도 같은 컴포넌트는 아니다

이름과 역할이 비슷한 컴포넌트라도, 전달해야 하는 props의 이름과 구조는 달랐습니다.

예를 들어 LegacyBox는 여러 레이아웃 정보를 하나의 문자열 prop에 담았습니다.

<LegacyBox flow="row wrap" align="center space-between" />

신규 디자인 시스템의 Flex에서는 각 속성의 역할을 나눠서 표현해야 합니다.

<Flex
  direction="row"
  wrap="wrap"
  align="center"
  justify="space-between"
/>

flowdirectionwrap으로, alignalignjustify로 나뉩니다. 이런 차이 때문에 import만 일괄 치환할 수 없었습니다.

더 까다로운 경우는 하나의 레거시 컴포넌트가 여러 신규 컴포넌트로 나뉠 때였습니다.

레거시 컴포넌트신규 컴포넌트선택 기준
LegacyBoxFlex하나의 문자열 prop을 역할별 prop으로 분리
LegacyTooltipTooltip / HoverCard짧은 용어 설명인지, 긴 상세 안내인지에 따라 선택
LegacyTagBadge / Chip상태 표시용인지, 상호작용용인지에 따라 선택

LegacyTag를 예로 들면, 단순 상태 라벨은 Badge로 바꾸고 클릭하거나 선택할 수 있는 경우에는 Chip을 사용했습니다. LegacyTooltip도 짧은 설명은 Tooltip, 여러 줄의 상세 안내는 HoverCard로 옮겼습니다.

이 과정에서 세운 원칙은 하나였습니다. 기존 prop의 모양이 아니라 UI의 의미와 상호작용을 기준으로 새 컴포넌트를 선택한다.

규칙은 컴포넌트별로, 전환은 파일 단위로

먼저 LegacyBox, LegacyButton, LegacyTag처럼 컴포넌트 종류별로 변환 규칙을 정리했습니다. 실제 마이그레이션은 컴포넌트 종류가 아니라 파일을 기준으로 진행했습니다. 한 파일 안에 여러 레거시 컴포넌트가 있다면 각각의 규칙을 적용하고, 해당 파일의 import와 타입 오류까지 정리한 뒤 다음 파일로 넘어갔습니다.

작업 순서는 다음과 같았습니다.

  1. 대상 파일에서 레거시 import와 사용처를 찾습니다.
  2. 각 사용처에 컴포넌트별 prop 변환 규칙을 적용합니다.
  3. import를 신규 디자인 시스템 패키지로 바꿉니다.
  4. 레거시 컴포넌트가 남아 있지 않은지 확인하고, 비어 있는 레거시 import를 제거합니다.
  5. tsc --noEmit으로 타입을 검사합니다.
  6. 동적으로 전달되는 prop이나 1:1로 대응하지 않는 컴포넌트처럼 규칙만으로 바꾸기 어려운 예외를 직접 수정합니다.
  7. 포매터를 실행해 코드 형식을 정리합니다.
  8. as-is와 to-be 스토리를 작성하고, Playwright 스냅샷과 pixel diff로 화면 변화를 검증합니다.
  9. 컴포넌트가 사용된 실제 페이지를 역추적해 URL과 위치, 접근 조건을 확인합니다.

타입 검사는 prop 변환에서 놓친 부분을 빠르게 드러냈습니다. 예를 들어 기존에는 prop으로 전달하던 텍스트를 신규 컴포넌트에서는 children으로 옮겨야 하는 경우가 있었습니다. 정해진 규칙으로 처리할 수 없는 동적 prop과 컴포넌트 분기는 타입 오류를 바탕으로 직접 수정했습니다.

하지만 타입 검사만으로는 충분하지 않았습니다. alignjustify를 잘못 나눠도 값이 유효하면 컴파일은 성공합니다. 컴포넌트의 높이나 여백이 달라져도 타입 오류는 발생하지 않습니다. 타입 검사는 API가 올바른지 알려줄 뿐, 화면이 이전과 같은지는 알려주지 못합니다.

두 UI를 같은 조건에서 비교한다

변경 전후를 안정적으로 비교하려면 두 화면이 같은 조건에서 렌더링되어야 합니다. 이를 위해 Feature Flag로 레거시 UI(as-is)와 신규 UI(to-be)를 분기했습니다.

return isDesignSystemMigrationEnabled
  ? <NewComponent />
  : <LegacyComponent />;

같은 배포본에서 플래그만 바꿔 두 UI를 비교할 수 있으므로 데이터나 주변 화면이 달라져 생기는 변수를 줄일 수 있었습니다. 예상하지 못한 문제가 발견되면 플래그를 내려 기존 UI로 돌아갈 수도 있었습니다.

다만 실제 서비스 화면만 눈으로 비교하면 확인 결과가 사람마다 달라지고, 반복 작업에도 시간이 많이 듭니다. 그래서 Storybook과 Playwright를 이용해 비교 과정을 고정했습니다.

컴포넌트가 아니라 사용 문맥을 Storybook에 옮긴다

버튼 하나를 고립해서 캡처하면 버튼 자체의 색상이나 크기 차이는 찾을 수 있습니다. 하지만 마이그레이션에서 자주 생기는 문제는 주변 요소까지 밀리는 레이아웃 회귀입니다.

VRT용 Storybook 스토리에는 대상 컴포넌트만 넣지 않았습니다. 실제 파일의 부모 컨테이너, 형제 요소, 간격과 정렬을 함께 재현했습니다. store나 번역 값처럼 스토리에 불필요한 의존성은 고정된 값으로 대체하되, 화면에서 컴포넌트가 놓이는 문맥은 유지했습니다. 같은 문맥에서 레거시 컴포넌트를 사용한 as-is와 신규 컴포넌트를 사용한 to-be 스토리를 각각 만들었습니다.

스토리에는 다음 정보도 함께 기록했습니다.

  • 원본 파일 경로
  • 실제 페이지의 URL 패턴과 예시 URL
  • 페이지에서 컴포넌트가 보이는 위치
  • 로그인이나 특정 데이터처럼 화면을 보기 위한 조건
  • 연결된 Feature Flag 이름

이 정보는 diff를 발견한 뒤 실제 화면으로 돌아가는 경로가 됐습니다. 단순히 “이미지가 다르다”에서 끝나지 않고, 어느 페이지의 어떤 상태를 확인해야 하는지 바로 알 수 있었습니다.

마이그레이션 스토리만 불러오는 VRT 전용 Storybook도 따로 구성했습니다. 기존 Storybook과 스토리 ID가 겹치는 문제를 피하고, 검증 대상을 마이그레이션 화면으로 한정하기 위해서였습니다.

픽셀 차이를 만들고, 차이가 생긴 위치를 본다

Playwright의 toHaveScreenshot()으로 as-is와 to-be를 각각 캡처했습니다. 이후 ImageMagick의 compare로 두 이미지 사이의 pixel diff를 만들고 변경 픽셀 수와 비율을 계산했습니다.

as-is와 to-be UI를 비교해 변경된 컴포넌트 영역을 빨간색으로 표시한 pixel diff 예시

위 예시처럼 diff가 대상 컴포넌트 안에만 나타나는지 확인했습니다.

Storybook에서 as-is / to-be 렌더링
Playwright로 두 스냅샷 캡처
ImageMagick으로 pixel diff 생성
변경 위치와 비율을 리포트에 기록
실제 서비스 URL에서 최종 확인

전체 페이지가 아니라 #storybook-root > div 영역만 캡처했습니다. 전체 화면을 기준으로 변경 비율을 계산하면 작은 컴포넌트의 차이가 넓은 빈 영역에 묻힐 수 있기 때문입니다. 비교하려는 UI 문맥만 캡처해야 작은 정렬 변화도 드러납니다.

판단할 때는 변경 비율보다 diff가 생긴 위치를 먼저 봤습니다.

diff의 위치판단
차이가 없음기존 UI와 시각적으로 동일
대상 컴포넌트 안에만 차이가 있음신규 디자인 시스템이 의도한 색상, 글꼴 굵기 등의 변화인지 확인
간격, 정렬, 주변 텍스트까지 차이가 번짐레이아웃 회귀로 보고 수정

변경 비율은 확인 우선순위를 정하는 보조 지표로 사용했습니다. 1% 미만은 서브픽셀이나 안티앨리어싱 차이일 수 있습니다. 1% 이상은 원인을 확인하고, 5% 이상이면 레이아웃 회귀 가능성을 우선 점검했습니다. 비율이 작다고 자동으로 통과시키거나, 크다고 곧바로 실패시키지는 않았습니다. 작은 차이라도 컴포넌트 밖에서 발생하면 문제일 수 있고, 큰 차이라도 신규 디자인 시스템이 의도한 스타일 변경이 컴포넌트 안에만 모여 있다면 수용할 수 있기 때문입니다.

각 결과는 원본 파일, URL, 화면 위치, 변경 픽셀 수, 변경 비율과 함께 VRT 리포트에 남겼습니다. 1% 이상 차이가 난 항목에는 diff 이미지와 원인, 수용 여부도 기록했습니다.

마지막 확인은 실제 서비스 화면에서 한다

Storybook은 비교 조건을 통제하기 좋지만 실제 서비스의 모든 상태를 담지는 못합니다. 따라서 VRT가 끝난 뒤에는 마이그레이션한 컴포넌트가 실제로 렌더링되는 페이지를 찾아 확인했습니다.

먼저 변경한 파일에서 export한 컴포넌트를 찾고, 이를 import하는 부모를 라우트까지 거슬러 올라갔습니다. 이후 라우트 설정에서 URL 패턴을 확인했습니다. 동적 파라미터와 페이지 안의 위치, 로그인이나 특정 데이터 같은 접근 조건도 함께 정리했습니다.

이 단계까지 마쳐야 다른 개발자도 같은 화면을 다시 확인할 수 있습니다. 파일 경로만 전달하면 코드를 다시 추적해야 하지만, URL과 위치, 접근 조건이 있으면 문제가 발생한 화면으로 바로 갈 수 있습니다.

반복한 판단을 마이그레이션 규칙으로 만든다

파일마다 같은 문제를 다시 판단하지 않도록 작업 과정에서 얻은 규칙을 문서로 모았습니다. 문서에는 컴포넌트별 prop 매핑뿐 아니라 다음 내용도 포함했습니다.

  • LegacyTagLegacyTooltip처럼 용도에 따라 대상을 나누는 기준
  • 동적 variant와 size처럼 자동 변환하기 어려운 예외
  • 타입 검사와 포맷팅 절차
  • Storybook 스토리 작성 규칙과 VRT 실행 방법
  • 실제 페이지 URL을 찾고 보고하는 방법
  • 자주 반복된 실수와 확인 항목

새 파일을 마이그레이션할 때는 이 문서의 규칙을 먼저 적용하고, 타입 오류나 시각적 차이에서 발견한 새 예외를 다시 문서에 반영했습니다. 작업할수록 다음 파일에서 내려야 할 결정이 줄어드는 구조를 만든 것입니다.

마치며

디자인 시스템 마이그레이션에서 타입 안정성과 시각적 안정성은 서로 다른 문제였습니다. 타입 검사는 잘못된 API 변환을 찾았고, 실제 문맥을 담은 Storybook과 Playwright VRT는 타입으로 잡을 수 없는 레이아웃 변화를 드러냈습니다. Feature Flag는 두 UI를 같은 조건에서 비교하고 문제가 있을 때 되돌릴 수 있게 했습니다.

가장 효과가 있었던 것은 이 검증을 파일 단위 작업 순서에 포함한 점이었습니다. 레거시 import를 바꾸고 끝내는 대신, 타입을 확인하고, 픽셀 차이의 위치를 보고, 실제 URL에서 다시 확인했습니다. 그리고 그 과정에서 얻은 판단을 규칙으로 남겼습니다.

위 과정 덕분에 마이그레이션을 작은 범위에서 검증하며 반복할 수 있었습니다. 디자인 시스템을 안전하게 바꾸려면 무엇을 교체할지 뿐만 아니라, 교체한 결과를 어떤 순서와 기준으로 확인할지도 함께 설계하는 과정이 필요할 것 같습니다.