229 lines
13 KiB
Markdown
229 lines
13 KiB
Markdown
# 관리자 라우트 지연 로딩 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` |
|