AI로 디자인시스템 마이그레이션하기 (1): 여정의 시작
안녕하세요. 인프랩 프론트엔드 개발자 고슈입니다.
현재 인프랩 프론트엔드 팀은 20여 개의 애플리케이션에서 사용하고 있는 디자인시스템을 최신 버전으로 마이그레이션하는 작업을 진행하고 있습니다. 정확히는 인프랩의 디자인시스템은 오픈 소스 Mantine v6를 래핑해서 사용하는 방식이기 때문에, 기존 디자인시스템을 v8로 옮기는 작업이었는데요.
작업 진행 중간에 v9 버전이 나왔으나, 안정성이 확보되어있고 v8로도 충분히 현재 팀의 문제를 해결할 수 있었기 때문에 상황을 유지했습니다. 하지만 이후, 필요하다면 마이그레이션이 쉬울 수 있도록 확장성을 고려하며 작업을 진행했습니다.
단순히 라이브러리 버전을 하나 올리는 일처럼 들릴 수 있지만, 실제로는 스타일의 기반 자체를 바꾸는 작업이었습니다. 그래서 1차 시도에서는 실패를 겪기도 했는데요. 그 원인을 분석해, 이번 2차 마이그레이션은 지속 가능한 방식으로 그 토대가 되는 컴포넌트 라이브러리를 교체하는 데 초점을 맞췄습니다.
그래서 이 작업은 한 사람이 손으로 끝낼 수 있는 규모가 아니었고, 본업과 병행해 사이드로 진행해야 했기에 더더욱, AI 에이전트를 본격적으로 활용하는 방식을 선택했습니다.
AI를 활용한 마이그레이션은 요즘 많은 팀이 시도하고 있는 만큼, 파급 범위가 넓은 기반 라이브러리를 옮긴 사례를 기록해두는 것이 현재까지 진행하고 있는 사내 팀원들과 또는 유사한 마이그레이션을 진행하고자 하는 다른 분들께 도움이 되고자 작성하게 되었습니다.
프로덕트 기능을 개발하면서 기술부채를 해결하는 일은 언제나 개발자로서 책무 같은 건데요. AI 시대가 찾아온 만큼, 해야지..해야지.. 하고 미루던 일들을 기능 개발하면서도 함께 진행할 수 있었던 점이 가장 큰 장점 같습니다.
이 글은 그 여정을 기록한 시리즈의 첫 번째 글입니다.
한 편에 모든 기술적 디테일을 담기보다는, 먼저 ‘왜’, ‘어떻게’에 해당하는 전체 흐름을 따라가 보려고 합니다. 그 과정에서 마주친 트러블슈팅도 존재하지만 간략하게 다루고, 이번 글에서는 흐름을 따라가는 데 집중하겠습니다.
그래서 이 시리즈는 “이렇게 하면 됩니다”라는 정답지가 아니라, “이런 시행착오를 거쳐 이런 선택을 했습니다”라는 일지에 가깝습니다. 시리즈 전체의 지도를 그리는 글이라고 생각하고 편하게 읽어주시길 바랍니다. 😊
1부 · 배경: 왜 바꿔야 했나
기존 디자인시스템 이야기
앞서 말씀드렸던 것처럼, 인프랩은 플랫폼 팀 없이 가지고 있는 자원으로 오픈 소스 기반의 디자인시스템인 만타인(Mantine)을 래핑하여 운영해 왔습니다.
이 디자인시스템이 어떤 배경에서 태어났고 어떻게 운영되어 왔는지는 이미 동료가 자세히 정리한 글이 있으니, 처음 접하시는 분은 먼저 읽어보시길 권합니다.
요약하면, 인프랩의 디자인시스템은 다음과 같은 모습입니다.
- Mantine v6를 기반으로, 인프런·랠릿의 공통 컴포넌트를 패키지 단위로 제공
- 컴포넌트별로
@inflearn/ds-*형태의 패키지를 발행하고,@inflearn/ds-react라는 엔트리 패키지로 묶어 사용 - 스타일링 라이브러리는 Emotion을 사용
- 마이크로프론트엔드 구조로 전사 20여 개 애플리케이션이 이 패키지를 직접 의존
디자인시스템 위에서 대부분의 인프런 서비스들이 개발되고 있습니다.
v8로 옮겨야 했던 이유
잘 쓰고 있는데 버전을 굳이 왜 올려야 하나요?
저희 팀이 v8로의 이전을 결심한 이유는 다음과 같습니다.
-
일부 컴포넌트 낮은 버전 미지원
Spoiler,RadioCard처럼 상위 버전(v7 이상)에서는 기본으로 제공되는 컴포넌트를, v6에서는 존재하지 않아 유사한 디자인으로 직접 구현해 유지보수하고 있었습니다. -
반응형 처리 DX 향상
v6에서는 반응형 처리를 위해 emotion css props로 mediaQuery를 직접 작성했으나, v8에서는 각 컴포넌트마다 반응형을 객체로 작성할 수 있어 스타일 응집도를 높일 수 있습니다.// Mantine v6 function DemoV6() { return <Box p={24} css={{ [mediaQuery.tablet]: { padding: 16, // ... }, [mediaQuery.mobile]: { padding: 12, // ... }, }} />; } // Mantine v8 function DemoV8() { return <Box p={{ base: 12, md: 16, lg: 24 }} />; } -
TypeScript 5 버전업
기존에는cssprop의 타입이 복잡해지면Expression produces a union type that is too complex to represent에러가 발생했습니다. 해당 문제는 TypeScript v5 이상일 때 발생했으며, Mantine 버전을 올렸을 때 해당 문제를 해결할 가능성이 생깁니다. 관련 이슈 -
RSC 등의 생태계 변화에 대비
Mantine 버전은 계속 올라갈 것이고, 언젠가 스타일링 기술(Emotion)을 교체할 가능성도 있습니다. 그때마다 처음부터 헤매지 않도록, 마이그레이션 방식 자체를 템플릿화해 두는 것이 목표였습니다. v9, v10이 나와도 같은 흐름을 재사용할 수 있도록 구성하였습니다. -
AI를 위한 Storybook 구축
기존 Storybook은 여러 개발자들이 수작업으로 진행했기 때문에 작성자에 따라 작성하는 방식이 달랐습니다. 이는 AI로 디자인시스템을 참고할 때 혼동을 줄 수 있으므로, 이를 rule로 적용하여 AI로 자동화하는 방향을 모색 중입니다.
2부 · 1차 시도, 그리고 실패에서 배운 것
1차에는 무엇을 시도했나
팀에서는 2025년에도 마이그레이션(v7)을 위해 1차 도전을 진행했습니다. 1차 마이그레이션은 신규 레포만 사전 실험으로 적용하였고, 레거시 레포에는 적용하지 못하는 한계가 있었습니다.
1차 마이그레이션 방식은 다음과 같은 특징이 있었습니다.
- 기존 디자인시스템 & 신규 디자인시스템 혼합 사용 불가 (의존성 충돌)
- yarn → pnpm 전환
- emotion (CSS-in-JS) → vanilla-extract (zero runtime CSS) 스타일 전체 전환
- 동일 레포 내 알파 버전(
xxx-next.x)으로 배포
왜 실패했나
핵심 병목은 레거시와 신규 디자인시스템을 혼합해서 사용할 수 없다는 점이었습니다. 내부 의존성 버전이 충돌해 두 시스템을 함께 쓸 수 없었고, 결국 한 번에 전환해야 하는 부담이 생겼기 때문입니다.
여기에 yarn → pnpm 전환, 스타일 전체 전환, 알파 버전 병행 관리 같은 복합적인 변경이 겹치면서 마이그레이션 난이도가 한층 높아졌습니다. 기능을 작업하고 있는 동료들 입장에서는, 이 모든 변화를 한꺼번에 받아들이며 마이그레이션을 진행하기가 부담스러울 수밖에 없었습니다.
다시 세운 원칙 - “한 번에 올리려 하지 말기”
그래서 저희는 디자인시스템을 함께 사용할 수 있는 환경을 먼저 구성하고, 패키지를 격리시키는 것이 마이그레이션의 첫 출발점이라고 판단했습니다.
이러한 방식에 대한 아이디어는 네트워킹을 통해 착안할 수 있었습니다. AI를 더 잘 써보자는 마음으로 토스의 프론트엔드 다이빙클럽 일곱번째 모임에 참여했었는데요. 여기서 강동윤 님 외 다른 훌륭한 타 회사 프론트엔드 개발자분들과 인프랩 팀이 고민하고 있는 문제를 함께 진지하게 고민해주셨고, 이를 통해 도출한 해결 방식입니다. 이 자리를 빌려 함께 고민해주셔서 감사드립니다.
// package.json - 1차 마이그레이션 방식 (대체)
dependencies: {
// "@inflearn/ds-react": "^3.14.0",
// "@mantine/core": "~6.0.0",
// ...
"@inflearn/ds-react": "^4.0.0-next.12",
"@mantine/core": "~8.0.0", // 의존성까지 변경해줘야함
// ...
}
// package.json - 2차 마이그레이션 방식 (혼합)
dependencies: {
"@inflearn/ds-react": "^3.14.0",
"@mantine/core": "~6.0.0",
// ...
"@inflearn/ds-react-next": "^1.0.0",
// ...
}1차가 기존 패키지를 대체하는 방식이었다면, 2차는 신규 패키지를 나란히 추가해 혼합하는 방식입니다.
번들 사이즈가 늘어나는 트레이드오프는 있지만, 한 번에 갈아엎는 방식보다 안정적이고 지속 가능하다는 점에서 의견이 모였습니다.
구체적으로 두 버전을 어떻게 격리하고 공존시켰는지는 다음 부에서 이어가겠습니다.
3부 · 2차 시도: ‘공존’을 설계하다
v6와 v8을 한 앱에서 공존시키기
이번에는 별도의 새 레포지토리(design-system-next)를 만드는 것에서 출발했습니다. 기존 디자인시스템을 직접 건드리지 않고, v8 기반의 디자인시스템을 독립된 공간에서 만들어가는 전략입니다.
핵심 전략은 v6와 v8이 한 애플리케이션 안에서 공존할 수 있게 한다는 것입니다. 전사를 한 번에 멈추고 갈아 끼우는 것은 불가능하므로, 두 버전이 같은 페이지에서 충돌 없이 동작해야 점진적 마이그레이션이 가능하기 때문입니다.
가장 먼저 핵심 Context 컴포넌트인 ThemeProvider를 신규로 구성해 npm 레지스트리에 배포하고, 레거시와 신규 디자인시스템을 혼합해서 쓸 수 있는지 PoC로 검증했습니다. 그 위에, 두 시스템의 스타일이 런타임에 섞이지 않도록 두 가지 격리 장치를 두었습니다.
- CSS 클래스 격리: v6는
.mantine-Button-root, v8은.mantine-next-Button-root처럼 신규 DS의MantineProvider에classNamesPrefix를 설정해 클래스 이름을 분리했습니다. 난수화된 클래스 충돌 없이 두 시스템을 격리된 상태로 함께 쓸 수 있는 구조를 확보한 것입니다. - Provider 분리: v6용
ThemeProvider와 v8용ThemeProvider를 따로 두어, 화면 영역별로 감싸 쓸 수 있게 했습니다.
import { ThemeProvider as ThemeProviderV6 } from '@inflearn/ds-theme-provider';
import { ThemeProvider as ThemeProviderV8 } from '@inflearn/ds-theme-provider-next';
function App() {
return (
<ThemeProviderV8>
<ThemeProviderV6>
<Page />
</ThemeProviderV6>
</ThemeProviderV8>
);
}의존성 격리 (peer → dependency)
두 버전이 한 앱에서 공존하려면, 디자인시스템이 의존하는 @mantine/core·@mantine/hooks 등의 버전부터 분리되어야 합니다. 그래서 신규 패키지에서는 이들을 peerDependencies 대신 dependencies에 포함시켜, v8 의존성을 패키지 안에 완전히 격리한 채 배포했습니다.
// package.json - ds-react (기존)
peerDependencies: {
"@mantine/core": "~6.0.0",
"@mantine/hooks": "~6.0.0",
// ...
}
// package.json - consumer: 개별 설치 ✅
dependencies: {
"@inflearn/ds-react": "^3.14.0",
"@mantine/core": "~6.0.0",
"@mantine/hooks": "~6.0.0",
// ...
}
// package.json - ds-react-next (신규)
dependencies: {
"@mantine/core": "~8.0.0",
"@mantine/hooks": "~8.0.0",
// ...
}
// package.json - consumer: 개별 설치 ❌
dependencies: {
"@inflearn/ds-react-next": "^1.0.0",
// ...
}기존 ds-react는 @mantine/core@6을 peerDependencies로 두기 때문에 애플리케이션이 직접 같은 버전을 설치해 맞춰야 하지만, 신규 ds-react-next는 v8 의존성을 자체적으로 들고 있어 애플리케이션이 Mantine을 따로 설치할 필요가 없습니다(개별 설치 ❌). 덕분에 한 앱 안에서 v6와 v8 의존성이 서로 간섭하지 않습니다.
번들 사이즈가 상대적으로 커지는 트레이드오프가 있지만, 이러한 방식이 가지는 장점을 고려하여 마이그레이션 기간 동안 감수하기로 했습니다. 패키지 이름도 @inflearn/ds-{component}-next 규칙으로 통일해, 기존 패키지와 이름 충돌 없이 나란히 설치할 수 있도록 했습니다.
스타일은 왜 Emotion을 유지했나
Mantine은 v7부터 Emotion(CSS-in-JS) 의존을 걷어내고 네이티브 CSS(CSS Modules·CSS 변수) 기반으로 전환했습니다. 런타임에 스타일을 생성하는 비용을 없애 성능을 높이고, 번들 사이즈를 줄이며, React Server Components(RSC)처럼 CSS-in-JS가 제약되는 환경까지 지원하기 위해서입니다. (Mantine v7 — Migration to native CSS)
사실 1차 마이그레이션을 성공적으로 마무리하지 못한 이유는, 이 흐름을 그대로 좇아 Emotion(css prop)을 vanilla-extract로 전면 교체하려 했던 것도 한몫했습니다. 스타일 기술 자체를 통째로 바꾸는 일은 그 자체로 또 하나의 거대한 마이그레이션이기 때문입니다. 그래서 2차에서는 스타일링 방식은 건드리지 않고, 신규 디자인시스템에서도 @mantine/emotion을 통해 Emotion을 그대로 유지하기로 했습니다.
“네이티브 CSS로 옮겨간 Mantine” 위에 “Emotion을 얹은” 구조가 된 셈인데, 이 선택이 뒤에서 다룰 스타일 우선순위 이슈의 배경이 됩니다.
AI를 선택한 이유와 작업 분류
들어가기 앞서, 이해를 돕기 위해 용어정리를 한번 진행하고 가겠습니다.
[용어 정리]
- 내부 마이그레이션: 기존 디자인시스템 코드를 v8로 전환 (ds-react-next 생성)
- 외부 마이그레이션: 애플리케이션(Consumer)에서 디자인시스템 컴포넌트들을 마이그레이션 (ds-react-next로 전환)
내부 마이그레이션을 진행하기 위해 기존 디자인시스템을 살펴봤습니다.
기존 디자인시스템에는 컴포넌트가 89개, 그중 엔트리 패키지에 포함된 것만 75개가 있었습니다. 이 컴포넌트들을 v8 API에 맞게 하나하나 옮기고, 동작이 그대로인지 검증하고, 스토리북까지 다시 만드는 일은 한 사람이 손으로 처리하기 어려운 양이었습니다.
그래서 AI와 함께, 레거시 디자인시스템 컴포넌트를 구성 방식에 따라 4가지로 분류해 체크리스트를 만들었습니다.
| 분류 | 설명 | 난이도 |
|---|---|---|
| Type 1 | 그대로 래핑한 것 (v6 → v8 순수 래퍼 / 재export) | ⭐ |
| Type 2 | 기존 소스에 커스텀한 것 (v6 + 커스텀 → v8 + 커스텀) | ⭐⭐ |
| Type 3 | 새롭게 만든 것 (커스텀이었으나 v8이 기본 제공) | ⭐⭐ |
| Type 4 | 여러 DS를 혼합해 만든 것 (완전 커스텀 유지) | ⭐⭐⭐ |
이렇게 분류하고 보니, 상당수 컴포넌트가 “공식 문서의 변경점을 읽고 → 대응되는 API로 바꾸고 → 기존 동작과 비교 검증하는” 반복적인 패턴을 가지고 있었습니다.
이 패턴은 AI 에이전트로 충분히 자동화할 수 있겠다 생각하여, 단순히 “AI에게 코드를 짜달라고 한다”가 아니라, 마이그레이션 과정 자체를 에이전트가 따라갈 수 있는 절차로 문서화하는 방향을 택했습니다.
마이그레이션 절차는 대략 다음과 같이 정의했습니다.
분석 단계
- 마이그레이션할 컴포넌트 선정
- 기존 v6 컴포넌트의 내부 로직을 확인 (단순 래핑인지, 커스텀인지)
- Mantine v6·v8 공식 문서를 비교해 무엇이 달라졌는지 표로 정리
- 컴포넌트별 패키지들 Type 분류
실행 단계 (Type별 내부 마이그레이션)
- v8 API에 맞춰 변환
- v6와 v8의 동작이 같은지 비교 테스트 작성
- 스토리북 추가 (기본 스토리북, v6/v8 비교 스토리북)
- 플레이그라운드에서 동일하게 동작하는지 확인
- 레지스트리 배포
이 절차를 사람이 아니라 에이전트가 수행하도록 만드는 것이 2차 진행의 핵심이었습니다. 이렇게 내부(디자인시스템) 마이그레이션이 끝나면, 이 컴포넌트들을 사용하는 애플리케이션을 옮기는 외부 마이그레이션으로 이어집니다.
4부 · 실행: 에이전트와 함께 옮기기
이 부분은 백엔드 개발자인 우드가 AI 프롬프트 관리·레거시 DS 서브모듈 구성·공식 문서 추출 등 마이그레이션 인프라를 함께 구성해주셨습니다.
사전 작업 1. 에이전트 기반 작업
AI를 사용한 작업은 항상 같은 결과를 기대하기 어렵습니다. 작업 범위가 넓을수록 매 실행마다 조금씩 다른 결과가 나올 확률이 높습니다. 하지만 디자인시스템은 20여 개 애플리케이션이 의존하는 기반인 만큼, 누가 언제 돌려도 같은 규칙과 같은 형태로 수렴해야 합니다.
그래서 본격적인 마이그레이션에 앞서, 에이전트가 추측에 기대지 않고 가장 정확한 1차 출처(공식 문서)와 팀의 컨벤션을 참고하며 일관되게 작업할 수 있는 토대를 먼저 만들었습니다.
-
Markdown 가이드 문서 설정
Mantine v6·v8의 공식 문서를 md 파일로 추출해 레포에 함께 두고(두 버전 합쳐 500여 개에 달하는 문서), 컴포넌트별 변경점·마이그레이션 절차·코딩 컨벤션을 에이전트가 참조할 수 있는 형태로 정리했습니다. 색인 파일(manifest.json)을 함께 두어 컴포넌트마다 필요한 문서만 찾아 읽도록 했고, 추측 대신 로컬에 받아둔 공식 문서를 1차 출처로 삼게 함으로써 존재하지 않는 API를 지어내는 일을 줄였습니다. -
마이그레이션 절차의 커맨드화
앞서 정리한 분석/실행 절차를, 에이전트가 그대로 따라갈 수 있는 세 개의 커맨드 파일(변환·테스트·검증)로 고정했습니다. (현재 Claude Code에서는 스킬(Skills)로 통합되었습니다.) 매 컴포넌트마다 같은 순서·같은 기준으로 작업하게 하고, 반복적으로 마주친 함정(예: 스타일 우선순위, 아이콘 크기 주입)의 해결책까지 커맨드 파일 안에 규칙으로 적어두어 같은 실수를 반복하지 않도록 했습니다.
사전 작업 2. 검증 환경 작업
컴포넌트별로 마이그레이션이 되는 동안, 실제로 v6 와 v8이 공존할 수 있는지를 확인하기 위해 내부 마이그레이션이 완료된 컴포넌트를 검증하는 환경을 구축하였습니다.
-
Provider 작업
v6와 v8이 공존할 수 있도록 v8용ThemeProvider와 서버 스타일 동기화 장치를 정비했습니다. -
프리뷰 스토리북 제작
v6와 v8 컴포넌트를 나란히 놓고 비교하는 스토리북을 만들어, 마이그레이션 결과가 기존과 시각적으로 동일한지 바로 확인할 수 있게 했습니다. 개발자뿐 아니라 디자이너도 두 버전의 차이를 한눈에 파악할 수 있는 환경이 되었습니다. -
v6 ↔ v8 자동 비교 테스트
눈으로 보는 것에 더해, 두 버전을 같은 조건으로 렌더링한 뒤 픽셀 단위로 자동 비교하는 테스트를 두었습니다. 일정 비율(10%) 이상 차이가 나면 테스트가 실패하도록 해, 마이그레이션 과정에서 의도치 않게 외관이 틀어지는 것을 회귀 테스트처럼 잡아낼 수 있게 했습니다. 이 비교는 뒤이어 설명할 검증(verify) 단계에서 자동으로 실행되어, 자동화가 아무리 빠르게 결과를 찍어내더라도 “기존과 같다”는 보장 없이는 통과하지 못하게 하는 게이트 역할을 했습니다. -
샘플 Next.js 앱 테스트
실제 서비스와 유사한 Next.js 환경을 workspace로 구성해, 패키지가 실제 앱에서 정상 동작하는지, 혼합 사용 시나리오에서 문제가 없는지를 배포 전에 검증하는 플레이그라운드로 사용했습니다.
사전 작업 3. 마이그레이션 스크립트 구성
컴포넌트 하나를 옮길 때만 해도 수많은 컨텍스트가 필요합니다. 기존 v6 구현체와 코드 컨벤션, v6/v8 공식 문서, 그리고 앞선 컴포넌트에서 얻은 교훈까지 매번 다시 챙겨야 합니다. 마이그레이션 대상이 수십 개인 상황에서, 사람이 매번 이 맥락을 일일이 제공하며 한 번에 끝내기란 사실상 불가능했습니다.
그래서 앞서 정의한 절차를 하나의 오케스트레이션 스크립트로 묶었습니다. 커맨드 파일로 고정해 둔 세 단계 — 변환(migrate) → 테스트 작성(test) → 빌드·검증(verify) — 을 컴포넌트 하나에 대해 순서대로 실행하고, 통과하면 다음 컴포넌트로 넘어가는 방식입니다.
실제 스크립트는 bash로 작성했지만, 전체 흐름을 그림으로 그리면 다음과 같습니다. 오케스트레이터가 컴포넌트 하나를 앞의 세 단계 파이프라인으로 흘려보내고, 통과하면 다음 컴포넌트로, 모두 끝나면 리포트로 빠집니다. 각 단계는 컨텍스트 출처(공식문서·서브모듈)를 읽어 들이고, 단계 사이의 맥락은 파일로 남는 기억(MIGRATION.md·WORK_LOG.md)에 적어 다음 단계로 넘깁니다.
DS 마이그레이션 오케스트레이터 — 커맨드 파일을 독립 세션 3단계로 돌리고, 컨텍스트는 공식문서·서브모듈에서 읽어 MIGRATION/WORK_LOG로 인계한다
오케스트레이터의 핵심은 사람이 쓰던 커맨드 파일을 그대로 프롬프트로 변환해, 각 단계를 독립된 헤드리스 세션으로 실행하는 것입니다. 한 세션에서 수십 개를 연달아 처리하면 컨텍스트가 비대해져 품질이 떨어지므로, 단계마다 세션을 새로 띄워 맥락을 가볍게 유지했습니다. 검증 단계는 실제로 빌드가 통과하는지 확인하고 실패하면 정해진 횟수만큼 재시도하며, 끝내 실패한 컴포넌트는 따로 기록해 두었다가 이어서 처리합니다.
세션이 매번 새로 뜨므로, 한 단계는 필요한 컨텍스트를 그때그때 불러오고 결과를 파일로 남겨 다음 단계에 넘깁니다. 예를 들어 변환(migrate) 단계는, 문서 색인(manifest)에서 대상 컴포넌트를 확인하고 → v6 원본 구현은 별도 서브에이전트로 분석하고 → v6·v8 공식 문서 중 해당 컴포넌트 것만 읽은 뒤 → 변경점을 MIGRATION.md(변경 명세서)에 적고 v8 코드를 생성하고 → 발생한 이슈와 다음 단계에서 확인할 점을 WORK_LOG.md에 남기고 커밋합니다.
test·verify 단계도 같은 골격입니다. 시작할 때 WORK_LOG.md를 읽어 이전 맥락을 복구하고, MIGRATION.md를 정답지 삼아 작업한 뒤, 끝에 결과를 적어 넘기고 커밋합니다. 특히 verify는 여기에 더해 빌드와 테스트(앞서 만든 v6↔v8 픽셀 비교 포함)를 실제로 돌려 통과 여부를 오케스트레이터에 돌려주고, 오케스트레이터는 그 결과로 재시도 여부를 결정합니다.
정리하면, 여러 세션과 전체 마이그레이션 과정 동안의 맥락을 유지하기 위해서
(1) 컨텍스트는 색인을 거쳐 매번 필요한 만큼만 불러오고(거대한 공식 문서를 통째로 읽지 않음),
(2) 에이전트의 “기억”은 휘발되는 세션 메모리가 아니라 MIGRATION.md·WORK_LOG.md 파일로 남겨 다음 단계에 인계하고,
(3) 단계마다 커밋해 중간에 멈춰도 git 이력으로 상태를 복구할 수 있게 한 것입니다.
이렇게 사람은 “무엇을, 어떤 순서로 옮길지”를 정하고 결과를 검토하는 일에 집중하고, 반복되는 변환·테스트·검증은 오케스트레이터에 맡기도록 구성했습니다. 실제 실행은 다음처럼 한 줄이면 충분합니다.
# Type 1(순수 래퍼) 중 아직 안 옮긴 것만 자동 처리
./scripts/migrate-components.sh --type 1 --auto
# 특정 컴포넌트만 (변환 → 테스트 → 검증)
./scripts/migrate-components.sh Button Input Modal내부(디자인시스템) 마이그레이션
사전 작업이 끝난 뒤, 디자인시스템 레포 내부의 컴포넌트들을 에이전트로 마이그레이션하기 시작했습니다.
가장 단순한 Type 1(순수 래퍼) 컴포넌트부터 시작해, 절차가 안정적으로 동작하는 것을 확인한 뒤 점점 난이도가 높은 컴포넌트로 범위를 넓혔습니다.
각 컴포넌트마다 에이전트는 앞의 절차를 따라가며 변환·테스트·스토리북 작성을 수행했고, 사람은 그 스토리북을 통해 결과를 검토하고 애매한 부분을 디자이너와 함께 검토하며 확인해나가는 방식을 거쳤습니다. AI가 반복 작업을 처리하고, 사람이 의사결정을 내리는 분업이 자리를 잡기 시작한 단계입니다.
에이전트가 정해진 절차에 따라 컴포넌트를 변환하는 모습 스토리북을 통한 컬러 비교 정의
외부(애플리케이션) 마이그레이션 — codemod
디자인시스템 내부 컴포넌트가 어느 정도 v8로 옮겨진 뒤에는, 이제 그 패키지를 사용하는 애플리케이션(Consumer) 을 옮길 차례였습니다.
여기서 핵심 도구가 codemod였습니다. 단순한 import 경로 변경부터 컴포넌트 prop 변환까지, 애플리케이션 코드에서 반복적으로 일어나는 변경을 AST를 분석하는 방식으로 스크립트를 작성하여 자동화했습니다. AI를 활용해 직접 설계·구현했고, 샘플 레포에 먼저 적용해 QA하며 다듬었습니다.
// Before
import { Button } from '@inflearn/ds-react';
<Button leftIcon={<Icon />} compact>Click</Button>
// After (codemod 실행 후)
import { Button } from '@inflearn/ds-react-next';
<Button leftSection={<Icon />} size="compact-sm">Click</Button>codemod는 @inflearn/ds-codemod라는 별도 패키지로 만들어, 각 애플리케이션에서 다음과 같이 적용할 수 있게 했습니다.
pnpm exec ds-codemod init # 설정 파일 생성
pnpm exec ds-codemod # 변환 실행 (--dry 로 미리보기 가능)초기화 스크립트를 이용해 polyrepo, monorepo 와 같은 환경에 적합한 yaml을 자동으로 생성하고, 특정 컴포넌트 또는 전체 컴포넌트를 마이그레이션할 수 있는 스크립트를 제작하였습니다.
# ds-codemod.config.yaml
# @inflearn/ds-codemod 설정 (Polyrepo)
# 마이그레이션 대상 경로
target: ./src
# 마이그레이션할 컴포넌트 (전체: components: all)
components:
- Button
- Text
- Input
- Select
- Checkbox
- Radio
- Switch
- ButtonGroup
- IconButton
css-to-styles:
components: all
# 옵션
options:
dry: false
물론 codemod가 모든 것을 해결해 주지는 않았습니다. 애플리케이션마다 환경이 조금씩 달랐고, 그 차이에서 비롯된 문제들은 뒤의 ‘마주친 대표적인 이슈들’에서 간략히 정리합니다.
진행 관리: 대시보드와 우선순위
마이그레이션 대상이 20개가 넘다 보니, 팀에게 공유하려면 지금 어디까지 진행됐는지를 한눈에 보는 것이 중요해졌습니다.
그래서 클로드(Claude, 앤트로픽의 AI 코딩 도구) 를 이용해, 각 애플리케이션의 마이그레이션 진행 상태(개발 중 / 운영 반영 등)를 한눈에 보여주는 마이그레이션 대시보드를 만들었습니다. 작업을 사람이 머릿속으로만 관리하지 않고 가시화해 두니, 우선순위를 정하고 누락을 막는 데 큰 도움이 되었습니다.
마이그레이션 진행 상황을 한눈에 보여주는 대시보드
옮기는 순서에도 전략이 필요했습니다. 커밋 수가 적은(=상대적으로 변경이 적고 단순한) 애플리케이션부터 마이그레이션했습니다. 이유는 아래와 같습니다.
- 위험을 낮추기 위해서입니다. 단순한 앱부터 옮기면 절차의 허점을 작은 범위에서 먼저 발견할 수 있습니다.
- 절차를 다듬기 위해서입니다. 초반에 마주친 문제와 그 해결책을 codemod와 가이드 문서에 반영해 두면, 뒤에 오는 복잡한 앱에서는 같은 문제를 겪지 않게 됩니다.
작은 앱에서 검증된 절차가 점점 단단해지면서, 나중에는 더 큰 앱도 비교적 수월하게 옮길 수 있었습니다.
마이그레이션 대시보드에 최신 커밋 일자를 받아와 우선순위 섹션을 나누어 한눈에 확인이 가능하도록 처리하였습니다.
최신 커밋 일자를 기준으로 우선순위를 나눈 대시보드
QA와 codemod 보강
자동화로 옮긴 결과라도, 결국 실제 화면이 기존과 같은지는 사람의 눈으로 확인해야 합니다.
특히 스타일 우선순위나 미디어 쿼리처럼 미묘하게 어긋날 수 있는 부분은 자동 테스트만으로 잡기 어렵기 때문에, 내부 팀원들과 함께 주요 화면을 QA했습니다.
팀원들과 함께 진행한 QA 체크리스트 QA 과정에서 점검한 화면 예시
QA를 거치며, 특정 애플리케이션에서 반복적으로 나타나는 문제 패턴들이 보였습니다. 대표적으로 Emotion css prop의 스타일 우선순위 문제가 있었습니다. v8 + Emotion 환경에서는 애플리케이션에서 css prop으로 넣은 스타일이 컴포넌트의 styles API보다 우선순위가 낮아, 의도한 스타일이 적용되지 않는 경우가 있었습니다.
이런 문제는 한 곳만 고친다고 끝나지 않으므로, codemod에 새로운 변환 규칙을 추가해 css prop을 styles.root로 자동 이관하도록 했습니다.
// Before
<Text css={{ flexShrink: 0 }}>...</Text>
// After (css-to-styles codemod)
<Text styles={{ root: { flexShrink: 0 } }}>...</Text>여러 레포를 점검하며 발견한 패턴을 codemod에 흡수시키는 과정을 반복하니, 한 번 고친 문제는 다음 레포에서 다시 겪지 않는 선순환이 만들어졌습니다.
마주친 대표적인 이슈들
현업을 굴리면서 v6와 v8을 공존시키다 보니, 예상하지 못한 함정들도 적지 않았습니다. 하나하나 깊게 풀어내면 글이 한참 길어지므로, 같은 길을 걷는 분들을 위해 가장 기억에 남는 이슈들만 간략히 남겨둡니다.
| 이슈 | 증상 | 원인 | 해결 |
|---|---|---|---|
| TS5 전용 문법 | v8 빌드 시 TS1139 타입 에러 |
Mantine v8 타입이 TS5 문법(const T extends string)을 쓰는데, 우리 패키지는 TS4 |
pnpm patch로 node_modules의 타입 선언을 임시 패치 (버전업 이후 제거 예정) |
| app-shell(module-federation) 싱글톤 충돌 | MANTINE_COLORS is not iterable |
app-shell이 @mantine/core를 싱글톤으로 선언해, 모든 import가 가장 높은 버전(v8)을 참조 |
core 싱글톤 선언 제거 (이미 싱글톤인 ds-react가 v6를 간접 보장) |
| pnpm peer chain | v6/v8 공존 시 peerDependency의 depth가 깊으면 잘못된 의존성을 참조 | notifications·modals의 peer인 core가 peer chain을 타고 root(v6)까지 올라가 resolve |
peer chain 중간 지점을 dependency로 선언해 chain을 끊음 |
| ESM 전용 패키지 + dynamic import | 모달 등 지연 로드 시 런타임 에러 | Next.js dynamic import가 내부적으로 CJS(require)를 쓰는데, 의존 패키지가 ESM 전용 |
CJS를 지원하는 하위 버전 사용 또는 네이티브 API(URLSearchParams 등)로 대체 |
| css prop 우선순위 | 애플리케이션에서 넣은 css 스타일이 적용되지 않음 |
v8 환경에서 컴포넌트의 styles API가 Emotion css prop보다 우선순위가 높음 |
css → styles.root 이관 codemod 추가 |
| 스타일 깨짐 / FOUC | 초기 렌더 깜빡임, 일부 스타일 깨짐 | Emotion 서버 스타일 동기화 누락, 필수 CSS import 누락·순서 문제 | 서버 스타일·Provider 점검, 필수 CSS import와 적용 순서 확인 |
이 표만 봐도 짐작하시겠지만, 두 버전을 공존시키느라 생긴 의존성 해석(resolve) 문제와, 네이티브 CSS로 옮겨간 Mantine 위에 Emotion을 유지하면서 생긴 스타일 우선순위 문제가 가장 번거로웠습니다.
5부 · 회고
1차 마무리
이렇게 해서, @inflearn/ds-react를 사용하는 주요 애플리케이션들의 v8 마이그레이션을 1차로 마무리할 수 있었습니다.
현재 작업은 “끝”이라기보다 “큰 산을 하나 넘었다”에 가깝습니다. 아직 기존 디자인시스템을 완전히 걷어내는 작업, 의존성 정리, 스토리북 체계화 등 후속 작업이 남아 있습니다. 그럼에도 이전처럼 개인 시간을 많이 할애하지 않고도, 전사 규모의 기반 라이브러리를 v8로 옮기는 1차 작업을 마무리했다는 점에서, 의미 있는 분기점이라 생각합니다.
AI는 또 다른 동료다 — 단, 토대가 있어야
가장 크게 느낀 것은, AI 에이전트는 “잘 정리된 절차와 문서” 위에서 일부의 융통성이 필요한 반복적인 작업에 진짜 힘을 발휘한다는 점이었습니다.
AI에게 막연히 “마이그레이션해줘”라고 맡기는 것과, 절차·검증·자동화 도구를 갖춰 두고 맡기는 것은 결과의 안정성이 완전히 달랐습니다.
앞으로도 플랫폼 팀이 없는 팀의 상황에서는 디자인시스템 메인테이너가 없기 때문에 이러한 룰과 토대가 중요할 것이라고 예상합니다. 이러한 상황 속에서 AI는 일관성있는 코드를 생산함으로써 모두가 메인테이너가 될 수 있는 환경을 제공합니다.
완벽한 마이그레이션은 없습니다
현업을 유지하면서 완벽한 마이그레이션을 해내는 것은 사실상 불가능합니다. 우리가 할 수 있는 것은 에러가 발생할 확률을 줄이는 과정을 반복하는 것뿐입니다.
서비스는 계속 배포되고, 애플리케이션마다 환경이 다르며, 라이브러리는 또 버전이 오릅니다. 이런 환경에서 “한 번에 완벽하게”는 코드 규모가 커질수록 더 어려워집니다.
따라서 리스크를 최소화하는 과정에 중점을 두었습니다. 위험을 잘게 쪼개고(단순한 앱부터), 발견한 문제를 자동화에 흡수시키고(codemod), 사람의 눈으로 최종 확인하는(QA) 사이클을 반복했습니다. 완벽을 좇기보다 실패 확률을 꾸준히 낮추는 쪽이, 현업을 유지하면서 큰 변화를 만드는 현실적인 방법이었습니다.
다음 후속 작업
1차 마무리 이후 남은 작업들은 대략 다음과 같습니다.
- 기존 디자인시스템 제거:
@inflearn/ds-react등 v6 패키지를 의존성에서 완전히 걷어내기 - 의존성 정리: 격리를 위해 dependency로 두었던 Mantine 패키지들을 다시 peer로 되돌리고, 모노레포 차원에서 버전을 한곳에서 관리하기
- 스토리북 체계화 및 배포
- TypeScript 5로 올리기
- 레거시 스타일 대응: Emotion
cssprop 등을 점진적으로 정리하기
아직 넘어야 할 산이 많지만, 꾸준히 부채를 해결해나가는 것도 개발자의 역할이라고 생각합니다.
이 글은 AI를 활용한 디자인시스템 마이그레이션 시리즈의 첫 번째 글이었습니다. 앞으로의 편들에서는 후속 작업이 진행되는대로 해당 이야기도 이어질 예정입니다.
긴 글 읽어주셔서 감사합니다. 모쪼록 바쁘게 프로덕트를 개발하는 와중에도 팀의 기술 부채를 해결하고자 하는 멋진 개발자분들에게 도움이 되는 글이길 바랍니다!

