perf(core): JSON 서빙 네트워크 오버헤드 최적화

실측(g7.dev) 결과 줄바꿈 제거·gzip 은 이미 적용된 상태였고,
남은 실질 개선 3종을 구현:

- 모든 API 응답에 JSON_UNESCAPED_UNICODE 적용 — 한글이 \uXXXX(6B)로
 팽창하던 것을 raw UTF-8(3B)로 직렬화 (lang pack gzip -9.1%)
- 공개 레이아웃 서빙에서 개발자용 comment/_comment 재귀 제거,
 편집 모드(with_source_meta)는 보존 (layout gzip -34.9%)
- 번들 JS/CSS(BinaryFileResponse) gzip 압축 사각지대 해소 —
 기존 미들웨어가 getContent==false 로 압축을 건너뛰던 문제 (-73~77%)
This commit is contained in:
HeuJung committed 2026-07-05 22:31:04 +09:00
1 parent a958443d69
commit c819820573
11 files changed
+638 -145

No files matched your search

+21
View File
@@ -587,6 +587,27 @@ class PublicLayoutController extends PublicBaseController
---
## JSON 인코딩 옵션 (JSON_UNESCAPED_UNICODE)
모든 `response()->json()` 호출은 `ResponseHelper::JSON_ENCODE_OPTIONS`(= `JSON_UNESCAPED_UNICODE`)를 인코딩 옵션으로 전달한다. 한글 등 멀티바이트 문자를 `\uXXXX`(6바이트)로 이스케이프하지 않고 raw UTF-8(한글 3바이트)로 직렬화하여 응답 전송 크기를 줄인다. 다국어 팩(lang) 응답처럼 한글 비중이 큰 페이로드에서 절감 효과가 크다.
```php
// ✅ 올바른 사용 — 네 번째 인자로 옵션 전달
return response()->json($payload, $status, [], ResponseHelper::JSON_ENCODE_OPTIONS);
// 헤더 배열이 필요한 경우 (세 번째 인자)
return response()->json($payload, $status, ['Cache-Control' => '...'], ResponseHelper::JSON_ENCODE_OPTIONS);
// ❌ 옵션 누락 — 한글이 \uXXXX 로 팽창
return response()->json($payload, $status);
```
`ResponseHelper` 의 모든 메서드(`success`/`error`/`validationError`/… )와 `BaseApiController::successWithCache`/`cachedJsonResponse` 는 이미 이 옵션을 적용한다. 컨트롤러/서비스는 `ResponseHelper` 를 사용하면 자동으로 적용되므로 별도 조치가 불필요하다. `response()->json()` 을 직접 호출하는 신규 코드에서만 옵션 전달을 잊지 않는다.
> 슬래시 언이스케이프(`JSON_UNESCAPED_SLASHES`)는 적용하지 않는다 — endpoint 경로의 `\/` 는 크기 영향이 미미하고, 기존 응답 본문 정확 매칭 테스트의 안전성을 우선한다.
---
## 관련 문서
- [index.md](index.md) - 백엔드 가이드 인덱스
+10
View File
@@ -435,6 +435,16 @@ php artisan template:cache-clear # 전체 번들 파일 정리 포함
> 개별 에셋 서빙 라우트(`/api/{type}/assets/...`, `*.map` 포함)는 소스맵·static 참조를 위해 존치한다.
### 전송 압축 (gzip)
번들 JS/CSS 는 `fileResponse()`(= `response()->file()` → `BinaryFileResponse`)로 서빙되며, `GzipEncodeResponse` 미들웨어가 gzip 압축을 적용한다. `BinaryFileResponse` 는 `getContent()` 가 `false` 를 반환하므로, 미들웨어는 파일 경로(`getFile()->getPathname()`)에서 본문을 읽어 압축한 뒤 헤더(Content-Type/ETag/Cache-Control)를 승계한 일반 `Response` 로 치환한다.
- 1KB 미만 번들(예: 빈 CSS)은 `MIN_COMPRESS_SIZE` 가드로 압축 생략.
- `Accept-Encoding: gzip` 미포함 요청, 이미 `Content-Encoding` 이 있는 응답, 304 응답은 압축 대상에서 제외.
- 회귀 테스트: `tests/Feature/Middleware/GzipEncodeResponseTest.php` (BinaryFileResponse 압축/헤더 승계/소용량 skip).
> `BinaryFileResponse` 를 압축 대상에 포함하지 않으면 번들이 비압축 전송되는 사각지대가 생긴다(모듈/플러그인 번들은 크기가 커 압축 이득이 특히 크다).
---
## 에셋 로딩 전략
+8 -1
View File
@@ -554,7 +554,7 @@ Laravel의 `FormRequest::validated()` 메서드는 `rules()`에 정의된 필드
| `id` | string | ✅ | 컴포넌트 고유 ID |
| `type` | string | ✅ | 컴포넌트 타입 (basic/composite/layout) |
| `name` | string | ✅ | 컴포넌트 이름 (components.json에 등록된 이름) |
| `comment` | string | ✅ | 시안 추적 코드 및 역할 설명 (예: "F-002: 상품명 검색 필드") |
| `comment` | string | ✅ | 시안 추적 코드 및 역할 설명 (예: "F-002: 상품명 검색 필드"). 공개 서빙 응답에서는 제거됨 (아래 참조) |
| `props` | object | ❌ | 컴포넌트에 전달할 props |
| `text` | string | ❌ | 텍스트 콘텐츠 (최우선 렌더링) |
| `children` | array | ❌ | 자식 컴포넌트 배열 (레이아웃/집합 컴포넌트에서 사용) |
@@ -575,6 +575,13 @@ Laravel의 `FormRequest::validated()` 메서드는 `rules()`에 정의된 필드
> - `actions`: [layout-json-features.md](layout-json-features.md#액션-시스템-actions)
> - `isolatedState`: [아래 섹션](#격리된-상태-isolatedstate)
### `comment` / `_comment` — 공개 서빙 응답에서 제거
`comment` / `_comment` 는 편집기·개발자용 설명 주석이며 런타임 렌더러는 참조하지 않는다. 공개 레이아웃 서빙(`GET /api/layouts/{template}/{layout}.json`) 응답에서는 전송 크기 절감을 위해 이 두 키가 노드 트리 전체에서 재귀적으로 제거된다(`LayoutService::stripDeveloperComments`).
- 편집 모드 서빙(`?with_source_meta=1`, 편집 권한 필요)에서는 편집기가 `comment` 를 편집 가능한 속성으로 노출하므로 **보존**된다. 공개/편집 응답은 별도 캐시 키로 분리되어 있어 조건부 제거가 안전하다.
- 따라서 `comment` 는 편집기·저장본에서는 유지되고, 일반 사용자에게 서빙되는 응답에서만 사라진다. 레이아웃 로직이 `comment` 값에 의존해서는 안 된다.
---
## 격리된 상태 (isolatedState)