8편까지는 코드 안의 안티패턴이었습니다. 이번 편은 그 코드를 만들어내는 구조입니다.
순서를 마지막에 둔 이유는 덜 중요해서가 아닙니다. 오히려 영향력이 가장 큽니다. 앞의 여덟 편에서 다룬 모든 것이 여기서 반복 생산되기 때문입니다. useEffect 오용 하나는 코드 리뷰에서 잡히지만, "리뷰할 시간이 없는 구조"는 그 오용을 무한히 생산합니다.
Cargo Cult — 제약 조건을 빼고 결론만 가져오기
"대형 서비스 A사가 마이크로 프론트엔드를 쓴대요. 우리도 도입하죠."
"B사가 모노레포로 갔대요."
"C사가 상태 관리를 X로 바꿨대요."무엇이 잘못됐나
기술 결정은 그 조직의 제약 조건에 대한 답입니다. 결론만 복사하면 제약 조건이 빠집니다.
마이크로 프론트엔드는 "수십 개 팀이 독립적으로 배포해야 하고, 서로 다른 프레임워크를 써야 하며, 배포 조율 비용이 런타임 통합 비용보다 크다" 는 조건에서 나온 답입니다. 개발자 다섯 명인 팀에 그 조건은 없습니다. 그러면 얻는 것 없이 비용만 치릅니다 — 런타임 통합 복잡도, 중복 의존성, 디버깅 난이도, 타입 공유 문제.
판별 질문 세 개
새 기술이나 구조를 도입하려 할 때:
- 이 기술이 푸는 문제를 우리가 지금 겪고 있는가? (예상되는 문제가 아니라 지금)
- 그 문제의 비용이 이 기술의 도입·유지 비용보다 큰가?
- 채택한 조직과 우리의 제약 조건 중 무엇이 같고 무엇이 다른가?
3번에 답하지 못하면 Cargo Cult입니다.
반대 방향의 안티패턴 — 맹목적 거부
"우리는 우리 방식대로"로 검증된 해법을 거부하는 것도 같은 병입니다. 근거 없이 따라하는 것과 근거 없이 거부하는 것은 둘 다 판단하지 않은 것입니다.
이력서 주도 개발
배우고 싶어서 도입합니다. 그 자체는 나쁘지 않지만, 제품 코드베이스가 학습 장소가 되면 비용을 다른 사람들이 치릅니다.
증상:
- 도입한 사람이 떠나면 아무도 유지보수를 못 함
- 팀 전체가 학습 곡선을 강제로 오름
- 문제에 비해 과한 도구 (단순 폼에 상태 머신 라이브러리, 정적 사이트에 GraphQL)
분리 원칙: 배우는 것은 사이드 프로젝트나 스파이크(시간 제한 실험)에서, 도입은 팀이 유지할 수 있는지를 기준으로 결정합니다.
도입 결정 체크리스트
1편의 일방통행 문/양방향 문 분류를 그대로 적용합니다.
□ 지금 겪는 문제가 무엇인가? (한 문장으로)
□ 이 도구 없이 그 문제를 풀 수 있는가?
□ 팀에서 몇 명이 이걸 유지보수할 수 있는가? (1명이면 위험)
□ 되돌리는 데 며칠이 드는가? (3일 이내면 양방향 문)
□ 번들 크기에 얼마를 더하는가?
□ 유지보수가 활발한가? (최근 릴리스, 이슈 응답, 기여자 수)
□ 문제가 생기면 우리가 고칠 수 있는 규모인가?"되돌리는 데 며칠"이 가장 강력한 질문입니다. 3일이면 그냥 해보고 아니면 되돌리세요. 한 달이면 스파이크로 검증하고 결정하세요.
"임시" 코드의 영속성
// TODO: 나중에 제대로 고치기
// FIXME: 임시 처리
// HACK: 배포 일정 때문에 우선 이렇게임시 코드의 중간값 수명은 사실상 영원입니다. 이유는 구조적입니다.
- 임시 코드는 동작합니다. 동작하는 것을 고치는 데는 일정이 안 잡힙니다
- 작성자만 왜 임시인지 압니다. 작성자가 떠나면 그 지식도 떠납니다
- 다음 사람은 그게 임시인 줄 모르고 그 위에 코드를 쌓습니다
대응
// 🔴 정보가 없는 TODO
// TODO: 리팩터링
// ✅ 만료일 + 조건 + 책임자 + 티켓
// TODO(2026-12-31, PROJ-1234): 레거시 주문 API가 제거되면 이 분기를 삭제한다.
// 서버팀 v3 마이그레이션 완료 시점. 담당: 프론트 플랫폼
if (order.legacyFormat) { /* ... */ }// 만료된 TODO를 CI에서 실패시키기
'no-warning-comments': ['error', { terms: ['fixme'], location: 'anywhere' }],
// 또는 eslint-plugin-expire-todo-comments 류의 도구임시임을 코드가 스스로 주장하게 만드세요. 주석이 아니라 CI가 말해야 합니다.
디자인 시스템 없이 시작하기
1년 뒤 코드베이스:
Button, PrimaryButton, BtnPrimary, ActionButton, SubmitBtn,
StyledButton, ButtonNew, ButtonV2, LegacyButton, ...각각은 필요해서 만들어졌습니다. 기존 것을 고치면 다른 화면이 깨질까 봐 새로 만든 거죠(3편의 잘못된 추상화가 만든 결과이기도 합니다).
최소 디자인 시스템
처음부터 완벽할 필요는 없습니다. 토큰만 있어도 절반은 해결됩니다.
:root {
/* 간격 — 이것만 있어도 13px, 15px, 17px이 안 생긴다 */
--space-1: 0.25rem; --space-2: 0.5rem; --space-3: 0.75rem;
--space-4: 1rem; --space-6: 1.5rem; --space-8: 2rem;
/* 타이포 */
--text-sm: 0.875rem; --text-base: 1rem; --text-lg: 1.125rem;
/* 색 (7편의 2계층) */
--color-primary: var(--blue-500);
--color-text: var(--gray-900);
/* 반경, 그림자, z-index */
--radius-md: 0.375rem;
--z-modal: 40;
}그리고 "새 컴포넌트를 만들기 전에 기존 것을 확인한다"는 합의. 도구보다 이 합의가 더 중요합니다.
리팩터링 예산 없음
모든 스프린트가 기능으로 꽉 차 있습니다. 기술부채 항목은 백로그 맨 아래에서 계속 밀립니다.
부채는 복리로 늘어난다
1주차: 기능 추가 = 1일
6개월: 기능 추가 = 2일 (우회해야 할 코드가 생김)
1년: 기능 추가 = 4일 (건드리면 안 되는 영역이 생김)
2년: 기능 추가 = 2주 (어디를 고쳐야 할지 아무도 모름)속도를 위해 부채를 쌓는데, 그 부채가 속도를 잡아먹습니다. 그리고 어느 시점부터는 "다시 만드는 게 낫다"는 말이 나오기 시작합니다 — 그건 부채가 이자를 못 갚는 수준이 됐다는 뜻입니다.
기술부채 사분면
Martin Fowler의 분류가 유용합니다. 모든 부채가 나쁜 게 아닙니다.
| 신중한 (prudent) | 무모한 (reckless) | |
|---|---|---|
| 의도적 | "지금은 출시하고, 다음 분기에 정리한다" | "설계할 시간 없어, 그냥 짜" |
| 비의도적 | "이제야 더 나은 구조를 알았다" | "레이어링이 뭔가요?" |
의도적·신중한 부채는 정상적인 엔지니어링 판단입니다. 문제는 그게 기록되지 않고 상환 계획이 없을 때입니다.
실용적 방법 세 가지
1) 보이스카웃 규칙 — 건드리는 파일은 들어왔을 때보다 조금 낫게 두고 나간다. 별도 예산이 필요 없고 가장 지속 가능합니다.
2) 고정 비율 — 각 스프린트의 15~20%를 부채 상환에 고정 배정. 협상 대상이 아니어야 유지됩니다.
3) 기능에 끼워넣기 — "주문 화면에 필터 추가" 티켓에 "그 과정에서 주문 상태를 판별 유니온으로 정리" 를 포함. 별도 승인이 필요 없습니다.
부채를 가시화하기
<!-- docs/tech-debt.md -->
## 주문 상태 관리 (우선순위: 높음)
- **현상:** boolean 4개로 상태를 표현, 유효하지 않은 조합 12가지 가능
- **증상:** 최근 3개월 관련 버그 5건
- **비용:** 이 영역 작업 시 매번 +1일
- **상환:** 판별 유니온으로 전환, 약 3일
- **티켓:** PROJ-2210"이 부채가 실제로 얼마를 먹고 있는가"를 숫자로 만들면 우선순위 논의가 감정에서 계산으로 바뀝니다.
코드 리뷰 안티패턴
너무 큰 PR
+2,847 −1,203 (파일 47개)리뷰할 수 없는 크기입니다. 그래서 LGTM이 찍힙니다. 연구에 따르면 리뷰의 효과는 400줄을 넘으면 급격히 떨어집니다.
대응: PR 크기 제한을 CI에서 경고하고, 큰 작업은 단계별 PR로 쪼갭니다. 리팩터링과 기능 변경을 같은 PR에 섞지 마세요 — 무엇이 동작 변경이고 무엇이 구조 변경인지 구분이 안 됩니다.
스타일 논쟁
들여쓰기, 따옴표, 줄바꿈으로 코멘트가 20개 달립니다.
대응: Prettier와 ESLint로 전부 자동화하고 논쟁을 끝내세요. 기계가 정할 수 있는 것에 사람의 시간을 쓰면 안 됩니다. 그리고 자동화하면 리뷰가 진짜 봐야 할 것 — 설계, 엣지 케이스, 이름 — 에 집중됩니다.
리뷰 지연
PR이 3일씩 열려 있으면 브랜치가 낡고, 충돌이 나고, 작성자는 문맥을 잊습니다. 결국 "일단 머지하고 나중에"가 됩니다.
대응: 리뷰 SLA를 정하고(예: 영업일 기준 하루), 작은 PR을 장려합니다. 작은 PR은 빨리 리뷰되고, 빨리 리뷰되면 또 작게 쪼갤 동기가 생깁니다.
승인 도장
LGTM 👍실제로 안 읽은 승인입니다. 이게 반복되면 리뷰 제도 자체가 형식이 됩니다.
대응: 리뷰어에게 무엇을 봐야 하는지 알려주세요. PR 템플릿에 "이 변경의 위험 지점", "특히 봐줬으면 하는 부분"을 적게 하면 리뷰 품질이 눈에 띄게 올라갑니다.
지식의 고립
"그 부분은 ○○님만 알아요"bus factor 1 상태입니다. 그 사람이 휴가만 가도 그 영역의 작업이 멈춥니다.
대응:
- 페어 프로그래밍이나 모브 리뷰를 핵심 영역에 적용
- 코드 소유자를 두되 최소 2명
- 결정의 근거를 ADR(Architecture Decision Record)로 남기기
<!-- docs/adr/0012-server-state-tool.md -->
# 12. 서버 상태 관리 도구로 TanStack Query 채택
## 상태: 채택됨 (2026-03-15)
## 맥락
Redux에 서버 데이터를 저장하면서 isLoading/error 보일러플레이트가
슬라이스마다 반복되고 있었다. 캐시 무효화를 수동 관리 중.
## 결정
서버 상태는 TanStack Query로 분리한다. Redux는 클라이언트 전역 상태만 담당.
## 고려한 대안
- SWR: 더 가볍지만 낙관적 업데이트 API가 우리 요구에 부족
- RTK Query: Redux 의존을 유지해야 함
## 결과
- 슬라이스 12개 중 9개 삭제 예정
- 팀이 새 도구를 학습해야 함ADR은 "왜"를 보존합니다. 1편의 마지막 판단 축이 조직 층위에서 실현되는 형태입니다. 2년 뒤 새로 온 사람이 "왜 상태 관리가 두 개죠?"라고 물었을 때 답이 있습니다.
문서의 반대 방향 안티패턴
문서가 너무 많아서 아무도 안 읽고 아무도 갱신 안 하는 상태도 흔합니다. 틀린 문서는 없는 문서보다 나쁩니다.
원칙: 코드로 표현할 수 있으면 코드로. 타입, 테스트, 린트 규칙이 문서보다 정확하고 자동으로 갱신됩니다. 문서에는 코드로 표현할 수 없는 것 — 왜 이 선택을 했는지, 어떤 대안을 버렸는지 — 만 남기세요.
이해 없이 머지되는 코드
AI 보조 도구가 보편화되면서 빠르게 확산되고 있는 안티패턴입니다.
형태: 생성된 코드가 동작하니 머지합니다. 왜 동작하는지는 확인하지 않습니다.
문제는 "AI가 짠 코드"가 아니라 "아무도 이해하지 못하는 코드가 코드베이스에 들어오는 것"입니다. 같은 문제가 스택오버플로에서 복사한 코드, 이해 없이 가져온 사내 라이브러리, 퇴사자가 남긴 코드에도 있습니다. 생성 도구는 그 속도를 크게 높였을 뿐입니다.
1편의 마지막 판단 축이 그대로 적용됩니다.
이 코드가 왜 이렇게 됐는지 설명할 수 있는가?
설명할 수 없으면 깨졌을 때 고칠 수 없습니다. 그리고 반드시 깨집니다.
실용적 기준:
- 머지 전에 코드를 한 줄씩 설명할 수 있는 사람이 최소 한 명 있을 것
- 생성된 코드에도 동일한 리뷰 기준을 적용할 것 (오히려 더 엄격하게 — 그럴듯해 보이는 오류가 섞이기 쉽습니다)
- 이해하지 못한 부분은 머지 전에 단순화하거나 직접 다시 쓸 것
- 특히 에러 처리, 보안, 경계 조건은 생성 코드에서 자주 빠집니다 (5·8편의 목록을 체크리스트로 쓰세요)
"나중에" 미루기의 구조
이 시리즈에서 반복해서 나온 문장들입니다.
"성능은 나중에 최적화하죠"
"접근성은 나중에 추가하죠"
"테스트는 나중에 쓰죠"
"타입은 나중에 붙이죠"전부 같은 구조입니다. 지연된 비용이므로 지금 안 아프고, 안 아프니까 안 합니다. 그리고 나중이 되면 고치는 비용이 처음부터 하는 비용의 몇 배가 되어 있습니다.
| 처음부터 | 나중에 | |
|---|---|---|
| 접근성 | 요소 선택만 바꾸면 됨 | DOM 구조·컴포넌트 API·포커스 흐름 전면 수정 |
| 타입 | 함께 작성 | 전 코드베이스에 any 제거 작업 |
| 테스트 | 설계가 테스트 가능해짐 | 테스트하려면 먼저 리팩터링해야 함 |
| 성능 예산 | 회귀가 안 들어옴 | 이미 들어온 것을 되돌려야 함 |
대응은 1편의 결론과 같습니다. 지연된 비용을 즉시 아프게 만드세요. CI에서 막으면 "나중에"가 불가능해집니다.
시리즈 전체 정리
9편 요약
| 편 | 영역 | 가장 큰 하나 |
|---|---|---|
| 1 | 판단 기준 | 지연된 비용에 측정 장치를 붙여라 |
| 2 | 상태·데이터 흐름 | useEffect로 파생 상태를 계산하지 마라 |
| 3 | 컴포넌트 설계 | 중복이 잘못된 추상화보다 싸다 |
| 4 | 렌더링·성능 | 병목은 대개 리렌더가 아니다 |
| 5 | 네트워크·실패 | 실패 경로가 없는 코드는 미완성이다 |
| 6 | 번들·경계 | 측정하지 않으면 번들은 반드시 자란다 |
| 7 | CSS·접근성 | !important는 소유권이 불분명하다는 증거다 |
| 8 | 견고함 | 타입은 컴파일 후 사라진다 |
| 9 | 조직 | 코드 안티패턴은 조직에서 반복 생산된다 |
관통하는 네 가지 판단 축
1편에서 세운 것을 여덟 편에 걸쳐 적용했습니다.
1. 비용 곡선이 선형인가 지수인가 중복(선형) vs 잘못된 추상화(지수), 요청 하나 추가(선형) vs N+1 구조(지수). 불확실하면 선형을 택합니다.
2. 되돌릴 수 있는가 컴포넌트 내부 구현(즉시) vs URL 구조(영구). 양방향 문은 빨리 결정하고, 일방통행 문은 검증하고 결정합니다.
3. 비용이 언제 나타나는가 즉시 아픈 것은 알아서 고쳐집니다. 지연된 것 — 성능, 접근성, 부채 — 에 측정 장치를 붙여 즉시 아프게 만듭니다.
4. 왜 이렇게 됐는지 설명할 수 있는가 설명할 수 있으면 고칠 수 있습니다. 코드 주석, ADR, 그리고 이해하고 머지하는 문화.
지금 바로 할 수 있는 것 — 우선순위 순
하루면 끝나고, 앞으로 들어올 안티패턴의 상당수를 막습니다.
1일차 — 측정 장치 (1편)
□ 번들 크기 예산을 CI에 추가 (size-limit)
□ eslint-plugin-jsx-a11y 활성화
□ TypeScript strict + noUncheckedIndexedAccess
□ @typescript-eslint/no-explicit-any: error
□ dependency-cruiser로 순환 참조 차단1주차 — 안전망
□ 에러 바운더리 3층 (전역/라우트/위젯)
□ window error + unhandledrejection 리스너
□ Web Vitals 실사용자 수집 (RUM)
□ 번들 애널라이저 한 번 돌려보기
□ API 경계에 스키마 검증 (Zod)1개월차 — 구조
□ 디자인 토큰 (간격·색·타이포·z-index)
□ 서버 상태와 클라이언트 상태 분리
□ 가장 큰 God Component 하나 쪼개기
□ 기술부채 목록 문서화 + 티켓화
□ 리팩터링 시간 비율 합의자가진단 체크리스트
각 항목에 "예"로 답할 수 없으면 해당 편을 다시 보세요.
□ 우리 앱의 실사용자 LCP/CLS/INP p75 값을 말할 수 있는가? (1·4편)
□ 초기 번들 크기를 KB 단위로 말할 수 있는가? (1·6편)
□ useEffect 중 "React 바깥과 동기화"가 아닌 것이 없는가? (2편)
□ 가장 큰 컴포넌트가 300줄 이하인가? (3편)
□ 성능 문제를 CPU 스로틀링 켜고 프로파일링해 본 적 있는가? (4편)
□ 모든 네트워크 요청에 타임아웃과 실패 UI가 있는가? (5편)
□ 클라이언트 번들에 비밀이 없다고 확인했는가? (6편)
□ 마우스 없이 Tab만으로 핵심 작업을 끝낼 수 있는가? (7편)
□ API 응답을 런타임에 검증하는가? (8편)
□ 위젯 하나가 실패해도 페이지가 살아있는가? (5·8편)
□ 우리 팀의 기술부채 목록이 문서로 존재하는가? (9편)
□ 어제 머지된 코드를 설명할 수 있는 사람이 있는가? (9편)마무리
이 시리즈의 전제는 하나였습니다.
안티패턴은 실수가 아니라 구조다.
그래서 "이렇게 하지 마세요" 목록을 외우는 것으로는 해결되지 않습니다. 각 안티패턴이 왜 그 순간 합리적으로 보였는지를 이해해야, 다음번에 같은 갈림길에서 다른 선택을 할 수 있습니다.
그리고 개인의 각오보다 구조가 더 강합니다. 사람은 바쁘면 놓치고, 새로 온 사람은 관례를 모르고, 마감은 언제나 있습니다. 그래서 이 시리즈가 반복해서 돌아온 결론은 같습니다.
규칙을 기계가 검사하게 만드세요. 린터가 막고, CI가 빨개지고, 타입이 컴파일을 거부하면, 그 안티패턴은 애초에 들어오지 못합니다.