๐Ÿš€ Next.js ์‹ฌํ™” 4์žฅ: Server Actions โ€” API ๋ผ์šฐํŠธ ์—†๋Š” ์„ธ์ƒ์˜ ํ˜๋ช…

2026๋…„ 4์›” 30์ผ ์ˆ˜์ •๋จ

๐Ÿ“‹ ๊ฐœ์š”

Server Actions๋กœ API ๋ผ์šฐํŠธ ์—†์ด ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  ํผ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํŒจํ„ด์„ ๋ฐฐ์›๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 15๋ถ„ (์ „์ฒด) / ํ•ต์‹ฌ ํŒŒํŠธ๋งŒ: 8๋ถ„

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ๋ฐฐ๊ฒฝ ์„ธ๊ณ„๊ด€: '์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ'

  • ์˜์ฒ (์‹ ์ž…): "๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ํผ ๋‹ค ๋งŒ๋“ค์—ˆ์–ด์š”! ๊ทผ๋ฐ, ์ด๊ฑธ DB์— ์ €์žฅํ•˜๋ ค๋ฉด ๋˜ app/api/posts/route.ts ํŒŒ์ผ ๋งŒ๋“ค์–ด์„œ POST ๋ฉ”์†Œ๋“œ ์งœ๊ณ , ํด๋ผ์ด์–ธํŠธ ์ชฝ์—์„  fetch๋กœ URL ํ•˜๋“œ์ฝ”๋”ฉํ•ด์„œ ๋ณด๋‚ด๊ณ , ์ƒํƒœ ๊ด€๋ฆฌ ์ฝ”๋“œ ์ˆ˜์‹ญ ์ค„ ์งœ๊ณ ... ๊ธฐ๋Šฅ ๊ตฌํ˜„๋ณด๋‹ค API ์„ธํŒ…ํ•˜๋Š” ๋ฐ ์‹œ๊ฐ„์ด ๋” ๋“œ๋„ค์š”. ์–ธ์ œ ์ด ์ง“์„ ๋‹ค ํ•˜์ฃ ?"
  • ์˜ํ˜ธ(๋ฆฌ๋“œ): "์˜์ฒ  ๋‹˜... ์–ธ์ œ ์  app/api๋ฅผ ์“ฐ๊ณ  ๊ณ„์‹  ๊ฒ๋‹ˆ๊นŒ! '์„œ๋ฒ„ ์•ก์…˜(Server Actions)' ์„ ์“ฐ๋ฉด ํ”„๋ก ํŠธ ํ•จ์ˆ˜ ์ฐฐํ™ ๋นš๋“ฏ์ด ๊ทธ๋ƒฅ function ํ•˜๋‚˜ ์„ ์–ธํ•ด๋†“๊ณ , ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ๋ฒ„ํŠผ์—์„œ ๊ทธ๊ฑธ ์ง์ ‘ importํ•ด์„œ ๋ƒ…๋‹ค ๋ˆ„๋ฅด๋ฉด ๋๋‚˜์š”! Next.js๊ฐ€ ๋’ค์—์„œ ๋‚จ๋ชฐ๋ž˜ API ํ…์ŠคํŠธ ํ†ต์‹  ๊ตฌ๋ฉ์„ ๋‹ค ํŒŒ์ค€๋‹ค๊ณ ์š”!"

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ํ๋ฆ„
API ๋ผ์šฐํŠธ์˜ ์ข…๋ง โ†’ use server ์„ ์–ธ๋ฒ• โ†’ ํด๋ผ์ด์–ธํŠธ ์—ฐ๋™ โ†’ ์‹ฌํ™” 3์žฅ์˜ ์บ์‹œ ๋ฌดํšจํ™”์™€์˜ ๊ฒฐํ•ฉ

๐ŸŽฏ ์ด ๋ฌธ์„œ๋ฅผ ๋‹ค ์ฝ์œผ๋ฉด ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • ๋ถˆํ•„์š”ํ•˜๊ฒŒ ๋Š˜์–ด๋‚˜๋Š” app/api ํด๋”์˜ RESTful ์—”๋“œํฌ์ธํŠธ ํŒŒ์ผ๋“ค์„ ์ „๋ถ€ ์‚ญ์ œํ•˜๊ณ , ๊น”๋”ํ•œ .ts ํŒŒ์ผ ๋‚ด์˜ ์„œ๋ฒ„ ํ•จ์ˆ˜ ๋ชจ์Œ์ง‘์œผ๋กœ ๋ฆฌํŒฉํ† ๋งํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋กœ๋”ฉ๋˜๊ธฐ ์ „์ด๊ฑฐ๋‚˜ ๋ธŒ๋ผ์šฐ์ €์—์„œ JS๊ฐ€ ๊บผ์ ธ(Disable)์žˆ๋Š” ํ™˜๊ฒฝ์—์„œ๋„, ๋‚ก์€ ๋ธŒ๋ผ์šฐ์ € ํผ ๋„ค์ดํ‹ฐ๋ธŒ ๋™์ž‘๋งŒ์œผ๋กœ ์„œ๋ฒ„์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅ์‹œํ‚ค๋Š” ๋งˆ๋ฒ•์„ ๊ฒฝํ—˜ํ•œ๋‹ค.

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€

๊ณผ๊ฑฐ React์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„(DB)๋กœ ๋˜์ ธ์„œ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ(Mutation) ํ•˜๋Š” ํ๋ฆ„์€ ์ง€๊ทนํžˆ ๋ป”ํ•˜๊ณ  ๋˜ ๊ณ ํ†ต์Šค๋Ÿฌ์› ์–ด.

  1. onSubmit ํ•ธ๋“ค๋Ÿฌ ์—ฐ๊ฒฐ
  2. e.preventDefault() ๋กœ ๋ธŒ๋ผ์šฐ์ € ๊นœ๋นก์ž„ ๋ง‰๊ธฐ
  3. ํ™”๋ฉด์— ๋ณด์—ฌ์ค„ loading ์ƒํƒœ ๋ณ€์ˆ˜ true ์ผœ๊ธฐ
  4. fetch('/api/...') ๋กœ ํ†ต์‹ 
  5. ์„ฑ๊ณตํ•˜๋ฉด loading ๋„๊ณ , ์‹คํŒจํ•˜๋ฉด ์—๋Ÿฌ state ์„ธํŒ…ํ•˜๊ณ ...
  6. ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์€ ๋ฐฑ์—”๋“œ๋Š” ๋˜ ๊ป๋ฐ๊ธฐ API ๊นŽ์•„์„œ DB์— ์ธ์„œํŠธํ•˜๊ณ ...

Vercel ์ฒœ์žฌ๋“ค์€ ์ด๊ฑธ ๋ณด๊ณ  ์ƒ๊ฐํ–ˆ์–ด.
"์–ด์ฐจํ”ผ ์š”์ฆ˜ ํด๋ผ์ด์–ธํŠธ๋„ JavaScript๊ณ , ๋ฐฑ์—”๋“œ ์„œ๋ฒ„ ์—”์ง„(Next.js)๋„ JavaScript์ธ๋ฐ ๋„๋Œ€์ฒด ์™œ ๊ฐ€์šด๋ฐ์— API๋ผ๋Š” ๊ฑฐ๋Œ€ํ•œ ํ…์ŠคํŠธ ๋ณ€ํ™˜ ์žฅ๋ฒฝ(Endpoint)์„ ๋ฌด๋ฆฌํ•˜๊ฒŒ ๋‘๊ณ  ๊ณ ํ†ต ๋ฐ›์•„์•ผ ํ•˜์ง€? ๋‘˜์„ ํ•˜๋‚˜์ธ ๊ฒƒ์ฒ˜๋Ÿผ ์ฐฐ์‹น ๋ถ™์—ฌ๋ฒ„๋ฆฌ๋ฉด ์•ˆ ๋ ๊นŒ?"

์ด ๊ณผ๊ฐํ•œ ๋ฐœ์ƒ์—์„œ ํƒ„์ƒํ•œ ๊ฒƒ์ด ๋ฐ”๋กœ ๋„ฅ์ŠคํŠธ์˜ ๊ฐ€์žฅ ์œ„๋Œ€ํ•œ ๋ฐœ๋ช…ํ’ˆ ์ค‘ ํ•˜๋‚˜์ธ Server Actions ์•ผ. ๋งˆ์น˜ ํ”„๋ก ํŠธ ๊ฐœ๋ฐœ์ž๊ฐ€ ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ ๋‚ด๋ถ€ ์ฝ”๋”ฉ ๋ฐฉ์‹ ๊ทธ๋Œ€๋กœ ์„œ๋ฒ„ DB๋ฅผ ์ง์ ‘ ์ฃผ๋ฌผ๋Ÿญ๊ฑฐ๋ฆฌ๋Š” ๋“ฏํ•œ ์••๋„์  DX(๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜)๋ฅผ ์„ ์‚ฌํ•˜๋Š” ์ด ๊ธฐ๋Šฅ์„ ๋ชจ๋ฅด๋ฉด ์‹œ๋‹ˆ์–ด๊ฐ€ ๋  ์ˆ˜ ์—†์–ด.


๐Ÿ—๏ธ ๋น„์œ ๋กœ ๋จผ์ € ์ดํ•ดํ•˜๊ธฐ

๐Ÿง’ 5์‚ด์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด? (์˜์ˆ˜๋„ค ๋งˆ๋ฒ• ํŽธ์ง€)

โŒ ๊ณผ๊ฑฐ (์˜์ฒ ์ด์˜ ํŒฉ์Šค ํ†ต์‹ )
์ง‘์— ์žˆ๋Š” ์˜์ฒ ์ด๊ฐ€ ์œ ์น˜์›(์„œ๋ฒ„) ์กฐ๋ฆฝ ๋ฐ•์Šค๋ฅผ ๋Œ๋ฆฌ๋ ค๋ฉด,
ํŒฉ์Šค ๋ฒˆํ˜ธ(API ์—”๋“œํฌ์ธํŠธ URL)๋ฅผ ์ฐพ์•„์„œ, ํŽธ์ง€ ์–‘์‹(JSON ํฌ๋งท)์— ๋งž์ถฐ ๊ธ€์”จ๋ฅผ ์ •์ž๋กœ ์“ฐ๊ณ , ์†ก์‹  ๋ฒ„ํŠผ ๋ˆ„๋ฅด๊ณ , ์ œ๋Œ€๋กœ ์ „์†ก๋๋Š”์ง€ ์‘๋‹ต ์šฉ์ง€ ๋Œ์•„์˜ฌ ๋•Œ๊นŒ์ง€ ๊ณ„์† ์˜†์—์„œ ๊ธฐ๋‹ค๋ ค์•ผ ํ–ˆ์–ด.

โœ… ํ˜„์žฌ (Server Actions - ์˜ํ˜ธ์˜ ๊ณต์œ  ์ปจํ…์ŠคํŠธ ๋ฒ„ํŠผ)
๊ทธ๋ƒฅ ์˜์ฒ ์ด ๋ฐฉ ๋ฆฌ๋ชจ์ปจ์— "์œ ์น˜์› ๋ฐ•์Šค ์กฐ๋ฆฝํ•ด์ค˜" ๋ฒ„ํŠผ์ด ์ƒ๊ฒผ์–ด.
์˜์ฒ ์ด๊ฐ€ ๋ฐฉ์—์„œ ๊ทธ ๋ฒ„ํŠผ์„ ์‘ฅ ๋ˆ„๋ฅด๋ฉด, ์œ ์น˜์›(์„œ๋ฒ„)์— ์žˆ๋Š” ๋งˆ๋ฒ• ์ƒ์ž๊ฐ€ ์•Œ์•„์„œ ์ „์›์„ ์ผœ๊ณ  ์กฐ๋ฆฝ์„ ์™„๋ฃŒํ•ด. ์˜์ฒ ์ด๋Š” ํŒฉ์Šค ๋ฒˆํ˜ธ๋‚˜ JSON ์–‘์‹ ๋”ฐ์œˆ ๋ชฐ๋ผ๋„ ๋ผ. ํ…์ŠคํŠธ ๋ณ€ํ™˜์€ ๊ณต์œ  ์ปจํ…์ŠคํŠธ(Next.js ์ž๋™ ์•”ํ˜ธํ™” ํ†ต์‹ )๊ฐ€ ์ „๋ถ€ ์•Œ์•„์„œ ์ฒ˜๋ฆฌํ•ด์ฃผ๊ฑฐ๋“ !


๐Ÿงฉ Server Actions์˜ ํƒ„์ƒ: use server์˜ ์ง„์งœ ์˜๋ฏธ ๐ŸŸข

use client์˜ ๋ฐ˜๋Œ€๋ง์ด use server ์ผ๊นŒ?
์•„๋‹ˆ์•ผ. ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ์•„๋ฌด ์ง€์‹œ์ž๊ฐ€ ์—†๋Š” '๊ธฐ๋ณธ๊ฐ’(default)'์ผ ๋ฟ์ด์•ผ.
use server ์˜ ์ง„์งœ ์˜๋ฏธ๋Š” "์„œ๋ฒ„ ์•ก์…˜(ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์›๊ฒฉ์œผ๋กœ ์ฐŒ๋ฅผ ์ˆ˜ ์žˆ๋Š” ๋งˆ๋ฒ•์˜ ์„œ๋ฒ„ ํ•จ์ˆ˜)์„ ์„ ์–ธํ•˜๊ฒ ๋‹ค" ๋Š” ๋ฌด์„œ์šด ์„ ์–ธ๋ฌธ์ด์•ผ.

์ƒํ™ฉ: DB์— ๊ฒŒ์‹œ๊ธ€ ์ €์žฅํ•˜๊ธฐ

// app/actions/post.ts (๐Ÿ’ก ์˜ค๋กœ์ง€ ์„œ๋ฒ„ ๋กœ์ง๋งŒ ๋‹ด๊ธด ์ˆœ์ˆ˜ ํŒŒ์ผ)
'use server' // ๐ŸŒŸ ์ด ํŒŒ์ผ ์•ˆ์˜ ๋ชจ๋“  ํ•จ์ˆ˜๋Š” ๋ธŒ๋ผ์šฐ์ €(ํด๋ผ์ด์–ธํŠธ)๊ฐ€ ์›๊ฒฉ์œผ๋กœ ํ˜ธ์ถœ(RPC) ํ•  ์ˆ˜ ์žˆ๋‹ค!
 
import db from '@/lib/db'
import { revalidatePath } from 'next/cache' // ์‹ฌํ™” 3์žฅ์—์„œ ๋ฐฐ์šด ๋„๋ผ
 
// ๋†€๋ž๊ฒŒ๋„ ์ด ํ•จ์ˆ˜๋Š” API ์—”๋“œํฌ์ธํŠธ URL์ด ์—†๋‹ค. ๊ทธ๋ƒฅ ํ•จ์ˆ˜๋‹ค!
export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;
 
  // ๋ธŒ๋ผ์šฐ์ € ์ชฝ์—์„  ์ ˆ๋Œ€ ๋ชจ๋ฅด๋Š” ์™„๋ฒฝํ•œ ๋ฐฑ์—”๋“œ ๋ณด์•ˆ ๊ตฌ๊ฐ„ (DB Insert)
  await db.post.create({ data: { title, content } });
 
  // ์„ฑ๊ณต ์‹œ, ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ•ด ํ™”๋ฉด์„ ๊ฐฑ์‹ !
  revalidatePath('/posts');
}

์ด๊ฑธ๋กœ ๋ฐฑ์—”๋“œ ์ฝ”๋“œ ์ƒ์„ฑ์€ ๋ ์ด์•ผ. app/api/... ํด๋” ํŒŒ๊ณ , NextResponse.json ๋ฆฌํ„ดํ•ด์ฃผ๊ณ  ํ•  ํ•„์š”๊ฐ€ ์•„์˜ˆ ์‚ฌ๋ผ์กŒ์–ด!


๐ŸŒฑ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์™€์˜ ๊ฒฐํ•ฉ: ํผ ๋„ˆ๋จธ์˜ ํ•จ์ˆ˜ ํ˜ธ์ถœ ๐ŸŸก

์ž, ์ด์ œ ์ € ๋งˆ๋ฒ•์˜ ์„œ๋ฒ„ ํ•จ์ˆ˜๋ฅผ ์ปดํฌ๋„ŒํŠธ ์ชฝ์—์„œ ๊ทธ๋ƒฅ ๋ถˆ๋Ÿฌ๋‹ค ์“ฐ๊ธฐ๋งŒ ํ•˜๋ฉด ๋ผ. ๋†€๋ผ์šด ์ ์€ ์„œ๋ฒ„ ๊ธฐ๋ฐ˜ <form> ๊ณผ, ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์˜ onClick ๋ฒ„ํŠผ ์–‘์ชฝ ๋ชจ๋‘์—์„œ ๋„ˆ๋ฌด๋‚˜ ์‰ฝ๊ฒŒ ๋™์ž‘ํ•œ๋‹ค๋Š” ๊ฑฐ์•ผ.

1) ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ์“ฐ์ง€ ์•Š๋Š” ์ˆœ์ • Form ๋™์ž‘ (Progressive Enhancement)

ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ๋งŒ๋“ค ๊ตฌ์ฐจํ•œ ์ด์œ ๊ฐ€ ์—†๋‹ค๋ฉด, ๊ทธ๋ƒฅ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋กœ ์ž‘์„ฑํ•ด๋„ ๋‹ค ๋Œ์•„๊ฐ€!

// app/posts/new/page.tsx (์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ์œ ์ง€!)
import { createPost } from '@/app/actions/post' // ๋ฐฉ๊ธˆ ๋งŒ๋“  ๊ทธ ์„œ๋ฒ„ ํ•จ์ˆ˜๋ฅผ ๊ทธ๋Œ€๋กœ ์ˆ˜์ž…!
 
export default function NewPostPage() {
  return (
    // ๐ŸŒŸ form์˜ 'action' ์†์„ฑ์— ๊ทธ์ € "์„œ๋ฒ„ ํ•จ์ˆ˜ ์ž์ฒด"๋ฅผ ๋ผ์›Œ ๋„ฃ๊ธฐ๋งŒ ํ•˜๋ฉด ๋!
    <form action={createPost}>
      <input type="text" name="title" />
      <textarea name="content" />
      <button type="submit">์ €์žฅํ•˜๊ธฐ</button>
    </form>
  )
}

์ด๊ฒŒ ์™œ ํ˜๋ช…์ ์ธ๊ฐ€?
๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ์—์„œ ์œ ์ €๊ฐ€ "์ €์žฅํ•˜๊ธฐ"๋ฅผ ๋ˆ„๋ฅด๋ฉด, ๋ธŒ๋ผ์šฐ์ €์˜ ์ˆœ์ •(Native) ํผ ์ „์†ก ๋ฐฉ์‹์ด ๋ฐœ๋™ํ•ด. JavaScript ์ฝ”๋“œ๊ฐ€ ๋‹ค์šด๋กœ๋“œ๋˜๊ธฐ ์ „์ด๋“ , JS ์ž์ฒด๊ฐ€ ์ฐจ๋‹จ๋œ ๋ณด์•ˆ ํ™˜๊ฒฝ์ด๋“  ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์Œฉ ๋ธŒ๋ผ์šฐ์ € Form Action์„ ๋‚š์•„์ฑ„์„œ ์•ˆ์ „ํ•˜๊ฒŒ ์„œ๋ฒ„๋ฅผ ์ณ์ค˜. e.preventDefault ๊ฐ€ ๊ทธ๋ฆฝ์ง€ ์•Š์€ ์‹œ์›ํ•œ ์พŒ๊ฐ!

2) ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ(use client) ๋‚ด๋ถ€์—์„œ ๋งˆ์Œ๊ป ํ˜ธ์ถœํ•˜๊ธฐ

๋‹น์—ฐํžˆ "์–ด ๋‚œ ํผ ํƒœ๊ทธ ์“ฐ๊ธฐ ์‹ซ๊ณ  ๊ทธ๋ƒฅ onClick ์œผ๋กœ ์ข‹์•„์š” ์ˆซ์ž ์˜ฌ๋ฆฌ๊ณ  ์‹ถ์€๋ฐ?" ๋„ 100% ์ง€์›๋ผ.

// app/components/LikeButton.tsx
'use client'
 
import { addLike } from '@/app/actions/post' // ์„œ๋ฒ„ ์—‘์…˜์„ ์ˆ˜์ž…!
import { useTransition } from 'react'
 
export default function LikeButton({ postId }) {
  const [isPending, startTransition] = useTransition();
 
  const handleLike = () => {
    // startTransition์œผ๋กœ ๊ฐ์‹ธ๋ฉด ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์„œ๋ฒ„ ์š”์ฒญ ์ค‘์—๋„ ์—๋Ÿฌ/๋กœ๋”ฉ UI๋ฅผ ์•ˆ ๋Š๊ธฐ๊ฒŒ ์ œ์–ด ๊ฐ€๋Šฅ!
    startTransition(async () => {
      await addLike(postId); // fetch? API ๋ผ์šฐํŠธ? ๊ทธ๋”ด ๊ฑฐ ์—†๋‹ค. ๊ทธ๋ƒฅ ๋น„๋™๊ธฐ ์„œ๋ฒ„ํ•จ์ˆ˜ ๋ƒ…๋‹ค ํ˜ธ์ถœ!
    });
  }
 
  return (
    <button onClick={handleLike} disabled={isPending}>
      {isPending ? '์ฒ˜๋ฆฌ์ค‘...' : 'โค๏ธ ์ข‹์•„์š”'}
    </button>
  )
}

Next.js ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ๋นŒ๋“œํ•  ๋•Œ ์ € addLike(postId) ๋ถ€๋ถ„์˜ ๋ฌธ๋งฅ์„ ๋ถ„์„ํ•ด์„œ, ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ฝ์„ ๋•Œ ๋ชฐ๋ž˜ ๋žœ๋ค API ์ฃผ์†Œ(์˜ˆ: POST /_next/server-action)๋กœ ์น˜ํ™˜๋˜๋Š” ์ฝ”๋“œ๋กœ ์•”ํ˜ธํ™”(RPC) ํฌ์žฅํ•ด๋ฒ„๋ฆฌ๋Š” ๋งˆ์ˆ ์„ ๋ถ€๋ฆฌ๋Š” ๊ฑฐ์•ผ. ํ”„๋ก ํŠธ๋Š” "๊ทธ๋ƒฅ ์„œ๋ฒ„ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ–ˆ์„ ๋ฟ!" ์ด์ง€.


๐Ÿ›ก๏ธ useActionState์™€ ๋‚™๊ด€์  UI(Optimistic UX) ์„ค๊ณ„ ๐Ÿ”ด

์‹œ๋‹ˆ์–ด๊ธ‰ ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ๊ณ ๋ฏผํ•˜๋Š” ๊ฐ€์žฅ ํฐ ํผ ์ „์†ก์˜ ๋‘ ๊ฐ€์ง€ ๋ฒฝ์ด ์žˆ์–ด.

  1. ํšŒ์›๊ฐ€์ž… ์ œ์ถœ ํ›„ "๋น„๋ฐ€๋ฒˆํ˜ธ ๊ธธ์ด๊ฐ€ ์งง์Šต๋‹ˆ๋‹ค" ๊ฐ™์€ ์„œ๋ฒ„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฐ˜ํ™˜ ์—๋Ÿฌ ๊ฐ’ ์„ ํ™”๋ฉด์— ์–ด๋–ป๊ฒŒ ๋ฟŒ๋ฆฌ์ง€?
  2. ์ข‹์•„์š” ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋Š” ์ˆœ๊ฐ„ ๋ฐ”๋กœ ์„œ๋ฒ„๊ฐ€ ์ฒ˜๋ฆฌํ•˜๊ธฐ๋„ ์ „์— ํ™”๋ฉด์—” 'ํ•˜ํŠธ ๋ฟ…!' ํ•˜๊ณ  ์„ ๋ฐ˜์˜(Optimistic Update)๋˜์–ด์„œ ๋นจ๋ผ ๋ณด์ด๊ฒŒ ์–ด๋–ป๊ฒŒ ํ•˜์ง€?

์‹œ๋‹ˆ์–ด์˜ ๋ฌด๊ธฐ: useActionState (React 19 ์ตœ์‹  ํ›…)

๊ธฐ์กด useFormState ๊ฐ€ ์ง„ํ™”ํ•œ ๋ํŒ์™• ํ›…์ด์•ผ.
์„œ๋ฒ„ ์•ก์…˜ ํ•จ์ˆ˜์˜ "๋กœ๋”ฉ ์—ฌ๋ถ€(isPending)" ์™€ "๊ฒฐ๊ด๊ฐ’ ๋„์ถœ(State)" ์„ ์™„๋ฒฝํžˆ ์—ฐ๊ฒฐํ•ด์ฃผ๋Š” ๋‹ค๋ฆฌ ์—ญํ• ์„ ํ•ด.

// app/actions/auth.ts
'use server'
 
export async function loginAction(prevState: any, formData: FormData) {
  const email = formData.get('email');
 
  if (!email.includes('@')) {
    // ๐ŸŒŸ ์„ฑ๊ณต/์‹คํŒจ ์—ฌ๋ถ€๋ฅผ ์ž์œ ์ž์žฌ๋กœ ๋ฆฌํ„ด ๊ฐ์ฒด๋กœ ๋‚ด๋ฑ‰๋Š”๋‹ค!
    return { error: '์ด๋ฉ”์ผ ํ˜•์‹์ด ์•„๋‹™๋‹ˆ๋‹ค!', success: false };
  }
 
  await db.login();
  return { error: null, success: true };
}
// app/Login.tsx
'use client'
import { useActionState } from 'react'
import { loginAction } from './actions/auth'
 
export default function LoginForm() {
  // ๐ŸŒŸ ๋งˆ๋ฒ•์˜ 3๋ฐ•์ž: [ํ˜„์žฌ์„œ๋ฒ„์‘๋‹ต์ƒํƒœ, ํผ์—์—ฐ๊ฒฐํ• ๋ฐœ์‚ฌํ•จ์ˆ˜, ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ๋”ฉ์—ฌ๋ถ€]
  const [state, formAction, isPending] = useActionState(loginAction, { error: null });
 
  return (
    <form action={formAction}>
      <input type="text" name="email" />
      <button disabled={isPending}>๋กœ๊ทธ์ธ</button>
 
      {/* ๐ŸŒŸ ์„œ๋ฒ„์—์„œ ๋Œ์•„์˜จ ์—๋Ÿฌ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ฆ‰์‹œ ํ™”๋ฉด์— ๋…ธ์ถœ (SPA ๊ฒฝํ—˜ ์™„๋ฒฝ ์œ ์ง€!) */}
      {state.error && <p className="text-red-500">{state.error}</p>}
    </form>
  )
}

๐Ÿ˜Ž ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ(useOptimistic)์™€์˜ ์กฐํ•ฉ
ํŽ˜์ด์Šค๋ถ ์ข‹์•„์š” ๋ฒ„ํŠผ์ฒ˜๋Ÿผ, ์„œ๋ฒ„ ์‘๋‹ต์ด ์˜ค๊ธฐ ์ „๊นŒ์ง€ ์ปดํฌ๋„ŒํŠธ UI์ƒ์—์„œ๋งŒ ๊ฐ€์งœ๋กœ +1 ์˜ฌ๋ฆฐ ์ƒํƒœ๋ฅผ ๋ณด์—ฌ์ฃผ๋‹ค๊ฐ€, ์„œ๋ฒ„ ์•ก์…˜์ด ์—๋Ÿฌ ๋‚˜๋ฉด ๋‹ค์‹œ ์Šค๋ฅด๋ฅต -1 ๋กœ ์›์ƒ ๋ณต๊ตฌ์‹œํ‚ค๋Š” ๋งˆ๋ฒ•์˜ useOptimistic() ํ›…์„ ์ด ์•ก์…˜๋“ค๊ณผ ๊ฒฐํ•ฉํ•˜๋ฉด ํ”„๋ก ํŠธ์˜ UX๋Š” ํ˜„์กด ์ตœ๊ณ ๋ด‰ ๋‹จ๊ณ„๋กœ ์˜ฌ๋ผ์„œ๊ฒŒ ๋œ๋‹ค.


๐Ÿ’ฅ ์—๋Ÿฌ ํ•ด๊ฒฐ ์นดํƒˆ๋กœ๊ทธ

์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๋ฉด Ctrl+F๋กœ ๊ฒ€์ƒ‰ํ•ด๋ด.

โŒ Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server"

์›์ธ: ๋ฌด์‹ฌ์ฝ” ๋ฐ์ดํ„ฐ๋ฅผ ์กฐ์ž‘ํ•˜๋Š” ์ผ๋ฐ˜ ์„œ๋ฒ„์šฉ ์œ ํ‹ธ ํ•จ์ˆ˜๋ฅผ ๋ธŒ๋ผ์šฐ์ € ์ปดํฌ๋„ŒํŠธ ๋ฒ„ํŠผ onClick ์—๋‹ค ํ”„๋กญ์Šค์ฒ˜๋Ÿผ ๋„˜๊ฒจ์ฃผ๋ ค ํ–ˆ์Œ. ํ•จ์ˆ˜ ๊ป๋ฐ๊ธฐ ์ž์ฒด๋Š” ํด๋ผ์ด์–ธํŠธ๋กœ ์ „์†ก์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.
ํ•ด๊ฒฐ์ฑ…: ๊ทธ ํ•จ์ˆ˜ ์ƒ๋‹จ(ํ˜น์€ ํŒŒ์ผ ์ตœ์ƒ๋‹จ)์— 'use server' ๋ถ€์ ์„ ๋ถ™์—ฌ์„œ ์ด ํ•จ์ˆ˜๊ฐ€ RPC ์›๊ฒฉ ํ”„๋กœ์‹œ์ € ํ˜ธ์ถœ ๋Œ€์ƒ์ž„์„ ํ”„๋ ˆ์ž„์›Œํฌ์— ์„ ์–ธ ์ธ๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค.

โŒ Only plain objects, and a few built-ins, can be passed to Client Components from Server Actions.

์›์ธ: ์„œ๋ฒ„ ์•ก์…˜ ํ•จ์ˆ˜ ์•ˆ์—์„œ ๋ณต์žกํ•œ DB ํด๋ž˜์Šค ์ธ์Šคํ„ด์Šค(Ex: Prisma User ๊ฐ์ฒด)๋‚˜ Date ๋ณ€์ˆ˜ ์ž์ฒด๋ฅผ ์Œฉ์œผ๋กœ return ํ•ด์„œ ๋ธŒ๋ผ์šฐ์ €์—๊ฒŒ ๋˜์ง€๋ ค ํ–ˆ์„ ๋•Œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.
ํ•ด๊ฒฐ์ฑ…: ๋ธŒ๋ผ์šฐ์ €๋Š” ๊ทธ๋Ÿฐ ๋ณต์žกํ•œ ๋ฉ”๋ชจ๋ฆฌ ํด๋ž˜์Šค๋ฅผ ์ฝ์ง€ ๋ชปํ•œ๋‹ค. ๋ฆฌํ„ด๊ฐ’์„ ๋‚ด๋ฆฌ๊ธฐ ์ „์— .toJSON() ์œผ๋กœ ์ •์ œํ•˜๊ฑฐ๋‚˜ ์ˆœ์ˆ˜ํ•œ Object(๋ฌธ์ž์—ด, ์ˆซ์ž, ๋ถˆ๋ฆฌ์–ธ ์กฐํ•ฉ์˜ DTO) ํ˜•ํƒœ๋กœ ํ‰ํƒ„ํ™”(Serialization) ํ•ด์„œ ํฌ์žฅํ•ด ๋ณด๋‚ด์•ผ ํ•œ๋‹ค.


๐Ÿ ์ด๋ฒˆ์— ๋ฐฐ์šด ๋‚ด์šฉ ์ด์ •๋ฆฌ

๋„๊ตฌ / ๋ฌธ๋ฒ•ํŠน์ง•๋Œ€์ฒด ๋‹นํ•œ ๊ธฐ์ˆ  (๊ณผ๊ฑฐ ์œ ๋ฌผ)
"use server"ํŒŒ์ผ์˜ ๋ชจ๋“  ํ•จ์ˆ˜๋ฅผ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์›๊ฒฉ ํ˜ธ์ถœํ•˜๋Š” ๋‹ค๋ฆฌ(RPC)๋กœ ๋ณ€์‹ ์‹œํ‚ด์ง€์ €๋ถ„ํ–ˆ๋˜ app/api/... RESTful ๋ผ์šฐํŒ… ํด๋”์™€ ํŒŒ์ผ๋“ค
<form action={...}>๋ธŒ๋ผ์šฐ์ € ์ „๋ฉด ๋กœ๋”ฉ ์—†์ด, ๋„ค์ดํ‹ฐ๋ธŒ ํผ ๋งŒ์œผ๋กœ ๋ฐฐํ›„์—์„œ ์„œ๋ฒ„ ๋งˆ์ˆ  ์ˆ˜ํ–‰๋ฒˆ๊ฑฐ๋กœ์› ๋˜ e.preventDefault(), Axios ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ์„ธํŒ… ๋กœ์ง
useActionState์„œ๋ฒ„ ์•ก์…˜์˜ ์‹œ์ž‘๊ณผ ๋, ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ„ด ์–ด๋ผ์šด๋“œ๋ฅผ ํ•˜๋‚˜์˜ State ๋ฃจํ”„๋กœ ๋ฌถ์–ด์คŒuseState(loading), useState(errorMsg) ๋“ฑ์˜ ์‚ฐ๋งŒํ–ˆ๋˜ ์ง€์—ญ ์ƒํƒœ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ

๐Ÿ’ก ์‹œ๋‹ˆ์–ด์˜ ๋ฉ˜ํƒˆ ๋ชจ๋ธ
Server Actions๋Š” ๊ฒฐ๊ตญ "์ˆจ๊ฒจ์ง„ POST API ์ž๋™ ์ƒ์„ฑ๊ธฐ" ์ด๋‹ค. ๋„ˆ๋ฌด๋‚˜ ์‚ฌ์šฉ์ด ํŽธํ•ด์ง„ ๋งŒํผ, ๋ณด์•ˆ์— ์ฒ ์ €ํ•ด์•ผ ํ•œ๋‹ค! ์ € ํ•จ์ˆ˜๋Š” URL์„ ํƒ€๊ณ  ๋ˆ„๊ตฌ๋‚˜ ํ˜ธ์ถœ ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ ๋‚ด๋ถ€ ์ตœ์ƒ๋‹จ์— ํ•ญ์ƒ if (!user) throw Error("ํ•ดํ‚น ๊ธˆ์ง€") ๊ฐ™์€ ์ธ์ฆ/์ธ๊ฐ€ ๊ฐ€๋“œ๋ฅผ ๋ฐ˜๋“œ์‹œ ์„ธ์›Œ๋‘๋Š” ๊ฒƒ์„ ์žŠ์ง€ ๋ง์ž.


๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

Q1. Server Action์„ <form action=>์— ์—ฐ๊ฒฐํ–ˆ์„ ๋•Œ ์–ป๋Š” ์‹ค๋ฌด์  ์žฅ์ ์€ ๋ฌด์—‡์ธ๊ฐ€?

โœ… ์ •๋‹ต: ํผ ์ œ์ถœ, ์„œ๋ฒ„ ์‹คํ–‰, ์บ์‹œ ๊ฐฑ์‹ , UI ๋ฐ˜ํ™˜์„ ํ•˜๋‚˜์˜ ์„œ๋ฒ„ ์™•๋ณต์œผ๋กœ ๋ฌถ์„ ์ˆ˜ ์žˆ๊ณ , ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ํผ์€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์—†์–ด๋„ ๊ธฐ๋ณธ ์ œ์ถœ ํ๋ฆ„์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค: Server Action์€ ๋‹จ์ˆœํžˆ API Route๋ฅผ ๋œ ์“ฐ๊ฒŒ ํ•ด์ฃผ๋Š” ๋ฌธ๋ฒ•์ด ์•„๋‹ˆ๋‹ค. mutation์„ ์„œ๋ฒ„ ํ•จ์ˆ˜๋กœ ๋ชจ๋ธ๋งํ•˜๊ณ , FormData๋ฅผ ํ†ตํ•ด ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ณธ ํผ ํ๋ฆ„๊ณผ ์—ฐ๊ฒฐํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ์ž…๋ ฅ๊ฐ’์„ ๋ชจ๋‘ useState๋กœ ๋“ค๊ณ  ์žˆ๋‹ค๊ฐ€ JSON์œผ๋กœ ํฌ์žฅํ•˜๋Š” ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค.


Q2. Server Action์ด ํด๋ผ์ด์–ธํŠธ์—์„œ ํ˜ธ์ถœ ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ์‚ฌ์‹ค ๋•Œ๋ฌธ์— ๋ฐ˜๋“œ์‹œ ์ง€์ผœ์•ผ ํ•  ๋ณด์•ˆ ์›์น™์€ ๋ฌด์—‡์ธ๊ฐ€?

โœ… ์ •๋‹ต: ๋ชจ๋“  Server Action ๋‚ด๋ถ€์—์„œ ์ธ์ฆ๊ณผ ๊ถŒํ•œ์„ ๋‹ค์‹œ ๊ฒ€์ฆํ•ด์•ผ ํ•œ๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค: ๊ณต์‹ ๋ฌธ์„œ๋„ Server Function/Action์€ ์ง์ ‘ POST ์š”์ฒญ์œผ๋กœ ํ˜ธ์ถœ๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๊ณต๊ฐœ API ์—”๋“œํฌ์ธํŠธ์ฒ˜๋Ÿผ ๋‹ค๋ฃจ๋ผ๊ณ  ์„ค๋ช…ํ•œ๋‹ค. ๋ฒ„ํŠผ์ด ํ™”๋ฉด์— ์•ˆ ๋ณด์ธ๋‹ค๋Š” ๊ฒƒ์€ ๋ณด์•ˆ์ด ์•„๋‹ˆ๋‹ค. auth(), ์—ญํ•  ๊ฒ€์‚ฌ, ์†Œ์œ ๊ถŒ ๊ฒ€์‚ฌ, ์ž…๋ ฅ ๊ฒ€์ฆ์„ ์•ก์…˜ ๋‚ด๋ถ€์— ๋‘ฌ์•ผ ํ•œ๋‹ค. ์˜ํ˜ธ๋ผ๋ฉด "UI ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์€ UX, ์„œ๋ฒ„ ๊ฒ€์ฆ์€ ๋ณด์•ˆ"์ด๋ผ๊ณ  ๋ถ„๋ฆฌํ•ด์„œ ๋ฆฌ๋ทฐํ•œ๋‹ค.


Q3. ์˜์ฒ ์ด์˜ ํ…Œ์ŠคํŠธ ํƒ€์ž„: ๋Œ“๊ธ€ ์ž‘์„ฑ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์ฆ‰์‹œ ๋Œ“๊ธ€์ด ๋ณด์ด๊ฒŒ ํ•˜๊ณ  ์‹ถ๋‹ค. ์‹คํŒจํ•˜๋ฉด ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋„ ๋ณด์—ฌ์•ผ ํ•œ๋‹ค. ์–ด๋–ค ์กฐํ•ฉ์ด ์ž์—ฐ์Šค๋Ÿฌ์šธ๊นŒ?

โœ… ์ •๋‹ต: Server Action์— ๊ฒ€์ฆ๊ณผ ์ €์žฅ์„ ๋‘๊ณ , ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” useActionState๋กœ pending/error ์ƒํƒœ๋ฅผ ๋ฐ›๊ณ  ํ•„์š”ํ•˜๋ฉด useOptimistic์œผ๋กœ ๋‚™๊ด€์  ๋Œ“๊ธ€์„ ํ‘œ์‹œํ•œ๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค: useActionState๋Š” ์•ก์…˜์˜ ๋ฐ˜ํ™˜ ์ƒํƒœ์™€ pending์„ UI์— ์—ฐ๊ฒฐํ•œ๋‹ค. ๋‚™๊ด€์  UI๋Š” ์ฒด๊ฐ ์†๋„๋ฅผ ๋†’์ด์ง€๋งŒ, ์„œ๋ฒ„ ์‹คํŒจ ์‹œ ๋˜๋Œ๋ฆด ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค. ์ €์žฅ ์„ฑ๊ณต ๋’ค์—๋Š” ๊ด€๋ จ ํƒœ๊ทธ๋‚˜ ๊ฒฝ๋กœ๋ฅผ ๊ฐฑ์‹ ํ•ด ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋„ ๋งž์ถฐ์•ผ ํ•œ๋‹ค. ์•ก์…˜์€ ํŽธํ•œ ๋งŒํผ ๊ฒ€์ฆ, ๊ถŒํ•œ, ์บ์‹œ ๊ฐฑ์‹ ์ด ํ•œ ์„ธํŠธ๋‹ค.

๐Ÿฃ ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ

์˜ค๋Š˜์€ Server Action์ด API Route๋ฅผ ์—†์• ๋Š” ๋งˆ๋ฒ•์ด ์•„๋‹ˆ๋ผ mutation์„ ๋” ๊ฐ€๊นŒ์šด ๊ณณ์—์„œ ์„ค๊ณ„ํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ๋„๊ตฌ๋ผ๋Š” ๊ฑธ ์•Œ์•˜๋‹ค. ํผ๊ณผ ์„œ๋ฒ„ ํ•จ์ˆ˜๊ฐ€ ์ง์ ‘ ์ด์–ด์ง€๋‹ˆ ์ฝ”๋“œ๊ฐ€ ์งง์•„์กŒ์ง€๋งŒ, ๊ทธ ํ•จ์ˆ˜๊ฐ€ ์™ธ๋ถ€์—์„œ ํ˜ธ์ถœ ๊ฐ€๋Šฅํ•œ ์„œ๋ฒ„ ์ž…๊ตฌ๋ผ๋Š” ์‚ฌ์‹ค๋„ ๋” ์„ ๋ช…ํ•ด์กŒ๋‹ค.

๐Ÿ’ก "Server Action์€ ํŽธํ•œ ํ˜ธ์ถœ ๋ฌธ๋ฒ•์ด ์•„๋‹ˆ๋ผ, ๊ณต๊ฐœ๋œ ๋ณ€๊ฒฝ ์ง€์ ์ด๋‹ค."

์•ž์œผ๋กœ ์•ก์…˜์„ ๋งŒ๋“ค ๋•Œ๋Š” ์ž…๋ ฅ ๊ฒ€์ฆ, ์ธ์ฆ/๊ถŒํ•œ, ์บ์‹œ ๊ฐฑ์‹ , ๋ฐ˜ํ™˜ ๊ฐ€๋Šฅํ•œ ์ˆœ์ˆ˜ ๊ฐ์ฒด๋ฅผ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋กœ ๋‘๊ฒ ๋‹ค. ์˜ํ˜ธ์—๊ฒŒ ๋ฆฌ๋ทฐ๋ฅผ ์š”์ฒญํ•˜๊ธฐ ์ „์— "์ด ๋ฒ„ํŠผ์„ ์šฐํšŒํ•ด์„œ POSTํ•ด๋„ ์•ˆ์ „ํ•œ๊ฐ€?"๋ฅผ ๋จผ์ € ๋‚ด ์ฝ”๋“œ์— ๋ฌผ์–ด๋ด์•ผ๊ฒ ๋‹ค.

๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ