# 아키텍처 개요

이 문서는 **작업을 시작하기 전에 전체 그림을 잡는 용도**다. 읽는 사람은 둘이다 — 이 앱을 이어서 만드는 AI, 그리고 나중에 인수받는 개발자.

규칙이 **무엇인지**는 `CLAUDE.md`와 `.claude/rules/`가 갖고 있다. 여기에는 **왜 그렇게 정했고 무엇을 포기했는지**만 적는다. 같은 규칙을 두 곳에 적지 않는다.

## 시스템 한눈에

- **무엇**: 브라우저에서만 도는 업무용 화면(대시보드·목록·폼). 서버 없이 정적 파일로 배포된다.
- **스택**: Vite + React 19 + React Router(CSR) · TypeScript · Tailwind v4 + shadcn/ui · TanStack Query(+Table) · React Hook Form + Zod · nuqs · Zustand · overlay-kit · ky + orval · React Compiler
- **구조**: Feature-Sliced Design — `app → pages → widgets → features → entities → shared` (위에서 아래로만 참조). 레이어별 배치 규칙은 각 `src/<레이어>/CLAUDE.md`에 있다.

## 핵심 설계 결정

### 1. 서버 렌더링 없이 CSR로 간다

- **결정**: Vite + React Router로 정적 빌드. Next.js·SSR 미도입.
- **이유**: 대상이 로그인 뒤에 쓰는 사내 업무 화면이라 SEO·첫 페인트 최적화의 값이 낮고, 서버가 없어야 비개발자가 배포까지 혼자 간다.
- **포기한 것**: 검색 노출, 서버 사이드 데이터 페칭. 공개 마케팅 페이지가 필요해지면 이 결정부터 다시 연다.

### 2. 슬라이스 공개 API(`index.ts`)를 쓴다

- **결정**: 슬라이스 바깥에서는 `pages/foo` 같은 입구 하나로만 import. 내부 파일 직접 참조 금지.
- **이유**: 입구가 하나면 내부를 마음대로 재배치해도 바깥이 안 깨지고, steiger·knip·oxlint fsd 세 게이트가 이 전제 위에서 맞물린다.
- **포기한 것**: 배럴을 통한 간접 참조 비용(트리셰이킹 손해). 화면 단위 `lazy` 분할로 상쇄한다.
- **기각안**: 배럴을 아예 금지하고 린트로만 경계를 지키는 방식(규모가 큰 사내 앱의 선택). 실측해 보니 규칙을 피해 상대경로로 우회하는 위반이 남았고, 우리 게이트 조합과도 맞지 않았다.

### 3. 게이트는 앱 밖에 둔다

- **결정**: 품질 검사는 린터·스크립트·훅·CI에만. 앱 런타임에 감시 코드를 심지 않는다.
- **이유**: 사용자에게 배포되는 번들에 우리 편의를 위한 코드가 섞이면 안 된다.
- **포기한 것**: 런타임에서만 잡히는 문제(무한 렌더 등)를 코드로 못 잡는다 → 대신 브라우저 바깥에서 Playwright 응답성 프로브로 검사한다.

### 4. 경고를 실패로 취급한다

- **결정**: `oxlint --max-warnings 0`. 경고 등급을 두지 않는다.
- **이유**: 사람 팀은 경고를 보고 판단하지만, AI 루프에서 경고는 그냥 지나간다. 지나갈 수 있는 신호는 없는 신호와 같다.
- **포기한 것**: "일단 두고 나중에" 같은 유연함. 룰이 자주 걸리면 코드가 아니라 룰 설정을 재검토한다.

### 5. 전체검증은 코드가 이 컴퓨터를 떠날 때만

- **결정**: 화면을 만드는 사이클에서는 최소관문(`pnpm preview`)만, 전체 게이트(`pnpm verify`)는 배포·푸시 직전에만 돌린다.
- **이유**: 매 사이클 전체검증을 돌리면 화면 한 장에 10~30분이 깜깜해진다(실사용에서 가장 큰 불만).
- **포기한 것**: 중간 상태의 완전성. 배포를 지나지 않은 코드에는 위반이 남아 있을 수 있다 — 실제로 실사용에서 관측됐다.

### 6. 화면 상태의 기본 위치는 URL이다

- **결정**: 검색어·필터·정렬·페이지·탭은 `useState`가 아니라 nuqs(주소 표시줄).
- **이유**: 새로고침·뒤로가기·링크 공유에서 살아남는다. "화면 링크 = 화면 상태".
- **포기한 것**: 주소가 길어진다. 대신 직렬화·파생 로직은 `model/`의 순수 모듈로 빼서 React 없이 테스트한다(골든: `pages/sample-table/model/postSearchParams.ts`).

### 7. API 타입은 손으로 쓰지 않는다

- **결정**: 서버 OpenAPI 스펙에서 orval로 클라이언트·타입을 생성하고, `_generated/`는 직접 고치지 않는다.
- **이유**: 스펙이 자주 바뀌는데 수기 타입은 조용히 어긋난다.
- **포기한 것**: 생성 코드의 자유로운 수정. 급해도 코드젠으로만 갱신한다.

### 8. 코드에 설명 주석을 남기지 않는다

- **결정**: 기본 무주석. 도구가 요구하는 사유(`oxlint-disable`·`@ts-expect-error`·`@public`·`TODO`)만 남긴다.
- **이유**: 설명 주석은 코드와 함께 낡아 거짓이 되고, 골든샘플의 주석은 복제되어 앱 전체로 퍼진다. "왜"의 거처는 이 문서와 `.claude/rules/`·`.claude/wiki/incidents.md`다.
- **기각안**: JSDoc으로 의도·함정을 남기는 방식. 지식은 남지만 갱신 책임이 분산돼 드리프트가 생긴다.

### 9. 디자인은 토큰 한 곳에서만 바꾼다

- **결정**: 색·간격·radius는 `src/app/index.css` 토큰이 단일 출처. 화면에서 색을 직접 쓰지 않는다.
- **이유**: "전체적으로 더 밝게" 한마디가 모든 화면에 반영되려면 출처가 하나여야 한다.
- **포기한 것**: 화면별 예외 스타일의 편의성.

## 관련 문서

- 작업 규칙·루프: `CLAUDE.md`
- 레이어별 배치: `src/<레이어>/CLAUDE.md`
- 판단이 필요한 술어: `.claude/rules/`
- 절차·템플릿: `.claude/skills/`
- 과거에 밟은 지뢰: `.claude/wiki/incidents.md`
