서버 사이드 로깅 및 모니터링
서버 컴포넌트·Route Handler·Proxy에 구조화 로그를 남기고 Vercel과 APM에서 오류·지연을 관찰합니다.
Next.js 애플리케이션은 브라우저(클라이언트)와 Node.js 환경(서버)에서 함께 실행됩니다.
앞 절에서 클라이언트 디버깅을 다뤘다면, 이번 절은 서버 사이드 로깅(Server-side Logging)과 모니터링(Monitoring)에 초점을 맞춥니다.
특히 서버 컴포넌트, Route Handler, Proxy에서 발생하는 문제를 어떻게 진단하고 해결할지 실전 관점에서 정리합니다.
서버 사이드 로깅의 중요성
클라이언트 측 오류는 브라우저 개발자 도구 콘솔에서 쉽게 확인할 수 있지만, 서버 측에서 발생하는 오류나 예외는 사용자에게 직접적으로 노출되지 않을 수 있습니다.
서버 사이드 로깅은 다음과 같은 이유로 매우 중요합니다.
- 오류 및 예외 추적: 서버에서 발생하는 런타임 오류, 데이터베이스 연결 문제, 외부 API 호출 실패 등 예측 불가능한 문제를 기록하여 신속하게 진단하고 수정할 수 있습니다.
- 성능 병목 현상 식별: 특정 API 요청의 응답 시간, 데이터베이스 쿼리 시간 등을 로깅하여 성능 저하의 원인을 파악할 수 있습니다.
- 사용자 행동 분석: 사용자의 요청 패턴, 특정 기능 사용 빈도 등을 로깅하여 애플리케이션 개선을 위한 통찰력을 얻을 수 있습니다.
- 보안 감사: 비정상적인 접근 시도, 인증 실패 등을 기록하여 보안 위협을 감지하고 대응할 수 있습니다.
- 디버깅 용이성: 프로덕션 요청을 멈추는 디버깅은 서비스에 영향을 줄 수 있습니다. 로그를 메트릭·트레이스·배포 이력과 함께 사용해 원인을 좁힙니다.
Next.js에서 서버 사이드 로깅 구현
아래 예제는 Node.js 런타임을 전제로 합니다. Edge Runtime을 선택한 Route Handler는 사용 가능한 API와 로거 지원 범위를 별도로 확인해야 합니다.
기본 console.log 활용
가장 간단한 방법은 console.log, console.error, console.warn 등을 사용하는 것입니다.
-
위치
- Route Handler:
app디렉터리의route.ts에 정의한 HTTP 메서드 함수. - Server Components:
app디렉토리 내의 서버 컴포넌트. - Server Actions:
actions파일 내의 서버 액션 함수. - Proxy:
proxy.ts파일. - 데이터베이스 연결 파일:
lib/db.ts등.
- Route Handler:
-
확인 방법
- 로컬 환경: 개발 서버의 터미널에 출력됩니다. 빌드 중 사전 렌더에서 실행된 로그는 빌드 출력에, 요청 처리 로그는 실행 중인 서버에 남습니다.
- Vercel 배포 환경: Vercel 대시보드의 특정 배포에 대한 Logs 탭에서 확인할 수 있습니다.
-
예시
app/api/books/route.ts (Route Handler) import { NextResponse } from 'next/server'; import connectToDatabase from '@/lib/db'; import Book from '@/models/Book'; export async function GET() { try { console.log('GET /api/books 요청 수신'); // 서버 측 로그 await connectToDatabase(); const books = await Book.find({}); console.log(`총 ${books.length}권의 책을 찾았습니다.`); // 서버 측 로그 return NextResponse.json(books); } catch (error) { console.error('API /api/books 처리 중 오류 발생:', error); // 서버 측 오류 로그 return NextResponse.json({ error: 'Failed to fetch books' }, { status: 500 }); } }다음 코드는 조회·로그 부분만 발췌했습니다.
notFound()호출과 JSX 반환은 생략되어 있어 완성된 페이지 예제가 아닙니다. 잘못된 ID의 조회 오류 처리도 별도로 필요합니다.app/books/[id]/page.tsx (Server Component) import connectToDatabase from '@/lib/db'; import Book, { IBook } from '@/models/Book'; interface BookDetailPageProps { params: Promise<{ id: string }>; } export default async function BookDetailPage({ params }: BookDetailPageProps) { const { id } = await params; console.log(`도서 상세 페이지 로드: ID ${id}`); // 서버 측 로그 await connectToDatabase(); const book: IBook | null = await Book.findById(id).lean(); if (!book) { console.warn(`ID ${id}에 해당하는 책을 찾을 수 없습니다.`); // 서버 측 경고 로그 // notFound(); } // ... }
로깅 라이브러리 활용 (Winston, Pino 등)
레벨 필터링·일관된 포맷·전송 제어가 필요하다면 로깅 라이브러리를 검토합니다. 구조화된 값을 표준 출력에 남기고 플랫폼에서 수집하는 방식으로 요구를 충족할 수도 있습니다.
-
장점
- 로그 레벨:
debug,info,warn,error등 다양한 로그 레벨을 지원하여 중요도에 따라 로그를 필터링할 수 있습니다. - 로그 포맷: JSON, 텍스트 등 다양한 포맷으로 로그를 출력할 수 있어 로그 분석 도구와 연동하기 용이합니다.
- 전송: 파일, 데이터베이스, 외부 로깅 서비스(Datadog, Sentry, CloudWatch 등)로 로그를 전송할 수 있습니다.
- 컨텍스트: 요청 ID 등 컨텍스트를 child logger나 명시적 필드로 전달할 수 있습니다. 라이브러리를 설치하는 것만으로 요청 간 연결 정보가 자동 생성·전파되지는 않습니다.
- 로그 레벨:
-
예시 (Winston)
설치:
npm install winstonlib/logger.ts생성lib/logger.ts import { createLogger, format, transports } from 'winston'; const { combine, timestamp, printf, colorize, align } = format; const logFormat = printf(({ level, message, timestamp, stack }) => { return `${timestamp} ${level}: ${message}${stack ? `\n${stack}` : ''}`; }); const logger = createLogger({ level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', // 프로덕션에서는 info 이상, 개발에서는 debug 이상 format: combine( timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), logFormat, // 프로덕션에서는 JSON 포맷 권장 // process.env.NODE_ENV === 'production' ? format.json() : colorize({ all: true }) ), transports: [ new transports.Console(), // 콘솔에 출력 // 프로덕션에서는 파일 또는 외부 서비스로 전송 // new transports.File({ filename: 'error.log', level: 'error' }), // new transports.File({ filename: 'combined.log' }), ], }); export default logger;사용app/api/books/route.ts import { NextResponse } from 'next/server'; import connectToDatabase from '@/lib/db'; import Book from '@/models/Book'; import logger from '@/lib/logger'; // 로거 임포트 export async function GET() { try { logger.info('GET /api/books 요청 수신'); await connectToDatabase(); const books = await Book.find({}); logger.debug(`총 ${books.length}권의 책을 찾았습니다.`); return NextResponse.json(books); } catch (error: unknown) { const cause = error instanceof Error ? error : new Error(String(error)); logger.error({ message: `API /api/books 처리 중 오류 발생: ${cause.message}`, stack: cause.stack, }); return NextResponse.json({ error: 'Failed to fetch books' }, { status: 500 }); } }
현재 코드의 출력 설정과 추가 구현이 필요한 운영 관측을 구분한다.
| 항목 | 현재 코드 | 범위 |
|---|---|---|
| 레벨 | 운영: info · 개발: debug | 운영에서는 책 개수의 logger.debug가 출력 대상에서 제외됩니다. |
| 형식 | timestamp + printf | 텍스트 출력입니다. 주석 처리된 JSON 설정은 적용되지 않습니다. |
| 오류 | 명시한 message와 stack | 스택이 있으면 formatter가 덧붙입니다. 실제 오류를 실행해 관측한 결과는 아닙니다. |
| 전송 | Console transport | 파일·외부 서비스 transport는 주석 상태입니다. 장기 보관을 구현하지 않습니다. |
| 연결 정보 | 요청 ID·구간별 소요 시간 없음 | 요청 추적이나 지연 집계에는 별도의 필드·계측 구현이 필요합니다. |
- 레벨
- 현재 코드: 운영:
info· 개발:debug범위: 운영에서는 책 개수의logger.debug가 출력 대상에서 제외됩니다. - 형식
- 현재 코드:
timestamp+printf범위: 텍스트 출력입니다. 주석 처리된 JSON 설정은 적용되지 않습니다. - 오류
- 현재 코드: 명시한
message와stack범위: 스택이 있으면 formatter가 덧붙입니다. 실제 오류를 실행해 관측한 결과는 아닙니다. - 전송
- 현재 코드:
Consoletransport범위: 파일·외부 서비스 transport는 주석 상태입니다. 장기 보관을 구현하지 않습니다. - 연결 정보
- 현재 코드: 요청 ID·구간별 소요 시간 없음범위: 요청 추적이나 지연 집계에는 별도의 필드·계측 구현이 필요합니다.
애플리케이션 모니터링
로깅은 발생한 이벤트와 오류를 기록하는 것이지만, 모니터링은 이러한 로그와 메트릭(metric)을 수집, 시각화하고, 시스템의 상태와 성능을 지속적으로 감시하는 활동입니다.
Vercel 대시보드 및 Logs
Vercel은 Next.js 애플리케이션 배포에 최적화되어 있으며, 기본 로깅 및 모니터링 기능을 제공합니다.
-
Logs 탭
- 용도: 요청 처리 중 서버 함수와 Proxy에서 발생한 로그를 확인합니다. 빌드 때 실행된 Server Component의 로그는 해당 빌드 로그에서 찾습니다. 정적으로 제공되거나 캐시된 결과는 요청마다 같은 서버 코드를 다시 실행하지 않을 수 있습니다.
- 활용: 프로덕션 환경에서 오류가 발생했을 때 가장 먼저 확인해야 할 곳입니다. 필터링, 검색 기능을 통해 특정 요청이나 시간대의 로그를 쉽게 찾을 수 있습니다.
-
Usage 탭
- 용도: 배포된 애플리케이션의 트래픽, 함수 호출 횟수, 데이터 전송량 등 사용량 통계를 제공합니다.
- 활용: 애플리케이션의 인기도나 부하를 파악하고, 비용 예측에 활용할 수 있습니다.
-
Speed Insights
- 용도: 실제 사용자 데이터(Real User Monitoring, RUM)를 기반으로 Core Web Vitals를 포함한 페이지 성능 지표를 모니터링합니다.
- 활용: 개발 환경에서의 테스트를 넘어, 실제 사용자들이 어떤 성능을 경험하는지 객관적인 데이터를 제공하여 성능 개선의 우선순위를 정하는 데 도움을 줍니다.
외부 APM 도구 연동
대규모 애플리케이션이나 복잡한 인프라를 가진 경우, Vercel 기본 모니터링 외에 전문 APM 도구 (Application Performance Monitoring)를 연동하는 것이 좋습니다.
-
Sentry
- 용도: 실시간 오류 추적 및 성능 모니터링 도구입니다. 클라이언트 측 JavaScript 오류부터 서버 측 Node.js 오류까지 통합적으로 관리할 수 있습니다.
- 장점: 상세한 스택 트레이스, 사용자 컨텍스트, 발생 빈도, 영향도 등을 제공하여 오류 진단 및 우선순위 결정에 매우 유용합니다. Next.js와의 통합이 용이합니다.
- 연동 방법: Sentry SDK를 설치하고, Next.js 설정 파일(
next.config.js)에 Sentry 설정을 추가합니다. 클라이언트 측과 서버 측 모두에서 오류를 캡처하도록 설정합니다.
-
Datadog, New Relic, Grafana + Prometheus
- 용도: 시스템 전체의 메트릭(CPU 사용량, 메모리, 네트워크, 응답 시간 등)을 수집하고 시각화하여 대시보드를 구축하며, 알림 시스템을 통해 이상 징후를 감지합니다.
- 장점: 인프라 전체에 대한 포괄적인 가시성을 제공하여 복잡한 분산 시스템의 문제 해결에 필수적입니다.
- 연동 방법: 각 도구의 에이전트나 SDK를 서버 환경에 설치하고, Next.js 애플리케이션에서 커스텀 메트릭을 전송하도록 설정합니다.
로깅 서비스
로깅 서비스 (Log Management System)는 대량의 로그를 효율적으로 수집, 저장, 검색, 분석하기 위한 시스템입니다.
-
ELK Stack (Elasticsearch, Logstash, Kibana)
- 용도: 오픈 소스 기반의 강력한 로그 관리 솔루션입니다. Logstash로 로그를 수집하고, Elasticsearch에 저장하며, Kibana로 시각화하고 검색합니다.
- 장점: 유연하고 확장성이 뛰어나며, 커스텀 대시보드와 강력한 검색 기능을 제공합니다.
-
Datadog Logs, Splunk, Sumo Logic
- 용도: 클라우드 기반의 통합 로깅 및 분석 서비스입니다.
- 장점: 설정 및 관리가 용이하며, APM, 메트릭 모니터링과 통합되어 엔드-투-엔드 가시성을 제공합니다.
효과적인 로깅 및 모니터링 전략
- 로그 레벨 활용: 개발/디버그 시에는
debug,info레벨을 사용하고, 프로덕션에서는 목적에 맞는 임계값을 정해 로그 볼륨을 관리합니다. - 구조화된 로그: JSON과 같은 구조화된 포맷으로 로그를 기록하여 로그 분석 도구에서 쉽게 파싱하고 검색할 수 있도록 합니다.
- 컨텍스트 정보 포함: 요청 ID와 작업 종류 등 필요한 필드를 명시적으로 전달해 같은 요청을 연결합니다. 토큰·비밀번호·연결 문자열은 제외하고 사용자 식별 정보는 필요한 범위로 제한합니다. 오류 객체의 메시지와 스택에도 민감 값이 섞일 수 있습니다.
- 경고 및 알림 설정: 중요한 오류나 임계치 초과(예: CPU 사용량 급증, 에러율 증가) 시 Slack, 이메일 등으로 알림을 받도록 설정하여 문제에 즉시 대응할 수 있도록 합니다.
- 로그 보존 정책: 법적 요구사항이나 디버깅 필요성에 따라 로그 보존 기간을 설정합니다.
- 성능 영향 고려: 로깅 자체가 애플리케이션 성능에 오버헤드를 주지 않도록 주의합니다. 전송 대기열·배압과 함수 종료 시 전송 완료를 확인합니다. 비동기 호출 자체가 로그 전달 완료를 보장하지 않으며, 서버리스 로컬 파일은 영구 보관소로 가정하지 않습니다.
서버 사이드 로깅과 모니터링은 Next.js 애플리케이션의 안정성과 신뢰성을 보장하는 데 필수적인 요소입니다.
개발 초기부터 체계적인 로깅 전략을 수립하고, 적절한 모니터링 도구를 활용하여 애플리케이션의 건강 상태를 지속적으로 확인하는 것이 중요합니다.
이를 통해 잠재적인 문제를 사전에 감지하고, 발생한 문제를 신속하게 해결하여 사용자에게 끊김 없는 서비스를 제공할 수 있습니다.