← posts/b.log()

blog92@web:~$ cat posts/frontend-antipatterns-9-organization.md

FRONTEND10 min read

프론트엔드 안티패턴 (9) — 조직과 프로세스: 안티패턴을 생산하는 공장

Cargo Cult, 이력서 주도 개발, 임시 코드의 영속성, 리팩터링 예산 부재. 코드 안에 없지만 코드를 만드는 안티패턴들과 시리즈 전체 정리.

8편까지는 코드 안의 안티패턴이었습니다. 이번 편은 그 코드를 만들어내는 구조입니다.

순서를 마지막에 둔 이유는 덜 중요해서가 아닙니다. 오히려 영향력이 가장 큽니다. 앞의 여덟 편에서 다룬 모든 것이 여기서 반복 생산되기 때문입니다. useEffect 오용 하나는 코드 리뷰에서 잡히지만, "리뷰할 시간이 없는 구조"는 그 오용을 무한히 생산합니다.


Cargo Cult — 제약 조건을 빼고 결론만 가져오기

text
"대형 서비스 A사가 마이크로 프론트엔드를 쓴대요. 우리도 도입하죠."
"B사가 모노레포로 갔대요."
"C사가 상태 관리를 X로 바꿨대요."

무엇이 잘못됐나

기술 결정은 그 조직의 제약 조건에 대한 답입니다. 결론만 복사하면 제약 조건이 빠집니다.

마이크로 프론트엔드는 "수십 개 팀이 독립적으로 배포해야 하고, 서로 다른 프레임워크를 써야 하며, 배포 조율 비용이 런타임 통합 비용보다 크다" 는 조건에서 나온 답입니다. 개발자 다섯 명인 팀에 그 조건은 없습니다. 그러면 얻는 것 없이 비용만 치릅니다 — 런타임 통합 복잡도, 중복 의존성, 디버깅 난이도, 타입 공유 문제.

판별 질문 세 개

새 기술이나 구조를 도입하려 할 때:

  1. 이 기술이 푸는 문제를 우리가 지금 겪고 있는가? (예상되는 문제가 아니라 지금)
  2. 그 문제의 비용이 이 기술의 도입·유지 비용보다 큰가?
  3. 채택한 조직과 우리의 제약 조건 중 무엇이 같고 무엇이 다른가?

3번에 답하지 못하면 Cargo Cult입니다.

반대 방향의 안티패턴 — 맹목적 거부

"우리는 우리 방식대로"로 검증된 해법을 거부하는 것도 같은 병입니다. 근거 없이 따라하는 것과 근거 없이 거부하는 것은 둘 다 판단하지 않은 것입니다.


이력서 주도 개발

배우고 싶어서 도입합니다. 그 자체는 나쁘지 않지만, 제품 코드베이스가 학습 장소가 되면 비용을 다른 사람들이 치릅니다.

증상:

  • 도입한 사람이 떠나면 아무도 유지보수를 못 함
  • 팀 전체가 학습 곡선을 강제로 오름
  • 문제에 비해 과한 도구 (단순 폼에 상태 머신 라이브러리, 정적 사이트에 GraphQL)

분리 원칙: 배우는 것은 사이드 프로젝트나 스파이크(시간 제한 실험)에서, 도입은 팀이 유지할 수 있는지를 기준으로 결정합니다.


도입 결정 체크리스트

1편의 일방통행 문/양방향 문 분류를 그대로 적용합니다.

text
□ 지금 겪는 문제가 무엇인가? (한 문장으로)
□ 이 도구 없이 그 문제를 풀 수 있는가?
□ 팀에서 몇 명이 이걸 유지보수할 수 있는가? (1명이면 위험)
□ 되돌리는 데 며칠이 드는가? (3일 이내면 양방향 문)
□ 번들 크기에 얼마를 더하는가?
□ 유지보수가 활발한가? (최근 릴리스, 이슈 응답, 기여자 수)
□ 문제가 생기면 우리가 고칠 수 있는 규모인가?

"되돌리는 데 며칠"이 가장 강력한 질문입니다. 3일이면 그냥 해보고 아니면 되돌리세요. 한 달이면 스파이크로 검증하고 결정하세요.


"임시" 코드의 영속성

typescript
// TODO: 나중에 제대로 고치기
// FIXME: 임시 처리
// HACK: 배포 일정 때문에 우선 이렇게

임시 코드의 중간값 수명은 사실상 영원입니다. 이유는 구조적입니다.

  • 임시 코드는 동작합니다. 동작하는 것을 고치는 데는 일정이 안 잡힙니다
  • 작성자만 왜 임시인지 압니다. 작성자가 떠나면 그 지식도 떠납니다
  • 다음 사람은 그게 임시인 줄 모르고 그 위에 코드를 쌓습니다

대응

typescript
// 🔴 정보가 없는 TODO
// TODO: 리팩터링
 
// ✅ 만료일 + 조건 + 책임자 + 티켓
// TODO(2026-12-31, PROJ-1234): 레거시 주문 API가 제거되면 이 분기를 삭제한다.
// 서버팀 v3 마이그레이션 완료 시점. 담당: 프론트 플랫폼
if (order.legacyFormat) { /* ... */ }
javascript
// 만료된 TODO를 CI에서 실패시키기
'no-warning-comments': ['error', { terms: ['fixme'], location: 'anywhere' }],
// 또는 eslint-plugin-expire-todo-comments 류의 도구

임시임을 코드가 스스로 주장하게 만드세요. 주석이 아니라 CI가 말해야 합니다.


디자인 시스템 없이 시작하기

text
1년 뒤 코드베이스:
Button, PrimaryButton, BtnPrimary, ActionButton, SubmitBtn,
StyledButton, ButtonNew, ButtonV2, LegacyButton, ...

각각은 필요해서 만들어졌습니다. 기존 것을 고치면 다른 화면이 깨질까 봐 새로 만든 거죠(3편의 잘못된 추상화가 만든 결과이기도 합니다).

최소 디자인 시스템

처음부터 완벽할 필요는 없습니다. 토큰만 있어도 절반은 해결됩니다.

css
: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;
}

그리고 "새 컴포넌트를 만들기 전에 기존 것을 확인한다"는 합의. 도구보다 이 합의가 더 중요합니다.


리팩터링 예산 없음

모든 스프린트가 기능으로 꽉 차 있습니다. 기술부채 항목은 백로그 맨 아래에서 계속 밀립니다.

부채는 복리로 늘어난다

text
1주차:  기능 추가 = 1일
6개월:  기능 추가 = 2일  (우회해야 할 코드가 생김)
1년:    기능 추가 = 4일  (건드리면 안 되는 영역이 생김)
2년:    기능 추가 = 2주  (어디를 고쳐야 할지 아무도 모름)

속도를 위해 부채를 쌓는데, 그 부채가 속도를 잡아먹습니다. 그리고 어느 시점부터는 "다시 만드는 게 낫다"는 말이 나오기 시작합니다 — 그건 부채가 이자를 못 갚는 수준이 됐다는 뜻입니다.

기술부채 사분면

Martin Fowler의 분류가 유용합니다. 모든 부채가 나쁜 게 아닙니다.

신중한 (prudent)무모한 (reckless)
의도적"지금은 출시하고, 다음 분기에 정리한다""설계할 시간 없어, 그냥 짜"
비의도적"이제야 더 나은 구조를 알았다""레이어링이 뭔가요?"

의도적·신중한 부채는 정상적인 엔지니어링 판단입니다. 문제는 그게 기록되지 않고 상환 계획이 없을 때입니다.

실용적 방법 세 가지

1) 보이스카웃 규칙 — 건드리는 파일은 들어왔을 때보다 조금 낫게 두고 나간다. 별도 예산이 필요 없고 가장 지속 가능합니다.

2) 고정 비율 — 각 스프린트의 15~20%를 부채 상환에 고정 배정. 협상 대상이 아니어야 유지됩니다.

3) 기능에 끼워넣기 — "주문 화면에 필터 추가" 티켓에 "그 과정에서 주문 상태를 판별 유니온으로 정리" 를 포함. 별도 승인이 필요 없습니다.

부채를 가시화하기

markdown
<!-- docs/tech-debt.md -->
## 주문 상태 관리 (우선순위: 높음)
- **현상:** boolean 4개로 상태를 표현, 유효하지 않은 조합 12가지 가능
- **증상:** 최근 3개월 관련 버그 5건
- **비용:** 이 영역 작업 시 매번 +1일
- **상환:** 판별 유니온으로 전환, 약 3일
- **티켓:** PROJ-2210

"이 부채가 실제로 얼마를 먹고 있는가"를 숫자로 만들면 우선순위 논의가 감정에서 계산으로 바뀝니다.


코드 리뷰 안티패턴

너무 큰 PR

text
+2,847 −1,203  (파일 47개)

리뷰할 수 없는 크기입니다. 그래서 LGTM이 찍힙니다. 연구에 따르면 리뷰의 효과는 400줄을 넘으면 급격히 떨어집니다.

대응: PR 크기 제한을 CI에서 경고하고, 큰 작업은 단계별 PR로 쪼갭니다. 리팩터링과 기능 변경을 같은 PR에 섞지 마세요 — 무엇이 동작 변경이고 무엇이 구조 변경인지 구분이 안 됩니다.

스타일 논쟁

들여쓰기, 따옴표, 줄바꿈으로 코멘트가 20개 달립니다.

대응: Prettier와 ESLint로 전부 자동화하고 논쟁을 끝내세요. 기계가 정할 수 있는 것에 사람의 시간을 쓰면 안 됩니다. 그리고 자동화하면 리뷰가 진짜 봐야 할 것 — 설계, 엣지 케이스, 이름 — 에 집중됩니다.

리뷰 지연

PR이 3일씩 열려 있으면 브랜치가 낡고, 충돌이 나고, 작성자는 문맥을 잊습니다. 결국 "일단 머지하고 나중에"가 됩니다.

대응: 리뷰 SLA를 정하고(예: 영업일 기준 하루), 작은 PR을 장려합니다. 작은 PR은 빨리 리뷰되고, 빨리 리뷰되면 또 작게 쪼갤 동기가 생깁니다.

승인 도장

text
LGTM 👍

실제로 안 읽은 승인입니다. 이게 반복되면 리뷰 제도 자체가 형식이 됩니다.

대응: 리뷰어에게 무엇을 봐야 하는지 알려주세요. PR 템플릿에 "이 변경의 위험 지점", "특히 봐줬으면 하는 부분"을 적게 하면 리뷰 품질이 눈에 띄게 올라갑니다.


지식의 고립

text
"그 부분은 ○○님만 알아요"

bus factor 1 상태입니다. 그 사람이 휴가만 가도 그 영역의 작업이 멈춥니다.

대응:

  • 페어 프로그래밍이나 모브 리뷰를 핵심 영역에 적용
  • 코드 소유자를 두되 최소 2명
  • 결정의 근거를 ADR(Architecture Decision Record)로 남기기
markdown
<!-- 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편의 목록을 체크리스트로 쓰세요)

"나중에" 미루기의 구조

이 시리즈에서 반복해서 나온 문장들입니다.

text
"성능은 나중에 최적화하죠"
"접근성은 나중에 추가하죠"
"테스트는 나중에 쓰죠"
"타입은 나중에 붙이죠"

전부 같은 구조입니다. 지연된 비용이므로 지금 안 아프고, 안 아프니까 안 합니다. 그리고 나중이 되면 고치는 비용이 처음부터 하는 비용의 몇 배가 되어 있습니다.

처음부터나중에
접근성요소 선택만 바꾸면 됨DOM 구조·컴포넌트 API·포커스 흐름 전면 수정
타입함께 작성전 코드베이스에 any 제거 작업
테스트설계가 테스트 가능해짐테스트하려면 먼저 리팩터링해야 함
성능 예산회귀가 안 들어옴이미 들어온 것을 되돌려야 함

대응은 1편의 결론과 같습니다. 지연된 비용을 즉시 아프게 만드세요. CI에서 막으면 "나중에"가 불가능해집니다.


시리즈 전체 정리

9편 요약

편영역가장 큰 하나
1판단 기준지연된 비용에 측정 장치를 붙여라
2상태·데이터 흐름useEffect로 파생 상태를 계산하지 마라
3컴포넌트 설계중복이 잘못된 추상화보다 싸다
4렌더링·성능병목은 대개 리렌더가 아니다
5네트워크·실패실패 경로가 없는 코드는 미완성이다
6번들·경계측정하지 않으면 번들은 반드시 자란다
7CSS·접근성!important는 소유권이 불분명하다는 증거다
8견고함타입은 컴파일 후 사라진다
9조직코드 안티패턴은 조직에서 반복 생산된다

관통하는 네 가지 판단 축

1편에서 세운 것을 여덟 편에 걸쳐 적용했습니다.

1. 비용 곡선이 선형인가 지수인가 중복(선형) vs 잘못된 추상화(지수), 요청 하나 추가(선형) vs N+1 구조(지수). 불확실하면 선형을 택합니다.

2. 되돌릴 수 있는가 컴포넌트 내부 구현(즉시) vs URL 구조(영구). 양방향 문은 빨리 결정하고, 일방통행 문은 검증하고 결정합니다.

3. 비용이 언제 나타나는가 즉시 아픈 것은 알아서 고쳐집니다. 지연된 것 — 성능, 접근성, 부채 — 에 측정 장치를 붙여 즉시 아프게 만듭니다.

4. 왜 이렇게 됐는지 설명할 수 있는가 설명할 수 있으면 고칠 수 있습니다. 코드 주석, ADR, 그리고 이해하고 머지하는 문화.


지금 바로 할 수 있는 것 — 우선순위 순

하루면 끝나고, 앞으로 들어올 안티패턴의 상당수를 막습니다.

1일차 — 측정 장치 (1편)

text
□ 번들 크기 예산을 CI에 추가 (size-limit)
□ eslint-plugin-jsx-a11y 활성화
□ TypeScript strict + noUncheckedIndexedAccess
□ @typescript-eslint/no-explicit-any: error
□ dependency-cruiser로 순환 참조 차단

1주차 — 안전망

text
□ 에러 바운더리 3층 (전역/라우트/위젯)
□ window error + unhandledrejection 리스너
□ Web Vitals 실사용자 수집 (RUM)
□ 번들 애널라이저 한 번 돌려보기
□ API 경계에 스키마 검증 (Zod)

1개월차 — 구조

text
□ 디자인 토큰 (간격·색·타이포·z-index)
□ 서버 상태와 클라이언트 상태 분리
□ 가장 큰 God Component 하나 쪼개기
□ 기술부채 목록 문서화 + 티켓화
□ 리팩터링 시간 비율 합의

자가진단 체크리스트

각 항목에 "예"로 답할 수 없으면 해당 편을 다시 보세요.

text
□ 우리 앱의 실사용자 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가 빨개지고, 타입이 컴파일을 거부하면, 그 안티패턴은 애초에 들어오지 못합니다.


시리즈 전체 목차

  1. 안티패턴을 보는 눈 — 비용 곡선과 되돌리기 비용
  2. 상태와 데이터 흐름 — useEffect는 왜 계속 오용되는가
  3. 컴포넌트 설계 — 성급한 추상화가 중복보다 비싼 이유
  4. 렌더링과 성능 — 병목은 대개 리렌더가 아니다
  5. 네트워크와 실패 경로 — 정상 경로만 짜고 배포하기
  6. 번들과 경계 — 트리셰이킹은 왜 실패하는가
  7. CSS와 접근성 — 조용히 실패하는 것들
  8. 견고함 — 거짓 안정감을 만드는 것들
  9. 조직과 프로세스 — 안티패턴을 생산하는 공장 (이 글)
COMMENTS (…)

댓글을 불러오는 중이에요.

NEW COMMENT0 / 1000
⌘↵ 전송

blog92@web:~$ cd ..