E2E — 라우트 매니페스트 자동 생성
E2E — 라우트 매니페스트 자동 생성
페이지 수십 · API 수백 개를 가진 관리자 앱의 E2E 테스트에서 가장 자주 하는 실수는 "스펙 파일에 새 라우트 추가를 잊는 것". 사람이 수작업으로 유지하지 않고, 파일 시스템에서 라우트 목록을 자동 생성해 하나의 spec 이 모든 라우트를 돌게 만드는 패턴이 이 비용을 크게 줄입니다.
1. 왜 매니페스트 자동 생성인가
- 누락 방지 — 새 페이지 · 라우트를 만들면 매니페스트 스크립트 한 번만 돌리면 전부 포함
- CI 에서 drift 검출 — 매니페스트 파일이 코드와 일치하는지 CI 가 검사 가능
- 스펙 하나로 충분 — 수백 개 라우트를 for-each 로 도는 단일 spec → 리뷰 · 유지가 쉬움
Next.js App Router 처럼 파일 시스템이 라우팅인 프레임워크와 특히 잘 맞습니다.
2. 페이지 매니페스트 — page.tsx 수집
// 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) => {
return '/' + f
.replace(/^src\/app\//, '')
.replace(/\/page\.tsx$/, '')
.replace(/\/\([^)]+\)/g, '') // (group) 제거
.replace(/\/\[(\.{3})?([^\]]+)\]/g, '/:$2') // [id] → :id
.replace(/\/_[^/]+/g, ''); // _private 제거
});
writeFileSync(
'e2e/pages/manifest.json',
JSON.stringify({ routes: routes.sort() }, null, 2)
);
출력 예:
{
"routes": [
"/admin/codingstairs/posts",
"/admin/codingstairs/posts/:id/edit",
"/admin/pryzeet/users",
"/admin/pryzeet/users/:id"
]
}
정규식은 프레임워크 라우팅 규칙에 맞춰. Next.js 의 dynamic segment [id] · catch-all [...slug] · route group (group) · private folder _* 4 종을 처리.
3. API 매니페스트 — route.ts + method 수집
// e2e/equivalence/generate-manifest.ts
import { readFileSync } from 'node:fs';
import fg from 'fast-glob';
const files = await fg('src/app/api/**/route.ts');
const endpoints: Array<{ path: string; methods: string[] }> = [];
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 === 0) continue;
const path = '/' + file
.replace(/^src\/app\//, '')
.replace(/\/route\.ts$/, '')
.replace(/\/\[(\.{3})?([^\]]+)\]/g, '/:$2');
endpoints.push({ path, methods });
}
writeFileSync(
'e2e/equivalence/manifest.json',
JSON.stringify({ endpoints }, null, 2)
);
export 된 메서드만 골라내므로 export async function GET 은 포함, 단순 util 은 제외.
4. 단일 spec 이 매니페스트 순회
// e2e/pages/all-pages.spec.ts
import { test, expect } from '@playwright/test';
import manifest from './manifest.json';
const TOLERATE_5XX = new Set([
'/admin/crawl/products', // 외부 DB 의존
'/admin/documents/edit/:id', // dummy UUID
]);
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);
await expect(page.locator('body')).toBeVisible();
});
}
for-each 로 test 를 동적 생성. Playwright 리포트에서 각 라우트가 별도 케이스로 보이므로 실패 원인 파악이 쉽습니다.
5. 쓰기 메서드 보호
전체 라우트를 순회하면서 DELETE · POST 를 실제로 쏘면 운영 DB 가 훼손됩니다. 두 층 보호.
- 스펙 태그 — 쓰기 테스트는
test.describe('... @write', ...)로 표시 - PROD 에서 자동 skip — Playwright config 의
grepInvert: /@write/를 PROD 환경일 때 활성 - 런타임 가드 — helper
skipWriteOnProd()를 쓰기 테스트 첫 줄에 호출
test('delete user @write', async ({ request }) => {
skipWriteOnProd(); // E2E_ENV === 'PROD' 면 test.skip()
// ...
});
한 층이 뚫려도 다른 층이 잡도록 중첩.
6. Drift 검출 — CI 단계
매니페스트 파일을 git 에 커밋하고, CI 에서 생성 스크립트를 다시 돌려 diff 가 있으면 fail.
- name: Regenerate E2E manifests
run: |
pnpm tsx e2e/pages/generate-manifest.ts
pnpm tsx e2e/equivalence/generate-manifest.ts
if ! git diff --exit-code -- e2e/pages/manifest.json e2e/equivalence/manifest.json; then
echo "E2E manifest drift. Run the generators locally."
exit 1
fi
이렇게 두면 "페이지 추가는 했는데 매니페스트 재생성을 잊은" 커밋이 CI 에서 걸립니다.
7. 동적 매개변수 더미 값
:id · :slug 가 섞인 URL 을 smoke test 할 때 더미 값 정책이 필요.
- UUID 자리:
00000000-0000-0000-0000-000000000000 - 정수 자리:
1 - slug 자리:
test-slug - catch-all: 빈 문자열 또는
a/b/c
더미 값이 404 나거나 500 을 유발하는 경우는 TOLERATE_5XX 에 추가하거나, 시드 데이터로 유효한 id 를 하나 준비해 :id → seed id 로 치환.
8. 자주 걸리는 자리
정규식 누락 변환 — Next.js 의 (auth) route group 을 남기고 URL 로 쏘면 404. @/ import alias 처럼 라우팅과 관계없는 경로 표기도 구분 필요.
지연 렌더 페이지 — Server Component 에서 큰 데이터를 fetch 하면 테스트 타임아웃. Playwright timeout 을 페이지별 override 하거나 시드 데이터를 작게.
인증 보호 라우트 — 모든 관리자 라우트가 401 로 리다이렉트되면 smoke 의미가 없음. playwright.config.ts 의 storageState 에 로그인 세션 쿠키를 미리 세팅.
동시 실행 간섭 — 쓰기 테스트를 병렬로 돌리면 같은 레코드를 두 테스트가 수정해 간섭. test.describe.configure({ mode: 'serial' }) 또는 도메인별 worker 분리.
매니페스트 diff 가 noise — OS · 개행 차이로 CI / 로컬 매니페스트가 어긋나면 drift 오탐. JSON indent: 2 고정 + UTF-8 LF 엔딩으로 정규화.
9. 성장 경로
- 1 단계 — 스펙 하나로 status < 500 만 smoke
- 2 단계 — 각 라우트에 대해
<h1>·<main>같은 구조 element 존재 확인 - 3 단계 — OpenAPI / typed route 정의를 생성 소스로 써서 응답 스키마까지 매니페스트화
- 4 단계 — 접근성 (axe-core) · Lighthouse 를 매니페스트 순회로 일괄 실행
처음부터 4 단계를 할 필요는 없고, 1 단계만으로도 "새 페이지 추가 시 흰 화면 회귀" 를 크게 줄일 수 있습니다.
하고픈 말
관리자 앱처럼 페이지 수가 많은 제품일수록 자동 매니페스트의 이득이 큽니다. 사람이 스펙에 추가를 잊어버리는 실수가 기본값이기 때문입니다. "매니페스트는 기계가 만든다" 를 규칙으로 고정해 두면 스펙 파일 자체도 가벼워지고 (for-each 30 줄) 리뷰 시 변경의 의도가 한눈에 보입니다.
Next
- testcontainers
- vitest-pytest-infra
Playwright Docs · fast-glob · axe-core · Next.js App Router 를 참고합니다.