글 목록

Next.js에서 자동화된 로깅 구조 만들기

서버 사이드와 클라이언트 사이드를 아우르는 자동화된 로깅 전략과 구현 방법을 알아봅니다.

프로젝트를 운영하다 보면 사용자가 어떻게 행동하는지, 어떤 에러가 발생하는지 확인해야 할 필요가 있어요. 이를 파악하기 위해 로그 데이터를 분석해야 하는데, 로그를 남기는 것을 로깅(Logging) 이라고 해요.

로깅을 통해 기록된 데이터로 다음과 같은 정보를 확인할 수 있어요

  • 사용자의 행동 패턴 추적
  • 에러 발생 상황 파악
  • 성능 병목 지점 발견

이번 글에서는 Next.js 환경에서 자동화된 로깅 시스템을 구축하여 개발자가 비즈니스 로직에 집중할 수 있도록 하는 방법을 소개합니다.

완성된 로깅 라이브러리를 그대로 제공하기보다는, Next.js의 실행 환경별로 어떤 지점에서 로그를 수집할 수 있는지 살펴보고 각 프로젝트에 맞는 로깅 아키텍처를 설계하는 기준을 정리합니다.

기존 로깅 방식의 문제점

자동화된 로깅 구조가 없었을 땐 중요한 로직마다 수동으로 로깅 코드를 추가해야 했어요.

typescript

이런 방식의 문제점

1. 코드 가독성 저하

  • 비즈니스 로직과 로깅 로직이 뒤섞여 있어요
  • 실제 중요한 로직을 파악하기 어려워요

2. 누락 가능성

  • 수동으로 로그를 추가하다 보면 빠뜨리기 쉬워요
  • 중요한 에러 상황을 놓칠 수 있어요

3. 유지보수 어려움

  • 로깅 형식 변경 시 모든 코드를 수정해야 해요
  • 확장성이 떨어져요

이런 문제들을 해결하기 위해 자동화된 로깅 시스템을 구축하였어요.

Next.js의 복잡한 실행 환경 이해하기

Next.js App Router는 여러 실행 환경에서 코드가 동작해요. 각 환경은 독립적으로 실행되기 때문에 환경별로 다른 로깅 전략이 필요해요.

주요 실행 환경

Client Component

  • 사용자의 브라우저에서 실행
  • JavaScript 런타임 에러, 네트워크 에러 발생 가능
  • window 객체 접근 가능

Server Component

  • Next.js 서버에서 실행
  • 데이터베이스 연결 에러, API 호출 에러 발생 가능
  • Node.js 환경에서 동작

Proxy

  • Edge Runtime에서 실행
  • 인증, 리다이렉션 로직에서 에러 발생 가능
  • 모든 요청(페이지 이동, 서버 액션)의 최상위에서 동작

Error Pages

  • error.tsx, not-found.tsx
  • 예상치 못한 에러나 404 상황 처리
  • Next.js 내부에서 핸들링

환경별 로깅 전략이 필요한 이유

각 환경은 서로 다른 특성을 가지고 있어요.

환경특징주요 에러 유형
Client브라우저 API 사용네트워크 에러, 런타임 에러
ServerNode.js API 사용서버 에러, 데이터베이스 에러
Proxy제한된 API인증 에러, 리다이렉션 에러
Error Pages에러 복구예상치 못한 에러

단계별 로깅 시스템 구축하기

이제 Next.js의 실행 환경별로 로그를 어디서 수집할 수 있는지 살펴봅니다.

1단계: 기본 로거 함수 만들기

먼저 통일된 로그 형식을 위한 기본 로거 함수를 만들어요. 로그 수집 함수는 "use server"를 명시해서 Server Action으로 수행하도록 해요.

Server Action은 모든 환경에서 호출할 수 있고 여러 보안적 이점을 갖고 있어요. 로그에 민감 정보를 잘못 남겨서 보안 사고가 발생할 수도 있기 때문에 Server Action을 사용했어요. 자세한 내용은 이전에 작성한 React Hook Form에서 Next.js Server Actions 사용하기에서 확인할 수 있어요.

typescript

2단계: 클라이언트 에러 자동 수집하기

브라우저에서 발생하는 에러를 자동으로 수집하는 컴포넌트를 만들어요.

  • 브라우저에서 발생하는 런타임 에러는 모두 window.addEventListener("error", ...)window.addEventListener("unhandledrejection", ...)로 전파돼요.
  • 이 원리를 이용해서 하나의 컴포넌트에서 모든 런타임 에러를 캡처할 수 있어요.
tsx

추가로 고려할 수 있는 예외 처리

  • Next.js 내부 redirect/notFound 에러 필터링
    • Next.js는 next/redirect 호출 시 렌더링을 멈추기 위해 의도적으로 에러를 throw해요. next/redirect
    • 이 때 logger에 잡힐 수 있으므로 예외처리가 필요할 수 있어요.
  • 이미지, script, link 같은 리소스 로드 실패 분리
  • userAgent, path, sessionId 같은 메타데이터 추가

3단계: Server Component 에러를 서버에서 수집하기

Server Component에서 발생한 에러는 error.tsx에 도달하기 전에 서버에서 수집해야 해요. production 환경에서는 서버 에러의 상세 메시지가 클라이언트로 그대로 전달되지 않기 때문이에요.

Next.js는 서버 사이드에서 throw가 발생하면 error.tsx로 fallback되어요. 이때 error.tsx에서 별도의 로깅을 남기고자 하여도, 빌드 환경에선 원본 에러 메시지를 확인하기 어려워요. error.message를 보면, 아래 에러 메시지를 보게 돼요.

An error occurend in the Server Component render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additionnal details about the nature of the error. Error digest : XXXXXXXXXXXXX

이 메시지를 보면, 빌드 환경에선 에러 메시지가 숨겨져 있어요. 그래서 error.tsx에서 처리하기보다, 공식문서에서 제공하는 instrumentation.tsonRequestError를 사용해요.

ts

운영 코드에서는 필요한 경우 request.headers 정규화, 민감정보 마스킹, requestId, userId, sessionId 같은 메타데이터를 추가할 수 있어요. 이 글에서는 어떤 지점에서 서버 에러를 공통으로 수집할 수 있는지에 집중하기 위해 핵심 구조만 예시로 보여드렸어요.

각 Parameter별로 아래 정보를 담고있어요.

  • err: Error 객체
  • request:
    • path: 현재 페이지 경로
    • method: GET
    • headers: next/headers 정보
      • logger함수에는 next/headers를 호출하여 헤더 정보를 저장하고 있지만, onRequestError 콜백 함수가 실행된 Edge Runtime 컨텍스트 내에선 빈 값이 나왔어요. 대신 request.headers를 사용할 수 있어요.
  • context:
    • routerKind: 라우터 종류 ("App Router" | "Page Router")
    • routePath: 현재 페이지 경로인데, dynamic route일 경우 "/reserve/[id]" 처럼 표현되어요.
    • routeType: "render" (아직 무엇이 더 존재하는지 확인 못했어요..)
    • renderSource: "react-server-components"

4단계: API 호출 경계에서 로그 수집하기

API 호출 로그는 모든 호출부에 직접 남기기보다, 프로젝트에서 사용하는 API 요청 경계에 모으는 것이 좋아요.

이 경계는 프로젝트 구조에 따라 달라질 수 있어요. 공통 fetch 함수가 있다면 그 안에서 수집할 수 있고, SDK lifecycle이나 query/mutation wrapper를 사용한다면 그 계층에서 수집할 수도 있어요.

중요한 건 비즈니스 로직 안에 로깅 코드가 흩어지지 않도록 하는 것이에요.

  • 에러가 발생할 경우, RSC, CSC는 throw를 통해 공통 에러 처리 영역으로 전파할 수 있지만, Server Action 에서 발생한 에러는 throw를 권장하지 않아요. 공식문서
  • 기본 형식 외 추가적인 메타 데이터를 담기 위해, API 요청 경계에서 공통으로 로그를 수집해요.
typescript

사용 예시

이제 깔끔한 코드로 API를 호출할 수 있어요:

typescript

이 예시에서 중요한 건 apiClient의 구현 방식이 아니라, API 요청이 지나가는 공통 경계에서 로그를 수집한다는 점이에요. 프로젝트에 따라 SDK lifecycle, TanStack Query의 mutation wrapper, Server Action helper 등 다른 계층을 선택할 수도 있어요.

5단계: Error Pages는 복구 UI로 다루기

참조: Next.js Learn Chapter 13 - Handling Errors

error.tsxnot-found.tsx는 에러를 수집하는 지점이라기보다, 사용자에게 복구 가능한 UI를 보여주는 지점으로 보았어요.

error.tsx

클라이언트 런타임 에러는 앞에서 만든 RuntimeLogger에서 수집하고, Server Component 에러는 instrumentation.tsonRequestError에서 수집해요.

그래서 error.tsx에서는 별도의 로깅을 추가하지 않고, reset을 통한 재시도나 홈으로 이동하는 버튼처럼 사용자가 다시 시도할 수 있는 흐름에 집중해요.

not-found.tsx

not-found.tsx는 404 에러를 처리해요.

다만 모든 404를 같은 의미로 로깅하면 노이즈가 커질 수 있어요. 사용자가 주소창에 잘못된 URL을 입력한 경우와, 앱 내부 링크나 programmatic navigation에서 예상하지 못한 404가 발생한 경우는 다르게 볼 수 있기 때문이에요.

ts

그래서 404 로그는 프로젝트의 라우팅 정책에 맞춰 선택적으로 수집하는 것이 좋아요. 예를 들어 깨진 내부 링크를 찾는 것이 중요하다면 referrer나 현재 path를 함께 남길 수 있고, 직접 접근으로 발생한 404는 별도로 제외할 수도 있어요.

6단계: Proxy의 로깅 책임 분리하기

Proxy는 요청이 페이지나 Server Component에 도달하기 전에 실행돼요. 그래서 앞에서 만든 RuntimeLogger, error.tsx, instrumentation.ts 흐름으로 처리되지 않는 에러가 생길 수 있어요.

Proxy는:

  • 기본적으로 Edge Runtime에서 실행돼요.
  • 이는 layout.js 보다 더 상위 레벨로, 사용할 수 있는 API도 제한되어 있어요. 참조

인증 확인, 토큰 갱신, redirect 분기처럼 요청 전처리 단계에서 실패할 수 있는 로직은 Proxy 안에서 별도로 수집해야 해요.

ts

로그가 잘 쌓이는지 확인하기

앞에서 수집한 로그가 실제로 운영 환경에서 확인 가능한 형태인지 확인해요.

아래 쿼리는 AWS CloudWatch Logs Insights 기준 예시예요. Datadog, Sentry, Grafana Loki 같은 도구를 사용한다면 같은 기준으로 필드를 바꿔 적용할 수 있어요.

sql

중요한 건 어떤 도구를 쓰느냐보다, 앞에서 정한 eventType, level, page.path 같은 필드로 실제 운영 상황을 확인할 수 있게 만드는 것이에요.

결과

위 방식으로 Next.js에서 효율적인 로깅 시스템을 구축할 수 있었어요. 비즈니스 로직 사이사이 끼워져있던 로깅 함수를 모두 거둬냈고, 로깅을 실수로 추가하기 못해 발생하는 문제도 없어졌어요. 이제 개발자들은 더 이상 로깅 코드를 추가하는 것을 걱정하지 않아도 되고, 비즈니스 로직에만 집중할 수 있게 되었어요.

최종적으로 중요한건 어디서 공통으로 처리가 가능할지 고려하는거였어요. Next.js는 다양한 실행 환경을 가지고 있어서 환경별로 어떻게 처리할 수 있을지를 가장 많이 고민했어요. 이 부분을 잘 고려하면 코드 가독성과 유지보수성을 높일 수 있어요.