Skip to content

기술부채 상환 캠페인 (2026-08-21)

앱 코드 전수 부채 조사(9항목)와 그 즉시 상환을 한 세션에 수행한 기록이다. 발견 중 3건은 실측 후 "부채 아님"으로 재판정됐고, 2건은 처방의 형태를 바꿔 집행했다. 마이그레이션 프리즈 중에는 무DB 작업만, 해제 후에는 엣지 배포까지 진행했다 — DB 마이그레이션은 캠페인 전체에서 0건이다.

관련 PR: #495 #496 #497 #499 #501 #504


1. 배경

오너가 "땜질식 처방, 기술 부채, 구조 개선이 필요한 곳"의 전수 조사를 발주했다. 부채 마커·구조·이름 잔재·죽은 코드·테스트 인프라를 훑은 조사가 9항목을 보고했고, 오너가 승인 없는 자율 완주를 위임하며 해석 하나를 정정했다: 클라이언트 키스페이스 lift-guild-*가 동결이라는 과거 해석은 존재하지 않는 결정이었다 — 바꿀 수 있는 것은 개명 대상이다.

시기가 특수했다. 마이그레이션 랜딩 프리즈가 걸려 있었고(무DB 작업만 허용), 같은 표면 위에 대형 트랙 두 개 — 옛 브랜드 개명 #492(199파일)와 베이스라인 스쿼시 #494(테스트 44파일 접촉) — 가 머지 대기 중이었다. 그래서 이 캠페인의 절반은 상환 자체가 아니라 무엇을 언제 랜딩해도 안전한가의 조율이었다.

2. 문제 제기

조사 발견을 성격별로 묶으면 네 덩어리였다.

부채가 "동결"이라는 이름으로 방치되어 있었다

레포의 래칫 게이트들은 부채의 성장을 잘 막고 있었지만, 동결된 재고 자체는 줄지 않았다. exhaustive-deps baseline 29건은 사실상 stale closure 후보 목록인데 baseline이라는 이름으로 쌓여 있었고, explicit-any 허용 목록 56파일 중 9건은 이미 any가 0인데 남아 재유입을 조용히 허용했다. camelCase/snake_case 이중 읽기 (row.fooBar || row.foo_bar)는 35파일 469곳으로, 게이트조차 없었다.

프로토타입 시대의 유물이 프로덕션에 남아 있었다

아티팩트 프리뷰 시대의 tweaks 패널(에디트모드 postMessage 프로토콜)이 프로덕션 번들에 실려 있었다 — 패널은 프로덕션에서 도달 불가라 항상 기본값만 렌더하면서. 옛 브랜드 키스페이스(lift-guild-* IndexedDB 2종·localStorage 캐시·디버그 키· HTTP 헤더)가 저장소와 와이어에 남아 있었고, git hash까지 동일한 자산 사본 24쌍(23.6MB)이 mobile/desktop 양쪽에 있었다 — 실참조는 desktop .webp 12개뿐.

진짜 부채와 의도된 설계가 섞여 있었다

조사 단계에서 부채로 보인 것 중 일부는 실측하면 설계였다. legacyDifficulty는 WodUp 인입 RPE가 지금도 착지하는 현역 칼럼이고, 엣지 배포 명령이 든 에러 문자열은 계약 테스트가 고정한 관리자 표면 전용 진단이며, 글리프 PNG 원본은 테스트가 고정한 아카이브 정책이었다. 겉보기 부채를 그대로 집행했으면 데이터 의미와 계약을 부쉈다.

검증 인프라가 상환을 비싸게 만들었다

테스트 ~40파일이 두 초대형 컨트롤러의 소스 텍스트를 정규식·슬라이스로 읽어서 코드 이동마다 깨졌고, 로더 6종은 매 렌더 재생성되는 클로저라 이펙트 deps에 정직하게 올릴 방법 자체가 없었다 — "deps에 추가"는 눈속임이고, 수리는 구조를 요구했다.

3. 해결 방안

원칙

  1. 실측 후 집행. 발견을 그대로 집행하지 않는다 — 생산자·참조·계약 테스트를 실측해 재판정하고, "부채 아님"도 결과로 기록한다.
  2. 무DB. 프리즈 준수, 마이그레이션 0건. 유저 데이터가 걸린 개명은 클라이언트 측 복사 마이그레이션을 동반한다.
  3. 충돌 순서 조율. #492·#494와의 파일 겹침을 실측해 무충돌분을 먼저 랜딩하고, 겹치는 작업은 #492 브랜치 위에 스택해 뒀다가 머지 후 리베이스한다.
  4. 일괄 제거가 불가한 것은 래칫으로 동결. dead-css 선례를 따른다 — 성장을 먼저 막고, 감축은 도메인별 후속으로 넘긴다.
  5. 매 PR 전 게이트 체인 + 빌드, 쓰기 경로가 걸린 검증은 새로 초기화한 로컬 Supabase DB의 e2e 왕복으로.

접근

웨이브 넷으로 나눴다. ① 무충돌 기계적 상환(자산·키스페이스 1차)을 즉시 랜딩, ② #492 표면과 겹치는 작업(tweaks 은퇴·디버그 키·래칫·any 목록)은 #492 베이스에 만들어 두고 머지 신호를 감시, ③ #492 랜딩 후 정합성(deps 소각)과 구조(1차 분해), ④ 프리즈 해제 후 와이어(헤더 개명 + 엣지 배포). 진행 상태는 HQ 세션에 단계 전환마다 보고했다.

4. 적용한 내용

랜딩 6건

PR내용
#495자산 단일본화: mobile/desktop 동일 사본 24쌍 → ui/shared/assets/exercises 단일본. 레포 -11.8MB. PNG 원본 보존 정책은 단일본 기준 예산으로 조정해 유지
#496키스페이스 1차: barbelic-workout-local-cache(IndexedDB)는 pendingSaves가 미동기 운동 기록이라 put-if-absent 복사 마이그레이션 후 옛 DB 삭제 — 블록되면 다음 부팅이 재시도(자가 치유). barbelic-read-snapshots·localStorage 캐시 3종은 재구축 가능이라 무마이그레이션 개명 + 부팅 유휴 청소(legacyClientKeyspace.ts)
#497tweaks 패널 은퇴(외형 계약을 APPEARANCE_DEFAULTS 상수로 고정, 값별 CSS 변형이 기본 룩이므로 data 속성 유지 — 시각 변화 0) + 디버그·에러 큐 키 2차 개명(옛 큐 1회 채택) + check:dual-key 래칫 신설(469곳 동결, 생산자 0 증명된 죽은 아암 1곳만 제거) + any 목록 상환(스테일 9건 제거 + 어댑터 1개 탈-any, 56→46)
#499exhaustive-deps baseline 29건 전건 소각: 수리 21(로더 안정 파사드·월 프리미티브 키·조건식 메모 승격 등) + 사유 명기 검토 억제 8. eslint legacy warn 블록(15파일) 은퇴 → 전 파일 error, eslintGate가 "빈 baseline"을 계약으로 고정
#501appController 1차 분해: 로그테이블 군집을 logTableController(291줄)로 verbatim 추출 + 이동분 탈-any. 4,123 → 3,926줄
#504HTTP 헤더 개명: x-lift-guild-*x-barbelic-*. 클라이언트 양쪽 병행 송신 + 엣지 양쪽 수신·노출로 배포 순서 무관 안전. 엣지 3종 DEPLOYMENT_VERSION 범프 + Production 배포·프로브 검증

주요 결정과 그 근거

  • 삭제 대신 중복 제거(자산): "죽은 자산 삭제"로 조사됐지만 staticAssetDelivery 테스트가 PNG 원본 보존을 정책으로 고정하고 있었다. 정책은 존중하고 중복만 걷었다.
  • 개명의 등급을 데이터 성격으로 나눴다: 재구축 가능한 캐시는 단순 개명+청소, 유저 데이터(pendingSaves)는 복사 마이그레이션, 와이어(헤더)는 양방향 호환 전이, DB 저장값(정책 ID lift-guild.e1rm 등)은 개명하지 않기로 판정 — 행 단위 백필
    • 불변 정책 스냅샷 개정이 필요한데 내부 불투명 식별자의 이름값이 그 비용을 정당화하지 못한다.
  • 이중 키는 일괄 제거 불가: HOME_DASHBOARD처럼 어댑터가 의도적으로 raw snake 페이로드를 통과시키는 프래그먼트가 있어 출처가 필드 단위로 갈린다. 맹목 제거는 UI 회귀 위험 → 래칫으로 동결하고 감축은 도메인별 후속.
  • deps "수리"의 기준: 함수가 안정일 때만 deps 추가가 수리다. 불안정 로더는 latest-ref 파사드로 안정화한 뒤 등재했고, 의도된 불변식(리비전 틱·장수 auth 구독·마운트 전용 시드·오너 전환 스냅샷)은 고치는 척하지 않고 사유를 명기한 억제로 전환했다.
  • 분해는 1차에서 종결: 로그테이블 추출 뒤 실측 결과 잔여는 "추출 가능한 소유 섬"이 아니라 조율 로직 그 자체였다(restoreWorkoutDraftForUser 하나가 상태 9개 조율). 이 판정이 같은 날 상태 재설계 캠페인의 발주 근거가 됐다 — appController 리팩토링 기록.

조사와 갈린 실측 — 부채가 아니었던 것

  • legacyDifficulty 이중 체계: exercise_sets.difficulty(1–10)는 WodUp 인입 RPE가 착지하는 현역 칼럼이고 effort_v1(5단계 자각 강도)과 도메인이 다르다. 백필 소각은 데이터 의미 파괴라 취소. 이름이 오해를 부르지만 write contract 와이어 키라 개명 비용이 가치를 초과한다.
  • 엣지 배포 명령 에러 문자열: deploymentPipeline 계약 테스트가 고정한 관리자 표면 전용 진단이며, userErrorPolicy가 유저 표면 직행을 이미 차단하고 있었다.
  • 글리프 PNG 원본: 죽은 자산이 아니라 테스트가 고정한 아카이브 정책. 부채는 중복뿐이었다.

작업 중 드러난 것

  • 경합 지도가 절반의 일이었다: #492가 조사 6번(lifteed 개명)을 이미 전부 포함 하고 있어 중복 작업을 회피했고, #494가 테스트 44파일을 만지고 있어 앵커 관련 작업을 그 뒤로 배치했다. 착수 전 겹침 실측이 없었으면 두 트랙의 리베이스를 전부 꼬았을 것이다.
  • \u0000/\u0001 이스케이프 소실: 소스의 이스케이프는 터미널 출력에서 제어문자로 렌더돼, 코드를 옮겨 적으면 조용히 사라진다. 이동 후 grep u0000으로 대조해 1건을 잡았다.
  • check:unused가 로컬 check 체인에 없다: 분해가 남긴 고아 import를 CI만 잡았다. 별도 실행이 필수다.
  • 소스 앵커 재지정 요령: 이동한 내부 단언은 새 파일을 읽게 하고, 호스트 쪽 계약(리셋 순서 등)은 새 진입점 경유로 분리하면 단언 의도를 잃지 않는다.
  • Docker 소켓 크래시 재발: 문서화된 우회(소켓 폴더 rename)는 두 폴더를 동시에 치워야 한다 — 하나씩 치우면 다음 소켓에서 순차 재크래시한다.

5. 적용 결과

항목결과
레포 크기자산 중복 제거 -11.8MB
exhaustive-deps baseline29건 → 0건 (전 파일 error 승격, 검토 억제 14건 등재)
explicit-any 허용 목록56 → 46파일
이중 키 허용469곳 동결 — 신규 게이트 check:dual-key (신설 게이트로 성장 차단)
appController.tsx4,123 → 3,926줄 (1차 분해 — 이후 상태 재설계 캠페인이 1,856줄까지)
클라이언트 키스페이스저장소 5종 + 디버그·에러 큐 키 6종 + HTTP 헤더 2종 개명, DB 마이그레이션 0건
엣지 함수3종 Production 배포 + 프로브 검증(신·구 헤더 병행, 20260821-barbelic-headers-v1/-stats-v1)
검증매 PR npm run check 전 체인(스위트 1,672) + 빌드. 마감 검증: 로컬 스택 CRUD 왕복 e2e 8/8(스킵 1 = 알려진 issue #321) — 스쿼시 베이스라인이 새로 초기화한 DB에 그대로 적용되는 것도 겸사 확인

6. 이번 개선으로 향상된 것

유저 브라우저의 옛 키스페이스가 스스로 이전된다

부팅 시 복사 마이그레이션(유저 데이터)과 유휴 청소(캐시)가 자동으로 돌고, 실패는 다음 부팅이 재시도한다. 데이터 손실 없이 keyspace가 barbelic으로 수렴한다.

훅 의존성 부채를 다시 쌓을 수 없다

빈 baseline이 계약이고 exhaustive-deps는 전 파일 error다. 새 경고는 랜딩되지 않고, 예외는 사유를 명기한 인라인 억제로만 가능하다.

이중 키가 더는 늘지 않는다

check:dual-key 래칫이 469곳을 동결했다. 새 이중 읽기는 게이트가 막고, 감축 델타는 baseline 재생성으로만 반영된다.

프로토타입 프로토콜이 번들에서 사라졌다

에디트모드 postMessage 리스너와 패널 코드가 제거됐다. 외형은 상수 계약으로 고정돼 시각 변화 없이 유물만 걷혔다.

부채 장부가 실제와 일치한다

any 허용 목록의 조용한 여유분이 사라져, 목록에 있는 파일은 실제로 any 경계가 있는 파일뿐이다. "부채 아님" 재판정 3건도 근거와 함께 기록으로 남아 같은 조사가 반복되지 않는다.

후속 작업의 안전망이 실증됐다

로컬 Supabase 스택 → db reset → e2e 왕복 절차가 이 캠페인에서 실측 검증됐고, 같은 날 상태 재설계 캠페인이 이 안전망 위에서 쓰기 경로를 재설계했다.

남은 것

  • 구 헤더 제거: 신 버전 클라이언트 보급 후 엣지에서 x-lift-guild-* 병행 노출을 걷는다(코드에 전이 기간 주석 명시).
  • 이중 키 감축: 도메인별 후속 — 래칫이 총량(469)을 감시한다.
  • 정책 ID(lift-guild.e1rm 등): 개명 비권고로 종결 — 재론하려면 행 단위 백필 + 새 정책 버전 발행 비용을 다시 산정해야 한다.
  • 조사 1번의 잔여였던 상태 재설계는 같은 날 별도 캠페인으로 완주됐다 — appController 리팩토링 기록.