๐ Next.js ์ฌํ 4์ฅ: Server Actions โ API ๋ผ์ฐํธ ์๋ ์ธ์์ ํ๋ช
๐ ๊ฐ์
Server Actions๋ก API ๋ผ์ฐํธ ์์ด ์๋ฒ ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ๊ณ ํผ์ ์ฒ๋ฆฌํ๋ ํจํด์ ๋ฐฐ์๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐งฉ Server Actions์ ํ์:
use server์ ์ง์ง ์๋ฏธ ๐ข - ๐ฑ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ ๊ฒฐํฉ: ํผ ๋๋จธ์ ํจ์ ํธ์ถ ๐ก
- ๐ก๏ธ
useActionState์ ๋๊ด์ UI(Optimistic UX) ์ค๊ณ ๐ด - ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 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) ํ๋ ํ๋ฆ์ ์ง๊ทนํ ๋ปํ๊ณ ๋ ๊ณ ํต์ค๋ฌ์ ์ด.
onSubmitํธ๋ค๋ฌ ์ฐ๊ฒฐe.preventDefault()๋ก ๋ธ๋ผ์ฐ์ ๊น๋นก์ ๋ง๊ธฐ- ํ๋ฉด์ ๋ณด์ฌ์ค
loading์ํ ๋ณ์true์ผ๊ธฐ fetch('/api/...')๋ก ํต์- ์ฑ๊ณตํ๋ฉด
loading๋๊ณ , ์คํจํ๋ฉด ์๋ฌstate์ธํ ํ๊ณ ... - ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ๋ฐฑ์๋๋ ๋ ๊ป๋ฐ๊ธฐ 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) ์ค๊ณ ๐ด
์๋์ด๊ธ ํ๋ก ํธ์๋๊ฐ ๊ณ ๋ฏผํ๋ ๊ฐ์ฅ ํฐ ํผ ์ ์ก์ ๋ ๊ฐ์ง ๋ฒฝ์ด ์์ด.
- ํ์๊ฐ์ ์ ์ถ ํ "๋น๋ฐ๋ฒํธ ๊ธธ์ด๊ฐ ์งง์ต๋๋ค" ๊ฐ์ ์๋ฒ ์ ํจ์ฑ ๊ฒ์ฌ ๋ฐํ ์๋ฌ ๊ฐ ์ ํ๋ฉด์ ์ด๋ป๊ฒ ๋ฟ๋ฆฌ์ง?
- ์ข์์ ๋ฒํผ์ ๋๋ฅด๋ ์๊ฐ ๋ฐ๋ก ์๋ฒ๊ฐ ์ฒ๋ฆฌํ๊ธฐ๋ ์ ์ ํ๋ฉด์ 'ํํธ ๋ฟ !' ํ๊ณ ์ ๋ฐ์(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("ํดํน ๊ธ์ง")๊ฐ์ ์ธ์ฆ/์ธ๊ฐ ๊ฐ๋๋ฅผ ๋ฐ๋์ ์ธ์๋๋ ๊ฒ์ ์์ง ๋ง์.
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
๋ฐฐ์ ์ผ๋ฉด ํ ๋ฒ ๋นํ์ด์ ํ์ธํด๋ด์ผ ํด.
- ์ํฉ: ์์ฒ ์ด๊ฐ ํ์ ์ ๋ณด ์์ ํ์ด์ง์์ ์ ๋ฉด ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ(
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๋ก ๋๊น ์๋ ๊ฒฝํ์ ์ ๋ฌผํ์!"
์ํธ ๋ฆฌ๋ ๋์ด "ํธํด์ง ๋งํผ ๋ณด์์ ๋ค๊ฐ ์ฑ๊ฒจ์ผ ํ๋ค" ๊ณ ๋ฐ๋ํ๊ฒ ์กฐ์ธํด ์ฃผ์ค ๋, ๋จ์ํ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ๊ฑธ ๋์ด ์์ ํ ์๋น์ค๋ฅผ ๋ง๋๋ ๊ฒ ๊ฐ๋ฐ์์ ์ง์ง ์ค๋ ฅ์ด๋ผ๋ ๊ฑธ ๋ค์ ํ๋ฒ ๊นจ๋ฌ์์ด. ์ค๋ ๋๋ฌด ์ด์ฌํ ๋ฌ๋ ธ๋๋ ๋น๋ถ ๋ณด์ถฉ์ด ์๊ธํด. ์ง์ ๊ฐ๋ ๊ธธ์ ๋ฌ๋ฌํ ์ด์ฝ๋ฐ ํ๋ ์ฌ ๋จน์ด์ผ์ง! ๋ด์ผ์ ๋ '๊ฐ๋ ฅํ๊ณ ์์ ํ' ์ก์ ์ ์ง๋ ๊ฐ๋ฐ์๊ฐ ๋ ๊ฑฐ์ผ! ๐ฃ