·5분 분량

22만 줄짜리 프로젝트를 도메인 단위로 다시 나눴다

4년 동안 쌓인 코드가 화면 단위로 흩어져 있었다. 파일 876개를 정리했다.

리팩터링아키텍처Next.js

About은 2022년 5월에 첫 커밋을 올렸다. 4년 4개월이 지난 지금 이렇다.

TypeScript 파일1,086개
코드226,860줄
라우트184개 (동적 라우트 27개)
도메인28개
커밋5,190개

스터디, 모임, 그룹, 카페 지도, 커뮤니티, 포인트, 결제, 채팅, 투표, 랭킹, 쿠폰, 리뷰. 여기에 카카오 로그인과 본인인증, 지도, FCM 푸시 알림이 붙어 있다. 실제 사용자가 매일 쓰는 서비스다.

이만큼 쌓이면 코드가 어디 있는지 찾는 데 시간이 든다.

스터디 화면 하나를 고치려면 pageTemplates/에서 화면을 열고, components/에서 카드를 찾고, hooks/에서 데이터 훅을 찾고, libs/에서 계산 로직을 찾았다. 폴더가 UI 계층(atom / molecule / organism)으로 나뉘어 있어서, 같은 기능의 코드가 그 계층을 따라 흩어졌다.

그래서 기준을 바꿨다. 같은 도메인의 코드는 한 폴더 아래에 모두 모인다.

features/<도메인>/
  screens/     페이지 단위 조합
  components/  도메인 전용 컴포넌트
  modals/      도메인 전용 모달
  hooks/       queries.ts / mutations.ts
  lib/         순수 비즈니스 로직

28개 도메인을 이 구조로 옮겼다. 커밋 72개, 파일 876개.

옮기기 전에 경로 별칭부터 바꿨다

바로 옮기려다 멈췄다. tsconfig.jsonbaseUrlpaths가 없어서 모든 import가 상대경로였다.

이 상태로 파일을 옮기면 그 파일 자신의 import가 전부 함께 바뀐다. pageTemplates/gather/detail/GatherHeader.tsx 하나를 옮기면 그 안의 import 20줄이 같이 수정된다.

이러면 “파일만 옮긴 커밋”이라는 게 성립하지 않는다. diff에 내용 변경이 섞이니 나중에 되돌릴 때 무엇이 이동이고 무엇이 수정인지 구분할 수 없다. git도 rename으로 인식하지 못한다.

그래서 순서를 바꿨다. @/* 별칭을 먼저 도입하고 저장소 전체의 상대경로 import를 일괄 전환했다. 802개 파일, 4,139개 import. diff가 import 문에만 국한되는지 확인했다.

이러고 나니 파일 이동이 git mv 더하기 호출부의 경로 문자열 치환이 됐다. 옮겨진 파일 내부는 한 글자도 안 바뀐다.

커밋에 무엇을 했는지 적었다

커밋 메시지 끝에 태그를 달았다.

  • [pure-move] — 파일 위치만 바뀜. 내용 무변경
  • [mechanical] — 기계적 일괄 변환
  • [rename] — 이름 변경

72개 커밋이 이렇게 갈렸다.

종류커밋 수
[pure-move]49
[mechanical]1
[rename]1
문서·설정12
실제로 코드가 바뀐 것9

876개 파일이 움직였지만 동작이 바뀔 수 있는 커밋은 9개다. 이 구분이 나중에 확인 범위를 정한다.

무엇이 공유인지는 도메인을 걷어내야 알 수 있었다

처음 계획은 공유 컴포넌트 계층을 먼저 만들고 도메인을 옮기는 것이었다. 공유 후보 12개를 뽑아뒀다.

실제로 열어보니 6개만 공유 자격이 있었다.

  • Avatar — user 도메인 규칙이 들어 있었다
  • BottomNav — 라우트 맵이 하드코딩돼 있었다
  • AlertDialog — “가입 거절” 문구가 고정돼 있었다
  • PageTracker — 5개 도메인의 글쓰기 단계 맵을 알고 있었다

이름과 위치만 공유 계층이었지 도메인 코드였다. 그래서 순서를 뒤집었다. 도메인을 먼저 걷어내고, 남은 것을 공유로 본다.

판단 기준도 정리했다. 사용처 개수로 정하지 않는다.

  1. 이 컴포넌트가 특정 도메인의 필드명·상태값·비즈니스 규칙을 아는가
  2. 이게 바뀌는 이유가 도메인 규칙 변경인가, 디자인 변경인가
  3. 누가 유지보수해야 자연스러운가
  4. 사용처 개수 — 위 셋으로 애매할 때만

GroupThumbnailCard는 5개 도메인에서 쓰이지만 group 엔티티 필드를 직접 다룬다. 그래서 features/group/이 소유하고, 다른 도메인이 가져다 쓴다.

공유 계층이 도메인 코드를 참조하는 위반이 20건 있었는데 14건을 해소했다. 그런데 그중 절반은 의존성을 뒤집을 일이 아니라 파일 위치가 틀린 문제였다.

  • GatherWritingConditionAgeRange — 이름만 gather였고 실제로는 agesetAge만 받는 범용 컴포넌트였다. 공유 계층으로 올리고 AgeRangePicker로 고치니 위반이 사라졌다
  • HeartIconIcons/ 폴더에 있었지만 아이콘이 아니라 user mutation을 호출하는 버튼이었다. 도메인으로 내리면 끝이었다
  • ImageSlider — 이건 진짜였다. 타입 문자열로 6개 슬라이드를 분기하는데 그중 둘이 도메인 코드였다. 슬라이드를 주입받도록 바꿨다

먼저 물어볼 것은 “이게 정말 그 도메인의 것인가, 아니면 이름과 위치만 틀린 범용 코드인가”였다.

테스트가 없는데 876개를 어떻게 확인했나

이 저장소에는 테스트 러너가 없다. Storybook 스토리는 9개뿐이고 전부 버튼·뱃지 같은 원자 컴포넌트다. “빌드가 통과했으니 동작이 보존됐다”가 성립하지 않는다.

그래서 세 겹으로 나눴다.

첫째, 기계가 잡는 것. tsc --noEmit, eslint, next build를 매 배치마다 돌렸다. 최종적으로 156개 정적 페이지가 전부 생성됐다. 파일 이동에서 생기는 실수는 대부분 import 경로가 깨지는 것이고, 이건 타입 검사가 전부 잡는다.

둘째, URL이 안 바뀌었는지. 이게 이 작업에서 가장 중요한 확인이었다. 명령 한 줄이다.

git diff --name-status -M origin/main..HEAD -- pages/ | grep -E "^(A|D|R)"

pages/ 아래 파일의 추가·삭제·이름변경을 찾는다. 결과가 비어 있으면 모든 URL이 이전과 동일하다. 실제로 비어 있었다. 사용자가 쓰던 링크, 검색엔진에 색인된 주소, 앱에서 여는 딥링크가 전부 그대로다.

셋째, 사람이 직접 볼 것. 앞에서 커밋을 태그해뒀기 때문에 이 목록이 짧아진다. [pure-move] 49개는 내용이 안 바뀌었으니 볼 필요가 없다. 실제로 마크업이나 컴포넌트 구성이 바뀐 화면은 3개였다.

  • /memberImageSlider가 주입 방식으로 바뀐 유일한 화면
  • /gather/writing/condition/group/writing/conditionAgeRangePicker로 옮긴 곳. 양쪽 다 확인
  • /gather — 랜딩 라우트를 232줄에서 5줄로 줄인 곳

876개 파일을 옮기고 눈으로 본 화면이 3개다. 태그를 붙여둔 것이 여기서 값을 냈다.

하지 않기로 한 것

남은 위반이 6건 있다. 전부 도메인 훅 호출을 호출부로 끌어올려야 풀리는 것들인데, 단순 이동으로는 위반이 다른 파일로 옮겨갈 뿐이라 남겨뒀다.

가장 큰 건 pages/ 아래 184개 라우트를 얇은 진입점으로 만드는 일이다. 지금은 라우트 파일 안에 화면 로직이 그대로 들어 있는 곳이 많다.

이걸 한 번에 하지 않기로 한 이유는 하나다. 컴파일이 잡아주지 않는다.

지금까지 한 작업은 실수하면 타입 검사가 잡았다. 그런데 useEffect를 라우트에서 화면 컴포넌트로 옮기면 시그니처는 그대로인데 실행 시점이 달라진다. 타입 검사도 빌드도 통과하고, 화면을 열어봐야 안다. 184개 라우트를 동시에 바꾸면 그 확인 부담을 사람이 전부 진다.

대신 그 페이지를 어차피 고칠 일이 생겼을 때 함께 정리하기로 했다. 위험 없이 같은 결과에 도달한다. /gather 랜딩이 그 예시다 — 모달을 분리할 일이 있어서 겸사겸사 232줄을 5줄로 줄였다.

남은 것들과 각각의 이유는 docs/architecture.md에 적어뒀다. 다음에 여기 오는 사람이 — 그게 나여도 — 왜 이게 안 고쳐져 있는지 다시 알아내지 않도록.

결과

커밋72개
파일876개
도메인 이관28개
공유→도메인 위반20건 중 14건 해소
정적 페이지156/156 생성
URL 변경0건

이 작업을 끝낸 다음 날 Node 20에서 24로, Next 14에서 15로 올렸다. 그건 따로 썼다.

Share:
Back to Blog