메인 홈
home
사이트 맵 - 한눈에
home

Next.js 에러 완벽 해결 가이드: 초보자도 쉽게 이해하는 모든 에러 종류와 해결법

Next.js로 개발하다 보면 갑자기 빨간색 에러 메시지가 화면을 가득 채우는 경험,
한 번쯤은 해보셨을 겁니다. 당황스럽고 막막하기만 한 그 순간, "도대체 뭐가 문제인 거야?"라는 생각이 들죠. 오늘은 Next.js에서 발생할 수 있는 모든 에러를 체계적으로 정리하고, 각각의 해결 방법을 누구나 이해할 수 있도록 쉽게 설명해드리겠습니다.

1. Next.js 에러의 두 가지 큰 분류

Next.js의 에러는 크게 두 가지로 나눌 수 있습니다. 마치 감기와 교통사고의 차이처럼, 예상할 수 있는 에러와 예상치 못한 에러가 있습니다.
1.
예상된 에러 (Expected Errors)
사용자가 잘못된 데이터를 입력했을 때
서버에서 데이터를 가져오는데 실패했을 때
권한이 없는 페이지에 접근했을 때
네트워크 연결이 끊겼을 때
2.
예상치 못한 에러 (Uncaught Exceptions)
코드에 버그가 있을 때
잘못된 문법을 사용했을 때
존재하지 않는 변수를 참조했을 때
메모리 부족 등 시스템 문제

2. 예상된 에러 처리하기: 우아하게 실패하는 방법

예상된 에러는 앱이 정상적으로 작동하는 중에도 충분히 일어날 수 있는 일들입니다. 이런 에러들은 사용자에게 친절하게 안내해주는 것이 중요합니다.

서버 액션에서의 에러 처리

1.
잘못된 방법 (try-catch 사용)
'use server' // 이렇게 하지 마세요! export async function createPost(formData) { try { const res = await fetch('<https://api.example.com/posts>', { method: 'POST', body: formData }) return { success: true } } catch (error) { throw error // 이렇게 에러를 던지면 안 됩니다 } }
JavaScript
복사
2.
올바른 방법 (에러를 반환값으로 처리)
'use server' export async function createPost(prevState, formData) { const title = formData.get('title') const content = formData.get('content') // 입력값 검증 if (!title || title.length < 5) { return { message: '제목은 5글자 이상 입력해주세요', type: 'error' } } const res = await fetch('<https://api.example.com/posts>', { method: 'POST', body: JSON.stringify({ title, content }) }) if (!res.ok) { return { message: '게시글 작성에 실패했습니다. 잠시 후 다시 시도해주세요.', type: 'error' } } return { message: '게시글이 성공적으로 작성되었습니다!', type: 'success' } }
JavaScript
복사
3.
클라이언트에서 에러 표시하기
'use client' import { useActionState } from 'react' import { createPost } from '@/app/actions' export function PostForm() { const [state, formAction, isPending] = useActionState(createPost, {}) return ( <form action={formAction}> <input name="title" placeholder="제목을 입력하세요" /> <textarea name="content" placeholder="내용을 입력하세요" /> {state?.message && ( <div className={state.type === 'error' ? 'text-red-500' : 'text-green-500'}> {state.message} </div> )} <button disabled={isPending}> {isPending ? '작성 중...' : '게시글 작성'} </button> </form> ) }
JavaScript
복사

3. 404 에러 처리: 페이지를 찾을 수 없을 때

웹사이트에서 가장 흔한 에러 중 하나가 바로 404 에러입니다. Next.js에서는 이를 아주 우아하게 처리할 수 있습니다.
1.
notFound 함수 사용하기
import { notFound } from 'next/navigation' export default async function BlogPost({ params }) { const post = await getPost(params.id) // 게시글이 없으면 404 페이지로 이동 if (!post) { notFound() } return ( <article> <h1>{post.title}</h1> <p>{post.content}</p> </article> ) }
JavaScript
복사
2.
커스텀 404 페이지 만들기
// app/not-found.js export default function NotFound() { return ( <div className="flex flex-col items-center justify-center min-h-screen"> <h1 className="text-6xl font-bold">404</h1> <p className="text-xl mt-4">! 페이지를 찾을 수 없습니다</p> <a href="/" className="mt-6 text-blue-500 hover:underline"> 홈으로 돌아가기 </a> </div> ) }
JavaScript
복사

4. 예상치 못한 에러 처리: Error Boundary 활용하기

예상치 못한 에러는 앱을 완전히 멈추게 할 수 있습니다. 이때 Error Boundary를 사용하면 에러가 발생해도 전체 앱이 죽지 않고 특정 부분만 에러 화면을 보여줄 수 있습니다.
1.
기본 Error Boundary 만들기
// app/dashboard/error.js 'use client' // Error boundary는 반드시 클라이언트 컴포넌트여야 합니다 import { useEffect } from 'react' export default function Error({ error, reset }) { useEffect(() => { // 에러 로깅 서비스에 에러 전송 console.error('에러 발생:', error) }, [error]) return ( <div className="flex flex-col items-center justify-center p-8"> <h2 className="text-2xl font-bold mb-4">문제가 발생했습니다!</h2> <p className="text-gray-600 mb-6"> 예상치 못한 오류가 발생했습니다. 불편을 드려 죄송합니다. </p> <button onClick={reset} className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" > 다시 시도하기 </button> </div> ) }
JavaScript
복사
2.
전역 에러 처리하기
// app/global-error.js 'use client' export default function GlobalError({ error, reset }) { return ( <html> <body> <div className="min-h-screen flex items-center justify-center"> <div className="text-center"> <h1 className="text-4xl font-bold mb-4">시스템 오류</h1> <p className="mb-6">심각한 오류가 발생했습니다.</p> <button onClick={reset} className="btn-primary"> 페이지 새로고침 </button> </div> </div> </body> </html> ) }
JavaScript
복사

5. 자주 발생하는 Next.js 에러와 해결법

5-1. Module not found 에러

1.
증상
Module not found: Can't resolve '@/components/Header'
Plain Text
복사
2.
원인
파일 경로가 잘못됨
파일명 대소문자 불일치
import 경로 오타
3.
해결법
// 잘못된 예 import Header from '@/components/header' // 파일명은 Header.js인데 소문자로 import // 올바른 예 import Header from '@/components/Header'
JavaScript
복사

5-2. Hydration 에러

1.
증상
Error: Hydration failed because the initial UI does not match what was rendered on the server
Plain Text
복사
2.
원인
서버와 클라이언트의 렌더링 결과가 다름
브라우저 전용 API를 서버에서 사용
조건부 렌더링의 불일치
3.
해결법
// 잘못된 예 export default function Component() { return ( <div> {typeof window !== 'undefined' && <ClientOnlyComponent />} </div> ) } // 올바른 예 'use client' import { useEffect, useState } from 'react' export default function Component() { const [isClient, setIsClient] = useState(false) useEffect(() => { setIsClient(true) }, []) return ( <div> {isClient && <ClientOnlyComponent />} </div> ) }
JavaScript
복사

5-3. Invalid Hook Call 에러

1.
증상
Error: Invalid hook call. Hooks can only be called inside of the body of a function component
Plain Text
복사
2.
원인
서버 컴포넌트에서 훅 사용
조건문 안에서 훅 호출
일반 함수에서 훅 사용
3.
해결법
// 잘못된 예 - 서버 컴포넌트에서 useState 사용 export default function ServerComponent() { const [count, setCount] = useState(0) // 에러! return <div>{count}</div> } // 올바른 예 - 클라이언트 컴포넌트로 변경 'use client' import { useState } from 'react' export default function ClientComponent() { const [count, setCount] = useState(0) return <div>{count}</div> }
JavaScript
복사

6. API 라우트 에러 처리

API 라우트에서도 에러 처리는 매우 중요합니다.
1.
기본적인 에러 처리
// app/api/users/route.js export async function GET(request) { try { const users = await fetchUsers() if (!users) { return Response.json( { error: '사용자를 찾을 수 없습니다' }, { status: 404 } ) } return Response.json(users) } catch (error) { console.error('사용자 조회 실패:', error) return Response.json( { error: '서버 오류가 발생했습니다' }, { status: 500 } ) } }
JavaScript
복사
2.
상세한 에러 응답
export async function POST(request) { try { const body = await request.json() // 유효성 검사 if (!body.email) { return Response.json( { error: '이메일은 필수 입력 항목입니다', field: 'email' }, { status: 400 } ) } const user = await createUser(body) return Response.json(user, { status: 201 }) } catch (error) { if (error.code === 'P2002') { // Prisma unique constraint 에러 return Response.json( { error: '이미 사용 중인 이메일입니다' }, { status: 409 } ) } return Response.json( { error: '사용자 생성 실패' }, { status: 500 } ) } }
JavaScript
복사

7. 이벤트 핸들러에서의 에러 처리

Error Boundary는 이벤트 핸들러의 에러를 잡지 못합니다. 따라서 별도로 처리해야 합니다.
1.
수동 에러 처리
'use client' import { useState } from 'react' export function UploadButton() { const [error, setError] = useState(null) const [isLoading, setIsLoading] = useState(false) const handleUpload = async () => { setError(null) setIsLoading(true) try { const response = await uploadFile() if (!response.ok) { throw new Error('파일 업로드 실패') } // 성공 처리 } catch (err) { setError(err.message) } finally { setIsLoading(false) } } return ( <div> <button onClick={handleUpload} disabled={isLoading}> {isLoading ? '업로드 중...' : '파일 업로드'} </button> {error && ( <p className="text-red-500 mt-2">{error}</p> )} </div> ) }
JavaScript
복사

8. 에러 모니터링과 로깅

프로덕션 환경에서는 에러를 추적하고 모니터링하는 것이 중요합니다.
1.
에러 로깅 서비스 연동
// app/error.js 'use client' import * as Sentry from '@sentry/nextjs' import { useEffect } from 'react' export default function Error({ error, reset }) { useEffect(() => { // Sentry에 에러 전송 Sentry.captureException(error) }, [error]) return ( <div> <h2>오류가 발생했습니다</h2> <button onClick={reset}>다시 시도</button> </div> ) }
JavaScript
복사
2.
커스텀 에러 로깅
// utils/errorLogger.js export function logError(error, context = {}) { const errorInfo = { message: error.message, stack: error.stack, timestamp: new Date().toISOString(), userAgent: typeof window !== 'undefined' ? window.navigator.userAgent : 'server', ...context } // 개발 환경에서는 콘솔에 출력 if (process.env.NODE_ENV === 'development') { console.error('Error logged:', errorInfo) } else { // 프로덕션에서는 로깅 서비스로 전송 sendToLoggingService(errorInfo) } }
JavaScript
복사

9. 에러 예방을 위한 베스트 프랙티스

에러를 처리하는 것도 중요하지만, 애초에 에러가 발생하지 않도록 예방하는 것이 더 중요합니다.
1.
TypeScript 사용하기
// 타입 정의로 많은 에러를 미리 방지 interface User { id: string name: string email: string } async function getUser(id: string): Promise<User | null> { // TypeScript가 반환 타입을 체크해줍니다 const user = await fetchUser(id) return user }
TypeScript
복사
2.
입력값 검증하기
// Zod를 사용한 스키마 검증 import { z } from 'zod' const userSchema = z.object({ name: z.string().min(2, '이름은 2글자 이상이어야 합니다'), email: z.string().email('올바른 이메일 형식이 아닙니다'), age: z.number().min(1).max(120) }) export async function createUser(data) { try { const validatedData = userSchema.parse(data) // 검증된 데이터로 처리 진행 } catch (error) { if (error instanceof z.ZodError) { return { error: error.errors[0].message } } } }
JavaScript
복사
3.
로딩 상태 관리
'use client' import { Suspense } from 'react' function LoadingSpinner() { return <div className="spinner">로딩 중...</div> } export default function Page() { return ( <Suspense fallback={<LoadingSpinner />}> <AsyncComponent /> </Suspense> ) }
JavaScript
복사
Next.js에서 에러는 피할 수 없는 존재입니다.
하지만 이제 여러분은 다양한 종류의 에러를 어떻게 처리해야 하는지 알게 되었습니다. 예상된 에러는 사용자에게 친절하게 안내하고, 예상치 못한 에러는 Error Boundary로 우아하게 처리하세요. 무엇보다 중요한 것은 에러가 발생했을 때 당황하지 않고 차근차근 원인을 파악하는 것입니다.
개발하다가 에러를 만나면 이 가이드를 다시 찾아보세요. 분명 해결책을 찾을 수 있을 것입니다. Happy Coding!