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

๐Ÿ“‹ ๊ฐœ์š”

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("ํ•ดํ‚น ๊ธˆ์ง€") ๊ฐ™์€ ์ธ์ฆ/์ธ๊ฐ€ ๊ฐ€๋“œ๋ฅผ ๋ฐ˜๋“œ์‹œ ์„ธ์›Œ๋‘๋Š” ๊ฒƒ์„ ์žŠ์ง€ ๋ง์ž.


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

๋ฐฐ์› ์œผ๋ฉด ํ•œ ๋ฒˆ ๋น„ํ‹€์–ด์„œ ํ™•์ธํ•ด๋ด์•ผ ํ•ด.

  1. ์ƒํ™ฉ: ์˜์ฒ ์ด๊ฐ€ ํšŒ์› ์ •๋ณด ์ˆ˜์ • ํŽ˜์ด์ง€์—์„œ ์ „๋ฉด ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ(use client) ํ™”๋ฉด์„ ๊ฐœ๋ฐœ ์ค‘์ด๋‹ค. ์ทจ๋ฏธ ๋ฐ•์Šค๋ฅผ <input> 10๊ฐœ์งœ๋ฆฌ ๊ฑฐ๋Œ€ํ•œ ํผ์œผ๋กœ ๋งŒ๋“ค์—ˆ๋Š”๋ฐ, ์˜์ฒ ์ด๋Š” ์ €์žฅ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ(onClick) ๋‚ด๋ถ€์—์„œ, 10๊ฐœ์˜ ๊ฐ’์„ ํ•˜๋‚˜ํ•˜๋‚˜ useState ๋กœ ๋‹ค ๋ฌถ์–ด์„œ ๋™๋™๋Œ€๋‹ค๊ฐ€ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๊ณ  ์„œ๋ฒ„ ์•ก์…˜ ํ•จ์ˆ˜ saveHobbies({ ๋ฌถ์€ ์ •๋ณด ๊ฐ์ฒด }) ๋กœ ์˜์•„๋ณด๋ƒˆ๋‹ค.
    ์ด ๋”์ฐํ•œ ๋ฆฌ์•กํŠธ ๋ Œ๋”๋ง ๋‚ญ๋น„ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ๋ฅผ ๋‹จ์ˆจ์— ๋‚ ๋ ค๋ฒ„๋ฆด ์ˆ˜ ์žˆ๋Š” ์˜ํ˜ธ ๋ฆฌ๋“œ์˜ "ํผ ์•ก์…˜ ๋„ค์ดํ‹ฐ๋ธŒ ๊ฐ์ฒด ์ ‘๊ทผ๋ฒ•" ๋ฆฌํŒฉํ† ๋ง ์กฐ์–ธ์€ ๋ฌด์—‡์ผ๊นŒ?

์ •๋‹ต ๋ฐ ํ•ด์„ค:
10๊ฐœ์˜ <input> ๊ฐ๊ฐ์— ๊ณ ์œ ์˜ name ์†์„ฑ(name="hobby1", name="hobby2")์„ ๋ถ€์—ฌํ•˜๋Š” ๊ฒƒ์ด ํ•ต์‹ฌ์ด๋‹ค.
๊ทธ๋ฆฌ๊ณ  ์–ด์„คํ”ˆ useState ์—ฐ๋™(์ œ์–ด ์ปดํฌ๋„ŒํŠธ)์„ ์ „๋ถ€ ์‚ญ์ œํ•˜๊ณ , ํผ ํƒœ๊ทธ ์ƒ๋‹จ์— <form action={saveHobbies}> ๋ผ๊ณ  ์„œ๋ฒ„ ์•ก์…˜์„ ๋ฐ”๋กœ ๊ฝ‚์•„๋ฒ„๋ฆฐ๋‹ค!
์ด๋Ÿฌ๋ฉด ๋„˜๊ฒจ๋ฐ›๋Š” ์„œ๋ฒ„ ์•ก์…˜ ํ•จ์ˆ˜์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ์ž๋™์œผ๋กœ ๋“ ๋“ ํ•œ formData ๋ธŒ๋ผ์šฐ์ € ๋„ค์ดํ‹ฐ๋ธŒ ๊ฐ์ฒด๊ฐ€ ๋˜๋ฉฐ, ์šฐ๋ฆฌ๋Š” formData.get('hobby1') ๋กœ ์šฐ์•„ํ•˜๊ณ  ๊ฐ€๋ณ๊ฒŒ(๋น„์ œ์–ด ๋ฐฉ์‹) ๊ฐ’์„ ์™์™ ๋ฝ‘์•„๋จน์–ด ์„ฑ๋Šฅ ์ตœ์ƒ์˜ ์†๋„๋ฅผ ๋‚ผ ์ˆ˜ ์žˆ๋‹ค.


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

์˜ค๋Š˜์€ ์ •๋ง ๋„ฅ์ŠคํŠธ์˜ ๊ฝƒ์ด๋ผ ๋ถˆ๋ฆฌ๋Š” 'Server Actions' ๋ฅผ ๋ฐฐ์šฐ๋ฉด์„œ ์‹ ์„ธ๊ณ„๋ฅผ ๊ฒฝํ—˜ํ–ˆ์–ด! ์˜ˆ์ „์—” ํผ ํ•˜๋‚˜ ์ œ์ถœํ•˜๋ ค๋ฉด API ๋งŒ๋“ค๊ณ , ํŽ˜์นญ ๋กœ์ง ์งœ๊ณ , ๋กœ๋”ฉ ์ƒํƒœ ๊ด€๋ฆฌํ•˜๋А๋ผ ์ง„์ด ๋‹ค ๋น ์กŒ๋Š”๋ฐ... ์ด์ œ๋Š” ํ•จ์ˆ˜ ํ•˜๋‚˜๋กœ ์ด ๋ชจ๋“  ๊ฒŒ ์—ฐ๊ฒฐ๋˜๋‹ค๋‹ˆ ์ •๋ง ์ถฉ๊ฒฉ์ ์ด์—ˆ์–ด.

๐Ÿ’ก ์˜ค๋Š˜์˜ ๊ตํ›ˆ: "๋ณต์žกํ•œ API ์—”๋“œํฌ์ธํŠธ ์„ค๊ณ„์— ๋งค๋ชฐ๋˜์ง€ ๋ง์ž. use server ์•ก์…˜ ํ•˜๋‚˜๋กœ ํ”„๋ก ํŠธ์™€ ์„œ๋ฒ„๋ฅผ ์šฐ์•„ํ•˜๊ฒŒ ์ž‡๊ณ , ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” useActionState ๋กœ ๋Š๊น€ ์—†๋Š” ๊ฒฝํ—˜์„ ์„ ๋ฌผํ•˜์ž!"

์˜ํ˜ธ ๋ฆฌ๋“œ ๋‹˜์ด "ํŽธํ•ด์ง„ ๋งŒํผ ๋ณด์•ˆ์€ ๋„ค๊ฐ€ ์ฑ™๊ฒจ์•ผ ํ•œ๋‹ค" ๊ณ  ๋”ฐ๋”ํ•˜๊ฒŒ ์กฐ์–ธํ•ด ์ฃผ์‹ค ๋•Œ, ๋‹จ์ˆœํžˆ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฑธ ๋„˜์–ด ์•ˆ์ „ํ•œ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒŒ ๊ฐœ๋ฐœ์ž์˜ ์ง„์งœ ์‹ค๋ ฅ์ด๋ผ๋Š” ๊ฑธ ๋‹ค์‹œ ํ•œ๋ฒˆ ๊นจ๋‹ฌ์•˜์–ด. ์˜ค๋Š˜ ๋„ˆ๋ฌด ์—ด์‹ฌํžˆ ๋‹ฌ๋ ธ๋”๋‹ˆ ๋‹น๋ถ„ ๋ณด์ถฉ์ด ์‹œ๊ธ‰ํ•ด. ์ง‘์— ๊ฐ€๋Š” ๊ธธ์— ๋‹ฌ๋‹ฌํ•œ ์ดˆ์ฝ”๋ฐ” ํ•˜๋‚˜ ์‚ฌ ๋จน์–ด์•ผ์ง€! ๋‚ด์ผ์€ ๋” '๊ฐ•๋ ฅํ•˜๊ณ  ์•ˆ์ „ํ•œ' ์•ก์…˜์„ ์งœ๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋  ๊ฑฐ์•ผ! ๐Ÿฃ


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