ํฐ์คํ ๋ฆฌ ๋ทฐ
Next.js๋ฅผ ํ์ฉํ ๊ฐ์ธ ๊ณผ์ ๋ฅผ Vercel์์ ๋ฐฐํฌํด๋ณด์๋ค.
๋ถ๋ช ๊ฐ๋ฐ ๋ชจ๋์์๋ ์๋ฌด ๋ฌธ์ ์์ด ์๋ํ๋ ํ๋ก์ ํธ๊ฐ ๋น๋ ๊ณผ์ ์์ ๋ง์ ์ค๋ฅ๋ฅผ ๋ฑ์ด๋๋ค.

์ฌ๋ฌ ์์ธ์ด ์์๊ณ ํ์ผ ๋ช ์ด ๋์์์ง๋ง, ์๋ฌ์ ์ต์ข ํ์ด์ง(?)๋ง ๋ํ๋์์๊ธฐ ๋๋ฌธ์
์ ํํ ์ด๋ ๊ณผ์ ์์ ์๋ฌ๊ฐ ๋ฐ์ํ ๊ฑด์ง ํ์ ํ๊ธฐ ์ด๋ ค์ ๋ค.
ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ์๋ฌ ์ฒ๋ฆฌ
ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ๋ useQuery๋ฅผ ํตํด ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ํ๊ณ ์์๋ค.
๋ํ ํด๋ผ์ด์ธํธ ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๋ผ์ฐํฐ ํธ๋ค๋ฌ์์๋ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ํ๊ณ ์์๋ค.
// rotation > page.tsx
...
export default function Rotationpage() {
const { data: imgUrl, isPending, isError, error } = getImgUrl();
const { data: rotation = [] } = getRotation();
if (isPending) {
return <Loading />;
}
if (isError) {
return (
<>
<div>์๋ฌ๊ฐ ๋ฐ์ํ์ต๋๋ค!</div>
<p>์๋ฌ:{error.message}</p>
</>
);
}
...
// api > rotation > route.ts
// ๋กํ
์ด์
API ๋ผ์ฐํธ ํธ๋ค๋ฌ + ํํฐ
export const GET = async (): Promise<NextResponse> => {
try {
const res = await fetch(ROTATION_API_URL, {
headers: {
"X-Riot-Token": process.env.RIOT_API_KEY!,
},
});
// ์ฌ์ฉ์ ์์ฒญ ์๋ฌ
if (!res) {
return NextResponse.json(
{ message: "error! ๋กํ
์ด์
์ฑํผ์ธ ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค!" },
{ status: 400 },
);
}
// ํํฐ
const rotationData: ChampionRotation = await res.json(); // freeChampionIds ๋ฐฐ์ด ํฌํจ ๊ฐ์ฒด
const championData: Champions[] = await fetchChampionList(); // ๋ชจ๋ ์ฑํผ์ธ ๋ฆฌ์คํธ ๋ฐฐ์ด
const filterData: Champions[] = championData.filter((el) => {
return rotationData.freeChampionIds.includes(parseInt(el.key));
}); // ์ฑํผ์ธ ํค๋ฅผ ํฌํจํ ๋กํ
์ด์
์์ด๋๋ฅผ ์ฑํผ์ธ ๋ฆฌ์คํธ์์ filter
return NextResponse.json(filterData, { status: 200 });
} catch (error) {
console.log("๋กํ
์ด์
๋ผ์ฐํธ ํธ๋ค๋ฌ ์๋ฌ ๋ฐ์", error);
// ์๋ฒ ์๋ฌ
return NextResponse.json(
{ message: "error! ๋กํ
์ด์
ํ์ด์ง๋ฅผ ์ฝ์ ์ ์์ต๋๋ค!" },
{ status: 500 },
);
}
};
์๋ฒ ์ฌ์ด๋ ์๋ฌ ์ฒ๋ฆฌ
๊ทธ๋ผ ์๋ฒ ์ปดํฌ๋ํธ๋ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ์ด๋์, ์ด๋ป๊ฒ ํด์ผํ ๊น?
์๋ฒ ์ปดํฌ๋ํธ ๋ด์์ ์ง์ API ๋ฐ์ดํฐ๋ฅผ ํ์นญํ๊ณ ์ฒ๋ฆฌํ๋ ์๋ฒ ์ก์ ๊ณผ ์๋ฒ ์ปดํฌ๋ํธ์์ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ํ ์ ์๋ค.
๐์๋ 1. ์๋ฒ ์ก์ ์์ try...catch ๋ฌธ์ผ๋ก ์๋ฌ ์ฒ๋ฆฌ
๋ค์๊ณผ ๊ฐ์ ์๋ฒ ์ก์
์์ try catch๋ฅผ ํ์ฉํด๋ณด์๋ค.
๊ทธ๋ฌ๋ 'Champion[]์ ํ์
์ error๊ฐ ์๋ค'๋ ์ค๋ฅ๊ฐ ๋ฌ๋ค.
Promise<Campions[]>๋ก ์ค์ ํด๋ ์ ๋ค๋ฆญ ํ์ ์ ๋ฐํด catch๋ฌธ์ return์ ๊ฐ์ด 'error' ํ์ ์ด๋ผ ์ค๋ฅ๊ฐ ๋ฌ๋ค.
// utils > serverApi.ts
// ์ฑํผ์ธ ๋ชฉ๋ก ํจ์นญ
export async function fetchChampionList(): Promise<Champions[]> {
try {
const chamUrl = await CHAMPION_LIST_URL();
const res = await fetch(chamUrl, { next: { revalidate: 86400 } });
const { data } = await res.json();
return Object.values(data);
} catch (error) {
console.log("์ฑํผ์ธ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ ์คํจ", error);
return { error: "์ฑํผ์ธ ๋ชฉ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค!" };
}
}
๋ฐ๋ผ์ ์๋ฒ ์ก์ ์์๋ ์๋ฌ๋ฅผ throwํ๊ณ , ํด๋น ์๋ฒ ์ก์ ์ ์ฌ์ฉํ๋ ์ปดํฌ๋ํธ์์ ์๋ฌ๋ฅผ ๋ฐ์์ผ ํ๋ค.
๐์๋ 2. ์๋ฒ ์ก์
์์ ์๋ฌ throw
throw new Error๋ฅผ ํตํด ์๋ฌ๋ฅผ ๋์ก๋ค.
// ์ฑํผ์ธ ๋ชฉ๋ก ํจ์นญ
export async function fetchChampionList(): Promise<Champions[]> {
const chamUrl = await CHAMPION_LIST_URL();
const res = await fetch(chamUrl, { next: { revalidate: 86400 } });
const { data } = await res.json();
//ํด๋น ํ์ด์ง๋ก ์๋ฌ ๋ณด๋ด๊ธฐ
if (!res.ok) {
throw new Error("์ฑํผ์ธ ๋ชฉ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค!");
}
return Object.values(data);
}
๐ค์๋ฌ๋ฅผ throw ํ ๋ 'new Error'๋ฅผ ์ฌ์ฉํ๋ ์ด์
- new Error("๋ฉ์ธ์ง")๋ฅผ ์ฌ์ฉํ๋ฉด ๋ ๋ง์ ์ ๋ณด๋ฅผ ํฌํจํ ํ์ค ์๋ฌ ๊ฐ์ฒด ์์ฑ
๐throw "๋ฌธ์์ด"
- ์ฌ์ฉ : throw "์๋ฌ ๋ฐ์!";
- ์คํ ๊ฒฐ๊ณผ : Uncaught (in promise) ์๋ฌ ๋ฐ์!
- ๋ฌธ์ ์
- throwํ ๊ฐ์ด ๋จ์ํ ๋ฌธ์์ด์ด๋ผ ์๋ฌ ๊ฐ์ฒด๊ฐ ์๋
- ์คํ ํธ๋ ์ด์ค(stack trace)๊ฐ ์ ๊ณต๋์ง ์์ → ๋๋ฒ๊น ์ด๋ ค์
- catch์์ error.message์ ์ฌ์ฉํ ์ ์์ (error ์์ฒด๊ฐ ๋ฌธ์์ด์ด๊ธฐ ๋๋ฌธ)
๐throw new Error("์๋ฌ ๋ฐ์")
- ์ฌ์ฉ : throw new Error("์๋ฌ ๋ฐ์!");
- ์คํ ๊ฒฐ๊ณผ
- Uncaught (in promise) Error: ์๋ฌ ๋ฐ์!
at fetchChampionList (actions.ts:5)
at async handleClick (component.tsx:10)
- Uncaught (in promise) Error: ์๋ฌ ๋ฐ์!
- ์ฅ์
- Error ๊ฐ์ฒด๋ฅผ ์์ฑํ์ฌ ํ์ค์ ์ธ ์๋ฌ ์ฒ๋ฆฌ ๊ฐ๋ฅ
- ์คํ ํธ๋ ์ด์ค(stack trace) ์ ๊ณต → ์๋ฌ๊ฐ ๋ฐ์ํ ์์น๋ฅผ ์ฝ๊ฒ ์ฐพ์ ์ ์์
- catch(error)์์ error.message๋ฅผ ์ฌ์ฉํ์ฌ ๋ฉ์์ง๋ง ์ถ์ถ ๊ฐ๋ฅ
์๋ฌ ํธ๋ค๋ง (error.tsx)
์ด์ throwํ ์๋ฌ๋ฅผ ์ปดํฌ๋ํธ์์ ๋ฐ์ผ๋ฉด ๋๋๋ฐ, ๋ชจ๋ ์ปดํฌ๋ํธ๋ง๋ค try catch๋ฅผ ์ฌ์ฉํด์ ์๋ฌ๋ฅผ ๋ฐ์์ผํ๋ค..
๊ทธ๋ ๋ฐ๊ฒฌํ ๊ฒ์ด '์๋ฌ ํธ๋ค๋ง' ์ด์๋ค.
๋ฅ์คํธ ๊ณต์๋ฌธ์์๋ ๋ค์๊ณผ ๊ฐ์ด ๋์์๋ค.
์๋ฌ๋ ์์๋ ์๋ฌ์ ์์์น ๋ชปํ ์์ธ ๋ ๊ฐ์ง ๋ฒ์ฃผ๋ก ๋๋ ์ ์์ต๋๋ค:
- ์์๋ ์๋ฌ๋ฅผ ๋ฐํ ๊ฐ์ผ๋ก ๋ชจ๋ธ๋ง: ์๋ฒ ์ก์ ์์ ์์๋ ์๋ฌ๋ฅผ try/catch๋ก ์ฒ๋ฆฌํ๋ ๊ฒ์ ํผํ์ญ์์ค.
useActionState๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฌํ ์๋ฌ๋ฅผ ๊ด๋ฆฌํ๊ณ ํด๋ผ์ด์ธํธ์ ๋ฐํํฉ๋๋ค.
- ์์์น ๋ชปํ ์๋ฌ๋ ์๋ฌ ๊ฒฝ๊ณ๋ก ์ฒ๋ฆฌ: error.tsx ๋ฐ global-error.tsx ํ์ผ์ ์ฌ์ฉํ์ฌ ์๋ฌ ๊ฒฝ๊ณ๋ฅผ ๊ตฌํํ๊ณ ์์์น ๋ชปํ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๋ฉฐ ๋์ฒด UI๋ฅผ ์ ๊ณตํฉ๋๋ค.
try catch๋ฅผ ์ฌ์ฉํ๋ฉด ์์๋ ์๋ฌ(thorwํ ์๋ฌ)๋ง์ ์ฒ๋ฆฌํ ์ ์๋ค๊ณ ํ๋ค.
error.tsx ๋ฐ global-error.tsx ํ์ผ์ ํตํด ์์์น ๋ชปํ ์๋ฌ๊น์ง ํธ๋ค๋ง์ ํ ์ ์๋ค.

error.tsx๋ ์ธ๊ทธ๋จผํธ ๋ด์ ๋จ ํ ๊ฐ์ ํ์ผ์ ๋์ด ํด๋น ์ธ๊ทธ๋จผํธ ๋ด๋ถ ํ์ด์ง์ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ๋ด๋นํ๋ค.
ํ์ผ๋ช ์ ์ ํํ ๊ธฐ์ฌํด์ผ Next์์ ์๋์ผ๋ก ์ธ์ํ ์ ์๋ค.
๐คํ์ด์ง์์ ์ง์ ์๋ฌ๋ฅผ ๋ฐ์ง ์๊ณ error.tsx์์ ์ผ๊ด์ ์ผ๋ก ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ๊ฐ?
๊ทธ๋ ๋ค!
error.tsx์์๋ง ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๋ ๊ตฌ์กฐ๋ก, ํ์ด์ง์์๋ ์๋ฌ๋ฅผ ์ง์ ์ฒ๋ฆฌํ ํ์๊ฐ ์๋ค.
ํ์ด์ง์์ ๋ฐ์ดํฐ ์์ฒญ์ด ์คํจํ๋ฉด, error.tsx ํ์ผ์ด ๊ทธ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๊ณ , ์ฌ์ฉ์์๊ฒ ์ ์ ํ UI๋ฅผ ๋ณด์ฌ์ฃผ๊ฒ ๋๋ค.
๐คํ์ผ ์์น์ ๋ฐ๋ฅธ ์ฐจ์ด๋? (ํด๋น ํ์ด์ง์ ์ธ๊ทธ๋จผํธ ๋ด๋ถ์ app ํ์ )
์ธ๊ทธ๋จผํธ ๋ด๋ถ์ ์์นํ ๊ฒฝ์ฐ ํด๋น ํ์ด์ง์๋ง ์ ์ฉํ๋ ์๋ฌ ์ปดํฌ๋ํธ๋ฅผ ์์ฑํ ์ ์๋ค.
app์ ์์นํ ๊ฒฝ์ฐ ํ์์ ๋ชจ๋ ํ์ด์ง์ ๋์ผํ ์๋ฌ ์ปดํฌ๋ํธ๋ฅผ ์ ์ฉํ ์ ์์ผ๋ฉฐ ์ฝ๋ ์ค๋ณต์ ๋ฐฉ์งํ๋ค.
๋ฐ๋ผ์ ๋๋ app ๊ฒฝ๋ก์ error.tsx ํ์ผ์ ๋์ด ๋ชจ๋ ํ์ด์ง์ ๋์ผํ ์๋ฌ ์ปดํฌ๋ํธ๋ฅผ ์ ์ํ๋๋ก ํ์๋ค.
// rotation > page.tsx
"use client";
import { getImgUrl, getRotation } from "../hooks/quries";
import Card from "@/_components/Card";
import Loading from "../loading";
export default function Rotationpage() {
const { data: imgUrl, isPending } = getImgUrl();
const { data: rotation = [] } = getRotation();
if (isPending) {
return <Loading />;
}
return (
<main className="m-[50px]">
<h1 className="flexCenter title mt-[80px] text-[30px]">
Champion Rotation (This week for free!)
</h1>
<div className="itemGrid mt-[30px] auto-rows-[minmax(200px,auto)]">
{rotation.map((champion) => (
<Card
key={champion.key}
id={champion.id}
name={champion.name}
title={champion.title}
image={champion.image}
img_Url={imgUrl ?? ""} // ์๋ฌ ์ฒ๋ฆฌ ์๋ต ์, data๋ undefined๊ฐ ๋ ๊ฐ๋ฅ์ฑ
/>
))}
</div>
</main>
);
}
useQuery ๋ก ์๋ฌ ์ํ ๊ด๋ฆฌํ๊ธฐ
๋ํ, error.tsx๋ (๊ฑฐ์ ๋ชจ๋ ๊ฒฝ์ฐ) ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์ฌ์ผ ํ๋ค.
error.tsx๋ '๋ค์ ์๋' ๋ฒํผ๊ณผ ๊ฐ์ด ์ฌ์ฉ์์์ ์ํธ์์ฉ์ด ํ์ํ๊ธฐ ๋๋ฌธ์ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ์ฌ์ฉํ ์ ์๋ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์ฌ์ผ ํ๋ค๋ ์ !!
๋ฐ๋ผ์ useQuery๋ก๋ ์๋ฌ ์ํ๋ฅผ ๊ด๋ฆฌํ ์ ์๋ค.
// app > error.tsx
"use client";
import { getChampionError } from "./hooks/quries";
import Loading from "./loading";
export default function Error() {
const { error, isPending, isError, refetch } = getChampionError();
if (isPending) {
return <Loading />;
}
if (isError) {
return (
<div className="flexCenter mt-[300px] flex-col gap-8">
<h1 className="title text-[50px]">์๋ฌ ๋ฐ์!</h1>
<p className="text-white">์๋ฌ ๋ฉ์ธ์ง : {error.message}</p>
<button
onClick={() => refetch()}
className="rounded-md border-2 border-emerald-100 p-4 text-white"
>
๋ค์ ์๋
</button>
</div>
);
}
}
๊ฒฐ๊ณผ์ ์ผ๋ก ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ useQuery๋ก ๊ด๋ฆฌํ๋ ์๋ฌ ์ํ๋ฅผ error.tsx์์ ๊ด๋ฆฌํ ์ ์๊ฒ ๋์๋ค.
app ๊ฒฝ๋ก์ error.tsx๋ฅผ ๋์ด ํด๋ผ์ด์ธํธ/์๋ฒ ์๋ฌ๋ฅผ ์ผ๊ด์ ์ผ๋ก ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํด์ก๋ค.
๋ง์ฐฌ๊ฐ์ง๋ก loading.tsx ํ์ผ๋ ๋์ผํ ๋ฐฉ์์ผ๋ก app์ ๋์ด ๋ชจ๋ ํ์ด์ง์ ์ ์ฉํ์๋ค.
// app > loading.tsx
export default function Loading() {
return (
<div className="m-auto my-5 flex h-[200px] w-[200px] items-center justify-center">
<div className="h-12 w-12 animate-spin rounded-full border-4 border-white/30 border-t-white"></div>
</div>
);
}

'Language > Next' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [/\/] Tanstack Query + Zustand ์ ์ญ ์ํ ๊ด๋ฆฌํ๊ธฐ (0) | 2025.03.21 |
|---|---|
| [/\/] Next.js์์ ๋งํ๋ '์๋ฒ'๋? (0) | 2025.03.19 |
| [/\/] Next์ Tanstack Query (0) | 2025.03.19 |
| [/\/] Loading UI - Suspense ์ Streaming SSR (0) | 2025.03.11 |
| [/\/] SSG, ISR, SSR, CSR (0) | 2025.03.10 |