호진방 블로그
경험

Next.js 블로그 에러 핸들링 적용해보기

# React Errorboundary 대신 Next.js에서 제공하는 에러핸들링을 적용해봅시다.

2024년 07월 11일

도입 배경

이번 Next.js 블로그 프로젝트에서는 에러 핸들링 방식을 React의 Error Boundary를 사용하는것이 아닌 Next.js가 제공하는 에러 핸들링으로 전환하려고 합니다. 해당 글에서는 이전 프로젝트에서 사용했던 React Error Boundary의 장단점을 살펴보고, Next.js의 에러 핸들링 방식으로 적용해보고 그 장점을 설명해보려고 합니다.


React Error Boundary

먼저, 이전 https://www.banghojin.site/tech/introduction-errorboundary 글에서 사용했던 React Error Boundary에 대해 복습해보는 시간을 가져보겠습니다.

React Error Boundary의 개념

class ErrorBoundary extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.log({ error, errorInfo });
  }
  render() {
    if (this.state.hasError) {
      if (this.props.fallbackComponent != null) {
        return <>{this.props.fallbackComponent}</>;
      }
 
      return <div>문제가 발생했어요.</div>;
    }
    return this.props.children;
  }
}

React Error Boundary는 컴포넌트 트리의 하위에서 발생하는 에러를 캐치하여 전체 애플리케이션이 망가지는 것을 방지하는 메커니즘입니다. componentDidCatchgetDerivedStateFromError 메서드를 활용하여 에러를 잡아내고, 해당 에러에 대한 대체 UI나 주입된 컴포넌트로 렌더링하는 방식으로 작동합니다.


export default WrapErrorBoundary;
 
function WrapErrorBoundary() {
  return (
    <ErrorBoundary
      fallbackComponent={
        <div className="mt-[100px] mb-[170px]">
          <div className="flex flex-col items-center justify-center">
            <Image src="/error.png" width={100} height={100} alt="error" />
            <div className="text-space-purple font-bold text-lg">
              글을 가져오는 중 일시적인 오류가 발생했습니다.
            </div>
            <div className="text-gray-500 text-md font-semibold">잠시 후 다시 시도해주세요</div>
          </div>
        </div>
      }
    >
      <PostDetail />
    </ErrorBoundary>
  );
}

이를 통해 특정 컴포넌트의 에러를 개별적으로 처리할 수 있다는 장점이 있습니다. 이전 프로젝트를 예를 들자면, 커뮤니티 페이지에서는 글 상세 데이터를 가져오는 API와, 해당 글의 댓글 데이터를 가져오는 API로 나뉘어집니다. 이 때 댓글 데이터를 가져오는 과정에서 오류가 발생한다면 글 상세 데이터는 정상적으로 가져와도 댓글 컴포넌트의 오류로 인해 화면 전체가 망가지는 현상이 발생했고, 이를 해결하기 위해 각 API별 오류 컴포넌트를 분리하여 에러 발생시 에러를 별도의 컴포넌트에서 처리하고, 그 외의 기능은 정상적으로 작동하도록 유지할 수 있었습니다.

하지만 React Error Boundary는 클라이언트 사이드에서 발생하는 에러를 잘 처리할 수 있지만, 서버 사이드 렌더링(SSR)과 관련된 에러는 잘 처리하지 못한다는 단점이 존재합니다. 예를 들어, 사용자가 페이지를 요청할 때 서버에서 데이터를 가져오는 과정에서 발생하는 에러는 React Error Boundary로 처리하기 어렵습니다.

또한, 라우팅이나 데이터 패칭과 같은 애플리케이션의 전반적인 흐름에서 발생하는 에러를 효과적으로 처리하려면 추가적인 설정이나 라이브러리가 필요합니다. 예를 들어, 라우팅 과정에서 페이지를 찾을 수 없거나 서버에서 데이터를 가져올 때 에러가 발생하면, React Error Boundary만으로는 이러한 상황을 모두 처리하기 어렵습니다.

Next.js의 에러 핸들링 방식

https://nextjs.org/docs/app/building-your-application/routing/error-handling

Next.js 에서는 에러 처리를 더욱 간편하고 효율적으로 관리할 수 있는 방법을 제공합니다. 기존의 React Error Boundary와 비교하여, Next.js의 error.js 파일 규칙을 사용하면 복잡한 설정 없이도 효과적인 에러 핸들링이 가능합니다.

에러 처리의 간편화

기존 React의 Error Boundary를 사용하려면 class와 더불어 componentDidCatchgetDerivedStateFromError 메서드를 활용하여 에러 처리 클래스를 작성해야 했습니다. 그러나 Next.js 13에서는 error.js 파일을 사용하여 이러한 복잡한 설정 없이도 에러 처리를 구현할 수 있습니다. error.js 파일에 에러 처리 화면과 로직을 작성하면, Next.js가 이를 자동으로 에러 바운더리로 래핑하여 에러 핸들링을 간편하게 처리합니다.

React Error Boundary 자동 래핑

Next.js 13의 error.js 파일은 React 오류 경계를 자동으로 생성하여 중첩된 자식 세그먼트나 페이지 컴포넌트를 래핑합니다. 이를 통해 개발자는 기존처럼 별도의 에러 class를 작성할 필요 없이, error.js 파일에 에러 처리 로직만 작성하면 됩니다.

특정 컴포넌트에 대한 오류 컴포넌트 생성

Next.js의 파일 시스템 계층 구조를 사용하면 특정 컴포넌트의 맞춤형 오류 UI를 생성할 수 있습니다. 예를 들어, 특정 라우트에서만 발생하는 에러에 대해 맞춤형 오류 페이지를 보여주고 싶다면, 해당 라우트 디렉토리에 error.js 파일을 추가하여 간단하게 구현할 수 있습니다.

오류를 영향 받은 컴포넌트 격리

Next.js 13의 에러 처리 방식은 오류가 발생한 세그먼트를 격리하여 나머지 애플리케이션이 정상적으로 작동하도록 합니다. 특정 부분에서 에러가 발생하더라도 전체 애플리케이션이 중단되지 않도록 합니다.

페이지 재로드 없이 오류 복구 기능 추가

Next.js 13의 error.js 파일은 페이지를 재로드하지 않고도 오류를 복구할 수 있는 기능을 제공합니다. 오류 컴포넌트에서는 reset() 함수를 사용하여 오류를 복구하고 다시 시도할 수 있는 기능을 제공할 수 있습니다.

💡 Next.js 13의 error.js 파일 규칙을 사용한 에러 처리는 기존 React의 Error Boundary보다 훨씬 간편하고 효율적입니다. 에러 처리 화면과 로직을 error.js 파일에 작성하기만 하면 자동으로 에러 바운더리로 래핑되어, 중첩된 라우트에서도 효과적인 에러 핸들링이 가능합니다. 또한, 특정 세그먼트에 맞춤형 오류 UI를 생성하고, 오류를 영향 받은 세그먼트로 격리하여 애플리케이션의 안정성을 높일 수 있습니다. 페이지 재로드 없이 오류를 복구할 수 있는 기능까지 제공하여 사용자 경험을 더욱 향상시킬 수 있습니다.


또한 앞서말한 React Error Boundary의 단점 또한 보완해줍니다.


Next.js 중첩 라우트 및 레이아웃에서의 에러 처리 방식

❗ 그렇다면 위 사진처럼 한 폴더안에 layout, errror, page 파일이 존재하는 중첩 라우트 경우에는 에러가 어떻게 처리되고 있을까요?


중첩된 파일 트리에서의 에러 처리

Next.js 13에서는 layout.js, error.js, global-layout.js, page.js 등의 특수 파일을 통해 React 컴포넌트를 생성하고, 이들을 중첩된 경로 구조에 따라 렌더링합니다. 중첩된 두 개의 세그먼트가 layout.js 파일과 errorjs 파일을 포함하고 있는 경우, Next.js는 계층 구조에서 다음과 같은 순서로 렌더링합니다.

layout > error > page;

해당 구조는 상위 레이아웃(layout.js)이 먼저 렌더링되고, 그 다음 에러 처리 컴포넌트(error.js), 그리고 최종적으로 페이지 컴포넌트(page.js)가 렌더링됨을 의미합니다.

error.js 파일은 중첩된 모든 하위 컴포넌트에 대한 오류를 처리합니다. 예를 들어, 특정 경로 맨 아래에 있는 여러 depth의 컴포넌트가 존재할 때, 최상위 error.js 파일은 해당 경로 아래의 모든 하위 컴포넌트에서 발생하는 에러를 처리할 수 있습니다.

중첩된 폴더에 error.js 파일을 서로 depth에 배치함으로써 세분화된 에러 처리 또한 구현할 수 있습니다. 예를 들어, 상위 폴더와 하위 폴더 각각에 error.js 파일을 배치하면, 상위 폴더의 error.js 파일은 상위 수준의 에러를 처리하고, 하위 폴더의 error.js 파일은 더 세부적인 에러를 처리할 수 있습니다. 이를 통해 각 세그먼트의 특성에 맞는 맞춤형 에러 처리가 가능합니다.

하지만 error.js 파일은 동일한 위치의 layout.js 컴포넌트에서 발생한 오류를 처리하지 않습니다. 이는 레이아웃 컴포넌트와 에러 처리 컴포넌트를 분리하여 각각의 역할을 명확히 하기 위함입니다.

💡 Next.js 13의 중첩 라우트 및 레이아웃에서의 에러 처리 방식은 특수 파일(layout.js, error.js, page.js)을 통해 체계적으로 구현됩니다. error.js 파일은 중첩된 모든 하위 컴포넌트에 대한 오류를 처리하며, 중첩된 폴더에 서로 다른 수준의 error.js 파일을 배치하여 세분화된 에러 처리가 가능합니다. 또한, error.js 파일은 동일한 세그먼트의 layout.js 컴포넌트에서 발생한 오류를 처리하지 않음으로써, 레이아웃과 에러 처리를 명확하게 분리합니다.


그렇다면 Root Layout은?

그렇다면 최상위 Root Layout의 에러는 어떻게 처리될까요? Next는 global-error.js 파일을 도입했습니다. global-error.js 파일은 애플리케이션의 루트 레벨에서 전역적인 오류를 처리하기 위해 사용됩니다. 이 파일은 Next.js 애플리케이션의 모든 페이지와 컴포넌트에 공통으로 적용되어, 어디서든 발생할 수 있는 에러를 일관되게 처리할 수 있습니다.

Next.js 공식문서에서 제공하는 GlobalError
'use client'
 
export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  )
}

Next.js의 에러 핸들링 적용해보기

먼저, PostHeader 컴포넌트를 수정하여 2초 후 50%의 확률로 에러를 발생시키는 로직을 추가합니다.


PostHeader.tsx
export function PostHeader({ posts }: PostHeaderType) {
  ...
 useEffect(() => {
    setTimeout(() => {
      const randomNumber = Math.random();
      if (randomNumber < 0.5) {
        throw new Error('에러발생!');
      } else {
        return;
      }
    }, 2000);
  }, []);
 
  return (
    <>
      ...
    </>
  );
}
 


PostHeader 컴포넌트에서 발생한 에러를 처리할 Error.tsx 를 작성 후 Layout 상위 세그먼트에 위치시킵니다

Error.tsx
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <html>
      <body>
        <div className="text-center min-h-screen flex flex-col justify-center">
          <div>
            <h2 className="text-2xl font-naverBold text-rest-700">Error.tsx Page</h2>
            <p className="text-gray-500 mt-4 font-naverSemi">
              해당 페이지를 가져오던 중 문제가 발생했습니다
            </p>
            <p className="mt-2 text-gray-500 text-sm">{error?.message}</p>
            <div className="mt-8">
              <button onClick={() => reset()} className="bg-rose-700">
                Reset
              </button>
            </div>
          </div>
        </div>
      </body>
    </html>
  );
}

이후 50% 확률로 에러가 발생한다면 header 위치에 설정한 Error컴포넌트가 렌더링됩니다. Reset버튼을 누를 시 다시 PostHeader 를 렌더링하며 정상 렌더링 될 때까지 Error컴포넌트가 렌더링됩니다.


Next.js의 에러 핸들링 적용하며 느낀점

Next.js의 error.tsx 파일을 생성하고 같은 레벨에 위치시키는것만으로도 전체 애플리케이션에 대한 에러 처리가 자동으로 설정되어 매우 편리하다고 느꼈고, 기존에 React 에러 경계를 사용하면서 느꼈던 몇 가지 불편함을 해결해주는 부분이 큰 장점이었습니다.

기존 React error boundary는 Class 컴포넌트를 사용하여 각 컴포넌트를 에러바운더리로 감싸야 했고, 이를 위해 코드를 일일이 수정하고 컴포넌트들을 묶어줘야 했기 때문에 번거로웠습니다. error.tsx 파일을 사용하면 페이지 레벨에서 에러 처리를 간소화할 수 있어 코드가 더 직관적이고 관리하기 쉬워졌습니다.

중요한 기능이나 데이터가 포함된 컴포넌트에서 에러가 발생한다면, React의 error boundary를 사용하여 전체 페이지가 아닌 일부 컴포넌트만 에러 처리하도록 하여 사용자에게 지속적으로 필요한 정보를 제공할 수 있는 장점을 이용하고, 전체적인 페이지 에러 처리는 error.tsx로 간단하게 처리하는 방식으로 두 방식을 적절하게 조합하면 좋지 않을까라는 생각을 하게 됐습니다.


me
@banhogu
안녕하세요 배움을 나누며 함께 전진하는 1년차 주니어 개발자 방호진입니다.