๐ Next.js 8์ฅ: Error Handling โ ๋ด ์ฑ์ ์งํค๋ ์ตํ์ ๋ฐฉ์ด์
๐ ๊ฐ์
error.tsx, not-found.tsx, ErrorBoundary๋ฅผ ์กฐํฉํด ์ฑ์ ์์ ํ๊ฒ ์งํค๋ ๋ฐฉ๋ฒ์ ๋ค๋ฃน๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐งฉ
error.tsx์ ๋ง๋ฒ: Error Boundary์ ์งํ ๐ข - ๐ฑ ์๋ฌ์ ์ง์ญํ (Localization) ์ ๋ต ๐ก
- ๐ฏ ํ ๊ฑธ์ ๋: ์ปดํฌ๋ํธ ๋จ์์ ์ ๋ฐ ํ๊ฒฉ ์๋ฌ ์ฒ๋ฆฌ
- ๐จ ์๋ฒ ๋ค์ด ์ตํ์ ๋ณด๋ฃจ:
global-error.tsx๐ด - ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 15๋ถ(์ ์ฒด) / ํต์ฌ ํํธ๋ง: 8๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
RSC ์๋ฌ ํญ๋ฐ์ ์ํ์ฑ โ error.tsx ๋ฐฉ์ด๋ง ์๋ฆฌ โ ์๋ฌ๋ฅผ ์๊ฒ ๋ถ์์ด ๊ฒฉ๋ฆฌํ๋ ์๋์ด ์ํคํ
์ฒ
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
-
try-catch๋ก๋ง ๋ก์น ํ๋ ํ์ ์ฝ๋์์ ๋ฒ์ด๋, ์ ์ธํ ์ง์์ ํ์ผ(error.tsx) ํ๋๋ก ํด๋ ๋จ์ ์๋ฌ๋ฅผ ์ฐ์ํ๊ฒ ์ ์ํ ์ ์๋ค. - ์๋ฌ๊ฐ ๋ ๊ณณ๋ง ๋ฒํผ์ผ๋ก
reset()ํจ์๋ฅผ ๋ถ๋ ค ํ์ด์ง ๊ฐ์ ์๋ก๊ณ ์นจ ์์ด ํด๋น ํํธ ์ปดํฌ๋ํธ๋ง ํ๋ณต์ํค๋ ๊ณ ๊ธ ๋ ๋ ์ปจํธ๋กค์ ๊ตฌํํ ์ ์๋ค.
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ์์ฒ (์๋ก ์จ ์ฃผ๋์ด): "์ด? ์ฌ์ด๋๋ฐ์์ ์ ์ ๋ญํน ๊ฐ์ ธ์ค๋ API ํ๋ ํฐ์ก๋ค๊ณ ์ฌ์ดํธ ์ ์ฒด๊ฐ ์์ฒญ ๋ฌด์์ด ์๋ฌ ์คํฌ๋ฆฐ์ผ๋ก ๋๋ฐฐ๋๋ฉด์ ๋ป์ด๋ฒ๋ ค์! ๋ฉ์ฉกํ ๋ค๋ฅธ ๊ฒ์ํ ๊ธ๋ค๋ ์ ๋ณด์ด๊ณ ์์ ๋ง๋น์์! ๐ญ"
- ์ํธ(FE ๋ฆฌ๋): "์์ฒ ๋... React ํธ๋ฆฌ๋ ์ปดํฌ๋ํธ ํ๋๊ฐ ์๋ฌ๋ก ํฐ์ง๋ฉด ์๊ธฐ ๋ถ๋ชจ ํ์ ๊น์ง ๋ค ์ฃฝ๋ ๋๋ฐ ์์ด(?) ์ ๋ฒ์น์ด ์๋จ ๋ง์ด์์! ๋ฅ์คํธ๊ฐ ์ ๊ณตํ๋ ๋ฐฉํ๋ฒฝ(
error.tsx) ํ์ผ๋ง ํญ ๋์ ธ๋จ์ด๋ ๋ญํน ์์ญ ํ๋๋ง ์ฐ์ํ๊ฒ ์ฃฝ๊ณ ๋ค๋ฅธ ๊ณณ์ ์ด์์ ํ ๋ฐ!"
๐ค ์ ์์์ผ ํ๋๊ฐ
์ค๋๋ SPA ๋ฐฉ์์์๋ API ์๋ฌ๊ฐ ๋๋ฉด ์ฃผ๋ก catch ์์์ alert("์๋ฌ์
๋๋ค") ๋์ฐ๊ณ ๋๋ฌ์ด.
ํ์ง๋ง Next.js์ ๋์ธ์ธ React Server Component(RSC) ์ธ์์ ์์ ํ ๋ฌ๋ผ.
์๋ฒ์์ ์กฐ๋ฆฝ๋๋ HTML ์ชผ๊ฐ๋ฆฌ๋ค์ด await db() ํ๋ค๊ฐ 1์ด ๋ค์ ํ! ํ๊ณ ํฐ์ ธ๋ฒ๋ ค(Exception Throw).
Next.js ์๋ฒ๊ฐ ์๋ฌ๋ฅผ ๋ฟ์ผ๋ฉด ์ด ๋ฌด์๋ฌด์ํ ์๋ฌ ๋ฉ์ด๋ฆฌ๊ฐ ๊ฑท์ก์ ์ ์์ด ์์ ๋ถ๋ชจ ์ปดํฌ๋ํธ๋ก ์ญ๋ฅํ๋ฉฐ ์ฌ๋ผ๊ฐ. ๋จ๋จํ ๋๊ป(Error Boundary)์ผ๋ก ์ด๋ ์ค๊ฐ ์ง์ ์์ ๋ฑ! ๋ง์๋ด์ง ์์ผ๋ฉด, ๊ฒฐ๊ตญ ์ต์๋จ ๋ฃจํธ ๋ ์ด์์๋ง์ ํญํ์ํค๋ฉฐ ์ฑ ์ ์ฒด๊ฐ ์ํ์ "Internal Server Error" ๋ฌด๋ค ์คํฌ๋ฆฐ์ผ๋ก ๋๋ณํด.
๊ทธ ์์ ์ฐ๊บผ๊ธฐ ์๋ฌ ํ๋ ๋๋ฌธ์ ๋ฉ์ฉกํ๊ฒ ๋์๊ฐ๋ ์ฌ์ดํธ ๋ฉ์ธ GNB์ ์ฌ์ด๋ํญ๋ง์ ์ฆ๋ฐํด๋ฒ๋ฆฌ๋ ๊ฒ์ ์ต์
์ UX(์ฌ์ฉ์ ๊ฒฝํ)์ผ.
์ด ๊ฑฐ๋ํ ๋๋ฏธ๋
ธ ๋ถ๊ดด๋ฅผ ๋ง์๋ด๋ ๋ฅ์คํธ ํ์ผ ์์คํ
์์ฝ์ด๊ฐ ๋ฐ๋ก error.tsx์ผ.
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด? (์์๋ค ๊ตญ๋ฐฅ์ง์ ๋ฐฉํ ์ ํฐ)
โ ๊ณผ๊ฑฐ (์์ฒ ์ด์ ์นธ๋ง์ด ์๋ ์๋น)
์ฃผ๋ฐฉ ํ๊ตฌ(API) ์ค ํ๋๊ฐ ๊ณผ์ด๋์ด ๋ถ์ด ๋ฌ์ด. ๊ทธ๋ฐ๋ฐ ๋ฐฉ์ ๊ฒฉ๋ฒฝ์ด๋ ์นธ๋ง์ด๊ฐ ํ๋๋ ์๋ค?
๋ถ๊ธธ์ด ํ์ฅ์ค์ ๋์ด ๋ณต๋๋ก ๊ฐ๊ณ , ์นด์ดํฐ๊น์ง ๋ฒ์ง๋๋ ์์๊ฐ์ ์๋น ์ ์ฒด(์ฑ ์ ์ฒด) ๊ฐ ํ๋ฒ๋ ค ๋ฉธ๋งํด.โ ํ์ฌ (
error.tsx์ ์ธํ ๋ฐฉํ ์ ํฐ)
์ฃผ๋ฐฉ์ฅ ์ํธ ๋์ด ์ฃผ๋ฐฉ ๊ตฌ์ญ๋ง๋ค, ๊ทธ๋ฆฌ๊ณ ๋ณต๋ ์ ๊ตฌ๋ง๋ค ํผํผํ ์ฒ ์ ์ ํฐ(๋ฐฉํ ๊ฒฉ๋ฒฝ = error.tsx) ๋ฅผ ํ๋์ฉ ์ค์นํด๋์ด.
์ฃผ๋ฐฉ์์ ๋ถ์ด ๋๋ฉด ์์ฉ์์ฉ! ์๋ฆฌ์ ํจ๊ป ๊ทธ ์ฆ์ ์ ์ผ ๊ฐ๊น์ด ์ ํฐ๊ฐ ์ฒ ์ปน! ํ๊ณ ๋ด๋ ค๊ฐ์ ๋ถ๊ธธ์ ๊ฐ๋ฌ๋ฒ๋ ค.
์ฃผ๋ฐฉ ์ผ๋ถ ๊ตฌ์ญ์ ์๋ฆฌํ๋๋ผ ๋ชป ์ฐ๊ฒ ์ง๋ง(fallback์๋ฌ UI) , ํ์์ ๊ตญ๋ฐฅ์ ๋จน๋ ์๋๋ค์ ์๋ฌด ์ผ ์๋ค๋ ๋ฏ ๋ง์๊ฒ ์์ฌ๋ฅผ ๊ณ์ํ ์ ์์ด!
๐งฉ error.tsx์ ๋ง๋ฒ: Error Boundary์ ์งํ ๐ข
๊ธฐ์กด React ๊ฐ๋ฐ์๋ค์ด Error Boundary ๋ฉํ๊ธฐ(Class Component)๋ฅผ ์์ ์ง๋ฉฐ ๊ณ ํต๋ฐ์๋ค๋ฉด, Next.js์์๋ ๊ทธ๋ฅ ํฐ์ง ๊ฒ ๊ฐ์ ํด๋์ ์ ์ด๋ฆ์ ํ์ผ์ "๋ง๋ค๊ธฐ๋ง ํ๋ฉด" ๋ผ.
1) ๋ฐฉ์ด๋ง ๊ธฐ๋ณธ ์๋ฆฌ
app/dashboard/ ํ์ด์ง๊ฐ ํฐ์ง๋ค๊ณ ๊ฐ์ ํ์.
app/
โโ dashboard/
โโ layout.tsx (1. ์ด๋๋ง์ ํฐ์ง๋ฉด ๋ ์์ชฝ์ผ๋ก ์ญ๋ฅํด ์ฌ๋ผ๊ฐ๋ฒ๋ฆฐ๋ค)
โโ error.tsx (2. ๊ฐ๋ ฅํ ๋ฐฉํญ๋ฌธ! ์ด ํ์ผ์ด ์กด์ฌํ๋ ํ page.tsx์ ํญ๋ฐ์ ๋ฌธ ๋ฐ์ ๋๊ฐ์ง ๋ชปํจ)
โโ page.tsx (3. ์๋ฌ ์ ๋ฐ์! ๋ด๋ถ์์ DB ์กฐํํ๋ค ๋ป์)๋ง์ฝ ํฐ์ง๋ฉด, Next.js๊ฐ page.tsx UI๋ฅผ ๋ ๋ ๋ฐ์ค์์ ์น ๋์ด๋ด๋ฒ๋ฆฌ๊ณ , ๊ทธ ์๋ฆฌ์ ๋๊ฐ error.tsx ํ์ผ์์ ๋ฆฌํด์ํจ Fallback UI ์ปดํฌ๋ํธ๋ก ์! ๋์ฒดํด๋ฒ๋ ค.
2) ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ ์ ์ธ ๊ฐ์ ๊ท์ฝ (use client)
โ ๏ธ ๊ฐ์ฅ ์ด๋ณด์๋ค์ด ๋ง์ด ํ๋ ์ค์ 1์์.
error.tsx๋ ๋ฌด์กฐ๊ฑด ์ฒซ ์ค์'use client'์ง์์ ๊ฐ ๋ฌ๋ ค ์์ด์ผ ํด.
์์ผ๊น? ์๋ฌ๋ ์ธ์ ์ด๋ ์๋๋ฆฌ์ค์์ ํฐ์ง์ง ๋ชฐ๋ผ.
์๋ฒ์์ HTML์ ์กฐ๋ฆฝํ๋ค๊ฐ(RSC) ํฐ์ง ์๋ ์๊ณ , ๋ด ํฐ์์ onClick ๋๋ฅด๋ค ๋ฐํ์์(Client) ํฐ์ง ์๋ ์์ง. ๋ชจ๋ ์๋ฌ(Server, Client ๋ฌด๊ด)๋ฅผ ์ก์์ฑ๊ณ ๋ธ๋ผ์ฐ์ ์ฝ์์์ Recovery(๋ณต๊ตฌ ๋ฒํผ) ๋์๊น์ง ์ง์ํ๋ ค๋ฉด ์ด ํ์ผ ์์ฒด๊ฐ ํด๋ผ์ด์ธํธ ๋ฒ ์ด์ค์์ ๊ตฌ๋๋์ด์ผ ํ๊ธฐ ๋๋ฌธ์ด์ผ.
// app/dashboard/error.tsx
'use client' // โค๏ธ ์์ผ๋ฉด ๋น๋ ์๋ฌ ๋จ!
import { useEffect } from 'react'
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string } // ์๋ฌ ์ ๋ณด ๊ฐ์ฒด
reset: () => void // ํญ๋ฐํ๋ page.tsx ์์ญ์ ์ฌ๋ ๋๋ง ์๋ํ๋ ๋ง๋ฒ์ ๋ฒํผ
}) {
useEffect(() => {
// Sentry, Datadog ๋ฑ์ผ๋ก ๋ชฐ๋ ์๋ฌ ์ ๋ฌธ์ ์์งํด ์ฌ๋ฆฐ๋ค
console.error('์! ๋์๋ณด๋์์ ์์ฒญ๋ ์๋ฌ๊ฐ:', error)
}, [error])
return (
<div className="bg-red-50 p-4 rounded-md">
<h2>๋ญ๊ฐ ๋จ๋จํ ์๋ชป๋์ด์. (๋ฐฉ์ ๊ฒฉ๋ฒฝ ๋ซํ)</h2>
{/* ๋ค์ ์๋ํ๊ธฐ ๋ฒํผ ํด๋ฆญ ์, ์ปดํฌ๋ํธ๋ง ๊ฐ์ ๋ก ์ฌ์์ฒญ (์ ์ฒด ์๋ก๊ณ ์นจ ๋ฌด๊ด) */}
<button onClick={() => reset()}>
๋ฌธ ๋ค์ ํ ๋ฒ ์ด์ด๋ณด๊ธฐ
</button>
</div>
)
}๐ฑ ์๋ฌ์ ์ง์ญํ (Localization) ์ ๋ต ๐ก
์ด์ ๋ฉด์ ๋จ๊ณจ ์ง๋ฌธ์ด์ ์๋์ด ์ํคํ
์ฒ์ ํต์ฌ ๋๋ฉ์ธ์ผ๋ก ๋ค์ด๊ฐ๋ณผ๊น?
"๋ ์ด์์ ์์์ ์๋ก ๋ค๋ฅธ ๋ ๊ฐ์ API๋ฅผ ๋ถ๋ ๋๋ฐ, ํ๋๋ง ํฐ์ก์ด. ๋ ์ด์์๊ณผ ์ ์ API ๋ทฐ๋ ์ด๋ฆด ์ ์๋๊ฐ?"
โ ์์ฒ ์ด์ ํต์ง ์๋ฌ ์ฒ๋ฆฌ (์์ ๋ฃจํธ๋ก ๋ชฝ๋ ์๋ฌ ๋ ๋๊ธฐ๊ธฐ)
์์ฒ ์ด๋ ๊ทธ๋ฅ ๊ท์ฐฎ์์ ๋ฃจํธ ํด๋(app/error.tsx) ํ๋๋ง ๋ง๋ค์ด ๋์ด.
๊ทธ๋์ ๋ ์ค์ํ ์ฌ์ด๋๋ฐ ์์ ฏ ํต์ ํ๋ ๋ป์๋๋ฐ, ๋ฉ์ธ ๊ฒ์๊ธ๊ณผ ํค๋ ์ ์ฒด ํธ๋ฆฌ๊น์ง ์ ๋ถ ์ํ์๊ฒ ๋ ์๊ฐ๋ฉด์ ์ ์๋ฌ ๋ฐฉ๋ฒฝ ํ
์คํธ๋ง ๋๋ ๋ ๋ฒ๋ฆฌ๊ฒ ๋์ง.
โ ์ํธ์ ๊ตญ์ง์ ๋ฐฉ๋ฒฝ ์ค๊ณ (ํํธ ์ชผ๊ฐ๊ธฐ + Suspense ์กฐํฉ)
export default function CommunityPage() {
return (
<article className="layout_grid">
<MainHeader />
{/* โ ๏ธ ๋ฉ์ธ ๊ฒ์ํ: ๋งค์ฐ ์ค์!! ์ฌ๊ธฐ์ ํฐ์ง๋ฉด ์ ์ฒด error.tsx ๊ฐ ์ก๋๋ก ๋
๋๋ค */}
<MainPostList />
{/* ๐ก๏ธ ๋ ์ค์ํ ์ฌ์ด๋๋ฐ ์์ ฏ:
์ฌ๊ธฐ๋ ํฐ์ ธ๋ ์ฌ์ดํธ ์ด์ฉ์ ๋ฌด๊ดํ๋ฏ๋ก ๋ณ๋์ ๋๋ฉ์ธ ํด๋๋ก ๋นผ์ ๊ฐ๋ณ error.tsx ๊ดํ ๊ถ์ผ๋ก ๋๊ฑฐ๋,
์๋ <ErrorBoundary> ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ตฌ์ญ์ผ๋ก ๋ฌถ๊ณ fallback UI๋ฅผ ์กฐ์ฉํ ๋์ ๋ฉ์ธ ํ๋ฉด์ ์ด๋ฆฐ๋ค! */}
<aside>
<SidebarRankingWidget />
</aside>
</article>
)
}๐ฏ ํ ๊ฑธ์ ๋: ์ปดํฌ๋ํธ ๋จ์์ ์ ๋ฐ ํ๊ฒฉ ์๋ฌ ์ฒ๋ฆฌ
์์ฒ ์ด๋ ๊ถ๊ธํด์ก์ด. "์ํธ ๋ฆฌ๋ ๋, ๊ทธ๋ผ error.tsx ํ์ผ ์์ด๋ ํน์ ์ปดํฌ๋ํธ ํ๋๋ง ๋ฑ! ์ก์์ ๊ฒฉ๋ฆฌํ ์ ์๋์? ํด๋๋ฅผ ๋งค๋ฒ ์๋ก ๋ง๋ค๊ธด ์ข ๋ฒ๊ฑฐ๋ก์์์."
์ํธ ๋ฆฌ๋ ๋์ด ์์ผ๋ฉฐ ๋๋ตํ์ง. "๋น์ฐํ ์์ฃ ! error.tsx๋ ๋ผ์ฐํธ ๋จ์์ ํฐ ๊ฒฉ๋ฒฝ์ด๋ผ๋ฉด, ์ฐ๋ฆฌ๊ฐ ์ง์ ErrorBoundary ์ปดํฌ๋ํธ๋ก ๊ฐ์ธ๋ฉด ๊ฐ๋ณ ์ปดํฌ๋ํธ ๋จ์๋ก ์ ๋ฐ ํ๊ฒฉ ๋ฐฉ์ด๊ฐ ๊ฐ๋ฅํด์."
1) react-error-boundary ํ์ฉ (๊ถ์ฅ)
๊ฐ์ฅ ์ฐ์ํ ๋ฐฉ๋ฒ์ ๊ฒ์ฆ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฐ๋ ๊ฑฐ์ผ. ํน์ ์ปดํฌ๋ํธ๊ฐ ํฐ์ ธ๋ ํ์ด์ง ์ ์ฒด๊ฐ ํ์๊ฒ ๋ณํ๋ ๊ฑธ ๋ง์์ฃผ์ง.
'use client'
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({ error, resetErrorBoundary }: any) {
return (
<div className="p-2 border border-red-200 bg-red-50 text-xs text-red-600">
<p>์์ ฏ ๋ก๋ ์คํจ ๐ญ</p>
<button onClick={resetErrorBoundary} className="underline">๋ค์ ์๋</button>
</div>
)
}
export default function Sidebar() {
return (
<aside>
<h3>์ค์๊ฐ ๋ญํน</h3>
{/* ๐ก๏ธ ์ด ์์ ฏ ํ๋๊ฐ ํฐ์ ธ๋ ์ฌ์ด๋๋ฐ ์ ์ฒด์ ๋ฉ์ธ ํ์ด์ง๋ ๋ฌด์ฌํด! */}
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// ๋ค์ ์๋ํ ๋ ์ํํ ๋ก์ง
}}
>
<SidebarRankingWidget />
</ErrorBoundary>
</aside>
)
}์ด๋ ๊ฒ ํ๋ฉด error.tsx๊ฐ ๋ด๋นํ๋ 'ํ์ด์ง ์ ์ฒด' ํน์ 'ํด๋ ์ ์ฒด' ๋จ์์ ๊ฑฐ์์ ๋ฐฉ์ด์, ์ปดํฌ๋ํธ ๋จ์์ ๋ฏธ์์ ๋ฐฉ์ด๋ฅผ ์กฐํ๋กญ๊ฒ ์์ด์ "์ ๋ ์ฃฝ์ง ์๋ ์ฑ" ์ ๋ง๋ค ์ ์์ด! ๐ฃ
ํต์ฌ ๋ฉํ ๋ชจ๋ธ: ๋ฐฉํญ๋ฌธ(error.tsx)์ ์ด๋ ๊ณ์ธต(ํด๋ ๋ ๋ฒจ)์ ๋ ๊ฒ์ธ๊ฐ?
์๋ฌ๊ฐ ๋ ๊ณณ์ error.tsx๊ฐ ์๋ค๋ฉด? ์๋ฌ ๋
์๊ณผ ํ๊ดด์ ํญํ์ ์๊ธฐ๋ฅผ ๋ง์์ฃผ๋ ๊ฐ์ฅ ๊ฐ๊น์ด ์์ ํด๋์ error.tsx๋ฅผ ๋ง๋ ๋๊น์ง ๋์์ด ๋ถ๋ชจ๋ฅผ ์ฐข์ผ๋ฉฐ ์ญํ(Bubble Up) ํ๋ค.
๋ฐ๋ผ์, ์ด๋ ค์ผ ํ๋ ํต์ฌ ๊ธฐ๋ฅ(Layout)์ ๋ด๋ถ์ ๋ฐฉ๋ฒฝ์ ์ค๋ฐ์กฐ๋ฐํ๊ฒ ๊น์์ฃผ๋ ๊ฒ์ด ํํธํ ์ค๊ณ์ ๊ฝ์ด๋ค!
๐จ ์๋ฒ ๋ค์ด ์ตํ์ ๋ณด๋ฃจ: global-error.tsx ๐ด
์ง๋ฌธ ํ๋ ํด๋ณด์.
"๋ง์ฝ์ ์ ์ผ ๋ฐ๊นฅ ๊ป๋ฐ๊ธฐ ๊ธฐ๋ฅ์ธ app/layout.tsx (html, body ํ๊ทธ๊ฐ ์ ์ธ๋ ๊ทธ๊ณณ!) ์์ฒด์์ ์ฝ๋ ์ง๋ค๊ฐ ์๋ฌ๊ฐ ํฐ์ง๋ฉด ์ด๋กํด? ์ ์ผ ๋์ ๊ณณ์ด๋๊น ๊ฑ๋ฅผ ๊ฐ์ธ์ฃผ๋ ๋ถ๋ชจ(์์) error.tsx๊ฐ ์ธ์์ ์กด์ฌํ์ง ์์ ํ
๋ฐ?!"
์ ๋ต์ด์ผ. app/error.tsx ์กฐ์ฐจ๋ ๋ฃจํธ layout.tsx์ ์๋ฌ๋ฅผ ๋ฐฉ์ดํด๋ด์ง ๋ชปํด. (์ค์ง layout ๋ด๋ถ์ ํ์ด์ง๋ค๋ง ๋ฐฉ์ดํจ).
๊ฐ์ฅ ๋ฌด๊ฑฐ์ด ์ , ๊ฐ์ฅ ์ต์์ layout.tsx์์ ํฐ์ง ์ ์ฒด์ ๋ช
์ ์๋ฌ๋ฅผ ๋ด์๋ด๋, ๋ง ๊ทธ๋๋ก ์ง์ง ์จ๊ฒจ์ง ๋ง์ง๋ง ๋ธ๋๋ฐ์ค๊ฐ ์์ฝ์ด๋ก ์กด์ฌํด.
// app/global-error.tsx (์ต์๋จ ๋ฃจํธ ๊ตฌ์ญ์๋ง ์์ฑ ๊ฐ๋ฅ)
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
// โ ๏ธ ์ฌ๊ธด layout์ด ํฐ์ก์ ๋ ๋์ฒด๋๋ ๊ณณ์ด๋๊น
// ๋ ์ค์ค๋ก๊ฐ ๋ฐ๋์ <html> <body> ๊ป๋ฐ๊ธฐ๋ฅผ ๋ง๋ค์ด๋ด์ผ๋ง ํด!!
<html>
<body>
<h1>์์คํ
์ฝ์ด ๋ถ๊ดด (Global Error ๋ฐ๋)</h1>
<p>์ ๋ฉด ์ฅ์ ์กฐ์น ์ค์
๋๋ค... {error.message}</p>
<button onClick={() => reset()}>์ฌ๋ถํ
์๋</button>
</body>
</html>
)
}๐ก ํ:
global-error.tsx๋ ํ๋ก๋์ ๋น๋์์๋ง ํ์ฑํ๋ผ. (๊ฐ๋ฐ์ ํ๊ฒฝ ํฐ๋ฏธ๋ ํ์ ์ ๋ฌด์ํ๊ณ ํ๋ฉด ํ์ธ ๋ถ๊ฐ). ์ค์ง ์ตํ์ ๋ ๋ฐฐํฌ ํ๊ฒฝ์์ ์ต์์ ์๋ฒ ์ปดํฌ๋ํธ(layout.tsx)๊ฐ ๋๋น ํต์ ๋ฑ์์ ์ฃฝ์ด๋๊ฐ ๋๋ง ํ์ด๋์ค๋ ๋ค์ดํ๋ ์ ์ฌ์ ์ญํ ์ ์ํํ์ง.
๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
์๋ฌ ๋ฉ์์ง๊ฐ ๋จ๋ฉด Ctrl+F๋ก ๊ฒ์ํด๋ด.
โ Error Components must be Client Components...
์์ธ: error.tsx ๋ global-error.tsx ํ์ผ ์ต์๋จ์์ 'use client' ์ ์ธ์ ๊น๋จน์์.
ํด๊ฒฐ์ฑ
: 1์ด ๋ง์ ์ต์๋จ์ 'use client' ์จ์ฃผ๊ธฐ. (๋ฐํ์ ์๋ฌ ๋ ๋๋ง์ ํ์)
โ ๋ฃจํธ layout.tsx๊ฐ ํฐ์ก๋๋ฐ app/error.tsx๊ฐ ์๋ ์ ํ๊ณ ํ์ ์ฃฝ์ ํ๋ฉด ๋น๋๋ค!
์์ธ: app/error.tsx ๊ตฌ์กฐ์ ์์ ์ด ์ํ layout.tsx ํ์ ํ์ผ์ ์๋ฌ๋ ๋ง์ง ๋ชปํจ (์ค์ผ ๋ฒ์น).
ํด๊ฒฐ์ฑ
: ๋ฃจํธ app/ ๋ ๋ฒจ ๊น์ด์์๋ layout.tsx๋ฅผ ๊ฐ์ธ ๋ฐฉ์ดํ ์ ์๋ global-error.tsx ํ์ผ์ ๋ณ๋๋ก ๋ง๋ค์ด์ผ ์๋ฒฝํ ์ฐจ๋จ๋จ.
๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
| ์๋ฌ ๋ฐ์ ์ปดํฌ๋ํธ ํ์ผ | ์๋ํ๋ ๋ฐฉ์ด ์ด๋ (์ฐ์ ์์) | ์ค๋ช |
|---|---|---|
app/dashboard/page.tsx | 1. app/dashboard/error.tsx | ๊ฐ์ ํด๋(ํ์ ๋ ๋ฒจ)์ error.tsx๊ฐ ์ต์ฐ์ ๊ฒฉ๋ฒฝ ๋ฐ๋ |
app/dashboard/layout.tsx | 1. app/error.tsx | ์๊ธฐ๊ฐ ํฐ์ก์ผ๋ฏ๋ก, ๋ด ํ์ error๋ ๋ฌด์ฉ์ง๋ฌผ! ์์ ๋ถ๋ชจ ํด๋์ error๊ฐ ์ก์์ค |
app/layout.tsx (๋ฃจํธ) | 1. app/global-error.tsx | ์ธ์์ ๋. ํ๋์ด ๋ฌด๋์ก์ ๋ ์ตํ ์ฌํ์. html ๊ป๋ฐ๊ธฐ ํ์ |
| 404 Not Found ๊ฐ์ ํธ์ถ | 1. app/not-found.tsx | (์ด๊ฑด 9์ฅ์์ ์ฌํ ํ์ต) ์์๋ ๋ฆฌ์์ค ์ ์ฉ ์์ ์๋ฌ |
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
์๋ฌ๋ ๋ฌผ๊ณผ ๊ฐ์์ ๋ฐฉ์๋ฌธ(error.tsx)์ด ์๋ ์๋ฐฉ(๋ถ๋ชจ ์ปดํฌ๋ํธ)์ ํฅํด ๊ฟ๋ญ๊ฟ๋ญ ์ฐจ์ค๋ฅธ๋ค.
์ค์ํ ๊ตญ์ง ์ปดํฌ๋ํธ ํธ๋ฆฌ์๋ ์ด์ดํ๊ฒ ์ด๋ ํ์ผ์ ํด๋ ๋จ์๋ก ์ฌ์ด๋์ผ, ์ฌ์ดํธ ์ ์ฒด๊ฐ ํ๊ดด๋๋ ๋์ฌ์ ์ค๋ฅ ํ๋ฉด ์คํฌ๋ฆฐ(YOD: Yellow Screen of Death)์ ๋ฉดํ ์ ์๋ค!
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
๋ฐฐ์ ์ผ๋ฉด ํ ๋ฒ ๋นํ์ด์ ํ์ธํด๋ด์ผ ํด.
Q1. ์์ฒ ์ด๊ฐ ์ผํ๋ชฐ ๊ฒฐ์ ํ์ด์ง ํด๋๋ฅผ ์ธํ
์ค์ด๋ค. app/purchase/ ํด๋ ๋ด๋ถ์ layout.tsx, page.tsx, error.tsx ํ์ผ ์ธ ๊ฐ๊ฐ ์๋ค. ์ด๋ ๋ page.tsx์์ ์๋ฒ ์ก์
์ ์ฒ๋ฆฌํ๋ค throw new Error("๊ฒฐ์ ์น์ธ ๊ฑฐ๋ถ") ๊ฐ ํฐ์ก๋ค. ์ด๋, ํ๋ฉด ์ ์ฒด ๋ ๋๋ง ์ธก๋ฉด์์ ๋ณด์์ ๋ purchase/layout.tsx ๋จ์์ ๊ทธ๋ ค์ฃผ๋ GNB(๊ธ๋ก๋ฒ ์๋จ ๋ค๋น๊ฒ์ด์
)์ ์ฌ์ด๋ ์นดํธ ๋ฐ๋ ์ ์ ํ๋ฉด์ ์ด์์์๊น? ๋ ์๊ฐ์ ์ ๋ณด์ผ๊น?
โ ์ ๋ต: ์๋ฒฝํ๊ฒ ์ด์์๋ค (์ ์ง๋๋ค).
๐ก ์์ธ ํด์ค: ๊ฐ์ ํด๋ ๊ตฌ์ญ์ ์๋ ๋ฐฉ์ด๋ฒฝ error.tsx๋, ์ค์ง ์์ ์ปดํฌ๋ํธ๋ค(ํต์ page.tsx)์ด ๋ฐ์์ํจ ์๋ฌ ๋ฒ์๊น์ง๋ง์ ๊ฒฉ๋ฆฌํ๊ณ ๊ทธ ๊ตฌ์ญ๋ง Fallback UI (์๋ฌ ๋ฉ์์ง)๋ก ๋ฎ์ด๋ฒ๋ฆฐ๋ค. ํ์ ํ์ผ์ธ layout.tsx๋ page.tsx๋ฅผ ํ๊ณ ์๋ ๊ฑฐ๋ํ ๊ป๋ฐ๊ธฐ(๋ถ๋ชจ ๊ฐ๋
ํธ๋ฆฌ)์ด๋ฏ๋ก, ์๋ฌ์ ์ถฉ๋ ์ฌํ(error.tsx ๋ฐฉ๋ฒฝ ๋ฐ๊นฅ)๋ฅผ ์ ํ ๋ฐ์ง ์๊ณ ํผํผํ๊ฒ ํ๋ฉด์ ๋จ์ GNB ์ญํ ์ ๋ฌด์ฌํ ์ํํ๋ค. ์ด๊ฒ์ด ํด๋ ๊ธฐ๋ฐ ๋ฐฉ์ด ์ํคํ
์ฒ์ ๊ฐ๋ ฅํจ์ด๋ค.
Q2. ๋ฐฉ์ด๋ฒฝ์ธ error.tsx ํ์ผ ์์์ ๋ฒํผ์ ํด๋ฆญํ๋ฉด ์๋ฌ๊ฐ ๋ฌ๋ ์๋ ์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ ๋ก ๋ป๊ฒ ๋ง๋ ์์ธ์ ๋ค์๊ณ ์ฒ์๋ถํฐ ๋ค์ fetch ํด๋ณด๋ผ๊ณ ์๋ฒ์ ์ฌ์์ฒญ(๊ฐ์ ํ๋ณต ๋ฆฌํ๋ ์)์ ๋ณด๋ด๋ ์ญํ ์ ํ๋ ํต์ฌ Props ํ๋ผ๋ฏธํฐ ํจ์ ์ด๋ฆ์ ๋ฌด์์ธ๊ฐ?
โ
์ ๋ต: reset() ํจ์์ด๋ค.
๐ก ์์ธ ํด์ค: error.tsx ์ปดํฌ๋ํธ๋ Next.js๋ก๋ถํฐ ๋ฌด์กฐ๊ฑด ๋ ๊ฐ์ ํ๋กญ์ค ํจํค์ง๋ฅผ ์ฅ๊ณ ํ์ด๋๋ค. ํ๋๋ ์๋ฌ ๋คํ ๋ฉ์ด๋ฆฌ์ธ error ์์, ๋ค๋ฅธ ํ๋๋ ์ฃฝ์ ์น์
์ ๋ค์ ๋ง์ดํธ์์ผ ๋ ๋๋ง ์๋ฒ์ ๋ค์ ํต์ ์ ๊ฐํ์ผ ํ๋ reset ์ก์
ํจ์์ด๋ค. onClick={() => reset()} ์ฝ๋๋ SPA ์ธ์์์ ์๋ก๊ณ ์นจ ์์ด ํด๋น ๋ถ๋ถ ์ปดํฌ๋ํธ๋ง ํ๋ณต ๋ง๋ฒ์ ๋ฐ๋์ํค๋ ๊ณ ๊ธ ์น์ ๊ธฐ(Heal)์ด๋ค.
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ ์ ๋ง ์ญ๋ ๊ฐ์ํ์ด. API ํ๋ ํฐ์ก๋ค๊ณ ์ฌ์ดํธ ์ ์ฒด๊ฐ ๋ป์ด๋ฒ๋ฆฌ๋ ๊ฑธ ๋ณด๋ฉด์ ํ๋ก ํธ์๋ ์ธ๊ณ์ ๋ฌด์์์ ๋๊ผ๊ฑฐ๋ . ํ์ง๋ง ์ํธ ๋ฆฌ๋ ๋์ด ๊ฐ๋ฅด์ณ์ฃผ์ '๋ฐฉํ ์ ํฐ(error.tsx)' ์ ๋ ์ธ๋ฐํ '๊ฐ๋ณ ๊ฒฉ๋ฒฝ(ErrorBoundary)' ์ ์๋ ฅ์ ํ์ธํ๊ณ ๋๋ ์ด์ ๋ ์ด๋ค ์๋ฌ๊ฐ ํฐ์ ธ๋ ๋นํฉํ์ง ์์ ๊ฒ ๊ฐ์!
๐ก ์ค๋์ ๊ตํ: "์๋ฌ๋ ๋ฌผ๊ณผ ๊ฐ์์ ๋ฐฉ์๋ฌธ(
error.tsx) ์ด ์์ผ๋ฉด ์๋ฐฉ์ผ๋ก ์ญ๋ฅํ๋ค. ๋ผ์ฐํธ ๋จ์์ ํฐ ์ ํฐ๋ฟ๋ง ์๋๋ผ, ์ปดํฌ๋ํธ ๋จ์์ ์ ๋ฐํ ๊ฒฉ๋ฒฝ(ErrorBoundary) ๋ ์ ์ฌ์ ์์ ๋ฐฐ์นํด ์ฑ์ ์์กด๋ ฅ์ ๊ทน๋ํํ์!"
๋จ์ํ try-catch ๋ก ์๋ฌ๋ฅผ ์จ๊ธฐ๋ ๊ฒ ์๋๋ผ, ์๋ฌ๊ฐ ๋ฐ์ํ ์ง์ ์ ๋ช
ํํ๊ฒ ๊ฒฉ๋ฆฌํ๊ณ ์ฌ์ฉ์์๊ฒ ๋ค์ ์๋ํ ๊ธฐํ(reset) ๊น์ง ์ฃผ๋ ์ค๊ณ๊ฐ ์ผ๋ง๋ ์ธ๋ จ๋ ๋ฐฉ์์ธ์ง ๊นจ๋ฌ์์ด. ์ค๋ ๊ธด์ฅํ๋๋ ์ด๊นจ๊ฐ ๋ป๊ทผํ๋ค. ์ด๋ ๊ฐ์ ์คํธ๋ ์นญ ์ข ํ๊ณ ํน ์ฌ์ด์ผ์ง! ๋ด์ผ์ ์๋ฌ ์๋ ๋ฌด๊ฒฐ์ ์ฝ๋๋ฅผ ์ง๋ '๋๋ฒ๊น
๋ง์คํฐ' ๊ฐ ๋๊ณ ๋ง ๊ฑฐ์ผ. ๐ฃ