Next.js 15 미들웨어 로깅 — Edge Runtime 에서 요청 추적하기

@JavaPark · March 10, 2023 · 11 min read

안녕하세요, 자바파커입니다.

운영 중인 Next.js 앱에서 "어떤 요청이 어디서 느려지는지" 감이 잡히지 않을 때가 있죠. console.log 는 Vercel 대시보드 어딘가에 흩어지고, 서버 액션과 Route Handler 가 섞이면 요청 흐름을 한눈에 보기 힘듭니다.

결론부터 말씀드리면 — Next.js 15 에서는 루트의 middleware.ts 하나면 모든 요청 앞단에 로깅 훅을 꽂을 수 있습니다. Edge Runtime 제약만 이해하면 요청 ID 주입, 응답 헤더 태깅, 외부 로그 서비스 전송까지 깔끔하게 통합됩니다.


1. 2023년 글을 2026년에 다시 쓰는 이유

3년 전 이 글의 초판은 server.js 에서 Express 미들웨어로 로그를 찍는 구조였습니다. 당시에는 그게 정석이었지만 지금은 상황이 완전히 달라졌습니다.

구분 Pages Router (구) App Router (Next.js 15)
로깅 진입점 server.js + Express middleware.ts (루트)
실행 환경 Node.js 서버 Edge Runtime 기본
파일 위치 pages/_middleware.ts (폐기) 프로젝트 루트
배포 커스텀 서버 필요 Vercel 서버리스/Edge 자동
관찰 도구 파일 로그, PM2 Vercel Log Drains, Sentry, Datadog

pages/_middleware.tsNext.js 12.2 에서 deprecated 되었고, 커스텀 server.js 를 쓰면 Vercel 의 Edge 최적화를 포기하게 됩니다. 그래서 이제는 루트 middleware.ts 가 표준입니다.


2. 기본 middleware.ts 뼈대

프로젝트 루트 (= app/ 과 같은 레벨) 에 middleware.ts 파일 하나만 만들면 됩니다.

// middleware.ts
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"

export function middleware(request: NextRequest) {
  const start = Date.now()
  const { pathname, search } = request.nextUrl
  const method = request.method

  console.log(`[req] ${method} ${pathname}${search}`)

  const response = NextResponse.next()
  response.headers.set("x-response-time", `${Date.now() - start}ms`)
  return response
}

export const config = {
  matcher: [
    // _next 정적 리소스와 favicon 은 제외
    "/((?!_next/static|_next/image|favicon.ico).*)",
  ],
}

핵심 포인트 세 가지입니다.

  1. NextRequest / NextResponse — Web 표준 Request 의 확장체입니다. request.nextUrl 로 파싱된 URL 에 접근합니다.
  2. matcher — 정규식 기반 경로 필터. 정적 자산을 빼야 로그가 폭주하지 않습니다.
  3. Edge Runtime 기본 — 별도 설정 없이 전역 엣지에서 실행됩니다.

3. Edge Runtime 제약 — 먼저 알고 가야 합니다

미들웨어는 기본적으로 Edge Runtime 에서 돌아갑니다. Node.js API 를 그대로 쓸 수 없다는 뜻입니다.

동작 Edge Runtime
console.log 사용 가능 (Vercel 로그로 수집)
fs, path, net 불가
process.env 빌드 타임에 인라인된 값만
fetch 사용 가능 (Web 표준)
Node Buffer 제한적
실행 시간 응답 전송 전까지 수 ms 이내 권장

즉, 파일 시스템에 로그를 쓰거나 Winston 같은 Node 전용 로거를 바로 불러오면 빌드가 깨집니다. 외부 전송은 fetch 로 해야 합니다.

정 Node API 가 필요하면 export const runtime = 'nodejs' 를 선언해 Node 런타임으로 전환할 수 있습니다. 단, 콜드 스타트와 비용이 늘어나니 되도록 Edge 를 유지하세요.


4. 실전 패턴 1 — 요청 ID 주입

분산 추적을 하려면 요청마다 고유 ID 가 있어야 합니다. 미들웨어에서 생성해 요청 헤더와 응답 헤더에 동시에 심어 주면 Route Handler, Server Component, 클라이언트 모두 같은 ID 를 볼 수 있습니다.

// middleware.ts
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"

export function middleware(request: NextRequest) {
  const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID()

  // 다운스트림에서 읽을 수 있도록 요청 헤더에 주입
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set("x-request-id", requestId)

  const response = NextResponse.next({
    request: { headers: requestHeaders },
  })

  // 클라이언트가 디버깅용으로 볼 수 있도록 응답 헤더에도 노출
  response.headers.set("x-request-id", requestId)

  console.log(
    JSON.stringify({
      level: "info",
      msg: "request",
      id: requestId,
      method: request.method,
      path: request.nextUrl.pathname,
      ua: request.headers.get("user-agent"),
      ip: request.headers.get("x-forwarded-for"),
    })
  )

  return response
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
}

Server Component 에서는 next/headersheaders() 로 같은 ID 를 읽습니다.

// app/any/page.tsx
import { headers } from "next/headers"

export default async function Page() {
  const h = await headers()
  const requestId = h.get("x-request-id")
  // ... 로그 남길 때 이 ID 를 같이 전송
}

포인트는 JSON 한 줄로 로그를 찍는 것 입니다. Vercel Log Drains, Datadog, Logflare 모두 구조화된 JSON 을 그대로 파싱합니다.


5. 실전 패턴 2 — Vercel Log Drains 연동

Vercel 에 배포하면 console.log 는 자동으로 Runtime Logs 에 적재됩니다. 대시보드의 Observability → Logs 에서 실시간 조회가 가능하고, Pro 플랜 이상에서는 Log Drains 로 외부 저장소(S3, Datadog, Axiom, BetterStack 등) 에 스트리밍할 수 있습니다.

설정 자체는 코드가 아니라 Vercel 대시보드에서 합니다.

  1. Project → Settings → Log Drains
  2. Destination 선택 (Datadog / Axiom / Custom Webhook)
  3. 필터 조건 설정 (미들웨어만, 특정 경로만 등)

애플리케이션 코드에서 할 일은 JSON 로그를 일관된 스키마로 찍는 것 뿐입니다. 위 4번 섹션의 JSON 포맷을 그대로 쓰면 Log Drains 에서 그대로 인덱싱됩니다.


6. 실전 패턴 3 — Sentry 로 에러 추적

Sentry 는 2025년부터 Next.js 15 App Router 와 Edge Runtime 을 공식 지원합니다. 설치는 한 번입니다.

npx @sentry/wizard@latest -i nextjs

마법사가 sentry.edge.config.ts, sentry.server.config.ts, sentry.client.config.ts 세 파일을 생성합니다. 미들웨어에서 예외를 Sentry 로 보내려면 다음 패턴을 씁니다.

// middleware.ts
import * as Sentry from "@sentry/nextjs"
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"

export async function middleware(request: NextRequest) {
  try {
    // ... 인증/리다이렉트 등 로직
    return NextResponse.next()
  } catch (error) {
    Sentry.captureException(error, {
      tags: { layer: "middleware" },
      extra: { path: request.nextUrl.pathname },
    })
    return NextResponse.json({ error: "internal" }, { status: 500 })
  }
}

sentry.edge.config.ts 덕분에 Edge Runtime 에서도 정상 작동합니다. 4번 섹션의 요청 ID 를 Sentry tag 로 함께 보내면 로그-트레이스 상관관계 분석이 됩니다.


7. 흔한 실수 체크리스트

  • 정적 자산까지 로깅matcher 누락으로 _next/static/* 가 전부 찍혀 로그 요금 폭탄
  • 동기 setTimeout 등으로 응답 지연 — 미들웨어는 모든 요청의 앞단 입니다. 외부 전송은 waitUntil 대신 fetchawait 하지 말고 fire-and-forget 패턴으로
  • 환경변수 런타임 변경 기대 — Edge 에서는 빌드 시점에 인라인됩니다. 런타임 변경은 Node 런타임에서만
  • pages/_middleware.ts 혼용 — 완전히 제거된 API 입니다. 루트 middleware.ts 하나만

FAQ

Q1. Edge Runtime 에서 console.log 가 로컬 터미널에 안 보이면?

로컬 next dev 는 터미널에 그대로 출력됩니다. 만약 보이지 않는다면 matcher 에서 해당 경로가 제외됐거나, 빌드 캐시 이슈일 확률이 높습니다. .next 폴더 삭제 후 재시작해 보세요. 배포 환경에서는 Vercel 대시보드의 Runtime Logs 를 확인해야 합니다.

Q2. Sentry 말고 Datadog APM 을 쓰고 싶어요.

Datadog 은 서버 런타임용 Node 라이브러리(dd-trace) 와 Edge 용 로그 수집을 구분합니다. 미들웨어(Edge) 에서는 Vercel Log Drains → Datadog Logs 경로를 쓰고, Route Handler 에서 export const runtime = 'nodejs' 를 선언한 구간만 dd-trace 로 APM 계측하는 분리 전략이 실무에서 가장 깔끔합니다.

Q3. 미들웨어 로깅이 성능에 영향을 주지 않나요?

console.log 한 줄과 헤더 주입 정도는 1ms 미만 입니다. 다만 외부 API 호출을 await 하면 전체 응답이 그만큼 지연됩니다. 외부 전송은 Log Drains 같은 비동기 경로를 쓰고, 미들웨어는 로컬 버퍼링만 담당하게 설계하세요.


관련 포스팅


참고 자료


여기까지가 2026년 현재 Next.js 15 에서 미들웨어 로깅을 실무에 적용하는 최소 구성입니다. 여러분은 어떤 로그 스택을 쓰고 계신가요? Axiom, BetterStack, Datadog 중 궁합이 좋았던 조합이 있다면 댓글로 공유해 주세요. 궁금한 점이나 막히는 부분도 편하게 남겨 주시면 같이 풀어 보겠습니다.

@JavaPark
AI 시대의 개발자 도구, 실전 경험을 공유합니다