Reese-log
  • About
  • Blog

© 2025 Reese. All rights reserved.

2025년 6월 7일

Next.js 에러 처리, 사용자와 개발자 모두를 위한 에러 시스템 만들기

#Next.js

1. 개요

Next.js App Router 환경에서, 서버에서 발생한 에러는 어떻게 처리하고 계신가요?

  • 데이터를 불러오다 실패하면 어떻게 하나요?
  • 인증이 만료되었거나, 네트워크가 끊겼거나, GraphQL 쿼리 결과가 비정상적이라면?
  • 이런 에러들은 어떻게 클라이언트로 전달되고, 사용자에게는 어떤 방식으로 피드백되고 있나요?
  • 이런 고민은 Next.js의 구조적 특징에서 비롯됩니다.

    App Router 기반의 Next.js에서는 서버에서 데이터를 미리 가져와, 초기 HTML 렌더링에 활용할 수 있습니다.

    덕분에 클라이언트는 별도 상태 관리 없이 UI만 깔끔하게 그리면 되는 구조를 가집니다.

    하지만 이 구조에서 에러가 발생했을 때는 얘기가 달라집니다.

    "에러는 서버에서 났는데, 사용자에겐 어떻게 보여주지?"

    왜 이런 문제가 생길까요?

    출처: https://nextjs.org/docs/app/api-reference/file-conventions/error

    그 이유는 Next.js의 의도적인 보안 설계 때문입니다.

    서버에서 발생한 Error 객체에는 API 경로나 DB 쿼리, 환경 변수 등 민감한 정보가 포함되어 있을 수 있기 때문에,

  • 프로덕션 환경에서는 Error 객체가 직렬화되지 않으며,
  • error.tsx, global-error.tsx에서도 에러 메시지는 기본적으로 마스킹 처리됩니다.
  • 예를 들어, 서버에서 throw new Error('UNAUTHORIZED')를 해도

    → 클라이언트에서는 "Something went wrong!" 같은 의미 없는 fallback 메시지만 보이게 됩니다.

    ❗ 그 결과 어떤 불편함이 생길까요?

  • 인증 오류가 발생해도 로그인 페이지로 리디렉션할 수 없고
  • 입력 오류가 있어도 사용자에게 적절한 피드백을 줄 수 없으며
  • 치명적인 서버 에러도 단 하나의 fallback UI로만 처리됩니다
  • 결국 에러의 맥락을 전달할 수 없기 때문에, 사용자 경험도, 개발자 대응도 모두 제한적일 수밖에 없습니다.

    특히 GraphQL + Apollo 환경에서는 더 복잡해집니다

    에러는 단일 객체가 아닌 중첩된 구조로 전달되며,

  • ApolloError
  • 처럼 계층을 따라 내려가야만 의미 있는 에러 정보를 추출할 수 있습니다.

    그래서 이러한 계획을 세우게 됩니다. ⇒ 직렬화 가능한 커스텀 에러 객체를 만들고, 그걸 명시적으로 클라이언트에 전달하자. 그래서 사용자에겐 정확하고 일관된 피드백을 개발자에겐 추적 가능하고 실용적인 디버깅 환경을 제공하자!

    시스템의 핵심 흐름

  • 서버에서 발생한 에러를 ApiActionError 포맷으로 정형화하고 직렬화하여 클라이언트에게 전달
  • 클라이언트에서는 category 값에 따라 alert, fallback, custom UI로 분기 처리
  • 예상치 못한 에러는 ErrorBoundary를 통해 graceful하게 fallback
  • 에러 정보는 소스맵 기반으로 Slack 등에 자동 리포팅
  • 실제 서비스 환경에서 발생할 수 있는 다양한 에러 상황을 Next.js App Router 구조에 맞게 일관되게 처리하고 싶다면, 이 글에서 소개하는 구조가 좋은 출발점이 되기를 바랍니다.

    2. 서버 사이드에서 발생한 에러의 정형화

    3.1 ApiActionError 인터페이스

    서버와 클라이언트가 공통으로 사용할 수 있는 에러 포맷을 정의합니다.

    typescript
    1export interface ApiActionError {
    2  code: ErrorCode;
    3  message: string;
    4  category: ErrorCategory;
    5  originalError?: unknown;
    6}

    3.2 ErrorCode와 ErrorCategory 정의

    에러 유형을 식별 가능한 코드화된 상수로 정의하고, UX를 위한 분기 기준인 카테고리도 함께 지정합니다.

    typescript
    export const ERROR_CODE_CONSTANTS = [
      'UNAUTHORIZED', 'NOT_FOUND', 'INTERNAL_ERROR', 'NETWORK_ERROR', 'POI_NOT_FOUND', 'UNKNOWN_ERROR',
    ] as const;
    
    export type ErrorCategory = 'ALERT' | 'CUSTOM' | 'ERROR_BOUNDARY' | 'AUTH';
  • ALERT: 사용자에게 모달로 보여줄 에러
  • CUSTOM: 컴포넌트에서 UI로 직접 처리할 에러
  • ERROR_BOUNDARY: fallback UI로 대체되어야 하는 심각한 에러
  • AUTH: 인증 관련 처리
  • 3. 서버 함수 공통 처리 구조

    3.1 withErrorHandling – 모든 서버 액션을 감싸는 진입점

    모든 액션마다 try-catch를 반복하는 대신, 고차 함수 withErrorHandling으로 감쌉니다.

    이 고차 함수는 서버 액션을 감싸고, try-catch 로직을 중앙 집중화하여 에러를 표준화된 형식으로 감싸 클라이언트에 전달합니다.

    typescript
    1type ActionResult<T> = {
    2  data: T | null;
    3  error: Omit<ApiActionError, 'originalError'> | null;
    4};
    5
    6function withErrorHandling<TArgs extends any[], TResult>(
    7  action: (...args: TArgs) => Promise<TResult>,
    8): (...args: TArgs) => Promise<ActionResult<TResult>> {
    9  return async (...args: TArgs) => {
    10    try {
    11      const result = await action(...args);
    12      return { data: result, error: null };
    13    } catch (error) {
    14      const apiError = await normalizeApolloError(error);
    15      console.error('Server Action Error:', apiError);
    16      const { originalError, ...serializable } = apiError;
    17      return { data: null, error: serializable };
    18    }
    19  };
    20}

    catch문에서 에러가 발생하면 normalizeApolloError를 통해 구조화 합니다.

    3.2 normalizeApolloError – ApolloError 정규화

    이 함수는 GraphQL 에러 구조의 특성을 이해하고, ApolloError, networkError, graphQLErrors 등을 분석하여 하나의 표준 에러로 정규화합니다.

  • extensions.code → 에러 코드 분류
  • CODE_CATEGORIES → 에러 카테고리 지정
  • message → 사용자에게 보여줄 메시지 선택
  • 정리된 결과는 ApiActionError 형태로 반환되어 클라이언트에서 즉시 분기 처리할 수 있습니다.

    typescript
    1export async function normalizeApolloError(
    2  error: ApolloError | Error | unknown
    3): Promise<ApiActionError> {
    4  if (isApiActionError(error)) return error;
    5
    6  if (error instanceof ApolloError) {
    7    if (error.networkError) {
    8      return {
    9        code: 'NETWORK_ERROR',
    10        message: ERROR_MESSAGES.NETWORK_ERROR ?? '네트워크 연결을 확인해주세요.',
    11        category: 'ERROR_BOUNDARY',
    12        originalError: error,
    13      };
    14    }
    15
    16    if (error.graphQLErrors?.length > 0) {
    17      const gqlError = error.graphQLErrors[0];
    18      const codeFromExtension = gqlError.extensions?.code as string;
    19      const messageFromGql = gqlError.message;
    20
    21      let determinedCode: ErrorCode = 'UNKNOWN_ERROR';
    22      let determinedCategory: ErrorCategory = 'ERROR_BOUNDARY';
    23
    24      if (codeFromExtension && ERROR_CODE_CONSTANTS.includes(codeFromExtension as ErrorCode)) {
    25        determinedCode = codeFromExtension as ErrorCode;
    26      }
    27
    28      for (const cat in CODE_CATEGORIES) {
    29        if (CODE_CATEGORIES[cat as ErrorCategory].has(determinedCode)) {
    30          determinedCategory = cat as ErrorCategory;
    31          break;
    32        }
    33      }
    34
    35      return {
    36        code: determinedCode,
    37        message: messageFromGql ?? ERROR_MESSAGES[determinedCode] ?? '알 수 없는 오류가 발생했습니다.',
    38        category: determinedCategory,
    39        originalError: error,
    40      };
    41    }
    42
    43    return {
    44      code: 'UNKNOWN_ERROR',
    45      message: error.message ?? ERROR_MESSAGES.UNKNOWN_ERROR,
    46      category: 'ERROR_BOUNDARY',
    47      originalError: error,
    48    };
    49  }
    50
    51  return normalizeError(error);
    52}
  • ApolloError 분석: networkError vs graphQLErrors
  • GraphQL extensions.code를 기준으로 ErrorCode, ErrorCategory 추론
  • 클라이언트 전달이 가능한 형태의 에러 객체 구성
  • 3.3 normalizeError – 일반 JS Error 정규화

    Apollo 외 일반 JS 에러도 동일한 포맷으로 변환합니다.

    이를 통해 에러 종류가 무엇이든 UI에서 분기처리가 가능하게 합니다.

    typescript
    1/**
    2 * 다양한 형태의 JS 오류를 ApiActionError 형태로 정규화합니다.
    3 * 
    4 * 이 함수는 서버에서 발생할 수 있는 모든 종류의 예외를 일관된 형태로 포맷하여,
    5 * 클라이언트에 직렬화 가능한 구조로 변환할 준비를 합니다.
    6 * 
    7 * originalError는 서버 측 로깅용이며, 클라이언트 전송 전에는 제거합니다.
    8 */
    9
    10export function normalizeError(error: unknown): ApiActionError {
    11  // 문자열을 던진 경우
    12  if (typeof error === 'string') {
    13    return {
    14      code: 'UNKNOWN_ERROR',
    15      message: error,
    16      category: 'ERROR_BOUNDARY',
    17    };
    18  }
    19
    20  // 표준 Error 인스턴스인 경우 (예: throw new Error('...'))
    21  if (error instanceof Error) {
    22    return {
    23      code: 'UNKNOWN_ERROR',
    24      message: error.message,
    25      category: 'ERROR_BOUNDARY',
    26      originalError: error, // 서버 측 로깅용 (클라이언트 전송 전 제거 필요)
    27    };
    28  }
    29
    30  // 객체이지만 Error 인스턴스는 아닌 경우
    31  if (
    32    typeof error === 'object' &&
    33    error !== null &&
    34    'message' in error &&
    35    typeof (error as any).message === 'string'
    36  ) {
    37    return {
    38      code: 'UNKNOWN_ERROR',
    39      message: (error as any).message,
    40      category: 'ERROR_BOUNDARY',
    41      originalError: error, // 서버 측 로깅용
    42    };
    43  }
    44
    45  // 위 조건에 모두 해당하지 않는 완전한 unknown (예: throw null, undefined, number, boolean 등)
    46  return {
    47    code: 'UNKNOWN_ERROR',
    48    message: '알 수 없는 오류가 발생했습니다.',
    49    category: 'ERROR_BOUNDARY',
    50    originalError: error, // 가능한 한 원본 형태를 서버에 남김
    51  };
    52}
    53

    3.4 흐름도

    MERMAID

    4. 실제 사용 예시

    4.1 서버 액션

    typescript
    1// 내부에서 Apollo Client로 GraphQL 쿼리를 실행하고, 에러가 있으면 ApolloError를 던짐
    2async function _getPoiDetail(id: string) {
    3  const { data, errors } = await client.query({
    4    query: POI_DETAIL_QUERY,
    5    variables: { id },
    6  });
    7
    8  // GraphQL 응답에 errors가 포함되어 있다면 명시적으로 ApolloError를 발생시킴
    9  if (errors && errors.length > 0) {
    10    throw new ApolloError({ graphQLErrors: errors });
    11  }
    12
    13  return data;
    14}
    15
    16// 고차 함수로 감싸 에러를 정형화하고 클라이언트로 전달 가능하게 변환
    17export const getPoiDetail = withErrorHandling(_getPoiDetail);

    4.2 서버 컴포넌트

    typescript
    1import ServerErrorForwarder from '@/components/common/server-error-forwarder';
    2
    3export default async function PoiPage({ params }: { params: { id: string } }) {
    4  const result = await getPoiDetail(params.id);
    5
    6  // 서버에서 발생한 에러가 있다면, 클라이언트에서 throw 하도록 위임
    7  if (result.error) {
    8    return <ServerErrorForwarder error={result.error} />;
    9  }
    10
    11  // 데이터가 정상적으로 도착하면 클라이언트 컴포넌트에 전달
    12  return <ClientPoiPage data={result.data} />;
    13}
    14

    5. ServerErrorForwarder – 서버에서 클라이언트로 에러 전달

    서버 컴포넌트는 말 그대로 서버에서 실행되며 HTML을 만들어 클라이언트에 전달하는 역할만 수행하고, 클라이언트에 자바스크립트 번들로 전달되지 않습니다.

    그 말은 즉, React의 클라이언트 라이프사이클(hooks, useEffect, state 등)을 사용할 수 없고, 그 안에서 발생한 throw는 React의 에러 바운더리(ErrorBoundary)에 잡히지 않는다는 의미이기도 합니다.

    React의 ErrorBoundary는 클라이언트에서 렌더링 중 발생한 에러를 잡기 위한 메커니즘이기 때문에, 서버에서 발생한 에러에는 반응하지 않습니다.

    그 결과는?

    서버 컴포넌트에서 서버 액션을 호출하다가 에러가 발생해도, 클라이언트에 전달되는 것은 단지 error.tsx 또는 global-error.tsx에서 사용하는 HTML fallback UI뿐입니다.

    하지만 이 fallback은 정확한 에러 객체나 에러 코드 없이 그저 "무언가 잘못됐습니다" 수준의 정보만을 사용자에게 제공하게 됩니다.

    이를 해결하기 위한 방법: ServerErrorForwarder

    서버에서 발생한 에러를 클라이언트로 직렬화 가능한 형태(ApiActionError)로 반환하고, 이 값을 클라이언트 컴포넌트로 전달한 후, 그 안에서 다시 throw하여 에러 바운더리를 트리거하는 방식입니다.

    서버 컴포넌트에서는 클라이언트 라이프사이클 훅이 동작하지 않기 때문에, 에러를 throw해도 ErrorBoundary에서 감지할 수 없습니다.

    서버 컴포넌트에서는 에러가 throw 되어도 클라이언트의 ErrorBoundary 에서 감지되지 않기 때문에, 클라이언트 컴포넌트에서 다시 throw를 발생시켜 ErrorBoundary를 트리거 합니다.

    typescript
    1'use client';
    2
    3import { useEffect } from 'react';
    4
    5/**
    6 * 서버 컴포넌트에서 직렬화된 에러를 클라이언트에서 다시 throw 하여
    7 * 클라이언트 측 ErrorBoundary가 감지할 수 있도록 만드는 컴포넌트
    8 */
    9export default function ServerErrorForwarder({ error }: { error: unknown }) {
    10  useEffect(() => {
    11    /**
    12     * 클라이언트에서 강제로 에러를 throw 함으로써
    13     * React ErrorBoundary가 이 에러를 감지하도록 유도
    14     */
    15    throw new Error(typeof error === 'string' ? error : JSON.stringify(error));
    16  }, [error]);
    17
    18  return null;
    19}

    6. 클라이언트에서 서버 액션 사용 시 핸들링 하는 useActionHandler

    클라이언트에서 서버 사이드의 함수를 실행하는 경우 에러 발생 시 적절한 UI 처리나 fallback 유도를 하기 위한 커스텀 훅입니다.

    typescript
    1import { useState, useCallback, useEffect } from 'react';
    2import { ApiActionError } from '@/utils/errors';
    3import { useSetAtom } from 'jotai';
    4import { alertModalAtom } from '@/atom/modal';
    5import { useRouter } from 'next/navigation';
    6import { useErrorBoundary } from 'react-error-boundary';
    7
    8interface UseApiActionOptions<T> {
    9  initialData?: T | null;
    10}
    11
    12// originalError는 클라이언트에는 전달되지 않도록 제거되어 있음
    13type ApiErrorObject = Omit<ApiActionError, 'originalError'>;
    14
    15interface UseApiActionResult<T, Args extends any[]> {
    16  execute: (...args: Args) => Promise<void>;
    17  data: T | null;
    18  isLoading: boolean;
    19  error: ApiErrorObject | null;
    20  reset: () => void;
    21}
    22
    23interface UseActionHandlerConfig<T, Args extends any[]> {
    24  // 서버 액션 함수. 결과는 data 또는 error 중 하나로 반환됨
    25  action: (...args: Args) => Promise<{ data: T | null; error: ApiErrorObject | null }>;
    26  options?: UseApiActionOptions<T>;
    27  key: string | number; // 키 변경 시 내부 상태 초기화 용도
    28}
    29
    30export default function useActionHandler<T, Args extends any[]>({
    31  action,
    32  options,
    33  key,
    34}: UseActionHandlerConfig<T, Args>): UseApiActionResult<T, Args> {
    35  const [data, setData] = useState<T | null>(options?.initialData ?? null);
    36  const [error, setError] = useState<ApiErrorObject | null>(null);
    37  const [isLoading, setIsLoading] = useState(false);
    38
    39  const setAlertModal = useSetAtom(alertModalAtom); // 전역 alert modal 제어
    40  const router = useRouter(); // history 제어용
    41  const { showBoundary } = useErrorBoundary(); // react-error-boundary 사용
    42
    43  // key가 바뀔 때마다 내부 상태 초기화 (주로 detail 페이지 등)
    44  useEffect(() => {
    45    setData(options?.initialData ?? null);
    46    setError(null);
    47    setIsLoading(false);
    48  }, [key, options?.initialData]);
    49
    50  // 실제 액션 실행 함수
    51  const execute = useCallback(
    52    async (...args: Args) => {
    53      setIsLoading(true);
    54      setError(null);
    55
    56      try {
    57        const result = await action(...args); // 서버 액션 실행
    58
    59        if (result.error) {
    60          // 에러가 발생한 경우
    61          setError(result.error);
    62          setData(null); // 이전 데이터 초기화
    63
    64          // 에러 category에 따라 UX 분기 처리
    65          switch (result.error.category) {
    66            case 'CUSTOM':
    67              // 클라이언트 컴포넌트에서 직접 처리하도록 함
    68              return;
    69
    70            case 'ALERT':
    71              // 모달을 띄우고 뒤로가기
    72              setAlertModal({
    73                visible: true,
    74                title: '알림',
    75                message: result.error.message,
    76                onConfirm: () => {
    77                  setAlertModal({ visible: false });
    78                  router.back(); // 뒤로가기
    79                },
    80              });
    81              return;
    82
    83            case 'ERROR_BOUNDARY':
    84              // ErrorBoundary를 트리거 (fallback UI로 연결)
    85              showBoundary(result.error);
    86              return;
    87          }
    88        }
    89
    90        // 정상적으로 결과가 있을 경우
    91        setData(result.data ?? null);
    92      } finally {
    93        setIsLoading(false); // 무조건 로딩 종료
    94      }
    95    },
    96    [action, setAlertModal, router, showBoundary],
    97  );
    98
    99
    100  return { execute, data, isLoading, error };
    101}
    102

    에러가 ERROR_BOUNDARY라면, throw하여 ErrorBoundary가 잡게끔 유도합니다.

    그 외엔 error 상태값으로 남겨 UI에서 직접 처리하게 할 수 있습니다.

    7. ErrorBoundary와 Fallback UI

    클라이언트 측 모든 예외는 ReactErrorBoundary로 포괄적으로 처리합니다.

    7.1 ErrorBoundary.tsx

    typescript
    1'use client';
    2
    3import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
    4import { ErrorCode, getErrorMessage, parseErrorObject, reportError } from '@/utils/errors';
    5import ErrorFallback from './error-fallback';
    6
    7/**
    8 * 클라이언트 전역에서 렌더링 중 발생하는 예외를 감지하고 fallback UI로 대체합니다.
    9 * - ReactErrorBoundary는 클라이언트에서만 동작합니다.
    10 * - 발생한 에러는 parseErrorObject로 파싱되고, Slack 등의 채널로 reportError를 통해 리포트됩니다.
    11 */
    12export default function ErrorBoundary({ children }: { children: React.ReactNode }) {
    13  return (
    14    <ReactErrorBoundary
    15      // 실제 에러와 stack trace를 외부로 전송
    16      onError={(error, info) => {
    17        reportError(error, info);
    18      }}
    19      // fallback UI는 에러 메시지를 가공하여 ErrorFallback 컴포넌트에 전달
    20      fallbackRender={({ error, resetErrorBoundary }) => {
    21        const errorObject = parseErrorObject(error);
    22        const errorMessage = getErrorMessage(errorObject.code as ErrorCode);
    23
    24        return (
    25          <div className='w-full min-h-[60vh] flex items-center justify-center px-5'>
    26            <ErrorFallback
    27              errorCode={errorObject.code as ErrorCode}
    28              errorMessage={errorMessage}
    29              onRetry={() => window.location.reload()} // Retry는 기본적으로 페이지 새로고침
    30            />
    31          </div>
    32        );
    33      }}
    34    >
    35      {children}
    36    </ReactErrorBoundary>
    37  );
    38}

    7.2 ErrorFallback.tsx

    이 컴포넌트는 일관된 UX를 위해 ErrorBoundary와 global-error.tsx에서 재사용 됩니다.

    typescript
    1'use client';
    2
    3import React from 'react';
    4
    5interface ErrorFallbackProps {
    6  errorCode: string;
    7  errorMessage: string;
    8  onRetry: () => void;
    9}
    10
    11/**
    12 * 클라이언트 에러 발생 시 사용자에게 보여주는 일관된 Fallback UI
    13 * - ErrorBoundary 또는 global-error.tsx에서 공통으로 사용
    14 * - 접근성(aria), 디자인 통일성, 사용자 행동 유도 등을 고려
    15 */
    16export default function ErrorFallback({
    17  errorCode,
    18  errorMessage,
    19  onRetry,
    20}: ErrorFallbackProps) {
    21  return (
    22    <div
    23      role="alert"
    24      aria-live="assertive"
    25      className="flex flex-col items-center justify-center gap-4 px-6 py-10 text-center bg-gray-50 rounded-md shadow-md"
    26    >
    27      <h2 className="text-xl font-semibold text-red-600">
    28        문제가 발생했어요
    29      </h2>
    30      <p className="text-sm text-gray-700">
    31        <strong>코드:</strong> {errorCode}
    32      </p>
    33      <p className="text-base text-gray-800">{errorMessage}</p>
    34      <button
    35        onClick={onRetry}
    36        className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition"
    37      >
    38        다시 시도하기
    39      </button>
    40    </div>
    41  );
    42}

    8. Global Error Handling: app/global-error.tsx

    global-error.tsx는 Next.js App Router에서 에러가 최종적으로 도달하는 fallback 처리 컴포넌트입니다.

    typescript
    1'use client';
    2
    3import { useEffect } from 'react';
    4import { parseErrorObject, reportError } from '@/utils/errors';
    5import ErrorFallback from '@/components/ui/common/error-fallback';
    6
    7/**
    8 * global-error.tsx는 Next.js App Router에서 렌더링 중 치명적인 에러가 발생했을 때
    9 * 마지막으로 fallback되는 전역 에러 처리 UI입니다.
    10 * - 클라이언트 컴포넌트이며, HTML 구조까지 직접 정의합니다.
    11 * - 서버 사이드 렌더링 중 발생한 에러에도 대응됩니다.
    12 */
    13export default function GlobalError({ error }: { error: Error }) {
    14  const parsed = parseErrorObject(error);
    15
    16  useEffect(() => {
    17    // error.stack 등을 포함한 추가 context와 함께 리포트 전송
    18    reportError(error, { componentStack: error.stack });
    19  }, [error]);
    20
    21  return (
    22    <html>
    23      <body>
    24        <ErrorFallback
    25          errorCode={parsed.code}
    26          errorMessage={parsed.message}
    27          onRetry={() => window.location.reload()} // 기본적으로 새로고침 제공
    28        />
    29      </body>
    30    </html>
    31  );
    32}
    33

    9. 전체 흐름 요약

    MERMAID

    위 구조를 통해 서버에서 발생한 에러를 클라이언트까지 일관되게 전달하고, UX에 따라 분기 처리할 수 있는 체계를 구축할 수 있었습니다.

  • 서버와 클라이언트 간 일관된 에러 포맷 공유
  • 에러의 유형을 분류하고, 분기에 따라 UX 제공
  • 예상치 못한 에러는 자동 fallback + Slack 리포트로 빠른 대응 가능
  • 10. 마무리

    Next.js App Router 환경에선 에러를 단순히 try-catch로 처리하는 것만으로는 충분하지 않습니다.

    서버와 클라이언트가 맞물려 작동하는 구조 속에서, 에러는 언제 어디서든 발생할 수 있고, 그에 대한 처리 방식은 서비스의 안정성과 사용자 경험에 직결됩니다.

    이번 글에서 소개한 에러 처리 구조는 단순한 예외 처리를 넘어, GraphQL → 서버 액션 → 클라이언트 렌더링에 이르는 전 과정을 하나의 흐름 안에서 정형화하고, 안전하게 전달하며, 일관되게 처리하기 위한 시도였습니다.

    Next.js에서 에러 처리를 고민하는 분들께 조금이나마 도움이 되었길 바랍니다.

    다음 글에선 Route Handler와 SourceMap을 활용해, 에러 발생 시 Slack으로 실시간 리포트를 전송하는 구조를 자세히 소개해보겠습니다.