목차
33 장

배포와 관측성 — Vercel · Cloudflare Pages · Sentry · PostHog

Next.js를 Vercel과 Cloudflare Pages에 배포, preview deploy, 환경변수, Sentry로 에러 추적, PostHog로 제품 분석. 출시 후 4주를 위한 도구 한 세트.

32장에서 인증과 세션을 다뤘습니다. 본 챕터는 5부 (운영 · 테스트 · 배포)의 마지막 챕터로, 만들어진 앱을 실제 운영 환경에 띄우고 그 이후를 운영하는 도구들을 살펴봅니다.

“배포"까지는 거의 모든 입문서가 다루지만, 배포 이후까지 다루는 자료는 의외로 적습니다. 출시 첫 4주가 가장 험난한 구간입니다. 새로운 사용자가 처음 만난 에러를 우리는 모르고, 어디서 이탈하는지도 모릅니다. 관측성(observability) 도구가 그 공백을 채웁니다. 본 챕터에서는 Vercel과 Cloudflare Pages로 배포하고, Sentry로 에러를 추적하고, PostHog로 제품 분석을 시작하는 한 사이클을 살펴봅니다.

본 챕터의 도구 세트는 다음 34장 (풀스택 Todo 캡스톤)에서 그대로 사용됩니다. 그리고 31장 (성능과 Web Vitals)의 실제 운영 RUM 데이터가 본 챕터의 PostHog와 만나게 됩니다.

호스팅 선택의 출발점 #

Next.js를 production에 띄우는 가장 보편적인 두 선택지가 VercelCloudflare Pages입니다. 둘 다 PR마다 preview deploy를 자동으로 만들고, GitHub과 통합되며, 한 commit 단위로 롤백이 가능합니다.

선택의 출발점은 다음 네 가지입니다.

  1. 트래픽의 모양 — 함수 호출이 많은 앱인가, 정적 비중이 큰 앱인가
  2. 부수 서비스 의존 — KV / D1 / Workers 같은 Cloudflare 서비스에 묶일 가치가 있는가
  3. 비용 곡선 — 무료 구간이 충분한가, 유료로 넘어갈 때의 단가
  4. 운영 편의 — Vercel은 Next.js의 본가, Cloudflare는 글로벌 edge

작은 사이드 프로젝트의 일반적인 선택은 Vercel로 시작 → 트래픽이 늘면 비용 재평가입니다. 본 챕터도 그 흐름을 따라가겠습니다.

Vercel 한 사이클 #

첫 배포 #

흐름
1. GitHub에 repo push
2. vercel.com → New Project → repo 선택
3. Framework: Next.js (자동 인식)
4. Environment Variables: AUTH_SECRET, AUTH_GITHUB_ID, AUTH_GITHUB_SECRET 등
5. Deploy 클릭

설정 거의 없이 1~2분 안에 production URL이 생깁니다. 같은 repo에 push가 들어올 때마다 새 deploy가 자동으로 만들어지고, PR마다 별도 preview URL이 생깁니다.

Preview Deploy의 가치 #

PR마다 새 URL이 생기는 게 단순한 편의가 아닙니다. 30장의 Playwright를 preview URL에서 돌리면 production 빌드 전용 버그를 잡습니다.

.github/workflows/preview-e2e.yml
name: preview-e2e

on:
  pull_request:

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: pnpm install --frozen-lockfile
      - run: pnpm exec playwright install --with-deps
      - name: Wait for Vercel preview
        uses: patrickedqvist/wait-for-vercel-preview@v1
        id: preview
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          max_timeout: 180
      - run: pnpm exec playwright test
        env:
          PREVIEW_URL: ${{ steps.preview.outputs.url }}

PR을 열면 Vercel이 preview를 만들고, GitHub Actions가 그 URL이 살아 있을 때까지 기다린 뒤 Playwright를 돌립니다. production 빌드 + 실 데이터베이스로 도는 E2E라 dev 서버에선 안 보이던 버그가 잡힙니다.

환경변수 — 환경별 분리 #

Vercel은 환경변수를 3개의 환경으로 분리합니다.

  • Production: main 브랜치 deploy
  • Preview: 모든 PR / 기타 브랜치 deploy
  • Development: 로컬 (vercel env pull로 동기화 가능)
.env.local과 vercel 환경변수의 관계
.env.local (gitignore됨, 로컬 개발만)
Vercel CLI: `vercel env pull`로 development 환경변수를 .env.local로 가져옴
Vercel 웹: Production / Preview / Development 각 환경별로 입력

AUTH_SECRET 같은 시크릿은 production과 preview에서 서로 달라야 합니다 (preview의 사고가 production에 영향 안 주도록). 단 OAuth callback URL은 preview URL이 동적으로 바뀌니, OAuth App 설정에서 wildcard 또는 별도 preview App을 따로 두는 게 일반적입니다.

가격과 함정 #

Vercel의 무료 (Hobby) 플랜은 학습 / 개인 프로젝트에 충분합니다. 다만 production을 띄우려는 시점에 다음을 확인합니다.

  • Function Invocations: Server Action / API 라우트 호출 수. 무료는 월 100k invocations.
  • 이미지 변환 (next/image): 무료는 월 5,000장. 트래픽 큰 사이트는 빠르게 한도에 닿습니다.
  • bandwidth: 무료 100GB / 월.
  • 상업적 사용: Hobby 플랜은 비상업적 한정. 수익이 발생하면 Pro 플랜으로 이동 필요.

next/image의 이미지 변환 한도가 의외의 함정입니다. CDN 캐시가 빠지는 시점에 변환이 다시 일어나면 한도가 빠르게 소진됩니다. 트래픽이 클 것 같은 사이트는 자체 이미지 호스팅 (Cloudflare R2 / Images, S3 + CloudFront)을 함께 고려하는 게 안전합니다.

Cloudflare Pages 한 사이클 #

Next.js를 Cloudflare에서 돌리기 #

Cloudflare Pages는 정적 사이트가 강점이고, Workers 기반의 동적 페이지도 지원합니다. Next.js의 RSC + Server Action을 Cloudflare에서 돌리려면 @cloudflare/next-on-pages 어댑터를 씁니다.

설치
pnpm add -D @cloudflare/next-on-pages
package.json (스크립트 추가)
{
  "scripts": {
    "build:cf": "next build && npx @cloudflare/next-on-pages",
    "preview:cf": "wrangler pages dev .vercel/output/static"
  }
}

next.config.ts에서 일부 옵션을 Cloudflare 호환으로 설정해야 합니다 (자세한 건 공식 문서를 참조).

Workers / KV / D1와의 결합 #

Cloudflare의 가치는 부수 서비스와의 자연스러운 결합입니다.

  • KV — 키-값 스토어. 세션이나 캐시 같은 단순 데이터.
  • D1 — SQLite 기반의 서버리스 DB. 가격이 매력적.
  • R2 — S3 호환 객체 스토리지. egress 비용 무료.
  • Images — 이미지 변환 + CDN. next/image의 한도를 크게 늘려 줌.

이 부수 서비스들을 같은 Workers 런타임 안에서 호출하면 latency가 극히 낮습니다. 글로벌 edge에 분산된 앱을 만들려고 한다면 Cloudflare가 유리합니다.

가격 비교 #

항목Vercel Hobby (무료)Cloudflare Pages 무료
Bandwidth100GB/월무제한
Function Invocations100k/월100k 요청/일
Build minutes6,000분/월500분/월
부수 서비스별도KV / D1 / R2 통합

수치는 정책 변경에 따라 자주 바뀌니, 출시 시점에 최신 정보를 다시 확인하는 게 안전합니다.

함정 — 도구별 파일 곱셈 #

Cloudflare Pages는 한 deploy당 파일 수 제한이 있습니다 (시점 기준 20,000개). Hugo / Next.js static export 같은 정적 사이트 생성기는 카테고리 / 태그별로 페이지를 곱셈 식으로 만들 수 있어, 글이 200개여도 실제 파일이 수만 개가 되는 경우가 있습니다. 사전 점검 없이 무료 한도를 가정하면 출시 직전에 막힙니다.

이건 schoolofweb.net 운영 중 직접 마주친 함정이기도 합니다. 정적 사이트 도구 + 카테고리 / 태그 / 시리즈 multiplier가 있는 환경에서는 빌드 후 파일 수를 미리 세 본 후 호스팅을 정합니다.

호스팅 선택 결정 트리 #

결정 트리
Next.js 풀스택 앱인가?
├── Yes
│   ├── 글로벌 edge가 필요한가? (전세계 사용자)
│   │   ├── Yes → Cloudflare Pages + Workers
│   │   └── No
│   │       ├── Vercel 한도 안에서 운영 가능한가?
│   │       │   ├── Yes → Vercel
│   │       │   └── No → Cloudflare Pages 또는 셀프 호스팅
└── No (정적 사이트)
    └── Cloudflare Pages 또는 Netlify 또는 GitHub Pages

초기엔 Vercel, 트래픽 / 비용이 문제 되면 Cloudflare로 이전이 가장 흔한 흐름입니다. 두 호스팅 모두 Next.js를 표준 입력으로 받아 가는 거라, 이전 비용이 크지 않습니다 (환경변수 / OAuth callback URL 정도).

환경변수와 시크릿 관리 #

빌드 타임 vs 런타임 #

Next.js의 환경변수는 두 시점에 평가됩니다.

  • 빌드 타임: NEXT_PUBLIC_ 접두사가 붙은 변수. 빌드 시점에 코드 안에 인라인됩니다.
  • 런타임: 그 외 변수. 서버 코드 실행 시점에 process.env로 접근.
구분
// 빌드 타임 (클라이언트로 노출됨)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;

// 런타임 (서버에서만, 클라이언트 번들에 안 포함)
const dbUrl = process.env.DATABASE_URL;
const authSecret = process.env.AUTH_SECRET;

NEXT_PUBLIC_ 접두사는 “이 값이 클라이언트 번들에 들어가도 안전하다"는 의도 선언입니다. 시크릿에는 절대 붙이지 마세요.

시크릿 노출 검증 #

production 빌드 후 .next/static/chunks/의 파일들을 grep해 시크릿이 포함되지 않았는지 한 번 확인하는 습관이 안전합니다.

시크릿 노출 검증
pnpm build
grep -r "AUTH_SECRET\|<실제 시크릿 일부>" .next/static

매치가 나오면 안 됩니다. 32장의 AUTH_SECRET이 production 번들에 흘러 들어갔다면 OAuth provider의 시크릿을 즉시 재발급하는 게 정석입니다.

Sentry — 에러 추적 #

production에서 발생한 에러를 자동으로 수집해 한곳에서 보는 도구입니다. 발생 위치, 스택 트레이스, 사용자 정보, 직전 동작 (breadcrumb)까지 함께 남깁니다.

설치와 셋업 #

Sentry 설치
pnpm add @sentry/nextjs
pnpm exec @sentry/wizard@latest -i nextjs

wizard가 자동으로 sentry.client.config.ts / sentry.server.config.ts / sentry.edge.config.ts 세 파일을 만들고, next.config.ts를 래핑합니다.

자동으로 추적되는 것 #

설치만 해도 다음이 자동으로 추적됩니다.

  • 처리되지 않은 자바스크립트 예외 (client / server 양쪽)
  • 처리되지 않은 Promise rejection
  • Server Action / API Route의 에러
  • React error boundary가 잡은 에러
명시 보고 (선택)
import * as Sentry from '@sentry/nextjs';

try {
  await riskyOperation();
} catch (err) {
  Sentry.captureException(err, { tags: { feature: 'payment' } });
  throw err;
}

태그와 컨텍스트를 함께 보내면 대시보드에서 필터링이 쉬워집니다.

Source map upload #

production 빌드는 minified라 스택 트레이스가 (anonymous):1:23456 같은 모양입니다. source map을 Sentry에 업로드하면 원본 코드 위치로 다시 매핑됩니다. wizard가 이 부분도 자동 설정합니다.

비밀 정보 자동 마스킹 #

기본으로 password, token, secret 같은 필드명을 자동으로 마스킹합니다. 다만 자체 정의 필드명이 있다면 명시적으로 추가합니다.

sentry.server.config.ts에서 비밀 필드 추가
import * as Sentry from '@sentry/nextjs';

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  beforeSend(event) {
    if (event.request?.data?.creditCard) {
      delete event.request.data.creditCard;
    }
    return event;
  },
});

beforeSend로 보내기 전에 한 번 가공할 수 있는 단계가 있습니다.

알림과 우선순위 #

Sentry 대시보드에서 같은 에러 (fingerprint 기준)가 N번 발생하면 Slack / 이메일로 알림이 가도록 설정합니다. 모든 에러에 즉시 알림은 노이즈. 다음 정도가 보통의 출발점.

  • 새로운 에러가 production에서 첫 발생 → 즉시 알림
  • 같은 에러가 시간당 100건 넘게 발생 → 즉시 알림
  • 그 외 → 일간 요약

PostHog — 제품 분석 #

Sentry가 “무엇이 잘못됐는가"를 본다면, PostHog는 “사용자가 무엇을 하는가"를 봅니다. 이탈 지점, funnel 전환율, feature 사용 빈도 같은 데이터를 모읍니다.

설치 #

PostHog 설치
pnpm add posthog-js posthog-node
src/app/posthog-provider.tsx
'use client';

import posthog from 'posthog-js';
import { PostHogProvider } from 'posthog-js/react';
import { useEffect } from 'react';

if (typeof window !== 'undefined') {
  posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
    api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
    person_profiles: 'identified_only',
  });
}

export function Providers({ children }: { children: React.ReactNode }) {
  return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
}

Autocapture와 명시 이벤트의 균형 #

PostHog의 강점 중 하나가 autocapture — 모든 클릭과 페이지뷰를 자동으로 수집합니다. 셋업만 해도 즉시 funnel 분석이 가능합니다.

다만 autocapture가 만능은 아닙니다. 사용자 인텐트가 있는 행동 (가입 완료, 결제 완료, 핵심 기능 사용)은 명시 이벤트로 따로 잡는 게 신뢰성이 높습니다.

명시 이벤트 발송
'use client';

import posthog from 'posthog-js';

function CheckoutButton() {
  function handleClick() {
    posthog.capture('checkout_completed', {
      amount: 9900,
      currency: 'KRW',
      plan: 'pro',
    });
    // ... 결제 처리
  }
  return <button onClick={handleClick}>결제</button>;
}

31장의 Web Vitals와의 결합 #

31장에서 useReportWebVitals로 수집한 Web Vitals 데이터를 PostHog로 보내면 실 사용자의 LCP / INP / CLS 분포를 대시보드에서 봅니다.

Web Vitals → PostHog
'use client';

import { useReportWebVitals } from 'next/web-vitals';
import posthog from 'posthog-js';

export function WebVitals() {
  useReportWebVitals(metric => {
    posthog.capture('web_vital', {
      name: metric.name,
      value: metric.value,
      rating: metric.rating,
    });
  });
  return null;
}

PostHog의 insights 화면에서 LCP의 p75 / p95를 시간 추이로 보면, 31장에서 본 lab 데이터와는 다른 실제 사용자 분포가 드러납니다.

Feature Flag #

PostHog의 부수 기능으로 feature flag가 있습니다. 새 기능을 일부 사용자에게만 노출하거나 A/B 테스트를 할 때 씁니다.

Feature Flag 사용
'use client';

import { useFeatureFlagEnabled } from 'posthog-js/react';

function PricingPage() {
  const newPricing = useFeatureFlagEnabled('new-pricing-page');
  return newPricing ? <NewPricing /> : <OldPricing />;
}

flag는 PostHog 대시보드에서 사용자 그룹별로 on/off 합니다. 코드 배포 없이 노출 비율을 조정할 수 있습니다.

CI 통합 — 5부 전체를 한 흐름으로 #

29~33장의 모든 도구를 묶은 CI 흐름을 한 번 그려 보겠습니다.

PR이 열렸을 때의 일
1. lint + type check (즉시)
2. Vitest 단위 / 통합 테스트 (29장)
3. Next.js build (production 빌드)
4. Vercel preview deploy (자동)
5. Playwright E2E (preview URL에서, 30장)
6. Lighthouse CI (preview URL에서, 31장)
7. PR 리뷰 + 머지
8. main으로 머지 → production deploy
9. Sentry / PostHog에 source map 자동 업로드

각 단계가 통과해야 다음으로 갑니다. 머지된 코드는 이미 production 빌드로 한 번 검증된 상태입니다.

이 흐름이 출시 후 4주를 무서워하지 않게 하는 토대입니다. 새 코드가 production에 도달하기 전에 자동 안전망을 통과하고, 도달한 이후엔 Sentry / PostHog가 무엇이 벌어지는지 알려 줍니다.

출시 후 첫 4주의 체크리스트 #

5부 전체의 마무리 격으로, 실제 출시 후 첫 4주에 자주 마주치는 항목을 정리해 두겠습니다.

  • 첫 24시간: Sentry 알림이 정상 동작하는지, 새 에러가 너무 많이 쌓이는지 모니터링. autocapture만으로 funnel이 그려지는지 PostHog에서 확인.
  • 첫 주: Web Vitals의 실 사용자 분포 확인 (lab과 다를 수 있음). LCP / INP의 p75가 “Good” 범위에 있는지.
  • 둘째 주: 가장 흔한 에러 세 개를 골라 우선 처리. 새 에러가 발생하지 않게 retry / fallback / 명확한 메시지 추가.
  • 셋째 주: 사용자 funnel 분석. 어디에서 가장 많이 이탈하는지 PostHog로 식별. 그 단계의 UX 또는 성능 개선.
  • 넷째 주: 한도 / 비용 점검. Vercel function invocations, 이미지 변환, bandwidth가 무료 한도 안에 있는지. 필요시 호스팅 / 이미지 호스팅 재평가.

직접 해보기 — 풀 사이클 한 번 돌리기 #

이 책의 예제 앱 또는 본인의 소품을 골라 다음을 한 번 끝까지 돌려 보세요.

  1. Vercel 배포: GitHub repo → Vercel import → 환경변수 입력 → production deploy.
  2. Preview deploy 확인: 새 브랜치를 만들어 한 줄 수정 후 PR을 엽니다. preview URL이 자동으로 만들어지는 것을 확인.
  3. Playwright on preview: 위의 preview-e2e.yml을 추가하고, PR을 다시 한 번 push해 CI가 preview URL에서 E2E를 도는 것을 확인.
  4. Sentry 셋업: wizard로 설치 후, 의도적으로 에러를 던지는 페이지 (throw new Error('test'))를 만들어 보세요. production에 deploy해 접속하면 Sentry 대시보드에 source map 위치로 매핑된 스택 트레이스가 도착합니다.
  5. PostHog 셋업: 위의 Providers를 추가하고, 가입 또는 핵심 동작 한 개에 명시 이벤트를 발송해 보세요. PostHog 대시보드에 funnel을 한 번 그려 봅니다.
  6. Web Vitals → PostHog: 31장의 useReportWebVitals를 PostHog로 연결해 실 사용자 분포가 흘러 들어오는 것을 확인합니다.

여섯 단계를 거치면 5부 전체의 도구가 하나의 흐름으로 자연스럽게 연결됩니다.

연습문제 #

  1. Vercel vs Cloudflare 선택. 다음 세 가지 앱에 어느 호스팅이 적합한지 답하고 이유를 적어 보세요. (a) 단일 도메인의 작은 SaaS, 트래픽 월 100k 페이지뷰 예상, 한국 사용자 위주, (b) 전세계 사용자를 노린 동영상 메타 사이트, 트래픽이 급격히 늘 가능성, (c) 정적 마크다운 블로그, 글 5000개, 카테고리 / 태그 multiplier.
  2. NEXT_PUBLIC_ vs 일반 변수. 다음 변수들 각각이 NEXT_PUBLIC_ 접두사가 붙어야 할지 답하세요. (a) Stripe publishable key, (b) Stripe secret key, (c) PostHog project API key, (d) DATABASE_URL, (e) Sentry DSN. 답을 적은 뒤 본문의 빌드 타임 vs 런타임 절을 참고합니다.
  3. 에러 vs 분석의 분담. 다음 다섯 가지 데이터를 Sentry / PostHog 중 어느 도구로 다루는 게 자연스러운지 답하세요. (a) Server Action에서 발생한 처리 안 된 에러, (b) 가입 완료 이벤트, (c) production의 Web Vitals 분포, (d) 결제 funnel 전환율, (e) 클라이언트의 JS 예외. 둘 다 가능한 항목이 있다면 어디에 두는 게 더 적합한지 한 줄 적습니다.

한 줄 요약: Next.js 풀스택은 보통 Vercel로 시작해 트래픽 / 비용에 따라 Cloudflare Pages로 이전하는 흐름이 표준이고, PR마다 만들어지는 preview deploy가 production 빌드 전용 버그를 잡는 핵심 안전망이다. NEXT_PUBLIC_ 접두사는 “이 값을 클라이언트로 노출해도 안전"의 의도 선언이고, 시크릿은 절대 붙이지 않는다. Sentry는 production의 에러를 source map과 함께 한곳에 모으고, PostHog는 autocapture와 명시 이벤트로 funnel / Web Vitals 분포를 잡는다. 29~33장의 도구를 한 흐름의 CI로 묶어 두면 출시 후 첫 4주가 무섭지 않다.

다음 챕터 #

본 챕터로 5부(운영 · 테스트 · 배포)가 완전히 마무리됩니다. 다음 34장 풀스택 Todo 앱 완성하기부터 6부(종합 실습)가 시작됩니다. 34장은 1~33장에서 만든 모든 도구를 한 작은 풀스택 앱으로 엮는 캡스톤입니다. RSC + Server Actions + 인증 + DB 영속 + 테스트 + 배포 + 관측까지, 책 전체의 흐름이 하나의 동작하는 서비스로 모이는 지점입니다.

X