ํฐ์คํ ๋ฆฌ ๋ทฐ
๋ฌธ์ ์ํฉ
Next.js๋ฅผ ์ฌ์ฉํด ์บ๋ฆฐ๋ ํ์ด์ง๋ฅผ ๊ฐ๋ฐํ๋ ์ค,
Supabase ํต์ ์ค ๋ฐ์ํ๋ ์๋ฌ๋ฅผ ํ์ด์ง ๋ผ์ฐํฐ ์ธ๊ทธ๋จผํธ ๋ด์ error.tsx๋ก ํตํฉํด์ ์ฒ๋ฆฌํ๋ ค ํ๋ค.
ํ์ง๋ง ์๋ฌ๋ฅผ ๋์ง ๋ ๋ด๊ฐ ์ง์ ํ ๋ฉ์์ง์ ๋ฐํ์ ์ค ๋ฐ์ํ๋ ์์คํ ๋ฉ์์ง๊ฐ ์์ฌ ๋์ค๋ ๋ฌธ์ ๊ฐ ์์๋ค.
๋ํ,
- app/api/**/route.ts (๋ผ์ฐํธ ํธ๋ค๋ฌ)
- supabase ํธ์ถ ํจ์(fetch)
- ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ (page.tsx)
- ํ ์คํ ์ฟผ๋ฆฌ(useQuery)
์ด๋ ๊ฒ ์ฌ๋ฌ ๋ ์ด์ด๋ฅผ ๊ฑฐ์น๋ฉด์ ์๋ฌ ํ๋ฆ์ ๊น๋ํ๊ฒ ์ก๋ ๊ฒ๋ ๊ณ ๋ฏผ์ด ํ์ํ๋ค.
์ต์ข ์ ์ธ ์๋ฌ ํ๋ฆ์ ์๋์ ๊ฐ๋ค.
- ๋ผ์ฐํธ ํธ๋ค๋ฌ
์ฑ๊ณตor์๋ฌ๋ฅผ ๊ฐ์ฒด ํํ๋ก ๋ฐ์์ return ๐๐ป ํ์ด์ง์์ ์๋ฌ ๊ฐ์ฒด๋ฅผ ์ก์์ throw ๐๐ป error.tsx ์ก์
import ENV from '@/constants/env.constant';
import { NextResponse } from 'next/server';
// ๊ณตํด์ผ ๋ผ์ฐํธ ํธ๋ค๋ฌ
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const year = searchParams.get('year') || new Date().getFullYear().toString();
try {
const url = `${ENV.HOLIDAY_URL}/getRestDeInfo?ServiceKey=${ENV.HOLIDAY_KEY}&solYear=${year}&_type=json&numOfRows=100`;
const res = await fetch(url);
if (!res.ok) {
throw new Error(`๊ณตํด์ผ API ์๋ต ์ค๋ฅ: ${res.statusText}`);
}
const data = await res.json();
// ๋ฐ์ดํฐ ๊ตฌ์กฐ ๊ฒ์ฆ
if (!data.response || !data.response.body) {
console.error('์ ํจํ์ง ์์ API ์๋ต ํ์:', data);
return NextResponse.json({ error: '๊ณตํด์ผ ๋ฐ์ดํฐ ํ์์ด ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค' }, { status: 500 });
}
return NextResponse.json(data); //์ ์ ์๋ต ๋ฐํ
} catch (error) {
console.error('๊ณตํด์ผ ๋ฐ์ดํฐ ํธ์ถ ์ค ์ค๋ฅ ๋ฐ์:', error);
return NextResponse.json({ error: '๊ณตํด์ผ์ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค' }, { status: 500 });
}
}
- supabase ํต์
๊ฐ ํต์ ํจ์์์ console.error & throw new Error ๐๐ป ํ์ด์ง ๋ด์ ์ฟผ๋ฆฌ ํ ์์ error ๊บผ๋ด๊ธฐ ๐๐ป ๊ฐ ์๋ฌ ์ฒ๋ฆฌ ํ throw ๐๐ป error.tsx ์ก์
1. plans ๋ฐ์ดํฐ ๋ถ๋ฌ์ค๊ธฐ
export const getPlans = async (planId: string): Promise<PlansType> => {
const { data, error } = await supabase.from(PLANS).select('*').eq('plan_id', planId).single();
if (error) {
console.error('์ฝ์์ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค:', error);
throw new Error('์ฝ์์ ๋ถ๋ฌ์ค๋ ์ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์.');
}
return data;
};
2. ์ฟผ๋ฆฌ ํ
์์ error ๊บผ๋ด๊ธฐ
const { data: holidays, error: holidaysError } = useGetHolidays(String(calendarYear)); //๊ณตํด์ผ
const { data: events, isPending, error: plansError } = useGetCalendarPlans(user, calendarYear, moment); //์ฝ์(readonly)
const {
data: selectedPlanData,
error: selectPlanError,
refetch: refetchSelectedPlan,
} = useGetSelectPlan(selectedPlanId ?? '');
const { mutate: updateEvent, error: updatePlanError } = useUpadateEventMutate();
3. ์๋ฌ ๋ชจ์์ throw
if (holidaysError || plansError || selectPlanError || updatePlanError) {
const error = new Error('์บ๋ฆฐ๋ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.') as Error & {
originalError?: Error | null;
};
error.originalError = holidaysError ?? plansError ?? selectPlanError ?? updatePlanError;
throw error;
}
์๋ฌ ํ๋ฆ ์ ์ฒด ๊ตฌ์กฐ
์๋ฌ ์ฒ๋ฆฌ๋ฅผ ๋ค์๊ณผ ๊ฐ์ ๊ท์น์ผ๋ก ์ ๋ฆฌํ๋ค.
1. ๋ผ์ฐํธ ํธ๋ค๋ฌ(app/api//route.ts)**
์๋ฒ ์ฌ์ด๋์์ ์๋ฌ ๋ฐ์ ์ → NextResponse.json({ error }, { status }) ๋ก ํด๋ผ์ด์ธํธ์ ์๋ฌ ์ ๋ฌ
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
2. ํต์ ๋ก์ง (Supabase ํธ์ถ ํจ์ ๋ฑ)
fetch ํจ์์์๋ ํต์ ์คํจ ์ throw new Error('์ปค์คํ ๋ฉ์์ง') ๋ก ์๋ฌ ๋์ง๊ธฐ
๐ฅ ์ด๋ ํต์ ์์ฒด๊ฐ ์ ๋๋ค๋ฉด ๋ฐํ์ ์๋ฌ๊ฐ ๋ฐ์ํ ์ ์์ผ๋ ์ฃผ์.
if (error) {
throw new Error('์ฝ์์ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
}
3. ํ์ด์ง ์ปดํฌ๋ํธ (ex. app/(routes)/calendar/page.tsx)
์ง์ ์ ์ธ ์ค๋ฅ ๋ฐ์ ์ throw new Error('๋ฉ์์ง') → error.tsx๋ก ์ฐ๊ฒฐ๋๋๋ก ์ค์
if (!data) {
throw new Error('๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.');
}
4. ํ ์คํ ์ฟผ๋ฆฌ (useQuery, useMutation)
์ฟผ๋ฆฌ ํ
๋ด๋ถ์์๋ Next.js์ error.tsx๋ฅผ ์ง์ ๊ฑด๋๋ฆฌ์ง ์๊ณ
error, isError ํ๋๊ทธ๋ฅผ ํตํด ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ ์์์ ์๋ฌ ์ฒ๋ฆฌ
const { data, error, isError } = useGetCalendarPlans();
if (isError) {
throw error;
}
โ ํ๋ฆ ์ ๋ฆฌ
- ๋ผ์ฐํธ ํธ๋ค๋ฌ์์ ๋ฐ์ํ๋ ์๋ฌ๋ json ์๋ต์ผ๋ก ๋ด๋ ค์ด
- ํต์ (fetcher) ๋จ์์ ๋ฐ์ ์๋ฌ๋ throw new Error๋ก ๋ช ์์ ์ผ๋ก ๋์ง
- ํ์ด์ง ์ปดํฌ๋ํธ์์๋ ํตํฉ์ ์ผ๋ก ์๋ฌ๋ฅผ ๋ฐ์ ํ์ํ ๊ฒฝ์ฐ ๋ค์ throw
- ์ต์ข ์ ์ผ๋ก error.tsx๊ฐ ๋ชจ๋ ์๋ฌ๋ฅผ ๋ฐ์ ์ฒ๋ฆฌํ๋ ๊ตฌ์กฐ
error.tsx
error.tsx์์๋ ์ด๋ ๊ฒ ๋ฉ์ธ ์๋ฌ์ ์๋ธ ์๋ฌ(originalError)๋ฅผ ๋๋์ด ์ถ๋ ฅํ๋ค.
- ๋ฉ์ธ ์๋ฌ: ์บ๋ฆฐ๋ ํ์ด์ง์์ throwํ ๋ฉ์ธ์ง '์บ๋ฆฐ๋ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.'
- ์๋ธ ์๋ฌ: supabase ํต์ ๋ก์ง์์ throwํ ๋ํ ์ผ ๋ฉ์ธ์ง '์ฝ์์ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์'
์๋ฌ๋ฅผ ์ฝ์์๋ ์ถ๋ ฅํด์ ๋๋ฒ๊น ํ๊ธฐ ์ฝ๊ฒ ์ค์ ํ๋ค.
'use client';
import GenericError from '@/components/Error';
export default function ErrorPage({ error, reset }: { error: Error & { originalError?: Error }; reset: () => void }) {
console.error('๋ฉ์ธ ์๋ฌ:', error);
if (error.originalError) {
console.error('์๋ ์๋ฌ:', error.originalError);
}
return (
<GenericError
error={error}
originalError={error.originalError ?? null}
refetch={reset} // reset ํจ์๋ Next.js๊ฐ ์ ๊ณตํ๋ ์ฌ์๋ ํจ์
/>
);
}
GenericError ์ปดํฌ๋ํธ
๋ ๋๋ง ์, error.message์ originalError.message ๋ฅผ ๊ฐ๊ฐ ๋ณด์ฌ์ค ์ ์๋๋ก ํ๋ค.
import { Warning } from '@phosphor-icons/react';
type GenericErrorProps = {
error?: Error | null;
originalError: Error | null;
refetch?: () => void;
};
export default function GenericError({ error, refetch, originalError }: GenericErrorProps) {
return (
<div className='flex min-h-screen flex-col items-center justify-center gap-8'>
<Warning size={60} className='text-secondary-600' />
<h2 className='title text-[45px] font-bold text-primary-700'>Error!</h2>
{error?.message && originalError?.message && (
<section className='flex flex-col items-center text-[23px] text-primary-500'>
<p>{error?.message}</p>
<p>Detail: {originalError?.message}</p>
</section>
)}
{refetch && (
<button
onClick={refetch}
className='rounded-md border-2 bg-primary-500 p-4 font-bold text-white hover:bg-primary-600'
>
๋ค์ ์๋
</button>
)}
</div>
);
}
์ผ๋ถ๋ฌ ํต์ ์๋ฌ๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ
Supabase ์์ฒญ ์ค ๋ฐ์ํ๋ ์๋ฌ๋ ์ ์กํ์ง๋ง,
์ฝ๋ ์์ฒด์์ ๋ฐํ์ ์๋ฌ๊ฐ ๋๋ฉด Supabase์ ๋๋ฌ์กฐ์ฐจ ๋ชป ํ๊ณ ์ฃฝ๋๋ค.
์๋ฅผ ๋ค๋ฉด ์๋์ ๊ฐ์ด Supabase ์์ฒญ์ ๋ณด๋ด๊ธฐ๋ ์ ์ ๋ฐํ์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
(๋ฐ๋ผ์ originalError๊ฐ ์กด์ฌํ์ง ์๋๋ค.)
const { error } = await supabase.from(PLAS).select('*'); // PLAS๊ฐ undefined
๋ฐ๋ผ์ Supabase ํต์ ์คํจ๋ฅผ ์ผ๋ถ๋ฌ ๋ง๋ค๊ณ ์ถ์ผ๋ฉด
- ์๋ชป๋ ํ ์ด๋ธ๋ช ์ฌ์ฉ
- ํ์ ์ปฌ๋ผ์ ๋๋ฝ
- ์กด์ฌํ์ง ์๋ ํ๋๋ฅผ ์ฟผ๋ฆฌ
๋ฑ์ ํตํด ์๋ฌ๋ฅผ ๋ฐ์์ํค๊ณ ,
์ ์์ ์ผ๋ก ์๋ต์ด ์ค๊ฒ ํ ๋ค ๊ฑฐ๊ธฐ์ error ์ฒดํฌ ํ ๋์ง๋ ์์ผ๋ก ์ฒ๋ฆฌํด์ผ ํ๋ค.
์ต์ข ๊ฒฐ๊ณผ ๋ฐ ๋๋์
![]() |
![]() |
์ด๋ฒ ์์ ์ ํตํด ๋ฐํ์ ์๋ฌ์ ํต์ ์๋ฌ๋ฅผ ๋ช ํํ ๊ตฌ๋ถํด์ ๋ค๋ค์ผ ํ๋ค๋ ๊ฑธ ๋ค์ ๊นจ๋ฌ์๋ค.
๋ํ throw new Error๋ฅผ ์ฐ๋ ์์น, ์๋ฌ๋ฅผ ๋ฐ๋ ์์น(error.tsx), ํ์ํ๋ ๋ฐฉ๋ฒ(GenericError ์ปดํฌ๋ํธ)๊น์ง ๋ชจ๋ ์ค๊ณํด์ผ
์๋ฌ ํธ๋ค๋ง์ด ์์ฑ๋๋ค๋ ๊ฒ์ ๊ฒฝํํ๋ค.
'Language > Next' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [/\/] Route Handler, Server-Action (0) | 2025.03.22 |
|---|---|
| [/\/] Tanstack Query + Zustand ์ ์ญ ์ํ ๊ด๋ฆฌํ๊ธฐ (0) | 2025.03.21 |
| [/\/] Next.js์์ ๋งํ๋ '์๋ฒ'๋? (0) | 2025.03.19 |
| [/\/] Error Handling (0) | 2025.03.19 |
| [/\/] Next์ Tanstack Query (0) | 2025.03.19 |

