디자인 다음에 기다리고 있던 큰 작업
지난 글에서 디자인 마이그 중 테스트 환경 안에서 13번 무너졌던 이야기를 정리했습니다. 그 작업이 끝나고 한 호흡 돌렸을 때, 더 큰 일이 한 가지 기다리고 있었습니다. 코드 자체를 통째로 갈아엎는 일이었습니다.
NB2B 코치 앱 — 수능선배·공시선배·세무선배 — 세 개의 클라이언트 코드를 vanilla JavaScript에서 React로 옮기는 작업이었습니다. 디자인이 화면 표면을 정돈하는 일이라면, 마이그레이션은 그 안쪽을 통째로 들어내는 일이었습니다.
처음엔 솔직히 망설였습니다. “지금 그럭저럭 굴러가고 있는데, 굳이 왜?” — 그 생각이 며칠 머릿속을 맴돌았습니다. 그래도 결정했습니다. 지금 안 하면 영영 못 한다는 직감 때문이었습니다. 사용자가 늘어난 뒤에는 라이브 환경의 부담 때문에 통째 갈아엎기가 어려워집니다. 알파 단계의 지금, 양해 받은 사용자 분들이 계실 때가 마지막 기회였습니다. 그래서 시작했고, 약 2.5주가 흘렀습니다. 매일 라이브에서 결함을 발견하고 다음 날 fix를 머지하는 짧고 강한 cycle이었습니다.
이 글은 그 2.5주에 알게 된 것들에 대한 기록입니다. 본 글에서는 마이그라는 일 자체에 대한 5가지를 묶었고, AI 에이전트에게 이 일을 맡겼을 때 일어나는 일에 대한 더 흥미로운 이야기는 다음 글에 따로 풀었습니다.
왜 하필 React인가
왜 React인가, 라는 질문이 있을 수 있습니다. 비개발자 입장에서는 지금 굴러가는 것과 새로 옮긴 것 사이에 눈에 보이는 차이가 적기 때문이지요.
가장 큰 이유는 AI 에이전트와의 협업이 훨씬 수월해진다는 것이었습니다. React는 화면을 *조각(컴포넌트)*으로 나눠서 만드는 방식인데, 이 조각 단위가 AI에게는 한 번에 다루기 적당한 크기입니다. 한 조각에서 문제가 생기면 그 조각 안에서만 추적하면 됩니다. vanilla JavaScript는 한 화면이 커질수록 어디서 무엇이 일어나는지 추적이 어려워지고, AI가 한 번에 봐야 하는 코드 범위도 함께 늘어납니다.
AI 시대에 비개발자가 제품을 끌고 가려면, 코드가 AI 친화적인 형태여야 합니다. 이번 마이그는 그 한 단계였습니다.
한 가지 미리 말씀드리고 싶습니다
이 글에 적힌 깨달음들은 절반쯤은 제가 직접 부딪혀 얻은 것이지만, 나머지 절반은 AI 에이전트가 먼저 가르쳐 준 것입니다. 같이 작업하다 보면 *“이 결정은 이런 이유로 위험하니 다시 한 번 보는 게 좋겠다”*는 식의 통찰을 AI가 먼저 꺼내 주는 경우가 자주 있었습니다. 비개발자 출신인 저에게는 그게 살아 있는 멘토에 가까웠지요.
그런데 흥미로운 건 — 그렇게 좋은 가르침을 줄 수 있는 그 AI가, 막상 실행 과정에서는 적지 않은 실수를 합니다. 데이터 키를 바꿔 쓰고, 옛 코드를 추론으로 옮기고, 프롬프트를 슬쩍 줄여 쓰고. 이 글과 다음 글 곳곳에 나오는 사고들의 절반 이상이 AI가 직접 만든 것이었습니다.
그래서 이번 cycle에서 가장 분명히 알게 된 것은 *“AI에게 그냥 믿고 맡긴다”*가 답이 아니라는 점입니다. AI와 어떻게 협업하느냐, 어떤 자세로 협업하느냐 — 그것이 마이그의 성패를 갈랐습니다. 이 글은 그 협업의 자세에 대한 기록이기도 합니다.
교훈 1 — 마이그레이션의 90%는 “그대로 옮기기”입니다. 개선 욕망을 경계하세요
옮기면서 더 좋은 이름으로 바꾸고 싶다는 욕심이 가장 먼저 들었습니다. 데이터베이스에 점수가 raw, standard라는 이름으로 저장돼 있었는데, 새 코드로 옮기면서 AI 에이전트가 *“더 명시적인 이름이 좋겠다”*고 판단했죠 — raw_score, standard_score로 변경. 빌드도 통과. 타입체크도 통과. 화면도 언뜻 보기엔 정상.
문제는, 데이터베이스에는 여전히 raw 키로 저장된다는 점이었습니다. 새 코드는 raw_score로 찾고 있었고요. 데이터를 못 찾지만 에러도 안 납니다. 그냥 빈 값으로 보일 뿐. 화면 어딘가에 *”-“*가 한두 개 더 있어도, 데이터가 원래 없는 건지 결함인지 구분이 안 됩니다. 6일 동안 이 결함과 함께 살았습니다. 매일 화면을 들여다보던 제가 끝내 라이브 환경에서 데이터를 직접 입력해 보고 나서야 발견했습니다.
이 사고 이후 룰을 신설했습니다 — 마이그 작업 전, 옛 코드가 어떤 키로 데이터에 접근하는지 전수 검색. 새 코드는 한 글자도 다르지 않은 동일한 키를 사용. 더 좋은 이름으로 바꾸고 싶으면 그건 별도 작업, 별도 PR로.
배운 것: 마이그레이션은 옮기는 작업이지 개선하는 작업이 아닙니다. 두 가지를 같은 PR에 섞으면 어느 쪽이 깨졌는지 추적할 수 없게 됩니다. 개선은 마이그가 끝난 후 별도 사이클로.
교훈 2 — 옛것과 새것의 공존 기간이 가장 위험합니다
마이그가 단번에 끝나는 경우는 없습니다. 옛 시스템 일부 + 새 시스템 일부가 같이 동작하는 기간이 길게는 몇 주씩 이어집니다. 저희 경험상 마이그 사고의 70% 이상이 이 공존 기간에 발생했습니다. 한 사례를 보여드리겠습니다. 저희 앱에서 화면 전환을 책임지는 영역이 4개의 단계로 나뉘어 있었습니다:
- 1단계 — CSS의 기본 룰 (“이 페이지는 보임/안 보임”)
- 2단계 — 어떤 페이지가 살아있는지 추적하는 자료구조
- 3단계 — 주소(URL)가 바뀔 때 모든 페이지의 활성 상태를 토글하는 옛 코드
- 4단계 — 새로 만든 React 컴포넌트 자체의 활성 토글
옛 코드인 3단계가 모든 페이지를 대상으로 활성 상태를 제거하다 보니, 새로 만든 4단계도 같이 죽이고 있었습니다. 한 탭에 들어갔다 나갔다 다시 들어가면 빈 화면. 한 번은 OK, 두 번째부터 망가짐. 의외의 곳에서 의외의 증상이 나왔습니다. 12개 PR에 걸쳐 4번의 hotfix를 시도했습니다. 매번 한 단계만 손대고, 다른 단계가 새 결함을 일으켰죠. 근본 해결은 3단계가 자기 영역만 다루도록 검색 범위를 좁히는 한 줄이었습니다.
배운 것: 옛것과 새것이 같이 동작하는 기간에는 경계가 어디인지를 명시적으로 표시해야 합니다. 옛 코드가 자기 영역만 청소하는지, 새 코드가 옛 코드의 영역을 침범하지 않는지 — 둘의 책임 범위가 의식적으로 분리돼 있어야 합니다. 그렇지 않으면 hotfix가 다른 hotfix를 부르는 무한 사이클에 빠집니다.
교훈 3 — 청소(cleanup)는 항상 미뤄지지만 미루면 영영 안 합니다
업계 자료들이 강조하는 것 — “마이그를 다 하고 옛 코드 청소를 잊지 마라.” 다들 압니다. 그런데 거의 모든 회사가 못 합니다. 이유는 단순합니다. 새 기능을 만드는 일보다 덜 흥미롭고, 위험만 있고 보상은 없어 보이기 때문입니다. “잘 동작하고 있는 옛 코드를 굳이 왜 건드리지?” 라는 질문이 매번 등장합니다.
저희는 처음부터 청소를 별도 sprint로 명시하기로 했습니다. “마이그 100% 도달” 시점을 종점으로 보지 않고, 옛 파일이 디스크에서 사라지는 시점을 종점으로 봤습니다. 결과: 마이그 막바지 며칠을 순수 청소 sprint로 운영하면서, 옛 코드 파일 네 개를 통째로 폐기했습니다. 누적 약 970줄 제거. 그날 이후 더는 옛 코드가 없었습니다.
청소할 때 따른 보조 규칙들:
- 파일 통째 삭제는 위험합니다. 한 파일 안에 살아 있는 데이터 코드 / 헬퍼 / 죽은 코드가 섞여 있을 수 있습니다. 함수 단위로 수술하세요.
- 의심되면 남겨두세요. *“이번 sprint에 죽은 코드 전부 정리”*는 함정입니다. 여러 라운드에 나눠서.
- 머지 후 며칠은 모니터링. 같은 영역의 신규 변경은 자제하고, 결함 발견 시 추가 fix가 아니라 되돌리기(revert).
배운 것: 청소를 로드맵에 명시해야 합니다. “여유 있을 때” 한다고 하면 영원히 못 합니다. 종점을 *마이그 100%*가 아니라 옛 코드 0줄로 정의하세요.
교훈 4 — 외부 best practice는 우리 상황에 맞춰 강도를 조절하세요
처음에는 대기업의 마이그 가이드(Shopify, LaunchDarkly 등)를 그대로 적용했습니다 — 호환 어댑터(옛 시스템의 함수 이름을 그대로 호출해도 새 시스템으로 연결해 주는 임시 다리), 7일 되돌리기 가능 기간, 일부 사용자에게만 먼저 배포해 보는 점진적 배포 등. 알파 단계에서는 대부분 과도했습니다. 호환 어댑터를 만드는 데 하루, 어차피 거의 사용되지 않는 길을 위한 호환 레이어, 7일 모니터링 때문에 다음 sprint가 1주씩 밀림.
깨달은 후 단계별 강도 매트릭스를 만들었습니다:
| 단계 | 사용자 수 | 되돌리기 기간 | 호환 어댑터 | 점진적 배포 |
|---|---|---|---|---|
| 알파 | 적음 | 생략 | 생략 | 생략 |
| 베타 | 1-10 | 24시간 | 권장 | 권장 |
| early prod | 10-100 | 7일 | 필수 | 필수 |
| prod | 100+ | 7일 + 영역 freeze | 필수 + 점진적 배포 | 필수 |
알파에서는 자동 검증 + 마지막에 사람이 한 번 직접 클릭해 보는 정도만 유지하고, 외부 best practice의 사용자 보호 부분은 생략했습니다. 자동 테스트와 마지막 클릭 확인은 단계와 무관하게 유지 — 안티패턴은 사용자 수와 무관하게 안티패턴이기 때문입니다.
배운 것: best practice는 문맥이 있습니다. Shopify의 가이드는 사용자 수백만 명을 보호하기 위한 것입니다. 알파 단계에 그대로 적용하면 불필요한 비용을 자기에게 부과하는 셈입니다. 자기 상황에 맞춰 강도를 조절하세요.
교훈 5 — 비슷한 시스템이 여러 개면 시차를 두지 말고 같이 머지하세요
저희는 같은 코드를 공유하는 3개의 앱이 있었습니다. 모든 변경이 3배가 됩니다. 처음엔 “한 앱에서 검증하고 시간차로 다른 두 앱에 옮기는” 방식으로 갔습니다. 한 sprint 후, 세 앱이 미묘하게 달라지기 시작했습니다. 한 앱에서 고친 결함이 다른 앱에 적용 안 됐고, 다른 앱에서 추가한 기능이 첫 번째 앱에 없었습니다. 한 앱에서만 발견된 버그가 알고 보니 세 앱 모두 가지고 있었다는 경우도 있었습니다. 이렇게 비슷한 시스템들이 조금씩 어긋나기 시작하는 현상을 영어로 drift라고 부르는데, 한 번 시작되면 누적됩니다.
해결: 모든 변경은 3앱을 한 묶음으로 같은 날에 머지. 한 작업이 3개의 PR로 나뉘더라도, 같은 commit message로 같이 머지. 시차를 두면 어긋남이 시작됩니다.
이게 빡빡한가, 싶죠. 네, 빡빡합니다. 한 앱만 수정해도 되는 작업이 3배의 PR이 됩니다. 하지만 시간이 지나면서 누적되는 어긋남을 잡는 비용에 비하면 훨씬 쌉니다. 저희가 직접 측정해 본 결과, 한 사고당 평균 12개 PR / 3~4일의 hotfix 사이클이 발생합니다. 처음부터 묶음 머지를 유지하는 게 항상 더 쌉니다.
배운 것: 비슷한 시스템이 여러 개 있다면, 변경의 동기화 방식을 처음부터 정해두세요. *“필요할 때 옮기기”*는 시작은 쉽지만 끝이 없습니다. 6개월 후 세 시스템이 별개의 무엇이 되어 있습니다.
마치며 — 다섯 줄로 정리되는 원칙들
이 2.5주는 알파 단계에서 사용자 분들의 양해가 있었기에 가능한 속도였습니다. 매일 라이브에서 결함이 발견되는 cycle이었지만, 양해 받은 상황에서 빠른 fix가 가능했습니다. 베타·프로덕션 단계로 넘어가면 같은 일에 3~4배의 시간이 들 것입니다. 호환 어댑터, 점진적 배포, 모니터링 기간 — 모두 강화 대상이지요.
그래도 이번 cycle에서 분명히 알게 된 한 가지가 있습니다. 마이그라는 일은 생각보다 단순한 원칙 위에 서 있다는 점입니다. 다섯 줄로 정리됩니다.
- 마이그의 90%는 그대로 옮기기다. 개선 욕망을 경계하라.
- 공존 기간이 가장 위험하다. 옛것과 새것의 책임 영역을 명시하라.
- *청소(cleanup)*는 별도 sprint로 로드맵에 명시하라. 안 그러면 영영 안 한다.
- 외부 best practice는 우리 상황에 맞춰 강도를 조절하라.
- 비슷한 시스템이 여러 개라면 같은 날 같이 머지. 시차는 어긋남의 시작이다.
다섯 줄로 적어두니 너무 당연해 보입니다. 그런데 이걸 몸으로 알기까지 2.5주가 걸렸습니다. 그 사이에 12개의 hotfix 시리즈를 만들었고, 6일짜리 silent 결함과 살았고, 970줄의 옛 코드를 결국 폐기했습니다. 다만 이번 마이그의 진짜 흥미로운 부분은 따로 있습니다 — AI 에이전트에게 이 일을 어떻게 시켰는지. AI에게 옮겨 달라고 했더니 옛 코드를 읽지 않고 추론하더라고요. 프롬프트를 옮겨 달라고 했더니 슬쩍 줄여 쓰고요. 다음 글에서 그 이야기를 풀어보겠습니다.