๐ 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("ํดํน ๊ธ์ง")๊ฐ์ ์ธ์ฆ/์ธ๊ฐ ๊ฐ๋๋ฅผ ๋ฐ๋์ ์ธ์๋๋ ๊ฒ์ ์์ง ๋ง์.
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
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ํด๋ ์์ ํ๊ฐ?"๋ฅผ ๋จผ์ ๋ด ์ฝ๋์ ๋ฌผ์ด๋ด์ผ๊ฒ ๋ค.