codingstairs
노트에듀라이프연락
⌕검색⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

연락하기

로그인 없이도 보낼 수 있어요. 답변이 필요하면 이메일을 함께 적어 주세요.

  • 익명 폼으로 의견 남기기 →
  • ✉ warragon112@gmail.com
  • 카카오톡 오픈채팅 ↗

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
노트›quality

E2E — 라우트 매니페스트 자동 생성

2026-05-06 게시· 2026-05-18 갱신·0회 조회

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 를 참고합니다.

quality 카테고리의 다른 글

카테고리 전체 보기 →
  • 실전 vitest · pytest 인프라
  • GitHub Actions
  • 최소 관측 — 로그·메트릭·트레이스
  • Vitest 와 테스트의 결
  • Testcontainers