Files
sam-react-prod/claudedocs/vercel/vercel-deployment-setup.md
유병철 55e0791e16 refactor(WEB): Server Action 공통화 및 보안 강화
- executeServerAction 공통 유틸 도입으로 actions.ts 대폭 간소화 (50+개 파일)
- sanitize 유틸 추가 (XSS 방지)
- middleware CSP 헤더 추가 및 Open Redirect 방지
- 프록시 라우트 로깅 개발환경 한정으로 변경
- 프로덕션 불필요 console.log 제거

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 16:14:06 +09:00

424 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Vercel 배포 프론트엔드 설정 내역
> 작성일: 2026-02-09
---
## 1. Puppeteer 패키지 교체
### 변경 내용
| 항목 | Before | After |
|------|--------|-------|
| 패키지 | `puppeteer` | `puppeteer-core` + `@sparticuz/chromium` |
### 왜 교체해야 하는가
`puppeteer`는 설치 시 **Chromium 브라우저 전체(~170MB)**를 함께 다운로드한다. 이는 로컬/Docker 환경에서는 문제없지만, Vercel 서버리스 함수는 **패키지 크기 제한(50MB 압축, 250MB 비압축)**이 있어 배포 자체가 불가능하다.
### 로컬 vs Vercel 차이
| 환경 | Chromium 제공 방식 | 설정 |
|------|-------------------|------|
| **로컬 (macOS)** | 사용자 PC에 설치된 Google Chrome 사용 | `PUPPETEER_EXECUTABLE_PATH` 환경변수로 경로 지정 |
| **Vercel (서버리스)** | `@sparticuz/chromium`이 AWS Lambda용 경량 Chromium 제공 | `chromium.executablePath()`로 자동 경로 획득 |
### 분기 처리 코드 (`src/app/api/pdf/generate/route.ts`)
```typescript
const isVercel = process.env.VERCEL === '1'; // Vercel이 자동 주입하는 환경변수
const browser = await puppeteer.launch({
args: isVercel ? chromium.args : ['--no-sandbox', ...],
executablePath: isVercel
? await chromium.executablePath() // Vercel: @sparticuz/chromium 경량 바이너리
: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/google-chrome-stable', // 로컬: 시스템 Chrome
headless: true,
});
```
- `process.env.VERCEL === '1'`: Vercel 배포 환경에서 자동으로 설정되는 값
- `chromium.args`: 서버리스 환경에 최적화된 Chromium 실행 인자 (싱글 프로세스, SwiftShader 등)
### 로컬 환경변수 설정 (`.env.local`)
```
PUPPETEER_EXECUTABLE_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
```
기존 `puppeteer`는 Chromium을 자체 번들했으므로 경로 지정이 불필요했지만, `puppeteer-core`는 브라우저를 포함하지 않으므로 로컬에서도 Chrome 경로를 명시해야 한다.
---
## 2. next.config.ts 변경
### serverExternalPackages
| Before | After |
|--------|-------|
| `['puppeteer']` | `['puppeteer-core', '@sparticuz/chromium']` |
Webpack이 이 패키지들을 번들에 포함하지 않고 Node.js 런타임에서 직접 로드하도록 지정. `@sparticuz/chromium`은 바이너리 파일을 포함하고 있어 번들링하면 깨진다.
### TypeScript / ESLint 빌드 검사
| 항목 | Before | After | 이유 |
|------|--------|-------|------|
| `typescript.ignoreBuildErrors` | `true` | `false` | Vercel 배포 시 타입 에러가 런타임 버그로 이어질 수 있으므로 빌드 단계에서 차단 |
| `eslint.ignoreDuringBuilds` | `true` | `true` (유지) | 기존 미사용 import 에러 791개 존재, 점진적 해결 예정 |
---
## 3. vercel.json 생성
```json
{
"regions": ["icn1"],
"functions": {
"src/app/api/pdf/generate/route.ts": {
"memory": 1024,
"maxDuration": 30
}
}
}
```
| 설정 | 값 | 이유 |
|------|-----|------|
| `regions` | `icn1` (서울) | 사용자가 한국에 위치, 백엔드 API도 한국 서버 |
| `memory` | 1024MB | Chromium 브라우저 실행에 최소 512MB 이상 필요, PDF 렌더링 안정성 확보 |
| `maxDuration` | 30초 | 복잡한 문서의 PDF 변환 시 기본 10초로는 부족 |
---
## 4. 환경변수 가이드
Vercel Dashboard에 등록해야 할 환경변수 목록은 `claudedocs/vercel/vercel-env-setup-guide.md` 참조.
핵심 포인트:
- `API_KEY`: Sensitive 체크 필수, `NEXT_PUBLIC_` 접두사 절대 금지
- `PUPPETEER_EXECUTABLE_PATH`: Vercel에서는 설정 불필요 (`@sparticuz/chromium`이 처리)
- `VERCEL=1`: Vercel이 자동 주입, 별도 설정 불필요
---
## 5. TypeScript 에러 수정
`ignoreBuildErrors: false`로 변경하면서 기존에 숨겨져 있던 TS 에러 16개+ 수정.
| 에러 유형 | 파일 수 | 원인 |
|-----------|--------|------|
| `WorkOrderItem` 프로퍼티 누락 | 5개 파일 | `orderNodeId`, `orderNodeName` 필드 추가 후 목업 데이터 미갱신 |
| `WorkOrder` 프로퍼티 누락 | 3개 파일 | `shutterCount` 필드 추가 후 합성 객체 미갱신 |
| `unknown``ReactNode` | 1개 파일 | `Record<string, unknown>` 값을 JSX에 직접 사용 |
| `object` 프로퍼티 접근 | 2개 파일 | `Object.entries()` 후 타입 narrowing 부족 |
| 타입 미export | 1개 파일 | `BomCalculationResult` import만 하고 re-export 안 함 |
| implicit `any` | 1개 파일 | 콜백 파라미터 타입 어노테이션 누락 |
---
## 변경 파일 전체 목록
```
수정:
src/app/api/pdf/generate/route.ts # Puppeteer 교체
next.config.ts # 빌드 설정
package.json / package-lock.json # 패키지 교체
.env.local # Chrome 경로 추가
.env.example # Chrome 경로 가이드 추가
신규:
vercel.json # Vercel 배포 설정
claudedocs/vercel/vercel-env-setup-guide.md # 환경변수 가이드
claudedocs/vercel/vercel-deployment-setup.md # 이 문서
TS 에러 수정 (13개 파일):
src/app/[locale]/(protected)/quality/qms/components/InspectionModalV2.tsx
src/app/[locale]/(protected)/quality/qms/mockData.ts
src/components/material/ReceivingManagement/actions.ts
src/components/orders/OrderSalesDetailView.tsx
src/components/production/WorkerScreen/index.tsx
src/components/production/WorkerScreen/WorkLogModal.tsx
src/components/production/WorkOrders/documents/InspectionReportModal.tsx
src/components/production/WorkOrders/WorkOrderDetail.tsx
src/components/production/WorkOrders/WorkOrderEdit.tsx
src/components/quotes/LocationDetailPanel.tsx
src/components/quotes/QuoteSummaryPanel.tsx
src/components/quotes/QuotePreviewContent.tsx
src/components/quotes/actions.ts
```
---
## 6. Vercel 비용 주의사항
### 비용 발생 구조
Vercel은 **서버리스 함수 호출 횟수 + 실행 시간 + 대역폭**으로 과금된다. 우리 프로젝트에서 비용이 튈 수 있는 요소를 분석한다.
### 6-1. API Proxy 패턴 (가장 큰 비용 요소)
현재 구조: `클라이언트 → /api/proxy/* (Vercel 서버리스) → PHP 백엔드`
| 문제 | 설명 |
|------|------|
| **이중 대역폭** | 모든 API 응답이 Vercel 서버리스를 거쳐 클라이언트로 전달 (대역폭 2배) |
| **함수 호출 폭발** | 모든 API 요청이 서버리스 함수 1회 호출 = 페이지 로드마다 수~수십 회 |
| **실행 시간 누적** | 각 함수가 백엔드 응답을 기다리는 시간만큼 과금 |
**왜 이 패턴을 쓰는가**: HttpOnly 쿠키에 저장된 `access_token`은 JavaScript로 읽을 수 없어서, 서버(서버리스 함수)에서 쿠키를 읽어 `Authorization` 헤더로 변환해야 한다.
**예상 영향**: 사용자 수가 늘어날수록 비용이 선형 증가. ERP 특성상 한 페이지에서 3-10개 API를 호출하므로, 동시 접속자 50명이면 분당 수백~수천 회 함수 호출 가능.
### 6-1-1. 30초 폴링 — 가장 심각한 비용 요소
프로젝트에 **30초 간격 폴링이 2개** 존재하며, 이것들이 서버리스 프록시를 통과한다.
| 폴링 | 파일 | 간격 | 호출 API |
|------|------|------|---------|
| **메뉴 폴링** | `src/hooks/useMenuPolling.ts` | 30초 | `refreshMenus()``/api/proxy/*` |
| **알림(Today Issue) 폴링** | `src/layouts/AuthenticatedLayout.tsx` | 30초 | `getUnreadTodayIssues()``/api/proxy/*` |
**폴링만의 서버리스 함수 호출 추정 (동시접속 50명, 8시간 근무 기준)**:
```
메뉴 폴링: 50명 × 2회/분 × 60분 × 8시간 = 48,000회/일
알림 폴링: 50명 × 2회/분 × 60분 × 8시간 = 48,000회/일
─────────────────────────────────────────────────────────
합계: 96,000회/일 (폴링만으로, 일반 API 호출 제외)
월간: 96,000 × 22일(근무일) = 약 2,112,000회/월
```
Vercel Pro 플랜 기준 서버리스 함수 포함 실행량 100만 회/월이므로, **폴링만으로 2배 초과**. 이 폴링 2개가 Edge Middleware로 전환 시 가장 큰 비용 절감 효과를 볼 수 있다.
> 참고: 두 폴링 모두 탭 비활성 시 일시정지하는 최적화가 적용되어 있지만, 활성 탭 기준으로는 위 수치가 그대로 적용된다.
### 6-2. PDF 생성 함수
| 항목 | 값 | 비용 영향 |
|------|-----|----------|
| 메모리 | 1024MB | 기본(128MB) 대비 8배 비용 |
| 최대 실행 시간 | 30초 | 긴 실행 = 높은 과금 |
| 빈도 | 낮음 (수동 PDF 변환) | 자동 배치가 아니므로 실제 비용 영향은 적음 |
PDF는 사용 빈도가 낮아 큰 문제는 아니지만, 메모리 1024MB로 설정했으므로 호출당 비용이 높다.
### 6-3. Server Actions (bodySizeLimit: 10MB)
`next.config.ts`에서 `serverActions.bodySizeLimit: '10mb'`로 설정. 이미지 업로드 시 큰 페이로드가 서버리스 함수를 통과하므로 대역폭 비용 발생. 단, 이미지 저장 자체는 PHP 백엔드가 처리하므로 Vercel 스토리지 비용은 없음.
### 6-4. 비용 안전 요소
| 항목 | 상태 | 이유 |
|------|------|------|
| 이미지/파일 저장 | 안전 | PHP 백엔드 서버에 저장, Vercel에 저장 안 함 |
| Vercel Image Optimization | 미사용 | `remotePatterns``placehold.co`만 등록 (개발용) |
| ISR/SSG | 미사용 | 전체 Client Component, 정적 생성 없음 |
---
## 7. API Proxy 비용 절감 방안
### 현재 문제
```
클라이언트 → [Vercel 서버리스 함수] → PHP 백엔드
↑ 매 API 요청마다 호출
↑ Node.js Runtime (비쌈)
```
`/api/proxy/[...path]/route.ts`가 **모든 백엔드 API 호출을 중계**하므로 서버리스 함수 호출 횟수가 폭발적으로 증가한다.
### 방안 1: Edge Middleware Rewrite (프론트엔드 변경만으로 가능)
```
클라이언트 → [Edge Middleware] → PHP 백엔드 (직접)
↑ Edge Runtime (매우 저렴)
↑ 쿠키 읽기 가능
```
**원리**: 현재 `middleware.ts`는 이미 Edge Runtime에서 쿠키를 읽고 있다. 이를 확장하여 `/api/proxy/*` 요청을 Edge에서 백엔드로 직접 rewrite하면 서버리스 함수 호출을 제거할 수 있다.
**장점**:
- Edge 함수는 서버리스 대비 **10-100배 저렴** (실행 시간이 아닌 요청 수 기준)
- 프론트엔드만 변경하면 됨 (백엔드 수정 불필요)
- 응답 지연(latency)도 감소 (중간 서버리스 단계 제거)
**한계**:
- Edge Runtime은 Node.js API 일부를 사용 못함
- **토큰 자동 갱신(refresh)** 처리가 복잡해짐 — 현재 `authenticatedFetch`가 401 감지 → refresh → retry를 처리하는데, Edge Middleware의 rewrite에서는 이 패턴 구현이 어려움
- FormData/바이너리 응답 처리에 제약 가능
**적용 가능 범위**: 단순 GET/POST API 호출 (토큰 갱신 없이 정상 동작하는 요청)
### 방안 2: 백엔드 CORS 허용 + 직접 호출 (백엔드 변경 필요)
```
클라이언트 → PHP 백엔드 (직접)
↑ Vercel을 거치지 않음
↑ 서버리스 비용 0
```
**원리**: PHP 백엔드가 Vercel 도메인에서의 CORS를 허용하고, 쿠키를 직접 설정(`SameSite=None; Secure`)하면 프록시 자체가 불필요해진다.
**장점**:
- 서버리스 함수 호출 **완전 제거**
- 가장 큰 비용 절감 효과
- 응답 속도 최적 (중간 단계 없음)
**한계**:
- **백엔드 수정 필수**: CORS 헤더, 쿠키 SameSite 정책 변경
- **HttpOnly 쿠키 도메인 문제**: 프론트(Vercel)와 백엔드(PHP)가 다른 도메인이면 3rd-party 쿠키로 분류되어 브라우저 차단 가능
- 같은 도메인/서브도메인 구조가 아니면 추가 설정 필요 (`app.example.com``api.example.com`)
### 방안 3: 하이브리드 (권장)
| 요청 유형 | 처리 방식 | 비용 |
|-----------|----------|------|
| 단순 데이터 조회 (GET) | Edge Middleware rewrite | 매우 저렴 |
| 데이터 변경 (POST/PUT/DELETE) | 기존 서버리스 프록시 유지 | 현행 유지 |
| 파일 업로드 (multipart) | 기존 서버리스 프록시 유지 | 현행 유지 |
| 토큰 갱신 필요 시 | 기존 서버리스 프록시 유지 | 현행 유지 |
GET 요청이 전체 API 호출의 60-80%를 차지하므로, 이것만 Edge로 옮겨도 비용을 절반 이상 줄일 수 있다.
### 방안 비교 요약
| 방안 | 비용 절감 | 프론트 변경 | 백엔드 변경 | 복잡도 | 권장 |
|------|----------|------------|------------|--------|------|
| **1. Edge Rewrite** | 중 (60-80%) | O | X | 중 | 단기 |
| **2. CORS 직접 호출** | 최대 (95%+) | O | O | 고 | 장기 |
| **3. 하이브리드** | 중상 (60-80%) | O | X | 중 | **현실적 1순위** |
### 다음 단계
1. Vercel 배포 후 실제 함수 호출 횟수/비용 모니터링
2. 비용이 예상보다 높으면 방안 3(하이브리드) 우선 적용
3. 장기적으로 백엔드팀과 협의하여 방안 2(CORS) 검토
---
## 8. 월간 비용 산정 (Vercel Pro, 서울 icn1 리전)
> 산정일: 2026-02-09 / 출처: [Vercel Pricing](https://vercel.com/pricing), [Fluid Compute Pricing](https://vercel.com/docs/functions/usage-and-pricing)
### 8-1. Vercel 요금 구조 (Pro 플랜)
| 항목 | Pro 포함량 | 초과 단가 |
|------|-----------|----------|
| **기본 요금** | $20/개발자 시트/월 | — |
| **사용 크레딧** | $20/월 포함 (사용량 차감) | — |
| **Invocations** (함수 호출) | 크레딧 차감 | $0.60 / 100만 회 |
| **Active CPU** (icn1) | 크레딧 차감 | $0.169 / 시간 |
| **Provisioned Memory** (icn1) | 크레딧 차감 | $0.014 / GB-시간 |
| **Edge Requests** | 1,000만 회/월 | ~$2 / 100만 회 |
| **Bandwidth** | 1TB/월 | 초과 시 GB당 과금 |
> Fluid Compute 모델: Active CPU는 **코드 실행 중에만** 과금 (I/O 대기 시 과금 안 됨). Provisioned Memory는 **인스턴스 활성 시간** 전체 과금.
### 8-2. 트래픽 추정 전제 (실제 사업 구조 반영)
| 전제 | 값 | 비고 |
|------|-----|------|
| 개발자 시트 | **2명** | 프론트엔드 2명 |
| 서비스 회사 수 | **50개 이하** (초기) | 점진적 확장 |
| 회사당 이용자 | **~5명** | ERP 실사용자 |
| 총 등록 사용자 | **~250명** | 50사 × 5명 |
| 동시접속률 | **30~40%** | ERP 특성 (전원 동시 사용 안 함) |
| **동시접속 사용자** | **~75~100명** | 250명 × 30~40% |
| 근무 시간 | 8시간/일 | |
| 월 근무일 | 22일 | |
| 메뉴 폴링 간격 | 30초 | `useMenuPolling.ts` |
| 알림 폴링 간격 | 30초 | `AuthenticatedLayout.tsx` |
| 페이지 이동 시 API 호출 | 평균 5개/페이지 | |
| 페이지 이동 횟수 | 평균 50회/사용자/일 | |
| PDF 생성 | 100건/월 | |
### 8-3. 월간 함수 호출 횟수 (동시접속 75명 기준)
```
① 메뉴 폴링: 75명 × 2회/분 × 480분 × 22일 = 1,584,000회
② 알림 폴링: 75명 × 2회/분 × 480분 × 22일 = 1,584,000회
③ 페이지 API: 75명 × 250회/일 × 22일 = 412,500회
④ PDF 생성: = 100회
──────────────────────────────────────────────────────────────
합계: 3,580,600회/월
```
### 8-4. 시나리오 A — 현재 구조 (전부 서버리스 프록시)
모든 API 호출이 `/api/proxy/[...path]` 서버리스 함수를 경유.
**API 프록시 1회당 비용 추정 (icn1)**:
- Active CPU: ~30ms (쿠키 읽기, 헤더 구성, 응답 파싱)
- Instance alive: ~300ms (백엔드 응답 대기 포함)
- Memory: 128MB (0.128 GB)
| 항목 | 계산 | 월 비용 |
|------|------|--------|
| **Invocations** | 3,580,000 × $0.60/1M | $2.15 |
| **Active CPU** | 3,580,000 × 0.03s = 107,400s = 29.8hr × $0.169 | $5.04 |
| **Provisioned Memory** | 3,580,000 × 0.128GB × 0.3s / 3600 = 38.2 GB-hr × $0.014 | $0.53 |
| **PDF CPU** | 100 × 5s / 3600 × $0.169 | $0.02 |
| **PDF Memory** | 100 × 1GB × 15s / 3600 × $0.014 | $0.01 |
| **함수 사용량 소계** | | **$7.75** |
| **기본 요금** | 개발자 2명 × $20 | **$40.00** |
| **사용 크레딧** | -$20 (포함) | **-$20.00** |
| **예상 월 합계** | | **~$28** |
> 함수 사용량 $7.75는 $20 크레딧 이내. 초과 과금 없음.
### 8-5. 시나리오 B — 하이브리드 (폴링 + GET을 Edge로)
폴링 2개 + 일반 GET을 Edge Middleware rewrite로 전환. POST/PUT/DELETE/파일업로드만 서버리스 유지.
```
Edge로 이동: 폴링 3,168,000 + GET ~300,000 = ~3,468,000 → Edge Requests (10M 포함 이내)
서버리스 유지: POST/PUT/DELETE ~112,500 + PDF 100 = ~112,600
```
| 항목 | 계산 | 월 비용 |
|------|------|--------|
| **Edge Requests** | 3,468,000회 (10M 포함 이내) | $0 |
| **Invocations** | 112,600회 × $0.60/1M | $0.07 |
| **Active CPU** | 112,600 × 0.05s / 3600 × $0.169 | $0.26 |
| **Provisioned Memory** | 112,600 × 0.128GB × 0.4s / 3600 × $0.014 | $0.02 |
| **PDF** | (위와 동일) | $0.03 |
| **함수 사용량 소계** | | **$0.38** |
| **기본 요금** | 개발자 2명 × $20 | **$40.00** |
| **사용 크레딧** | -$20 (포함) | **-$20.00** |
| **예상 월 합계** | | **~$20** |
### 8-6. 성장 단계별 비용 비교
| 단계 | 서비스 회사 | 총 사용자 | 동시접속 | 시나리오 A | 시나리오 B |
|------|-----------|----------|---------|-----------|-----------|
| **초기** | 10사 | 50명 | ~20명 | **~$22** (함수 $2) | **~$20** (함수 $0.1) |
| **안정기** | 50사 | 250명 | ~75명 | **~$28** (함수 $8) | **~$20** (함수 $0.4) |
| **성장기** | 100사 | 500명 | ~150명 | **~$35** (함수 $15) | **~$21** (함수 $0.8) |
| **확장기** | 200사 | 1,000명 | ~300명 | **~$50** (함수 $30) | **~$22** (함수 $1.5) |
> 모든 단계에서 고정 비용 = 개발자 2시트 $40 크레딧 $20 = **$20**. 함수 비용은 이 $20 크레딧에서 차감되며, 초기~안정기에는 크레딧 이내로 해결됨.
### 8-7. 결론
- **초기~안정기 (50사 이하)**: 월 **~$22~28**. 함수 비용 $8 이하로 **크레딧 $20 이내**. 최적화 불필요.
- **성장기 (100사)**: 함수 비용 $15. 아직 크레딧 이내이지만 여유 줄어듦. 하이브리드 전환 검토 시점.
- **확장기 (200사 이상)**: 함수 비용이 크레딧 초과 → 추가 과금 발생. 하이브리드 전환 필수.
- **핵심 비용은 개발자 시트**. 프론트 2명 기준 고정 $20/월. 함수 비용은 초기에 미미.
- 폴링 2개만 Edge로 전환해도 함수 비용 **95% 절감** 가능 (100사 기준 $15 → $0.8).
---
## 배포 전 체크리스트
- [x] `puppeteer-core` + `@sparticuz/chromium` 설치
- [x] PDF 라우트 로컬/Vercel 분기 처리
- [x] `next.config.ts` 설정 변경
- [x] `vercel.json` 생성
- [x] TypeScript 에러 0개 확인
- [x] 로컬 Chrome 경로 설정 (`.env.local`)
- [ ] 로컬 PDF 변환 테스트
- [ ] Vercel Dashboard 환경변수 등록
- [ ] Vercel 배포 및 PDF 변환 테스트