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

Navigation

  • Intro
  • Blog
  • Life

연락하기

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

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

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
에듀›중앙 관리자 플랫폼 — 여러 도메인을 한 허브에서›8단계

8단계

E2E 매니페스트 + 배포

0회 조회

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 (후속 강좌)

← 7단계

백업 자동화 — pg_dump + cron

🎉 중앙 관리자 플랫폼 — 여러 도메인을 한 허브에서 완주를 축하해요

이어서 어떤 걸 배워 볼까요?

다음: 로컬 LLM · pgvector · RAG 챗봇 만들기 →전체 강좌 둘러보기