서버 사이드 로깅 및 모니터링
서버 컴포넌트·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 등을 쓰는 것입니다. app 디렉터리의 route.ts에 정의한 Route Handler의 HTTP 메서드 함수, app 디렉터리 안의 서버 컴포넌트, 서버 액션 함수, proxy.ts 파일, lib/db.ts 같은 데이터베이스 연결 파일처럼 서버에서 실행되는 코드라면 어디에서나 쓸 수 있습니다.
로컬 환경에서는 이 로그가 개발 서버의 터미널에 출력됩니다. 빌드 중 사전 렌더에서 실행된 로그는 빌드 출력에, 요청을 처리하며 남긴 로그는 실행 중인 서버에 남습니다. Vercel에 배포한 환경에서는 Vercel 대시보드의 Logs 탭에서 확인할 수 있습니다. 다음은 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로 조회할 때의 오류 처리도 따로 필요합니다.
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();
}
// ...
}이 로그는 서버 컴포넌트 안에 있으므로 브라우저 콘솔이 아니라 앞의 Route Handler와 같은 서버 쪽 출력에 남습니다. Node.js에서 console.warn은 console.log와 달리 표준 오류로 출력되므로, 로그를 수집하는 도구에서 일반 로그와 다른 수준으로 분류될 수 있습니다.
로깅 라이브러리 활용
레벨 필터링, 일관된 포맷, 전송 제어가 필요하다면 Winston이나 Pino 같은 로깅 라이브러리를 검토합니다. 구조화한 값을 표준 출력에 남기고 플랫폼에서 수집하는 방식으로 충분한 경우도 있습니다. 로깅 라이브러리가 주는 장점은 다음과 같습니다.
- 로그 레벨:
debug,info,warn,error같은 여러 레벨을 지원해 중요도에 따라 로그를 거를 수 있습니다. - 로그 포맷: JSON, 텍스트 등 여러 형식으로 출력할 수 있어 로그 분석 도구와 연동하기 쉽습니다.
- 전송: 파일, 데이터베이스, 외부 로깅 서비스(Datadog, Sentry, CloudWatch 등)로 로그를 보낼 수 있습니다.
- 컨텍스트: 요청 ID 같은 정보를 child logger나 명시적인 필드로 넘길 수 있습니다. 다만 라이브러리를 설치하는 것만으로 요청 사이의 연결 정보가 자동으로 생기고 전파되지는 않습니다.
Winston을 예로 들어 봅니다. 먼저 npm install winston으로 설치하고, 로거 설정을 담은 lib/logger.ts를 만듭니다.
import { createLogger, format, transports } from 'winston';
const { combine, timestamp, printf, colorize } = 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;level은 출력할 가장 낮은 수준을 정하므로, 운영에서는 error, warn, info만, 개발에서는 debug까지 출력됩니다. printf로 만든 형식은 시각 수준: 메시지 모양의 한 줄을 만들고, stack 필드가 있으면 다음 줄에 덧붙입니다. 그다음 Route Handler에서 console 대신 이 로거를 씁니다. catch의 error는 unknown 타입이라 Error가 아닐 수도 있으므로, 아래 코드는 먼저 Error 객체로 맞춘 뒤 메시지와 스택을 꺼냅니다.
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 });
}
}Next · Winston example contract
Winston 설정과 관측의 범위
Node.js 예제는 레벨 필터와 timestamp·printf 텍스트를 Console에 출력합니다. 주석의 JSON·파일·외부 전송이나 요청 ID·소요 시간 계측은 현재 코드에 적용되지 않습니다.
| 항목 | 현재 코드 | 범위 |
|---|---|---|
| 레벨 | 운영: 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·구간별 소요 시간 없음
- 범위
- 요청 추적이나 지연 집계에는 별도의 필드·계측 구현이 필요합니다.
그림은 위 Winston 예제가 실제로 설정한 범위를 항목별로 나눕니다. 레벨은 운영에서 info, 개발에서 debug이므로 운영에서는 책 개수를 남기는 logger.debug가 출력 대상에서 빠집니다. 형식은 timestamp와 printf로 만든 텍스트이며, 주석으로 남긴 JSON 포맷과 File 전송은 실행되는 설정이 아닙니다. 오류는 명시한 message에 stack이 있으면 덧붙이고, 전송은 Console 하나뿐이라 장기 보관을 구현하지 않습니다. 요청 ID나 구간별 소요 시간도 없으므로, 요청 추적이나 지연 집계에는 별도의 필드와 계측이 필요합니다. 기본 레벨의 우선순위와 전송 설정은 Winston 저장소의 설명에서 확인할 수 있습니다.
추가 필드를 넘기는 것과 출력하는 것은 다릅니다. 위 printf는 level, message, timestamp, stack만 문자열에 넣습니다. logger에 requestId나 durationMs를 더 넘겨도 이 formatter를 그대로 쓰면 그 필드는 출력 문자열에 나타나지 않습니다. Winston의 포맷 설명처럼 formatter에서 필요한 필드를 고르거나 JSON 포맷으로 바꾸고, 실제 출력을 확인해야 합니다.
요청마다 같은 의미의 완료 기록 만들기
다음 함수는 전송기가 아니라, 기록할 객체를 만드는 작은 예제입니다. requestId는 서버가 요청마다 만든, 개인정보가 없는 불투명 식별자라고 가정합니다. 문자 검사만으로 개인정보 여부나 요청 권한을 판정할 수는 없으므로 사용자 입력을 그대로 넣지 않습니다. status는 서버가 정한 HTTP 상태 코드이고, durationMs는 같은 요청 안에서 잰 밀리초 값입니다.
export function completionRecord(input: {
requestId: string;
status: number;
durationMs: number;
}) {
const { requestId, status, durationMs } = input;
if (!/^[A-Za-z0-9_-]{1,64}$/.test(requestId)
|| !Number.isInteger(status) || status < 100 || status > 599
|| !Number.isFinite(durationMs) || durationMs < 0) return null;
return {
event: 'request.completed', requestId, status,
durationMs: Math.round(durationMs),
};
}반환 객체는 정해진 필드만 복사하므로 요청 본문, 토큰, 임의의 오류 객체를 통째로 섞지 않습니다. 결과가 null이면 잘못된 측정값으로 집계하지 않고, 호출하는 쪽에서 원인을 확인합니다. 올바른 객체를 JSON.stringify로 직렬화하면 requestId, status, durationMs가 남지만, 여기서는 Console 출력이나 외부 전송까지 구현하지는 않았습니다. 출력 포맷과 수집기에서도 같은 필드 이름, 단위, 자료형을 유지해야 합니다.
시간은 요청 안의 시작값과 종료값을 짝지어 차이를 구합니다. Node.js의 performance.now 같은 경과 시간용 시계를 쓸 수 있으며, DB 구간만 쟀다면 그 값을 전체 요청 지연이라고 부르지 않습니다. 예를 들어 A가 100에 시작해 160에 끝나면 60ms, B가 120에 시작해 200에 끝나면 80ms입니다. 가장 먼저 시작한 A와 가장 늦게 끝난 B를 이은 100ms는 어느 요청의 소요 시간도 아닙니다. 공유 전역 변수 하나에 현재 요청 ID나 시작 시간을 덮어쓰지 말고, 요청별 값으로 넘깁니다.
애플리케이션 모니터링
로깅이 발생한 이벤트와 오류를 기록하는 일이라면, 모니터링은 이런 로그와 메트릭(metric)을 수집하고 시각화해 시스템의 상태와 성능을 계속 살피는 활동입니다.
모니터링에서는 로그, 메트릭, 트레이스를 같은 사건에 연결합니다. 로그는 특정 요청의 실패를 찾고, 메트릭은 같은 시간대의 요청 수, 5xx 비율, 지연 분포를 비교하며, 트레이스는 요청 안의 DB나 외부 호출 구간이 어디서 시간을 썼는지 보여 줍니다. 예를 들어 10분 동안 요청 1,000개 가운데 5xx 응답이 20개라면 이 정의의 오류율은 2%입니다. 로그 줄 수는 요청 수와 같지 않으므로, 오류 로그 20줄을 전체 로그 50줄로 나눈 40%를 요청 오류율이라고 부르지 않습니다. 누락, 샘플링, 캐시 응답 때문에 달라지는 수집 범위와 분모도 확인합니다.
requestId만 기록했다고 여러 서비스의 트레이스가 자동으로 연결되지는 않습니다. OpenTelemetry의 로그 설명처럼 SDK나 bridge로 활성 trace와 span 문맥을 연결해야 합니다. 한 필드의 값을 맞추는 일과 실제로 계측하고 전파하는 일은 구분합니다.
Next.js의 instrumentation 파일
Next.js에서는 프로젝트 루트(또는 src 폴더)의 instrumentation.ts 파일로 관측 도구를 연결합니다. 이 파일이 내보낸 register 함수는 새 서버 인스턴스가 시작될 때 한 번 실행되므로, OpenTelemetry나 APM SDK의 초기화를 여기에 둡니다. 같은 파일에서 onRequestError를 내보내면 서버가 처리하던 요청에서 잡힌 오류를 요청 경로와 라우트 정보와 함께 관측 도구로 보낼 수 있습니다. Vercel에 배포한다면 @vercel/otel 패키지로 OpenTelemetry 설정을 간단히 시작할 수도 있습니다.
더 깊이: register와 onRequestError가 받는 정보
Next.js 문서의 instrumentation.js에 따르면 register는 비동기 함수로 만들 수 있고, 서버가 요청을 받기 전에 끝나야 합니다. 그래서 여기서 오래 걸리는 초기화를 하면 그만큼 서버가 늦게 준비됩니다. 이 파일은 Node.js 런타임과 Edge 런타임에서 모두 실행되므로, 한쪽에만 있는 SDK를 쓸 때는 process.env.NEXT_RUNTIME 값으로 나누어 런타임별 모듈을 불러옵니다.
Next.js 15에서 추가된 onRequestError(error, request, context)는 세 인자를 받습니다. request에는 쿼리 문자열을 포함한 경로(path), 메서드, 헤더가 들어 있고, context에는 라우터 종류(routerKind), /app/blog/[dynamic] 같은 라우트 파일 경로(routePath), 오류가 난 곳이 서버 컴포넌트 렌더링인지 Route Handler, 서버 액션, Proxy인지를 나타내는 routeType, 재검증 중이었는지를 알리는 revalidateReason 등이 들어 있습니다. 같은 경로에서 난 오류라도 렌더링 중이었는지 서버 액션 중이었는지에 따라 따로 모아 볼 수 있다는 뜻입니다.
이 함수에서 비동기 작업을 한다면 반드시 await해야 합니다. 또 서버 컴포넌트를 렌더링하다 난 오류는 React가 한 번 처리한 뒤 넘겨줄 수 있어 처음 던진 오류 객체와 다를 수 있으므로, 오류의 digest 속성으로 실제 오류를 가려냅니다. error.tsx가 받는 오류의 digest도 서버 로그의 같은 오류를 찾는 데 쓰는 값이므로, 관측 도구에 digest를 함께 기록해 두면 사용자가 본 오류 화면과 서버 기록을 잇기 쉽습니다. 다만 request.headers에는 쿠키나 인증 헤더도 들어 있을 수 있으므로, 외부 관측 도구로 보낼 때는 필요한 헤더만 골라 보냅니다.
Vercel 대시보드 및 Logs
Vercel은 Next.js 애플리케이션의 배포에 맞춰 기본 로깅과 모니터링 기능을 제공합니다.
Logs 탭에서는 요청을 처리하며 서버리스 함수와 Proxy에서 남긴 로그를 확인합니다. 빌드 때 실행된 서버 컴포넌트의 로그는 해당 빌드 로그에서 찾습니다. 정적으로 제공되거나 캐시된 결과는 요청마다 같은 서버 코드를 다시 실행하지 않을 수 있습니다. 프로덕션에서 오류가 생겼을 때 가장 먼저 확인할 곳이며, 필터와 검색으로 특정 요청이나 시간대의 로그를 찾을 수 있습니다.
Usage 화면은 배포한 애플리케이션의 트래픽, 함수 호출 횟수, 데이터 전송량 같은 사용량 통계를 보여 주어, 부하를 파악하고 비용을 예측하는 데 씁니다. Observability 탭에서는 함수 호출, 오류, 지속 시간 같은 지표를 시간대별로 살펴볼 수 있습니다. Speed Insights는 실제 사용자 데이터(Real User Monitoring, RUM)를 바탕으로 Core Web Vitals를 포함한 페이지 성능 지표를 모니터링합니다. 개발 환경의 테스트를 넘어 실제 사용자가 겪는 성능을 보여 주므로, 성능 개선의 우선순위를 정할 때 도움이 됩니다.
외부 APM 도구 연동
대규모 애플리케이션이나 복잡한 인프라라면 Vercel의 기본 모니터링 외에 전문 APM(Application Performance Monitoring) 도구를 연동하는 편이 좋습니다.
Sentry는 실시간 오류 추적과 성능 모니터링 도구로, 클라이언트의 JavaScript 오류부터 서버의 Node.js 오류까지 함께 관리할 수 있습니다. 자세한 스택 트레이스, 사용자 컨텍스트, 발생 빈도, 영향 범위 등을 제공해 오류를 진단하고 우선순위를 정하는 데 유용합니다. 연동할 때는 Sentry의 Next.js 설정 안내에서 쓰는 Router와 번들러에 맞는 절차를 고릅니다. SDK와 빌드 설정뿐 아니라 클라이언트와 서버의 초기화, 오류 수집 지점도 연결하고, 일부러 발생시킨 오류가 실제로 수집되는지 확인합니다. 설정 파일에 이름을 더한 것만으로 양쪽의 계측이 끝나지는 않습니다.
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 같은 형식에 일관된 필드 이름, 자료형, 단위를 정해 기록합니다. JSON 문법이 맞아도 요청마다
duration과durationMs를 섞어 쓰거나 숫자와 문자열을 섞으면 같은 의미로 집계하기 어렵습니다. - 컨텍스트 정보: 요청 ID와 작업 종류 등 필요한 필드를 명시적으로 넘겨 같은 요청의 기록을 연결합니다. 토큰, 비밀번호, 연결 문자열은 빼고, 사용자 식별 정보는 필요한 범위로 제한합니다. 오류 객체의 메시지와 스택에도 민감한 값이 섞일 수 있습니다.
- 경고와 알림: 중요한 오류나 기준치 초과(CPU 사용량 급증, 오류율 증가 등)가 생기면 Slack이나 이메일로 알림을 받도록 설정해 바로 대응합니다.
- 로그 보존 정책: 법적 요구사항과 디버깅 필요에 따라 로그의 보존 기간을 정합니다.
- 성능 영향: 로깅 자체가 애플리케이션에 부담을 주지 않도록, 전송 대기열과 배압(backpressure), 함수가 끝날 때의 전송 완료를 확인합니다. 비동기 호출 자체가 로그 전달의 완료를 보장하지 않으며, 서버리스의 로컬 파일을 영구 보관소로 가정하지 않습니다.
서버 사이드 로깅과 모니터링은 Next.js 애플리케이션을 안정적으로 운영하는 기반입니다. 개발 초기부터 기록할 필드와 레벨을 정하고, 알맞은 모니터링 도구로 애플리케이션의 상태를 계속 확인하면, 잠재적인 문제를 미리 감지하고 생긴 문제를 빠르게 해결할 수 있습니다.
정리
- 서버의 오류와 지연은 사용자에게 직접 보이지 않으므로, 실행 위치에 맞는 로그(터미널, 빌드 출력, Vercel Logs)로 확인합니다.
- 로깅 라이브러리는 레벨, 포맷, 전송을 제어하지만, formatter가 고르지 않은 필드는 출력되지 않고 요청 ID도 자동으로 생기지 않습니다.
- 요청 시간은 같은 요청 안의 시작값과 종료값으로 계산하고, 요청별 값을 전역 변수에 덮어쓰지 않습니다.
- 오류율의 분모는 요청 수이며, 로그 줄 수로 나눈 비율은 요청 오류율이 아닙니다.
instrumentation.ts의register와onRequestError로 관측 도구를 연결하고, 실제로 수집되는지 확인합니다.
연습 문제
1. 운영 설정에서 남는 로그
위 Winston 예제의 GET에서, 두 권 조회에 성공한 요청 하나와 DB 연결에서 실패한 요청 하나를 차례로 처리한다고 합시다. production에서 Console에 남는 로그 호출은 몇 개이며 각각 어느 레벨일까요? 개발 설정이라면 무엇이 더해질까요? 이 기록만으로 요청별 DB 소요 시간을 알 수 있을까요?
해설 보기
production에서는 성공 요청의 info 한 개와, 실패 요청의 info와 error 두 개가 남아 모두 세 개입니다. 성공 뒤에 부른 debug는 걸러집니다. 개발 설정은 debug도 허용하므로 성공 요청의 도서 수 기록 한 개가 더해져 모두 네 개입니다. 오류를 catch한 경우에는 error의 message와 stack을 printf 포맷으로 출력합니다.
이 코드에는 DB 작업 전후의 타이머나 요청 ID의 생성과 전파가 없으므로, 이 기록만으로 DB 소요 시간이나 동시 요청 사이의 관계를 확정할 수 없습니다. 운영용 오류의 message와 stack에 연결 문자열이나 개인정보가 들어갈 수 있는지도 점검하고, 공개할 필드를 정해 필요한 값만 기록합니다. Console 전송을 설정했다는 사실만으로 파일 저장이나 외부 수집까지 끝났다고 보지 않습니다.
2. formatter와 완료 기록 함수
위 printf를 그대로 쓰면서 info 객체에 requestId와 durationMs를 더했습니다. Console 문자열에 두 값이 나타날까요? completionRecord에 서버가 만든 ID req_A, 상태 200, 시간 30.6ms를 넣으면 어떤 필드와 시간이 반환될까요? 시간이 -1이거나 NaN이면 어떻게 될까요?
해설 보기
printf formatter가 두 필드를 고르지 않으므로 출력 문자열에 나타나지 않습니다. completionRecord는 event: 'request.completed', requestId: 'req_A', status: 200, durationMs: 31을 반환합니다. 시간은 정수 밀리초로 반올림하며, -1과 NaN은 null이 됩니다. 객체를 만든 뒤에도 직렬화, 출력, 수집 포맷까지 연결해야 실제로 기록을 확인할 수 있습니다.
3. 겹친 요청의 시간
같은 시계로 A는 100에서 160까지, B는 120에서 200까지 실행되었습니다. 각 요청의 시간을 구하고, 잘못된 전역 시작값으로 계산한 200 − 100과 구분하세요. 요청 ID를 전역 변수 하나에 보관하면 왜 A의 종료 로그에 B의 ID가 붙을 수 있을까요?
해설 보기
A는 60ms, B는 80ms이며, 100ms는 서로 다른 요청의 시작과 끝을 섞은 값입니다. B가 공유 ID를 덮어쓴 뒤 A가 그 변수를 읽으면 A의 기록에 B의 ID가 붙습니다. 요청 안의 값으로 시작과 종료를 짝짓고, 같은 요청 ID를 그 요청의 처리 흐름으로 넘겨야 합니다.
4. 요청 오류율의 분모
같은 10분 동안 실제 요청 1,000개 가운데 5xx가 20개였고, 수집된 로그는 50줄 가운데 오류가 20줄입니다. 요청 오류율은 얼마일까요? 40%라는 계산은 무엇을 나타내며, 수집된 로그만으로 전체 요청의 분포를 확정할 수 있을까요?
해설 보기
20 ÷ 1,000 × 100 = 2%입니다. 20 ÷ 50 × 100 = 40%는 수집된 로그 줄 가운데 오류 줄의 비율입니다. 한 요청이 여러 로그를 남기거나, 로그가 누락되거나 샘플링되거나, 캐시 응답이 로그를 남기지 않으면 분모가 달라지므로 전체 요청 오류율과 같게 보지 않습니다. 같은 시간대와 요청 범위의 카운터를 따로 확보해야 합니다.
5. 전송 호출과 저장 완료
함수가 기록 객체를 만들고 비동기 전송을 부른 직후 종료되었습니다. 그 기록이 외부 보관소에 저장되었다고 결론 낼 수 있을까요? requestId 필드만 더했을 때 분산 트레이스의 연결까지 끝났다고 볼 수 있는지도 설명하세요.
해설 보기
결론 낼 수 없습니다. 객체 생성, 대기열에 넣기, transport의 완료, 외부 저장의 확인은 서로 다른 단계입니다. 해당 전송기와 호스팅 환경이 종료, flush, 오류를 어떻게 처리하는지 확인해야 하며, Winston의 stream finish도 모든 외부 보관소의 장기 보존을 한꺼번에 보장하지는 않습니다. 분산 트레이스는 실제 SDK나 bridge의 계측과 trace, span 문맥의 전파가 있어야 연결됩니다.
instrumentation.ts의 사용법은 Next.js의 instrumentation 안내에서, register와 onRequestError의 형식은 instrumentation.js API 참조에서 확인할 수 있습니다.
이것으로 18장을 마칩니다. 18장에서는 자주 만나는 오류를 발생 단계별로 진단하고, 성능 병목을 측정해 개선하며, 브라우저 개발자 도구와 서버 쪽 로깅, 모니터링으로 문제의 위치를 좁히는 방법을 살펴봤습니다. 다음 장의 유용한 Next.js 라이브러리 소개부터는 Next.js 개발에 도움이 되는 라이브러리와 학습 자료를 알아봅니다.