에러 핸들링 (Error Handling)
페이지 또는 컴포넌트에서 발생하는 에러를 처리하는 방법을 알아봅니다.
Next.js 애플리케이션을 개발하다 보면 예상치 못한 에러가 발생할 수 있습니다. 이러한 에러를 적절히 처리하지 못하면 사용자 경험이 저하되고 애플리케이션의 안정성이 위협받을 수 있습니다. 이번 회차에서는 Next.js가 제공하는 강력한 에러 핸들링 메커니즘을 이해하고, 이를 통해 견고하고 사용자 친화적인 애플리케이션을 구축하는 방법을 학습합니다.
📌 이번 회차 학습 목표
- Next.js에서 에러를 처리하는 기본적인 원리를 이해할 수 있습니다.
error.js파일을 사용하여 특정 세그먼트의 에러를 격리하고 처리할 수 있습니다.- React의 에러 바운더리(Error Boundaries) 개념을 Next.js에 적용할 수 있습니다.
- 클라이언트 및 서버 컴포넌트에서 발생하는 에러를 구분하여 처리할 수 있습니다.
- 사용자에게 더 나은 에러 경험을 제공하는 방법을 습득할 수 있습니다.
📝 개념 설명
에러 핸들링의 중요성
웹 애플리케이션에서 에러는 불가피하게 발생합니다. 네트워크 문제, API 호출 실패, 잘못된 데이터 처리, 예상치 못한 사용자 입력 등 다양한 원인으로 에러가 발생할 수 있습니다. 이러한 에러를 적절히 처리하지 않으면 애플리케이션이 갑자기 중단되거나, 사용자에게 혼란스러운 메시지가 표시되어 서비스 신뢰도가 떨어질 수 있습니다. 에러 핸들링은 이러한 문제들을 예측하고, 발생한 에러를 우아하게 처리하여 사용자에게 일관되고 안정적인 경험을 제공하는 필수적인 과정입니다.
Next.js의 에러 핸들링 전략: error.js
Next.js 13부터 도입된 App Router는 error.js 파일을 통해 특정 UI 세그먼트에서 발생하는 에러를 자동으로 처리할 수 있는 기능을 제공합니다. 이 파일은 React의 에러 바운더리 기능을 활용하여, 자식 컴포넌트 트리에서 발생하는 JavaScript 에러를 포착하고 fallback UI를 렌더링합니다. error.js는 해당 세그먼트 및 그 하위 세그먼트에서 발생하는 에러에만 영향을 미치며, 상위 레이아웃에는 영향을 주지 않습니다.
error.js 감지error.js 파일의 동작 원리
- 에러 바운더리 역할:
error.js파일은 자동으로 React 에러 바운더리 역할을 수행합니다. 이는 해당 파일이 위치한 세그먼트와 그 하위 세그먼트에서 발생하는 런타임 에러를 포착합니다. - 에러 격리: 에러가 발생하면
error.js에 정의된 UI가 렌더링되고, 에러가 발생한 컴포넌트 트리는 언마운트됩니다. 이로써 상위 레이아웃이나 다른 UI 요소들이 에러의 영향을 받지 않고 정상적으로 작동할 수 있습니다. - 클라이언트 컴포넌트:
error.js는 반드시 클라이언트 컴포넌트여야 합니다.'use client'지시어를 파일 상단에 추가해야 합니다. - props:
error.js컴포넌트는error(에러 객체)와reset(에러 상태를 재설정하는 함수) 두 가지 props를 받습니다.
에러 바운더리(Error Boundaries)란?
React의 에러 바운더리는 자식 컴포넌트 트리 내의 JavaScript 에러를 포착하고, 에러가 발생한 컴포넌트 트리를 대체할 UI를 렌더링하는 React 컴포넌트입니다. 이는 전체 애플리케이션이 중단되는 것을 방지하고, 에러가 발생했을 때 사용자에게 의미 있는 정보를 제공할 수 있도록 돕습니다. Next.js의 error.js는 이 에러 바운더리 패턴을 App Router에 맞게 추상화한 것입니다.
global-error.js를 통한 전역 에러 처리
만약 error.js가 위치한 레이아웃(layout.js)에서 에러가 발생하면, 해당 error.js는 에러를 포착할 수 없습니다. 이러한 경우를 대비하여 global-error.js 파일을 사용할 수 있습니다. 이 파일은 애플리케이션의 최상위 에러 바운더리 역할을 하며, 모든 layout.js 및 error.js 파일에서 포착하지 못한 에러를 처리합니다. global-error.js는 반드시 과 태그를 포함하여 전체 문서 구조를 렌더링해야 합니다.
| 항목 | error.js | global-error.js |
|---|---|---|
| 적용 범위 | 해당 세그먼트 및 하위 세그먼트 | 전역 (모든 세그먼트, 레이아웃 포함) |
| 컴포넌트 유형 | 클라이언트 컴포넌트 ('use client') | 클라이언트 컴포넌트 ('use client') |
| 받는 props | error, reset | error, reset |
| 특징 | 상위 레이아웃은 정상 작동 | 전체 문서 구조 렌더링 (, 포함) |
💡 예제 & 실습
다음 예제를 통해 error.js 파일을 사용하여 에러를 처리하는 방법을 단계별로 살펴보겠습니다.
실습 1: 기본적인 error.js 구현
특정 경로에서 에러를 발생시키고, 이를 error.js 파일로 처리하는 예제입니다.
- 프로젝트 설정:
새 Next.js 프로젝트를 생성하거나 기존 프로젝트를 사용합니다.
npx create-next-app@latest my-error-app --typescript --eslint --app cd my-error-app - 에러를 발생시킬 페이지 생성:
app/dashboard/page.tsx파일을 생성하고, 의도적으로 에러를 발생시키는 코드를 작성합니다.// app/dashboard/page.tsx 'use client'; import { useState, useEffect } from 'react'; export default function DashboardPage() { const [hasError, setHasError] = useState(false); useEffect(() => { // 클라이언트 사이드에서만 에러를 발생시킵니다. // 서버 컴포넌트에서는 throw new Error()가 즉시 500 에러를 발생시킵니다. if (typeof window !== 'undefined' && Math.random() > 0.5) { setHasError(true); } }, []); if (hasError) { throw new Error('대시보드 로딩 중 에러가 발생했습니다!'); } return ( <div> <h1>대시보드 페이지</h1> <p>여기는 중요한 대시보드 정보가 표시됩니다.</p> </div> ); }해설: 이 페이지는 렌더링 시 50% 확률로
hasError상태를true로 설정하고, 이 경우throw new Error()를 통해 에러를 발생시킵니다.'use client'를 사용하여 클라이언트 컴포넌트로 만들었습니다. error.js파일 생성:app/dashboard/error.tsx파일을 생성하고, 에러 발생 시 보여줄 UI를 정의합니다.// app/dashboard/error.tsx 'use client'; // 에러 바운더리는 반드시 클라이언트 컴포넌트여야 합니다. import { useEffect } from 'react'; interface ErrorProps { error: Error; reset: () => void; } export default function Error({ error, reset }: ErrorProps) { useEffect(() => { // 에러 로깅 서비스에 에러를 보고하는 것이 좋습니다. console.error(error); }, [error]); return ( <div style={{ padding: '20px', border: '1px solid red', borderRadius: '8px', backgroundColor: '#ffe6e6' }}> <h2>앗! 뭔가 잘못되었습니다!</h2> <p>{error.message || '알 수 없는 에러가 발생했습니다.'}</p> <button onClick={ // 에러 바운더리를 재설정하여 다시 렌더링을 시도합니다. () => reset() } style={{ marginTop: '10px', padding: '8px 15px', backgroundColor: '#0070f3', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }} > 다시 시도 </button> </div> ); }해설: 이 컴포넌트는
error객체와reset함수를 props로 받습니다.useEffect를 사용하여 에러를 콘솔에 로깅하고, ‘다시 시도’ 버튼을 클릭하면reset()함수를 호출하여 에러 바운더리를 재설정하고 컴포넌트 트리를 다시 렌더링하도록 시도합니다.- 애플리케이션 실행 및 확인:
npm run dev브라우저에서
http://localhost:3000/dashboard로 접속합니다. 페이지를 새로고침할 때마다 50% 확률로 에러 페이지가 나타나는 것을 확인할 수 있습니다. ‘다시 시도’ 버튼을 눌러 에러를 재시도할 수도 있습니다.
실습 2: global-error.js 구현
최상위 레이아웃에서 발생하는 에러를 처리하기 위한 global-error.js를 구현해봅니다.
app/layout.tsx수정:최상위 레이아웃에서 에러를 발생시키기 위해
app/layout.tsx파일을 수정합니다. 주의: 실제 프로덕션 환경에서는 레이아웃에서 에러를 의도적으로 발생시키는 것은 피해야 합니다. 이 예제는 학습 목적입니다.// app/layout.tsx 'use client'; // 레이아웃에서 클라이언트 사이드 에러를 발생시키기 위해 'use client' 추가 import { useState, useEffect } from 'react'; import './globals.css'; export default function RootLayout({ children }: { children: React.ReactNode }) { const [layoutError, setLayoutError] = useState(false); useEffect(() => { // 레이아웃 로딩 시 20% 확률로 에러 발생 if (typeof window !== 'undefined' && Math.random() < 0.2) { setLayoutError(true); } }, []); if (layoutError) { throw new Error('최상위 레이아웃 로딩 중 치명적인 에러 발생!'); } return ( <html lang='ko'> <body> <header style={{ padding: '10px', backgroundColor: '#f0f0f0' }}>Next.js 앱 헤더</header> {children} <footer style={{ padding: '10px', backgroundColor: '#f0f0f0', marginTop: '20px' }}>Next.js 앱 푸터</footer> </body> </html> ); }해설:
layout.tsx를 클라이언트 컴포넌트로 만들고, 20% 확률로 에러를 발생시킵니다. 이 에러는app/dashboard/error.tsx가 아닌global-error.tsx에 의해 포착되어야 합니다.global-error.tsx파일 생성:app/global-error.tsx파일을 생성하고, 전역 에러 발생 시 보여줄 UI를 정의합니다. 이 파일은 반드시과태그를 포함해야 합니다.// app/global-error.tsx 'use client'; import { useEffect } from 'react'; interface GlobalErrorProps { error: Error; reset: () => void; } export default function GlobalError({ error, reset }: GlobalErrorProps) { useEffect(() => { console.error('전역 에러 발생:', error); }, [error]); return ( <html lang='ko'> <body> <div style={{ padding: '40px', textAlign: 'center', backgroundColor: '#fdf3f2', border: '2px solid #e0b4b4', borderRadius: '10px' }}> <h1>🚨 시스템 오류 발생 🚨</h1> <p>죄송합니다. 애플리케이션에 치명적인 문제가 발생했습니다.</p> <p>{error.message || '알 수 없는 전역 에러입니다.'}</p> <button onClick={() => reset()} style={{ marginTop: '20px', padding: '10px 20px', backgroundColor: '#d9534f', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', fontSize: '16px' }} > 앱 다시 로드 </button> </div> </body> </html> ); }해설:
global-error.tsx는 전체 페이지를 대체하므로,및태그를 직접 렌더링해야 합니다. 이 컴포넌트도error와resetprops를 받습니다.- 애플리케이션 실행 및 확인:
npm run dev브라우저에서
http://localhost:3000으로 접속합니다. 페이지를 새로고침할 때 20% 확률로global-error.tsx에 정의된 전역 에러 페이지가 나타나는 것을 확인할 수 있습니다. 이 경우, 헤더와 푸터도 함께 사라지고 전역 에러 UI만 표시됩니다.
⚠️ 자주 틀리는 것 / 주의사항
error.js는 반드시'use client'지시어를 포함해야 합니다. React 에러 바운더리는 클라이언트 컴포넌트에서만 작동하기 때문입니다.error.js는 해당 세그먼트의layout.js에서 발생하는 에러를 포착하지 못합니다. 레이아웃 에러는 상위error.js또는global-error.js에 의해 처리됩니다.global-error.js는과태그를 포함하여 전체 문서 구조를 렌더링해야 합니다. 이는global-error.js가 최상위 에러 바운더리로서 전체 페이지를 대체하기 때문입니다.- 서버 컴포넌트에서
throw new Error()를 사용하면 즉시 500 에러 페이지가 렌더링됩니다.error.js는 클라이언트 사이드 런타임 에러를 포착하는 데 더 적합합니다. 서버 컴포넌트의 데이터 페칭 에러 등은try-catch블록으로 처리하거나, Next.js의notFound()함수를 사용하여 404 페이지를 렌더링하는 것이 일반적입니다. - 에러 로깅은 필수입니다.
error.js나global-error.js의useEffect훅 내에서console.error(error)를 사용하거나, 실제 서비스에서는 Sentry, Bugsnag와 같은 에러 로깅 서비스에 보고해야 합니다.
- Next.js의
error.js파일은 특정 UI 세그먼트의 클라이언트 사이드 런타임 에러를 포착하고 대체 UI를 렌더링하는 에러 바운더리 역할을 합니다. error.js는'use client'지시어가 필요하며,error객체와reset함수를 props로 받습니다.global-error.js는 최상위 레이아웃을 포함하여 애플리케이션 전반에서 발생하는 에러를 처리하는 전역 에러 바운더리입니다.global-error.js는과태그를 포함하여 전체 문서 구조를 렌더링해야 합니다.- 에러 발생 시 사용자에게 친화적인 메시지와 재시도 옵션을 제공하여 사용자 경험을 개선하는 것이 중요합니다.
🔗 다음 회차 예고
이번 회차에서는 Next.js에서 에러를 효과적으로 처리하는 방법을 학습했습니다. 다음 14회차에서는 ‘데이터 페칭 (Data Fetching)’을 주제로, Next.js 애플리케이션에서 데이터를 가져오는 다양한 전략과 최적화 기법에 대해 심층적으로 다룰 예정입니다. 서버 컴포넌트와 클라이언트 컴포넌트에서의 데이터 페칭 차이점, 캐싱 전략 등을 학습하며 더욱 효율적인 데이터 관리를 위한 기반을 다질 것입니다.