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
복사

