Home > AI > Frontend > 같은 버그를 두 번 고친 날, V1/V2 이중 스택을 정리하기로 했다

같은 버그를 두 번 고친 날, V1/V2 이중 스택을 정리하기로 했다
AI Claude Code 리팩터링 트러블슈팅

계기: 같은 버그를 양쪽에서 따로 고쳤다

한 화면에 V1과 V2 두 벌의 구현이 공존하고 있었다. 신규 작성 화면은 V2를 쓰고, 월 업데이트 화면은 V1을 쓰는 식이었다.

카테고리 상태가 어긋나는(desync) 버그가 나왔을 때, 이걸 V1 쪽에서 고치고 며칠 뒤 V2 쪽에서 또 고쳤다. 같은 원인, 같은 증상, 같은 수정을 두 번 한 것이다. 이 시점에 “이중 스택을 유지하는 비용”이 명확한 숫자로 드러났다.

Phase 0~5로 나눠 통합했다

한 번에 갈아엎지 않고 6단계로 쪼갰다. 월 업데이트 화면을 V2 스택으로 옮기는 게 목표였다.

Phase 내용
0-1 isUpdate·updateMonth props 배선, 수집월 폴백
2 다중 카테고리 저장 + sticky mount
3 카테고리 편집 차단·월 기준 표시·리본 경고
4 사이드패널 접기
5 V1 스택 제거

핵심은 Phase 1의 성질이다. 호출부가 isUpdate를 넘기지 않으면 기본값이 false라서, 생성·수정 화면의 동작이 그대로다. 배선만 깔아두고 아무것도 바꾸지 않는 단계를 먼저 두면 되돌리기 쉽다.

Phase 5에서 V1 전용 파일 6개, 2,389줄을 삭제했다. 이때 조심한 게 공용 부품이다. AllocationScopePickerModal, DataIOButtonGroups, DataIOTableWrapper 등 5개는 V2도 import하고 있어서 남겼다. 그중 하나는 다른 화면 2곳에서도 쓰고 있었다. “V1 디렉토리에 있으니 V1 것”이라는 가정은 틀린다.

통합 과정에서 드러난 숨은 결함 4종

두 스택을 나란히 놓고 비교하니 한쪽에만 있던 결함들이 드러났다.

1) 엑셀 업로드가 replace로 동작했다

헤더의 엑셀 업로드 input이 uploadExcel을 호출하면서 세 번째 인자(isUpdatedOrAdded)를 넘기지 않고 있었다. 그래서 업로드가 append가 아니라 replace로 처리됐다. 6월 데이터가 있는 상태에서 7월 엑셀을 올리면 6월 데이터가 지워졌다.

2) 거짓 저장 성공 보고

저장 후 “N건 저장됨”이라는 토스트가 떴는데, 이 N이 실제로 전송된 건수가 아니라 전송을 시도한 건수였다. 일부가 실패해도 전체 건수가 그대로 표시됐다. 사용자는 저장이 다 됐다고 믿고 화면을 떠난다.

3) 빈 카테고리 편집분 무경고 유실

특정 조건에서 편집한 내용이 아무 경고 없이 사라졌다.

4) 저장 함수가 실패해도 true를 반환했다

가장 근본적인 것이다. 저장 함수의 반환값이 성공 여부를 반영하지 않았다. 2번의 거짓 보고도 결국 여기서 흘러나온 것이다.

네 개 다 “이중 스택이라서 생긴 버그”는 아니다. 한쪽 스택에만 있던 결함인데, 통합하려고 두 구현을 나란히 놓고 읽으니 차이가 눈에 띈 것이다.

트랜잭션이 없는 상태에서의 부분 실패 설계

저장 API가 한 번에 하나만 받는 구조였다. 다중 카테고리 저장을 지원하려면 N회 순차 호출을 해야 했고, 그러면 중간에 실패할 수 있다.

문제는 트랜잭션이 없어서 롤백이 불가능하다는 것이다. 3건 중 2건이 저장되고 3번째가 실패하면 앞의 2건을 되돌릴 방법이 없다.

BE에 배치 API를 요구하는 게 정석이지만 일정상 불가능했다. 그래서 “실패를 숨기지 않는” 방향으로 설계했다.

  • 실패 항목을 명시한다. “N건 저장됨”이 아니라 어떤 항목이 실패했는지 나열한다.
  • 검증 실패와 서버 실패를 구분한다. 입력값이 잘못된 건 사용자가 고칠 수 있고, 서버 오류는 재시도할 일이다. 대응이 다르니 메시지도 달라야 한다.
  • 되돌릴 수 없는 다음 단계로 넘어가지 않는다. 부분 실패 상태에서 마감 같은 비가역적 동작으로 진행하지 못하게 막았다.

일관성을 보장할 수 없다면, 최소한 불일치 상태를 사용자가 인지하고 수동으로 복구할 수 있게 만드는 것이 차선이었다.

자기 계측 오류를 정정한 기록

PR 기록에 남은 대목 하나가 인상적이다. 검증 과정에서 “POST 요청이 0건”이라는 계측 결과가 나왔고, 이건 저장이 아예 안 되고 있다는 뜻이라 심각하게 다뤄졌다.

그런데 이후 정정이 올라왔다. 계측 자체가 틀렸던 것이다. 네트워크 요청을 캡처하는 정규식이 [A-Z_]+ 패턴이라 소문자가 섞인 URL을 전부 놓치고 있었다. 실제로는 POST가 정상적으로 나가고 있었다.

측정 결과가 이상할 때 코드를 의심하기 전에 측정 도구를 의심하는 단계가 있었다는 뜻이다. 이게 없었으면 멀쩡한 저장 로직을 “고치느라” 시간을 썼을 것이다.

남는 교훈

이중 스택은 유지 비용이 눈에 잘 안 보인다. 두 벌을 다 돌아가게 유지하는 건 어렵지 않다. 비용은 버그를 고칠 때 발생한다 — 어느 쪽을 고쳐야 하는지 판단해야 하고, 종종 양쪽 다 고쳐야 하며, 한쪽을 빠뜨리면 그게 다음 버그가 된다.

“같은 버그를 두 번 고쳤다”는 사건이 통합의 계기가 된 건 그게 비용을 처음으로 셀 수 있게 만들어줬기 때문이다. 그전까지는 “언젠가 정리하면 좋겠다” 정도였다.