티스토리 뷰

사용자가 웹 서비스를 이용하다가 갑자기 마주치는 '하얀 화면'이나 '알 수 없는 오류' 메시지는 서비스의 신뢰도를 급격히 떨어뜨립니다. 특히 현대의 복잡한 웹 애플리케이션에서는 네트워크 불안정, 잘못된 URL 접근, 서버 로직 오류 등 변수가 너무나 많습니다.
Next.js의 앱 라우터(App Router)는 이러한 예기치 못한 상황을 우아하게 대처할 수 있도록 파일 기반의 에러 핸들링 시스템을 제공합니다. 개발자가 일일이 try-catch를 남발하지 않아도, 시스템이 알아서 오류를 포착하고 사용자에게 적절한 가이드를 제시하는 구조를 만드는 법을 깊이 있게 살펴보겠습니다.
1. 핵심 개념: 에러를 계층적으로 격리하기 (Deep Dive)
Next.js의 에러 핸들링 핵심은 **'폭포수(Waterfall) 방지'**와 **'부분적 격리'**에 있습니다.
작동 원리: React Error Boundary의 자동화
우리가 error.js 파일을 만들면, Next.js는 해당 경로의 page.js와 하위 컴포넌트들을 React Error Boundary로 감쌉니다.
- 격리: 특정 페이지나 컴포넌트에서 에러가 발생해도 전체 앱이 죽지 않습니다.
- 복구: 사용자가 새로고침을 하지 않고도 에러 발생 지점만 다시 시도(Reset)할 수 있는 기능을 제공합니다.
비유로 이해하기: 대형 쇼핑몰의 차단기
웹 서비스를 하나의 거대한 쇼핑몰이라고 생각해 보세요.
- error.js: 쇼핑몰의 특정 매장에서 정전이 발생했을 때, 쇼핑몰 전체의 전원을 내리는 게 아니라 해당 매장에만 '임시 점검 중' 표지판을 세우고 수리 기사를 부르는 것과 같습니다.
- not-found.js: 고객이 지도에도 없는 존재하지 않는 매장 번호를 찾아갔을 때, 안내 데스크에서 "찾으시는 매장은 현재 없습니다"라고 친절히 안내하는 것과 같습니다.
2. 실전 예제: 이커머스 상세 페이지 적용 (Hands-on)
실제 이커머스 서비스에서 상품 정보를 불러오다 에러가 발생하거나, 존재하지 않는 상품 ID로 접근했을 때를 가정하여 구현해 보겠습니다.
(1) 상품 상세 에러 처리 (error.js)
상품 정보를 가져오는 중 서버 통신 에러가 발생했을 때 보여줄 컴포넌트입니다.
'use client'; // 에러 컴포넌트는 반드시 클라이언트 컴포넌트여야 합니다.
import { useEffect } from 'react';
export default function Error({ error, reset }) {
useEffect(() => {
// 에러 발생 시 로그 분석 서비스(예: Sentry)로 에러를 전송하는 로직을 넣기 좋습니다.
console.error('애플리케이션 에러 기록:', error);
}, [error]);
return (
<div style={{ padding: '40px', textAlign: 'center', border: '1px solid #ff4d4f', borderRadius: '8px' }}>
<h2>앗! 상품 정보를 불러오지 못했습니다.</h2>
<p style={{ color: '#666' }}>잠시 후 다시 시도해 주세요.</p>
{/* reset() 함수는 에러 경계 내의 내용을 다시 렌더링하려고 시도합니다. */}
<button
onClick={() => reset()}
style={{
marginTop: '20px',
padding: '10px 20px',
backgroundColor: '#0070f3',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer'
}}
>
다시 시도하기
</button>
</div>
);
}
(2) 존재하지 않는 상품 처리 (not-found.js)
잘못된 URL로 접근하거나 notFound() 함수가 호출되었을 때 실행됩니다.
import Link from 'next/link';
export default function NotFound() {
return (
<div style={{ padding: '60px', textAlign: 'center' }}>
<h1 style={{ fontSize: '48px', marginBottom: '10px' }}>404</h1>
<p style={{ fontSize: '18px', color: '#888' }}>
찾으시는 상품이 품절되었거나 존재하지 않는 페이지입니다.
</p>
<Link href="/products" style={{ color: '#0070f3', textDecoration: 'underline' }}>
전체 상품 목록으로 돌아가기
</Link>
</div>
);
}
(3) 데이터 페칭에서 강제 발생시키기
실제 page.js에서 데이터가 없을 때 404를 유도하는 방법입니다.
import { notFound } from 'next/navigation';
async function getProduct(id) {
const res = await fetch(`https://api.example.com/products/${id}`);
// 데이터가 없으면 즉시 not-found.js를 트리거합니다.
if (res.status === 404) {
notFound();
}
// 기타 에러는 가장 가까운 error.js로 전달됩니다.
if (!res.ok) {
throw new Error('데이터 로드 실패');
}
return res.json();
}
3. 트러블슈팅 및 주의사항 (Trade-offs)
왜 내 error.js가 작동하지 않죠?
- 위치 문제: error.js는 동일한 경로의 layout.js에서 발생한 에러는 잡지 못합니다. 레이아웃까지 감싸려면 상위 경로의 error.js나 global-error.js를 사용해야 합니다.
- 클라이언트 선언: 'use client' 지시어를 누락하면 런타임 에러가 발생합니다. 에러 핸들러는 사용자와의 상호작용(다시 시도 버튼 등)이 필수적이기 때문입니다.
고려해야 할 한계
- Global Error: 루트 레이아웃(app/layout.js)에서 발생하는 에러는 특수한 파일인 app/global-error.js가 필요합니다. 이는 HTML 태그 자체를 다시 렌더링해야 하므로 구현 시 주의가 필요합니다.
- 서버 에러 메시지 보안: 서버에서 발생한 구체적인 에러 스택을 클라이언트(error.js)에 그대로 노출하면 보안상 위험할 수 있습니다. 사용자에게는 정제된 메시지만 보여주세요.
4. 결론 및 제언
error.js와 not-found.js는 단순히 오류 화면을 보여주는 도구가 아닙니다. 이는 사용자에게 **"문제가 발생했지만, 우리가 제어하고 있다"**는 안정감을 주는 장치입니다.
효과적인 에러 핸들링은 단순히 파일을 만드는 것에서 끝나지 않습니다. 발생한 에러를 수집하고 분석하여 실제 코드의 결함을 줄여나가는 선순환 구조를 만드는 것이 중요합니다. 현재 진행 중인 프로젝트에서 모든 API 요청에 대해 예외 케이스를 정의하고 계신가요? 혹은 모든 404 페이지가 사용자에게 유의미한 '돌아갈 길'을 제시하고 있는지 점검해 보시기 바랍니다.
'Frontend > Next.js' 카테고리의 다른 글
| Server Actions를 이용한 폼 데이터 처리와 서버 사이드 로직 (0) | 2026.03.13 |
|---|---|
| Dynamic Routes: 동적 파라미터([id]) 처리와 상세 페이지 (0) | 2026.03.13 |
| Loading UI와 Suspense를 활용한 사용자 경험 개선 (0) | 2026.03.12 |
| page.js에서 데이터 페칭(Fetching)과 캐싱 전략 (0) | 2026.03.12 |
| 서버 컴포넌트(Server)와 클라이언트 컴포넌트(Client)의 구분과 조합 (0) | 2026.03.12 |
- Total
- Today
- Yesterday
- on-device ai
- sLLM
- Rag
- 멀티모달
- TypeScript
- 카카오
- LLM
- 엣지컴퓨팅
- HTML
- java
- MSA
- CSR
- 협력
- react
- CSS
- prompt engineering
- It용어
- 웹기초
- SSR
- AI
- 구글
- 스마트안경
- Nextjs
- Javascript
- HBM
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |