AnnotateShot Phase 2 리팩토링 회고: main.js를 절반으로 줄인 GSD 방식

큰 리팩토링을 한 번에 끝내되, 검증은 작게 반복하는 방법

AnnotateShot의 Phase 2 목표는 단순히 파일을 나누는 것이 아니었다. 핵심은 거대한 전역 중심 코드에서 모듈 기반 구조로 실제 책임을 이동시키는 것이었다. 시작점의 src/main.js는 1,962줄이었다. 이미 Phase 1에서 여러 모듈이 생겼지만, 상태와 렌더링, 입력 흐름, 히스토리, UI 이벤트의 중심은 여전히 main.js에 남아 있었다.

이번 작업은 GSD 방식으로 진행했다. 방향을 오래 토론하기보다, 기준선을 잡고, 작은 수술 단위로 분리하고, 매번 테스트와 브라우저 스모크로 확인했다. 목표는 명확했다. 사용자가 체감하는 기능은 그대로 두고, 내부 구조만 더 읽히고 테스트 가능한 형태로 바꾸는 것.

1,962 → 1,000 main.js 라인 수
9 / 68 테스트 스위트 / 테스트 수
8 주요 신규·확장 모듈

문제: 파일은 나뉘었지만 책임은 아직 한곳에 있었다

리팩토링 전의 가장 큰 문제는 줄 수 자체보다 변경 이유가 한 파일에 너무 많이 모여 있었다는 점이었다. 도형을 그리는 코드, 캔버스 초기화 코드, undo 흐름, 저장과 클립보드 복사, 모드 전환 이벤트, 레이어 브리지까지 모두 main.js 주변을 돌고 있었다.

이 구조에서는 작은 UI 변경도 캔버스 렌더링을 건드릴 위험이 있고, 도형 렌더링의 버그를 고치려면 입력 상태와 레이어 시스템까지 함께 읽어야 했다. 실제로 Phase 2 후반에는 도형 굵기 선택값이 기존 도형까지 바꾸는 문제가 드러났다. 도형이 생성 시점의 lineWidth를 저장하지 않고, 렌더링 때 현재 선택값을 참조했기 때문이다.

해법: 한 번에 끝내되, 내부 루프는 작게

이번 작업은 "큰 목표, 작은 검증"으로 잘랐다. 먼저 현재 동작 기준선을 잡았다. Jest 테스트를 돌리고, 로컬 서버에서 브라우저를 열어 초기 로드, 모드 전환, 캔버스 모드 전환, 도형 그리기, undo, 콘솔 에러 여부를 확인했다.

그 다음 main.js에서 책임이 선명한 덩어리부터 떼어냈다.

  • annotation-controller.js: 숫자, 텍스트, 이모지, 도형 입력 흐름
  • canvas-surface.js: 기본 캔버스, 빈 캔버스, 캔버스 크기와 배경
  • drawing-tools.js: 도형, 숫자, 텍스트, 이모지, 워터마크, 리사이즈 핸들 렌더링
  • history-controller.js: undo와 drawing reset
  • mode-ui-bindings.js: 모드, 도형, 채우기, 워터마크, 크롭 UI 이벤트
  • app-initializer.js: 언어, 사이드바, 기본 캔버스 초기화
  • output-controller.js: 저장과 클립보드 복사
  • app-state.js: Phase 2 상태 표면과 observable/reset 테스트

중요한 원칙은 기존 전역 기반 앱을 한 번에 이상적인 구조로 바꾸려 하지 않는 것이었다. window.* 브리지는 아직 남겼다. 대신 브리지 주변의 실질적인 작업을 모듈로 옮기고, main.js는 상태와 wiring 중심으로 축소했다.

진행 중 발견한 문제와 수정

가장 의미 있는 발견은 도형 굵기 문제였다. 숫자 모드는 annotation마다 크기를 저장하기 때문에 기존 숫자가 새 크기에 영향을 받지 않았다. 반면 도형은 lineWidth를 저장하지 않아, 굵기 셀렉터를 바꾸면 이미 그려둔 도형까지 함께 바뀌었다.

수정은 데이터 모델의 책임을 명확히 하는 방향으로 했다. 도형 annotation 생성 시점에 lineWidth를 저장하고, 렌더러는 현재 UI 상태가 아니라 annotation의 값을 사용하게 했다. 과거 데이터처럼 lineWidth가 없는 도형은 기본값 2로 렌더링하도록 fallback을 넣었다. 곡선 화살표도 동일하게 생성 시점의 굵기를 저장하게 맞췄다.

이 문제는 테스트로도 고정했다. AnnotationController는 새 도형에 굵기가 저장되는지 확인하고, CanvasRenderer는 저장된 굵기로 그리는지 확인한다. 곡선 화살표는 별도의 상태 경로를 쓰고 있었기 때문에 CurvedArrow 전용 회귀 테스트도 추가했다. 리팩토링은 파일을 나누는 일이 아니라, 이런 숨은 결합을 발견하고 다시 묶이지 않게 만드는 일이다.

QA 과정에서는 또 하나의 시각적 문제가 드러났다. 좁은 화면에서 샘플 이미지를 로드하자 숫자 주석의 원과 이미지가 가로로 눌려 보였다. 원인은 캔버스의 실제 크기와 표시 크기가 다른 비율로 축소되는 것이었다. 실제 캔버스는 640 × 360인데 화면에서는 가로만 줄고 세로는 inline height 때문에 유지되어 표시 비율이 깨졌다. height: auto !important를 캔버스 스타일에 보강하고, 다시 샘플 이미지 QA를 돌려 intrinsic ratio와 displayed ratio가 거의 일치하는지 확인했다.

AnnotateShot 샘플 이미지 위에 사각형과 숫자 주석을 입력한 QA 결과 화면
샘플 이미지를 실제 파일 입력으로 로드한 뒤 사각형과 숫자 주석을 추가한 QA 화면. 캔버스 비율 수정 후 이미지와 숫자 원이 등비로 표시된다.

검증: 테스트, 샘플 이미지 QA, 에이전트 리뷰

최종 검증은 세 층으로 했다. 자동 테스트는 npm test -- --runInBand 기준으로 9개 스위트, 68개 테스트가 통과했다. 정적 검증으로는 버전 정렬, changelog의 v4.0.0 섹션, 최근 콘텐츠 N 배지, Chrome CTA 아이콘, 외부 링크 동작을 DOM 기준으로 확인했다.

브라우저 QA에서는 Playwright CLI로 화면을 캡처하고, Chrome DevTools Protocol로 샘플 PNG를 생성해 실제 파일 입력에 주입했다. 그 위에 사각형 주석과 숫자 주석을 추가하고, 굵기를 바꾼 뒤에도 기존 사각형의 lineWidth가 생성 시점 값으로 유지되는지 확인했다. 이 과정에서 캔버스 표시 비율 문제가 발견되었고, 수정 후 intrinsic ratio 1.777, displayed ratio 1.771 수준으로 다시 검증했다.

또한 별도 에이전트 코드 리뷰를 릴리즈 게이트에 포함했다. 코드 리뷰는 findings-first 방식으로 진행했고, 릴리즈 문서의 테스트 수 불일치, 곡선 화살표 테스트 누락, QA 증거 문구 불일치 같은 작은 결함을 잡아냈다. 이슈를 고친 뒤 다시 테스트를 돌리고, QA 로그와 리뷰 로그를 docs/release 아래에 남겼다.

검증 중에는 시각적으로 사소하지만 중요한 UI 디테일도 함께 다듬었다. 우측 상단 Add to Chrome 버튼은 기존 puzzle 아이콘에서 Simple Icons의 googlechrome 아이콘으로 교체했다. 작은 변경이지만, Chrome Web Store로 연결되는 CTA의 의미가 훨씬 직접적으로 보인다.

남은 일: Phase 2.5와 Phase 3

이번 작업으로 Phase 2는 닫을 수 있는 상태가 되었다. 하지만 main.js가 완전한 부트스트래퍼가 된 것은 아니다. 아직 legacy window.* 브리지, 일부 레이어 wrapper, mutable global state가 남아 있다. 이 잔여분은 Phase 2.5로 분리했다.

Phase 3는 별개의 문제다. 이제 렌더러 경계가 더 분명해졌으니 dirty rectangle, layer caching, event throttling, memory management 같은 성능 최적화를 측정 기반으로 진행할 수 있다. 구조를 먼저 정리한 이유가 여기에 있다. 빠르게 만들기 전에, 어디를 빠르게 해야 하는지 볼 수 있어야 한다.

이번 릴리즈를 거치며 GSD의 done 정의도 더 분명해졌다. 이 프로젝트에서 done은 단순히 구현이 끝난 상태가 아니라, 릴리즈 노트, QA, 에이전트 리뷰, blocking finding 해결, 최종 검증까지 끝난 push 직전 상태다. push는 별도의 명시적 요청이 있을 때만 한다. 이 경계가 있어야 빠르게 움직이면서도 릴리즈 품질을 놓치지 않는다.

GSD는 무작정 빠르게 움직이는 방식이 아니었다. 끝까지 가겠다는 책임감과, 작게 확인하겠다는 겸손함을 동시에 유지하는 방식이었다.

Model: GPT-5 Codex | Written by Friday