# 관리자 라우트 지연 로딩 PRD ## 문서 정보 | 항목 | 내용 | |---|---| | 문서 상태 | 구현 완료 | | 작성일 | 2026-08-06 | | 최종 수정일 | 2026-08-06 | | 대상 제품 | AI 캐릭터 관리자 웹 | | 작성자·결정권자 | Codex 작성, 사용자 결정 | | 관련 API Contract | 불필요 — API와 payload 변경 없음 | | 관련 구현 계획 | [plan-task.md](./plan-task.md) | | 관련 review | [Phase 1 관리자 라우트 지연 로딩 리뷰](./reviews/phase1-admin-route-lazy-loading.md) | ### 요구사항 상태 | 상태 | 의미 | 구현 처리 | |---|---|---| | 확정 | 제품·기술 결정이 완료된 구현 기준 | `plan-task.md`의 Task와 완료 증거로 추적 | | 미결 | 추가 결정 필요 | 구현 전 결정 | | 외부 의존 | 프론트엔드 밖의 제공 필요 | 제공 전 관련 구현 중단 | | 권고 | 확정 전 추천안 | 수용 기준으로 사용하지 않음 | | 제외 | 이번 범위에서 구현하지 않음 | 포함 조건을 Decision Log에 기록 | ### 문서 우선순위와 갱신 순서 1. 초기 bundle과 라우트 로딩 결정은 이 PRD가 소유한다. 2. API 변경이 없으므로 별도 API Contract를 만들지 않는다. 3. 구현 순서와 완료 증거는 `plan-task.md`가 소유한다. 4. 결정이 바뀌면 Decision Log → 요구사항 → 계획 순서로 갱신한다. ## 1. Overview 보호된 관리자 페이지를 `React.lazy()` 기반 동적 import로 분리한다. 최초 접속에는 현재 라우트에 필요한 코드만 내려받고, 다른 페이지 코드는 해당 라우트에 처음 진입할 때 로드한다. 기능·API·권한·데이터 흐름은 유지한다. ## 2. Problem Statement 현재 `protected-admin-shell.tsx`는 14개 보호 페이지를 정적으로 import한다. - production build가 310개 module을 하나의 `598.78kB` minified JS chunk로 출력한다. - Vite 8.1.5 기본 기준 `500kB`를 넘어 build마다 chunk size warning이 발생한다. - gzip 전송량은 `158.94kB`지만 browser가 최초 접속에 전체 chunk를 다운로드·파싱·실행한다. - 이미지 cropper처럼 현재 라우트에서 사용하지 않는 기능도 초기 module graph에 포함된다. 문제를 해결했다는 판단은 production build가 보호 페이지를 여러 chunk로 분리하고 모든 JS chunk가 `500,000 bytes` 이하이며, 기존 사용자 흐름이 그대로 통과할 때로 한다. 구현 완료 build는 37개 JS chunk, 최대 JS `315.09kB`, chunk size warning 0건이다. ## 3. Goals ### 3.1 제품 목표 - 최초 접속에서 현재 관리자 화면에 필요하지 않은 페이지 코드를 지연 로드한다. - Vite의 `500kB` 초과 chunk warning을 실제 code splitting으로 제거한다. - 직접 URL, 내부 이동, 뒤로 가기와 권한 검사를 기존과 동일하게 유지한다. ### 3.2 UX 목표 - 첫 화면의 다운로드·파싱·실행 부담을 줄인다. - 미로드 라우트 최초 진입에는 명확한 loading 상태를 표시한다. - loading 중 keyboard focus, 관리자 shell과 현재 URL을 유지한다. ## 4. Non-Goals - `chunkSizeWarningLimit` 상향으로 경고만 숨기기 - `manualChunks` 또는 vendor chunk 설정 추가 - `react-advanced-cropper`, `zod`, React Query 교체·제거 - router library, bundle 분석 library 또는 새 runtime dependency 추가 - API, 인증·권한, route path, page props와 상태 관리 변경 - 서버 rendering, prefetch, service worker cache 또는 offline 지원 추가 Non-Goal을 변경하려면 Decision Log와 `plan-task.md`를 먼저 갱신한다. ## 5. Target Users and Permissions | 사용자 | 목표 | 주요 작업 | 사용 환경 | |---|---|---|---| | ADMIN | 관리자 화면에 빠르게 진입 | 캐릭터·오디오·시리즈·커뮤니티·FanTalk 관리 | desktop, tablet, mobile | | 인증되지 않은 사용자 | 보호 코드 노출 없이 로그인 | 로그인, 인증 후 관리자 진입 | desktop, tablet, mobile | - 기존 ADMIN probe, 401 session 제거와 403 접근 거부 정책을 유지한다. - lazy page loading은 권한 검사 성공 뒤에만 보호 UI를 표시한다. ## 6. 핵심 사용자 흐름 1. 사용자가 로그인 또는 보호된 직접 URL로 접속한다. 2. 기존 인증·ADMIN probe가 완료된다. 3. 현재 라우트 page chunk가 없으면 관리자 shell 안에 loading 상태를 표시한다. 4. chunk가 로드되면 기존 page를 같은 props와 URL로 표시한다. 5. 다른 메뉴에 처음 진입하면 해당 page chunk만 추가로 받고, 이후 browser cache를 재사용한다. 6. 내부 이동·뒤로 가기·새로고침과 mutation 흐름은 기존과 동일하게 동작한다. ## 7. 정보 구조와 라우팅 ```text /login # eager 유지 /access-denied # eager 유지 /ai-characters # protected page lazy /ai-characters/new # protected page lazy /ai-characters/:characterId/** # protected page lazy ``` - `App`, 인증 provider와 `ProtectedAdminShell`은 application shell로 유지한다. - `ProtectedAdminShell`이 현재 판정하는 모든 보호 page component만 lazy boundary로 이동한다. - route path parser와 URL 상태는 변경하지 않는다. ## 8. 기능 요구사항 ### 8.1 Code splitting | ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 | |---|---|---|---|---| | `ARL-001` | 확정 | `ProtectedAdminShell`의 보호 page 정적 import를 `React.lazy()` 동적 import로 전환한다. | 14개 page component가 현재 route에서 render될 때 해당 module을 import한다. | contract 불필요, `P1-T1` | | `ARL-002` | 확정 | named export를 유지하며 page component의 public props를 변경하지 않는다. | page export·호출부 type과 기존 test가 변경 없이 통과한다. | contract 불필요, `P1-T1` | | `ARL-003` | 확정 | 보호 page 영역을 `Suspense`로 감싸 loading 상태를 표시한다. | 미로드 page 진입 시 `화면을 불러오는 중` status가 관리자 shell 안에 표시된다. | contract 불필요, `P1-T1` | | `ARL-004` | 확정 | production build의 모든 minified JS chunk를 `500,000 bytes` 이하로 유지한다. | production graph test가 JS chunk 2개 이상과 최대 chunk `<=500,000 bytes`를 확인하고 Vite 경고가 없다. | contract 불필요, `P1-T1`, `P1-GATE` | ### 8.2 기능 보존 | ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 | |---|---|---|---|---| | `ARL-005` | 확정 | 로그인, ADMIN probe, 401·403와 malformed route 처리를 유지한다. | 기존 App auth·protected 오류 test가 모두 통과한다. | 기존 인증 계약 유지, `P1-GATE` | | `ARL-006` | 확정 | 직접 URL, 내부 이동, 뒤로 가기와 route별 page props를 유지한다. | 기존 App route test와 mock Chromium E2E가 모두 통과한다. | 기존 route contract 유지, `P1-GATE` | | `ARL-007` | 확정 | 각 page의 조회·생성·수정·삭제, upload와 댓글 동작을 변경하지 않는다. | 전체 unit과 mock Chromium E2E에서 신규 실패가 0건이다. | 기존 domain 계약 유지, `P1-GATE` | | `ARL-008` | 확정 | mock module은 production bundle에서 계속 제외한다. | 기존 `production-graph.test.ts`의 mock 제외 assertion이 통과한다. | 기존 production boundary 유지, `P1-T1` | ## 9. 반응형 기능 범위 | 기능 | Desktop | Tablet | Mobile | 비고 | |---|---:|---:|---:|---| | 보호 page lazy loading | 지원 | 지원 | 지원 | 동일 route boundary | | loading 상태 | 지원 | 지원 | 지원 | 관리자 main 안에 표시 | | 직접 URL·뒤로 가기 | 유지 | 유지 | 유지 | URL 변경 없음 | - 320px와 200% zoom에서 loading 상태와 page가 수평 overflow를 만들지 않아야 한다. - 기존 모바일 조회·수정 capability 정책은 변경하지 않는다. ## 10. UI/UX Expectations ### 10.1 디자인과 component 원칙 - 기존 `PageState`를 loading fallback으로 재사용한다. - 관리자 shell, navigation, header와 success notification은 page chunk loading 중 유지한다. - 새 spinner, skeleton, animation 또는 styling을 추가하지 않는다. ### 10.2 화면 상태 - lazy page가 준비되지 않았을 때 `화면을 불러오는 중`을 표시한다. - page가 준비되면 같은 main 영역에서 기존 page로 교체한다. - 기존 API loading·empty·error·success 상태는 page 내부 책임으로 유지한다. ### 10.3 접근성 - fallback은 기존 `PageState`의 semantic status를 사용한다. - keyboard focus 순서, skip link와 route 전환 focus 정책을 변경하지 않는다. - 200% zoom과 axe critical·serious 0건을 유지한다. ## 11. API 계약 ### 11.1 공통 규칙 - lazy loading은 module 전달 방식만 변경한다. - endpoint, method, payload, response, 오류와 pagination 계약을 변경하지 않는다. ### 11.2 Endpoint 추적 | 요구사항 | Method | Path | 계약 상태 | API Contract | 소유 Goal | |---|---|---|---|---|---| | `ARL-001~008` | 해당 없음 | 해당 없음 | 변경 불필요 | 기존 domain 계약 유지 | `P1-T1`, `P1-GATE` | ### 11.3 외부 제공 대기 계약 없음. ## 12. 보안과 데이터 취급 - 인증 token 저장·전달, 401 clear와 403 route 정책을 변경하지 않는다. - 보호 page chunk는 기존과 같은 정적 asset이므로 권한 경계를 대체하지 않는다. - log, analytics와 외부 전송을 추가하지 않는다. - production mock 제외 경계를 유지한다. ## 13. 성능과 품질 요구사항 - 기준 build: Vite 8.1.5, 단일 JS `598.78kB`, gzip `158.94kB`, 310 modules. - 완료 build: JS chunk 2개 이상, 각 minified JS `<=500,000 bytes`, chunk size warning 0건. - `chunkSizeWarningLimit` 기본값 `500`을 변경하지 않는다. - 새 dependency와 custom chunk configuration을 추가하지 않는다. - production graph test, App unit, 전체 unit, mock Chromium E2E, typecheck, lint와 production build를 Gate로 사용한다. ## 14. 성공 기준 ### 14.1 기능 수용 기준 - [x] 모든 보호 page가 직접 URL과 내부 이동에서 기존 기능을 제공한다. (`ARL-001~003`, `ARL-005~007`) - [x] 인증·권한·API request와 page props가 변경되지 않는다. (`ARL-002`, `ARL-005~008`) ### 14.2 UI/UX 수용 기준 - [x] 미로드 route에 기존 `PageState` loading 상태가 표시된다. - [x] 320px·200% zoom·keyboard 흐름과 axe critical·serious 0건을 유지한다. - [x] 첫 route 이후 다른 route 최초 진입만 추가 chunk loading을 수행한다. ### 14.3 성능·추적성 완료 기준 - [x] production build에 `500kB` 초과 chunk warning이 없다. (`ARL-004`) - [x] 모든 JS chunk가 `500,000 bytes` 이하임을 자동 test로 검증한다. - [x] `ARL-001~008`이 `P1-T1` 또는 `P1-GATE` 완료 증거로 연결된다. - [x] API Contract가 불필요함을 기록했다. ## 15. Open Questions 없음. route-level `React.lazy()`를 선택했고 경고 임계값 상향과 수동 vendor 분리는 제외했다. ## 16. 요구사항 추적표 | 요구사항 범위 | API Contract | 계획 Phase | Goal | 자동 검증 | 수동 검증 | |---|---|---:|---|---|---| | `ARL-001~004`, `ARL-008` | 불필요 | 1 | `P1-T1` | production graph, App focused unit, production build | Network의 route chunk loading | | `ARL-005~007` | 기존 계약 유지 | 1 | `P1-GATE` | 전체 unit, mock Chromium E2E, typecheck, lint | 직접 URL·내부 이동·뒤로 가기·320px·200% zoom | | `ARL-006~007` CJK zoom 회귀 | 기존 계약 유지 | 1 | `P1-R1` | CJK E2E, ResourcePagination unit, mock mobile Chrome E2E | 320px·200% zoom 한국어 줄바꿈 | ## 17. Decision Log | 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계약·Goal | |---|---|---|---|---|---| | 2026-08-06 | `ARL-DEC-001` | 확정 | 보호된 관리자 page를 route-level `React.lazy()`로 분리한다. | 단일 chunk의 원인이 모든 보호 page 정적 import이며 실제 초기 loading 비용도 줄일 수 있다. | `ARL-001~008`, `P1-T1`, `P1-GATE` | | 2026-08-06 | `ARL-DEC-002` | 확정 | `chunkSizeWarningLimit` 상향과 `manualChunks`는 적용하지 않는다. | 경고만 숨기거나 초기 총량을 유지하는 방식 대신 실제 지연 loading을 선택한다. | `ARL-004`, Non-Goals | | 2026-08-06 | `ARL-DEC-003` | 확정 | 기존 `PageState`만 fallback으로 재사용하고 새 loading component를 만들지 않는다. | 현재 디자인·접근성 관례를 유지하는 최소 구현이다. | `ARL-003`, `P1-T1` | | 2026-08-06 | `ARL-DEC-004` | 확정 | 공통 `ResourcePagination`의 mobile movement controls는 동일 폭 2열 대신 1열 stacked 배치로 대체한다. | 320px·200% zoom에서 한국어 버튼 텍스트가 음절 단위 세로 열로 분리되는 것을 막고, desktop/tablet 배치는 기존 `sm:flex`로 유지한다. | `ARL-006~007`, `P1-R1`, `P1-GATE` |