Next.js ์„œ๋ฒ„ ์•ก์…˜๊ณผ ํด๋ผ์ด์–ธํŠธ Fetch๋ฅผ ํ˜ผํ•ฉํ•  ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์บ์‹ฑ ๋ฌธ์ œ ํ•ด๊ฒฐ๋ฒ•

ํ…Œ
ํ…Œ์ŠคํŠธ์ €์žAuthor
5 min read

Next.js 13 ์ดํ›„ App Router๊ฐ€ ๋„์ž…๋˜๊ณ , 14์—์„œ Server Actions๊ฐ€ ๊ณต์‹ ๊ธฐ๋Šฅ์œผ๋กœ ๊ฐ•ํ™”๋˜๋ฉด์„œ ์„œ๋ฒ„ ์ค‘์‹ฌ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์ ์šฉํ•˜๋Š” ํ”„๋กœ์ ํŠธ๊ฐ€ ์ฆ๊ฐ€ํ•˜๊ณ  ์žˆ๋‹ค. ์ด ๊ณผ์ •์—์„œ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ๊ณตํ†ต์ ์œผ๋กœ ๊ฒช๋Š” ์–ด๋ ค์›€์ด ์žˆ๋‹ค. ๋ฐ”๋กœ Server Actions์™€ ํด๋ผ์ด์–ธํŠธ Fetch ์š”์ฒญ์ด ํ•จ๊ป˜ ์“ฐ์ผ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์บ์‹ฑ ๋ฌธ์ œ๋‹ค.

Next.js๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ "์„œ๋ฒ„ ์ค‘์‹ฌ ๋ Œ๋”๋ง๊ณผ ์บ์‹ฑ"์„ ๊ฐ•๋ ฅํ•˜๊ฒŒ ๋ฐ€์–ด๋ถ™์ด๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด ๊ตฌ์กฐ๋Š” Server Actions์™€ fetch API๊ฐ€ ๋’ค์„ž์ธ ์‹ค์ œ ํ™˜๊ฒฝ์—์„œ๋Š” ์˜ˆ๊ธฐ์น˜ ๋ชปํ•œ ๋ฒ„๊ทธ๋ฅผ ๋งŒ๋“ ๋‹ค. ์ด ๊ธ€์—์„œ๋Š” ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋“ค์ด ์™œ ๋ฐœ์ƒํ•˜๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ์ด๋ฅผ ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์‹ค์ œ ์ฝ”๋“œ ๊ธฐ๋ฐ˜์œผ๋กœ ์ •๋ฆฌํ•ด๋ณธ๋‹ค.


1. ์™œ Server Actions์™€ Fetch๋ฅผ ํ˜ผ์šฉํ•˜๋ฉด ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋Š”๊ฐ€

Next.js์˜ fetch๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์„œ๋ฒ„์—์„œ ์‹คํ–‰๋  ๋•Œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŠน์ง•์„ ๊ฐ€์ง„๋‹ค.

  • ๋™์ผํ•œ fetch ์š”์ฒญ์€ ๋™์ผํ•œ ์ž…๋ ฅ์— ๋Œ€ํ•ด ์ž๋™ ์บ์‹ฑ๋œ๋‹ค.
  • Response๋Š” ๋นŒ๋“œ ์‹œ์ , ์žฌ์š”์ฒญ ์‹œ์ , ํ˜น์€ ๊ฒฝ๋กœ์˜ ์บ์‹ฑ ์ •์ฑ…์— ๋”ฐ๋ผ ๊ณต์œ ๋œ๋‹ค.
  • Server Component์—์„œ๋Š” fetch๊ฐ€ ์„œ๋ฒ„ ์บ์‹œ๋ฅผ ์ ๊ทน์ ์œผ๋กœ ํ™œ์šฉํ•œ๋‹ค.

๋ฐ˜๋ฉด Server Actions๋Š” ๋‹ค์Œ ํŠน์„ฑ์„ ๊ฐ€์ง„๋‹ค.

  • ์‹คํ–‰๋  ๋•Œ ์ž๋™์œผ๋กœ revalidate๋ฅผ ํŠธ๋ฆฌ๊ฑฐํ•˜์ง€ ์•Š๋Š”๋‹ค.
  • Action ํ˜ธ์ถœ ํ›„ UI๊ฐ€ ์ž๋™์œผ๋กœ ๊ฐฑ์‹ ๋˜์ง€ ์•Š๋Š”๋‹ค.
  • Server Component๊ฐ€ ์•„๋‹ˆ๋ผ Client Component์—์„œ ํ˜ธ์ถœ๋  ์ˆ˜ ์žˆ๋‹ค.

์ฆ‰, ์ด ๋‘˜์ด ์กฐํ•ฉ๋˜๋Š” ์ƒํ™ฉ์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ „ํ˜•์ ์ธ ๋ฌธ์ œ๊ฐ€ ์‰ฝ๊ฒŒ ๋ฐœ์ƒํ•œ๋‹ค.


2. ์ „ํ˜•์ ์ธ ๋ฌธ์ œ ์‚ฌ๋ก€

๋ฌธ์ œ 1. Server Action์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ •ํ–ˆ์ง€๋งŒ UI๋Š” ๊ทธ๋Œ€๋กœ์ธ ๊ฒฝ์šฐ

์˜ˆ์‹œ ์ฝ”๋“œ:

// server action
export async function updateName(id: string, name: string) {
  await prisma.user.update({ where: { id }, data: { name } });
}

๊ทธ๋ฆฌ๊ณ  ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ์ดํ„ฐ ํŒจ์นญ:

const user = await fetch("https://api.example.com/user/1").then(res => res.json());

๋ฌธ์ œ๋Š” updateName ์‹คํ–‰ ํ›„์—๋„ fetch๊ฐ€ ์ด์ „ ์บ์‹œ๋œ ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค๋Š” ์ ์ด๋‹ค.

์ฆ‰, DB ๋ณ€๊ฒฝ์€ ์™„๋ฃŒ๋˜์—ˆ์ง€๋งŒ Next.js๋Š” ์ด๋ฅผ ๋ชจ๋ฅด๋Š” ์ƒํƒœ๋‹ค.


๋ฌธ์ œ 2. ํด๋ผ์ด์–ธํŠธ Fetch๋Š” ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค์ง€๋งŒ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ์บ์‹ฑ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์œ ์ง€

ํด๋ผ์ด์–ธํŠธ์—์„œ ๋‹ค์Œ์ฒ˜๋Ÿผ ์žฌ์š”์ฒญํ•˜๋ฉด:

const res = await fetch("/api/user/1", { cache: "no-store" });

ํ•ญ์ƒ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ํŽ˜์ด์ง€๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ํŒจ์นญ๋œ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณด์—ฌ์ค€๋‹ค.


๋ฌธ์ œ 3. fetch๊ฐ€ ์ž๋™ ์บ์‹ฑ๋œ๋‹ค๋Š” ์ ์„ ์ธ์ง€ํ•˜์ง€ ๋ชปํ•œ ์ƒํƒœ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๊ต์ฐจ ๋ฒ„๊ทธ

App Router์˜ ๊ธฐ๋ณธ fetch ์ •์ฑ…:

  • ์„œ๋ฒ„์—์„œ ์‹คํ–‰: default = cache: โ€œforce-cacheโ€
  • ํด๋ผ์ด์–ธํŠธ Fetch: default = cache: โ€œno-storeโ€

์ฆ‰, ๊ฐ™์€ URL์„ ํ˜ธ์ถœํ•ด๋„ ์„œ๋ฒ„/ํด๋ผ์ด์–ธํŠธ ๊ฒฐ๊ณผ๊ฐ€ ๋‹ฌ๋ผ์ง„๋‹ค.


3. ํ•ด๊ฒฐ ์ „๋žต ์ •๋ฆฌ

์ด ๋ฌธ์ œ๋Š” ์—ฌ๋Ÿฌ ๋ฐฉ์‹์œผ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค. ๋Œ€ํ‘œ์ ์ธ ์ „๋žต 4๊ฐ€์ง€๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

1) fetch์—์„œ ์บ์‹ฑ์„ ๋ช…์‹œ์ ์œผ๋กœ ์ œ์–ดํ•œ๋‹ค

๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•์ด๋‹ค.

์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ธฐ๋ณธ fetch๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋Œ€์‹  ๋‹ค์Œ์ฒ˜๋Ÿผ ๋ช…์‹œํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

await fetch(url, { cache: "no-store" })

๋˜๋Š”

await fetch(url, { next: { revalidate: 0 } })

์ด ๋‘ ๋ฐฉ์‹์€ ์™„์ „ํžˆ ์บ์‹ฑ์„ ๋ฌดํšจํ™”ํ•œ๋‹ค.

๋‹ค๋งŒ, ๋ชจ๋“  ์š”์ฒญ์—์„œ no-store๋ฅผ ์“ฐ๋Š” ๊ฒƒ์€ ์„ฑ๋Šฅ ๋ฉด์—์„œ ํฐ ์†์‹ค์ด ์žˆ๋‹ค.


2) Server Actions์—์„œ revalidatePath ๋˜๋Š” revalidateTag ์‚ฌ์šฉ

Server Actions๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด UI๊ฐ€ ์ตœ์‹  ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ๊ฐ•์ œ๋กœ revalidation์„ ํŠธ๋ฆฌ๊ฑฐํ•ด์•ผ ํ•œ๋‹ค.

๊ฐ€์žฅ ๊ถŒ์žฅ๋˜๋Š” ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ ๋‘ ๊ฐ€์ง€์ด๋‹ค.

revalidatePath

import { revalidatePath } from "next/cache";

export async function updateName(id: string, name: string) {
  await prisma.user.update({ where: { id }, data: { name } });
  revalidatePath("/users"); // ํŠน์ • ๊ฒฝ๋กœ ๋‹ค์‹œ ๋ Œ๋”๋ง
}

revalidateTag

fetch์— ํƒœ๊ทธ๋ฅผ ๋ถ™์ด๊ณ  revalidateTag๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.

// ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ
await fetch(url, { next: { tags: ["users"] } });

// ์„œ๋ฒ„ ์•ก์…˜
revalidateTag("users");

์ด ๋ฐฉ์‹์€ ๊ฐ€์žฅ ์•ˆ์ •์ ์ธ UI ๊ฐฑ์‹  ๋ฐฉ์‹์„ ์ œ๊ณตํ•œ๋‹ค.


3) Route Handler(API)์™€ Server Actions์˜ ์—ญํ• ์„ ๋ถ„๋ฆฌ

๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ํ”ํžˆ ๊ฒช๋Š” ๋ฌธ์ œ๋Š” ๋‹ค์Œ ๊ตฌ์กฐ์—์„œ ๋ฐœ์ƒํ•œ๋‹ค.

  • ๋ฐ์ดํ„ฐ ์กฐํšŒ: ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ fetch
  • ๋ฐ์ดํ„ฐ ์ˆ˜์ •: ์„œ๋ฒ„ ์•ก์…˜
  • ์žฌ์กฐํšŒ: ํด๋ผ์ด์–ธํŠธ fetch

์ด ๊ตฌ์กฐ๋Š” ์บ์‹œ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•˜์ง€ ๋ชปํ•œ๋‹ค.

์ด๋•Œ ํ•ด๊ฒฐ ์ „๋žต์€ ๋งค์šฐ ๊ฐ„๋‹จํ•˜๋‹ค.

๋ฐ์ดํ„ฐ ์ฝ๊ธฐ์™€ ์“ฐ๊ธฐ๋ฅผ โ€œRoute Handler(API)โ€ ์ค‘์‹ฌ์œผ๋กœ ํ†ตํ•ฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

์˜ˆ:

// GET /api/users
export async function GET() {
  const users = await prisma.user.findMany();
  return Response.json(users);
}

๊ทธ๋ฆฌ๊ณ  fetch ํ•  ๋•Œ:

fetch("/api/users", { cache: "no-store" });

Server Actions๋Š” ์˜ค์ง mutate ์—ญํ• ๋งŒ ํ•˜๋„๋ก ๊ตฌ์„ฑํ•œ๋‹ค.

์ด๋ ‡๊ฒŒ ๋ถ„๋ฆฌํ•˜๋ฉด ์บ์‹ฑ ์ถฉ๋Œ์ด ํฌ๊ฒŒ ์ค„์–ด๋“ ๋‹ค.


4) ์„œ๋ฒ„ ์•ก์…˜ ๋‚ด๋ถ€์—์„œ ๊ฐ€๋Šฅํ•œ fetch ํ˜ธ์ถœ์„ ํ”ผํ•œ๋‹ค

๋งŽ์€ ๊ฐœ๋ฐœ์ž๊ฐ€ ์„œ๋ฒ„ ์•ก์…˜ ๋‚ด๋ถ€์—์„œ fetch๋ฅผ ์žฌํ™œ์šฉํ•˜๋ ค๊ณ  ํ•œ๋‹ค.

await fetch("/api/user/1", { cache: "force-cache" });

๊ทธ๋Ÿฌ๋‚˜ Server Actions๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ Next.js์˜ fetch ์บ์‹œ์™€ ์ถฉ๋Œํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋ถˆํ•„์š”ํ•œ ์ค‘๋ณต ์ž‘์—…์„ ๋งŒ๋“ ๋‹ค.

์„œ๋ฒ„ ์•ก์…˜์—์„œ๋Š” ๋ฐ์ดํ„ฐ ์†Œ์Šค(DB, Prisma ๋“ฑ)์— ์ง์ ‘ ์ ‘๊ทผํ•˜๋Š” ํŽธ์ด ์•ˆ์ „ํ•˜๋‹ค.


4. ์‹ค์ „ ์ ์šฉ ์˜ˆ์ œ

์•„๋ž˜๋Š” Next.js 14 ๊ธฐ์ค€์œผ๋กœ ๊ฐ€์žฅ ์•ˆ์ •์ ์ด๊ณ  ๊ถŒ์žฅ๋˜๋Š” ๊ตฌ์กฐ๋‹ค.

1) ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ํŒจ์นญ

// app/users/page.tsx
export default async function UsersPage() {
  const users = await fetch("https://example.com/api/users", {next: { tags: ["users"] },
  }).then((res) => res.json());

  return <UsersList users={users} />;
}

2) ์„œ๋ฒ„ ์•ก์…˜์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์ • + revalidateTag ํ˜ธ์ถœ

"use server";

import { prisma } from "@/lib/prisma";
import { revalidateTag } from "next/cache";

export async function updateUserName(id: string, name: string) {
  await prisma.user.update({ where: { id }, data: { name } });
  revalidateTag("users");
}

3) ํด๋ผ์ด์–ธํŠธ์—์„œ Server Actions ํ˜ธ์ถœ

"use client";
import { updateUserName } from "./actions";

export default function EditForm() {
  async function onSubmit() {await updateUserName("1", "New Name");
  }

  return <button onClick={onSubmit}>Update</button>;
}

์ด ๊ตฌ์กฐ๋Š” ์บ์‹ฑ ์ถฉ๋Œ ์—†์ด ํ•ญ์ƒ ์ตœ์‹  UI๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.


5. ๊ฒฐ๋ก 

Next.js์˜ Server Actions์™€ fetch๋Š” ๋งค์šฐ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์ด์ง€๋งŒ, ์„œ๋กœ ๋‹ค๋ฅธ ์บ์‹ฑ ์ „๋žต์„ ๊ฐœ๋ณ„์ ์œผ๋กœ ๊ฐ€์ง€๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํ˜ผ์šฉ๋  ๋•Œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ ์‰ฝ๋‹ค.

ํ•ต์‹ฌ ์š”์•ฝ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. Next.js ์„œ๋ฒ„ fetch๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์บ์‹ฑ๋œ๋‹ค.
  2. Server Actions๋Š” ์ž๋™์œผ๋กœ revalidate๋ฅผ ํ•˜์ง€ ์•Š๋Š”๋‹ค.
  3. Server Action ์‹คํ–‰ ํ›„ UI๋ฅผ ๊ฐฑ์‹ ํ•˜๋ ค๋ฉด revalidatePath ๋˜๋Š” revalidateTag๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.
  4. fetch ์š”์ฒญ์€ no-store ๋˜๋Š” revalidate ์˜ต์…˜์„ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•œ๋‹ค.
  5. Route Handler์™€ Server Actions์˜ ์ฑ…์ž„์„ ๋ถ„๋ฆฌํ•˜๋ฉด ์บ์‹ฑ ์ถฉ๋Œ์„ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค.

์ด ์›๋ฆฌ๋ฅผ ์ดํ•ดํ•˜๋ฉด App Router ๊ธฐ๋ฐ˜์˜ Next.js ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์•ˆ์ •์ ์ด๊ณ  ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ๋‹ค.

Next.js ์„œ๋ฒ„ ์•ก์…˜๊ณผ ํด๋ผ์ด์–ธํŠธ Fetch๋ฅผ ํ˜ผํ•ฉํ•  ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์บ์‹ฑ ๋ฌธ์ œ ํ•ด๊ฒฐ๋ฒ•