프로젝트 구현에 본격적으로 들어가기 전에 에러를 관리할 방법들을 정리해보려한다
에러 타입에 따라 분류하기
1. 예상된 에러 (Expected Errors)
서비스 정상 작동 중에 발생할 수 있는 에러를 의미한다
ex. 폼 검증 실패, API 요청 오류 등
2. 예상치 못한 예외 (Uncaught Exceptions)
정상 흐름에서 발생하지 않아야하는 예외 상황을 의미한다
서버액션에서 에러 처리하기
서버액션에서 발생하는 에러의 경우 try/catch문을 지양하고 특히 form을 사용하는 경우 useFormState (React 18 이하), useActionState (React 19 이상)를 사용해 모델링하는 것을 권장한다
useFormState 사용 예시
// app/actions.ts
'use server';
import { redirect } from 'next/navigation';
export async function createUser(prevState: any, formData: FormData) {
// API 호출
const res = await fetch('https://api.example.com/users', {
method: 'POST',
body: formData
});
const data = await res.json();
// 예상된 에러를 검증
if (!res.ok) {
return { success: false, message: '유효한 이메일을 입력해주세요.' };
}
// 성공시 리다이렉트
redirect('/dashboard');
}
'use client';
import { useFormState } from 'react-dom';
import { createUser } from '@/app/actions';
const initialState = { success: false, message: '' };
export function Signup() {
const [state, formAction] = useFormState(createUser, initialState);
return (
<form action={formAction}>
<input type='email' name='email' required />
{ /* 예상된 에러에대한 메세지 표시 */ }
{!state.success && state.message && (
<p className='error'>{state.message}</p>
)}
<button type='submit'>가입하기</button>
</form>
)
}
useFormState를 사용하지 않는 경우
useState, useTransition 등을 활용할 수 있다
// app/actions.ts
export async function deleteItem(id) {
const res = await fetch(`/api/items/${id}`, { method: 'DELETE' })
// 예상된 에러 처리
if (!res.ok) {
return { success: false, message: '삭제 실패' }
}
return { success: true }
}
'use client'
import { useState } from 'react'
import { deleteItem } from '@/app/actions' // 서버 액션 import
export default function DeleteItemButton({ id, itemName }) {
const [error, setError] = useState('')
const [success, setSuccess] = useState(false)
async function handleClick() {
setError('')
setSuccess(false)
const result = await deleteItem(id)
if (!result.success) {
setError(result.message)
} else {
setSuccess(true)
}
}
return (
<div>
<button onClick={handleClick}>
삭제하기
</button>
{error && (
<p>{error}</p>
)}
{success && (
<p>성공적으로 삭제되었습니다.</p>
)}
</div>
)
}
컴포넌트에서 에러 처리하기
서버 컴포넌트와 클라이언트 컴포넌트 모두 응답 상태에따라 조건부렌더링을 활용할 수 있다
클라이언트 컴포넌트의 경우 상황에따라 useState 등의 훅을 활용해야하고, 서버 컴포넌트의 경우 Next.js의 redirect(), notFound() 등을 바로 활용할 수 있다는 특징이 있다
서버 컴포넌트 예시
// app/posts/[id]/page.tsx
import { notFound } from 'next/navigation'
export default async function PostPage({ params }) {
const res = await fetch(`https://api.example.com/posts/${params.id}`)
if (!res.ok) {
if (res.status === 404) {
notFound() // 404 에러 페이지 표시
}
// 다른 오류의 경우 오류 메시지 표시
return <div>콘텐츠를 로드하는 데 문제가 발생했습니다</div>
}
const post = await res.json()
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
클라이언트 컴포넌트 예시
'use client'
import { useState, useEffect } from 'react'
export default function UserProfile({ userId }) {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
async function fetchUserData() {
try {
setLoading(true)
const res = await fetch(`/api/users/${userId}`)
if (!res.ok) {
throw new Error('사용자 데이터를 불러올 수 없습니다')
}
const userData = await res.json()
setData(userData)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
fetchUserData()
}, [userId])
if (loading) return <div>로딩 중...</div>
if (error) return <div className="error">{error}</div>
return (
<div className="user-profile">
<h2>{data.name}의 프로필</h2>
{/* 사용자 데이터 표시 */}
</div>
)
}
예상치 못한 예외 처리하기
Next.js에서는 Error boundary를 사용해 예외를 처리할 수 있다
특정 경로에 대한 에러 처리를 위해서 해당 디렉토리에 error.tsx를 추가하거나 전역 에러 처리를 위한 global-error.tsx를 추가해 처리한다
// app/error.tsx
'use client'
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// 에러 로깅 서비스에 에러 보고
console.error('예상치 못한 에러 발생:', error)
}, [error])
return (
<div className="error-container">
<h2>문제가 발생했습니다</h2>
<p>죄송합니다. 예상치 못한 오류가 발생했습니다.</p>
<button
onClick={() => reset()}
className="retry-button"
>
다시 시도하기
</button>
</div>
)
}
// app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<div className="global-error">
<h1>서비스에 문제가 발생했습니다</h1>
<p>죄송합니다. 시스템 오류가 발생했습니다.</p>
<button onClick={() => reset()}>새로고침</button>
</div>
</body>
</html>
)
}
'Next.js' 카테고리의 다른 글
[Next.js / Tanstack Query] 내 prefetchQuery가 작동하지 않는 이유 (1) | 2024.11.19 |
---|---|
[Vercel] 기본 도메인을 커스텀도메인으로 redirection하기 (0) | 2024.11.12 |
[Next.js + tailwind] tailwind public 폴더에서 버튼 backgroundImage 설정하기 (0) | 2024.10.30 |
[Next.js] prefetching이 잘 되었는지 확인하는 방법 (0) | 2024.10.21 |
[Next.js] SSR 이해하기 (1) | 2024.10.07 |