8단계
E2E 매니페스트 + 배포
35 분
E2E 매니페스트 + 배포
페이지가 많아지면 "새 페이지 추가 후 smoke spec 추가를 깜빡함" 이 일상이 됩니다. 파일시스템에서 라우트를 자동 수집하는 매니페스트로 이 비용을 없앱니다.
1. 페이지 매니페스트 생성
// e2e/pages/generate-manifest.ts
import fg from 'fast-glob';
import { writeFileSync } from 'node:fs';
const files = await fg(['src/app/**/page.tsx', '!src/app/api/**']);
const routes = files.map((f) =>
'/' + f
.replace(/^src\/app\//, '')
.replace(/\/page\.tsx$/, '')
.replace(/\/\([^)]+\)/g, '')
.replace(/\/\[(\.{3})?([^\]]+)\]/g, '/:$2')
);
writeFileSync(
'e2e/pages/manifest.json',
JSON.stringify({ routes: routes.sort() }, null, 2)
);
pnpm tsx e2e/pages/generate-manifest.ts
2. API 매니페스트 생성
const files = await fg('src/app/api/**/route.ts');
const endpoints = [];
for (const file of files) {
const src = readFileSync(file, 'utf-8');
const methods = ['GET','POST','PUT','PATCH','DELETE']
.filter((m) => new RegExp(`export\\s+async\\s+function\\s+${m}\\b`).test(src));
if (!methods.length) continue;
endpoints.push({ path: '/' + file.replace(/^src\/app\//, '').replace(/\/route\.ts$/, ''), methods });
}
3. 단일 spec 으로 순회
// e2e/pages/all-pages.spec.ts
import { test, expect } from '@playwright/test';
import manifest from './manifest.json';
const TOLERATE_5XX = new Set<string>([]);
for (const route of manifest.routes) {
test(`page ${route} smoke`, async ({ page }) => {
const url = route
.replace(/:id/g, '00000000-0000-0000-0000-000000000000')
.replace(/:slug/g, 'test-slug');
const resp = await page.goto(url);
const status = resp?.status() ?? 0;
if (TOLERATE_5XX.has(route)) {
expect(status).toBeGreaterThanOrEqual(200);
return;
}
expect(status).toBeLessThan(500);
});
}
4. 쓰기 메서드 보호
// e2e/setup/smoke-helpers.ts
import { test } from '@playwright/test';
export function skipWriteOnProd() {
if (process.env.E2E_ENV === 'PROD') test.skip();
}
쓰기 테스트 첫 줄에 skipWriteOnProd(). playwright.config.ts 의 PROD 프로젝트에는 grepInvert: /@write/ 를 같이 두어 2 중 보호.
5. CI drift 검출
- name: regenerate manifests
run: |
pnpm tsx e2e/pages/generate-manifest.ts
pnpm tsx e2e/equivalence/generate-manifest.ts
git diff --exit-code e2e/**/manifest.json
매니페스트 갱신을 빠뜨린 PR 은 CI 에서 바로 실패.
6. 배포 — Docker standalone
Next.js 16 standalone 빌드로 이미지 크기 최소화.
// next.config.ts
export default { output: 'standalone' } satisfies NextConfig;
# Dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN corepack enable && pnpm build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN apk add --no-cache postgresql-client
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
postgresql-client 는 pg_dump 실행에 필요.
7. docker-compose (개발)
services:
admin:
build: .
env_file: .env.dev
ports: ["127.0.0.1:3000:3000"]
volumes:
- ./backups:/app/backups
depends_on:
- postgres-blog
- postgres-market
postgres-blog:
image: postgres:15-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: blog
volumes:
- blog-data:/var/lib/postgresql/data
ports: ["127.0.0.1:5435:5432"]
postgres-market:
image: postgres:15-alpine
# ...
127.0.0.1: prefix 로 loopback 바인딩 — 외부 인터넷에 PG 노출 방지.
8. Caddy 역프록시 (프로덕션)
admin.example.com {
reverse_proxy admin:3000
}
관리자는 VPN · IP 화이트리스트가 있으면 더 좋지만, 최소 HTTPS 는 Caddy 자동.
9. 체크리스트
- 매니페스트 스크립트가 CI 에서 재생성 → diff 없음
- 모든 쓰기 테스트에
@write태그 +skipWriteOnProd - cron 이
DISABLE_CRON=1로 개발 환경에서 꺼짐 - PG 컨테이너가
127.0.0.1:바인딩 - 월 1 회 백업 복원 리허설
하고픈 말
관리자 허브 1 개가 여러 도메인을 감당하게 되면 "이 페이지는 왜 느리지?" · "감사 누락이 어디서 났지?" 같은 질문이 한 곳에서 해결됩니다. 처음에 놓은 기반 (풀 분리 · 공용 테이블 · 감사로그 · 백업 · E2E 매니페스트) 5 개가 장기적으로 전부 이자를 붙여 돌려 줍니다.
Next
- architecture-patterns (후속 강좌)
🎉 중앙 관리자 플랫폼 — 여러 도메인을 한 허브에서 완주를 축하해요
이어서 어떤 걸 배워 볼까요?