- executeServerAction 공통 유틸 도입으로 actions.ts 대폭 간소화 (50+개 파일) - sanitize 유틸 추가 (XSS 방지) - middleware CSP 헤더 추가 및 Open Redirect 방지 - 프록시 라우트 로깅 개발환경 한정으로 변경 - 프로덕션 불필요 console.log 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
19 KiB
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)
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 생성
{
"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순위 |
다음 단계
- Vercel 배포 후 실제 함수 호출 횟수/비용 모니터링
- 비용이 예상보다 높으면 방안 3(하이브리드) 우선 적용
- 장기적으로 백엔드팀과 협의하여 방안 2(CORS) 검토
8. 월간 비용 산정 (Vercel Pro, 서울 icn1 리전)
산정일: 2026-02-09 / 출처: Vercel Pricing, Fluid Compute 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 특성 (전원 동시 사용 안 함) |
| 동시접속 사용자 | 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 |
| (위와 동일) | $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사 이하): 월
$2228. 함수 비용 $8 이하로 크레딧 $20 이내. 최적화 불필요. - 성장기 (100사): 함수 비용 $15. 아직 크레딧 이내이지만 여유 줄어듦. 하이브리드 전환 검토 시점.
- 확장기 (200사 이상): 함수 비용이 크레딧 초과 → 추가 과금 발생. 하이브리드 전환 필수.
- 핵심 비용은 개발자 시트. 프론트 2명 기준 고정 $20/월. 함수 비용은 초기에 미미.
- 폴링 2개만 Edge로 전환해도 함수 비용 95% 절감 가능 (100사 기준 $15 → $0.8).
배포 전 체크리스트
puppeteer-core+@sparticuz/chromium설치- PDF 라우트 로컬/Vercel 분기 처리
next.config.ts설정 변경vercel.json생성- TypeScript 에러 0개 확인
- 로컬 Chrome 경로 설정 (
.env.local) - 로컬 PDF 변환 테스트
- Vercel Dashboard 환경변수 등록
- Vercel 배포 및 PDF 변환 테스트