어제의 나보다 성장한 오늘의 나

[Next.js] 서버 컴포넌트에서의 에러 처리와 ErrorBoundary 본문

Next.js

[Next.js] 서버 컴포넌트에서의 에러 처리와 ErrorBoundary

today_me 2025. 7. 3. 00:26

 

 

문제 상황

위와 같은 에러 화면이 출력 되었다.

 

처음엔 단순한 인증 오류로 보였다. 그런데 CSS도 전혀 적용되지 않고, 페이지 렌더링도 중단된 듯하다.

서버 컴포넌트 렌더링 자체가 실패한 것처럼 보였고, 에러 바운더리(error.tsx)가 이를 제대로 처리하지 못한 것 같다.

 

뭐가 문제일까?

 

문제가 발생한 페이지의 코드의 형식은 아래와 같다.

export default async function Page() {
  const data = await dataService.getData(); // 이 부분에서 에러 발생

  return (
    <div>
      <DataTable data={data} />
    </div>
  );
}

 

dataService.getData()에서 예외가 발생하여 throw Error를 하였고, 이를 명시적으로 try-catch로 감싸지 않았다.

에러는 상위로 전파되며 결국 서버에서의 렌더링에 문제가 생긴 것으로 보였다.

 

그런데 분명 app/error.tsx 파일이 있는데 왜 이 에러를 처리하지 못했을까?

 

 

error.tsx

'use client' // Error boundaries must be Client Components
 
import { useEffect } from 'react'
 
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error(error)
  }, [error])
 
  return (
    <div>
      
      ...
 
      <Button variant="outline" size="lg" onClick={reset}>
            다시 시도하기
          </Button>
      <Link href="/login">
        <Button variant="delete" size="lg" className="w-full">
              로그인 화면으로 이동하기
        </Button>
       </Link>
    </div>
  )
}

 

클라이언트 컴포넌트로 잘 작성되어 있었다. 에러가 났던 화면에 코드에서의 버튼도 보이는 걸 보면 error.tsx가 렌더링 되긴 했다.

좀 부실해보이지만..

 

다만 서버의 서비스 단에서 던진 에러 메세지는 error.tsx까지 도달하지 않았다.

 

로컬에서 재연

음?? 로컬에서는 에러 바운더리가 서버 에러를 잘 잡아낸다.
그래서 검색을 해보았다.

 

서버 컴포넌트의 에러는 Error Boundary로 전달되지 않는다.

Next.js 공식 문서 및 관련 GitHub Discussions에 따르면

 

1. 서버 컴포넌트에서 렌더링 도중 잡지 못한 에러는 React의 Error Boundary(error.tsx)로 전달되지 않는다.

2. 그러나 개발 모드에서는 디버깅을 위해 서버의 에러 메세지를 그대로 전달하여 보여 준다. 

프로덕션에서는 보안 상 이유 (DB connection 에러 정보 등)로 해당 에러 메시지를 클라이언트에 전달않고 단순화 해서 보여준다.

 

 

그럼 Error Boundary는 언제 동작하고, 언제 동작하지 않는가?

React의 Error Boundary는 컴포넌트 트리 내에서 발생하는 예외를 잡아내고, fallback UI를 렌더링할 수 있게 해주는 안전장치다.

하지만 그 역할과 한계를 명확히 이해하고 써야 한다.

 

error-boundary.js

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  // 렌더링에 영향을 주는 상태 업데이트
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  // 실제 에러 로깅은 여기서 수행
  componentDidCatch(error, errorInfo) {
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>문제가 발생했습니다.</h1>;
    }

    return this.props.children;
  }
}

 

여기서 핵심은 componentDidCatch는 클래스 컴포넌트의 생명주기 메서드라는 점이다.

함수형 컴포넌트에서는 메서드를 사용할 수 없기 때문에, Error Boundary는 반드시 클래스 컴포넌트로 작성해야 한다.

 

그런데 중요한 제약이 있다

Error Boundary는 오직 “렌더링 중” 발생한 에러만 처리할 수 있다.

 

처리할 수 있는 상황

- render() 중 터진 에러

- 생명주기 메서드 (constructor, render, getDerivedStateFromProps 등)

 

처리할 수 없는 상황

- async/await비동기 함수에서 발생한 에러

- useEffect, 이벤트 핸들러(onClick 등)처럼 마운트 이후에 발생하는 에러

- 서버 사이드 렌더링(SSR) 중 발생하는 에러 (특히 Next.js의 서버 컴포넌트)

 

useEffect(() => {
  throw new Error("이 에러는 Error Boundary가 못 잡는다.");
}, []);

 

서버 컴포넌트의 에러는 왜 안 잡힐까?

Next.js에서는 서버 컴포넌트가 서버에서 렌더링되고, 클라이언트로 HTML이 전송된다.

이때 서버에서 발생하는 에러는 React Error Boundary가 작동하기 전에 이미 렌더링이 중단되기 때문에, 클라이언트에 전달되지 않는다. 그래서 브라우저에 망가진 HTML이 전달 되게 된다.

 

이런 경우에 서버 컴포넌트에서 직접 try/catch로 처리하고,

redirect() 해주거나 fallback UI를 return 해주면 된다.



 


 

주저리 주저리 말이 길어졌는데 결국은 기존에 error boundary의 역할을 정확히 이해하지 못했던 게 문제였다.

에러가 개발 서버에서는 잘 잡아서 보여주다보니 착각을 했었던 것이다 ㅠㅠ

서버 컴포넌트의 에러 핸들링에 대한 처리가 없어서 무방비한 상태였던 것이고, 이를 핸들링 해줌으로써 문제를 해결했다.

 

 

 

📚 참고 자료

 Vercel Discussion #66999

 Vercel Discussion #49426

 공식 문서: Server Component에서의 에러 처리

• https://ko.legacy.reactjs.org/docs/error-boundaries.html

 

Comments