๐ Next.js 15v์ Server Action
1. Server Action์ด๋?
Next.js 15๋ฒ์ ์ ์๋กญ๊ฒ ๋์ ๋ Server Action์ ์๋ฒ์์ ์ง์ ์คํ๋๋ ํจ์๋ก, ํด๋ผ์ด์ธํธ์ ์๋ฒ ๊ฐ์ ๋ณต์กํ ๋ฐ์ดํฐ ์์ฒญ ํ๋ฆ์ ๋จ์ํํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค. ์ด๋ฅผ ํตํด ์๋ฒ์์ ์ฒ๋ฆฌํด์ผ ํ ์์ ๋ค์ ํด๋ผ์ด์ธํธ ์ฝ๋์์ ๋ถ๋ฆฌํ๊ณ , ํด๋ผ์ด์ธํธ์์ ์๋ฒ ํจ์ ํธ์ถ์ ๊ฐํธํ๊ฒ ํ ์ ์์ต๋๋ค.
2. Server Action์ด ํด๊ฒฐํ๋ ๋ฌธ์ ๋ค
- ๋ณต์กํ ๋ฐ์ดํฐ ์์ฒญ ๋ก์ง: ๊ธฐ์กด์๋ ํด๋ผ์ด์ธํธ์์ ๋ฐ์ดํฐ๋ฅผ fetchํ๊ฑฐ๋ ์๋ฒ์ ํต์ ํ๊ธฐ ์ํด ์ฌ๋ฌ API ํธ์ถ ์ฝ๋๋ฅผ ์์ฑํด์ผ ํ์ต๋๋ค.
- ํด๋ผ์ด์ธํธ-์๋ฒ ๋ถ๋ฆฌ: ํด๋ผ์ด์ธํธ ์ฝ๋์์ ์๋ฒ ๋ก์ง์ ๋ถ๋ฆฌํจ์ผ๋ก์จ ์ฝ๋์ ์ ์ง๋ณด์์ฑ๊ณผ ๊ฐ๋ ์ฑ์ ํฅ์์ํต๋๋ค.
- ์ง์ ์ ์ธ ์๋ฒ ์คํ: Server Action์ ํตํด ์๋ฒ์์ ์คํํ ์ ์๋ ๋ก์ง์ ํด๋ผ์ด์ธํธ์์ ๊ฐ๋จํ ํธ์ถํ ์ ์์ผ๋ฏ๋ก, ๋ฒ๊ฑฐ๋ก์ด REST API๋ GraphQL ์ค์ ์ด ํ์ํ์ง ์์ต๋๋ค.
REST API ์ค์ ์ ๋ฒ๊ฑฐ๋ก์ ์์
// api/handler.js
import db from './db';
export default async function handler(req, res) {
if (req.method === 'POST') {
const { userId } = req.body;
const user = await db.users.findOne({ id: userId });
res.status(200).json(user);
} else {
res.status(405).json({ error: 'Method not allowed' });
}
}
// ํด๋ผ์ด์ธํธ์์ ํธ์ถ ์ฝ๋
import axios from 'axios';
async function fetchUser(userId) {
const response = await axios.post('/api/handler', { userId });
return response.data;
}
export default function UserComponent() {
const handleFetch = async () => {
const user = await fetchUser('123');
console.log(user);
};
return <button onClick={handleFetch}>Fetch User</button>;
}
Server Action ์ฌ์ฉ ์
'use server';
export async function fetchUser(userId) {
const user = await db.users.findOne({ id: userId });
return user;
}
'use client';
import { fetchUser } from './actions/fetchUser';
export default function UserComponent() {
const handleFetch = async () => {
const user = await fetchUser('123');
console.log(user);
};
return <button onClick={handleFetch}>Fetch User</button>;
}
์ด์ ๊ฐ์ด Server Action์ ์ฌ์ฉํ๋ฉด API ํธ๋ค๋ฌ ์์ฑ๊ณผ ํด๋ผ์ด์ธํธ์์์ ๋ณ๋ HTTP ์์ฒญ ์ฝ๋ ์์ฑ์ด ํ์ ์์ต๋๋ค.
3. Server Action์ ์ฌ์ฉ๋ฒ
Server Action์ Next.js์ ํน์ํ ํ์ผ ๊ตฌ์กฐ์ ํจ๊ป ์ฌ์ฉ๋ฉ๋๋ค. ์๋ฅผ ๋ค์ด
// app/actions/myAction.js
'use server';
export async function myAction(data) {
// ์๋ฒ์์ ์คํ๋ ๋ก์ง
console.log('Received data:', data);
return { success: true };
}
ํด๋ผ์ด์ธํธ์์ ์ด๋ฅผ ํธ์ถํ๋ ค๋ฉด
'use client';
import { myAction } from './actions/myAction';
export default function MyComponent() {
const handleAction = async () => {
const result = await myAction({ key: 'value' });
console.log(result);
};
return <button onClick={handleAction}>Call Action</button>;
}
โ๏ธ React 19v์ useActionState
1. useActionState๋?
React 19๋ฒ์ ์ ์ถ๊ฐ๋ useActionState
๋ Server Action๊ณผ ํจ๊ป ์ฌ์ฉ๋๋ฉฐ, Action์ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ React ํ
์
๋๋ค. ์ฃผ๋ก Action์ ์์ฒญ ์ํ(pending, success, error)๋ฅผ ์ถ์ ํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค.
2. useActionState๊ฐ ํด๊ฒฐํ๋ ๋ฌธ์ ๋ค
- ๋น๋๊ธฐ ์ํ ๊ด๋ฆฌ: ๊ธฐ์กด์๋ Action ์ํ๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํด useState์ useEffect๋ฅผ ์กฐํฉํด ์์ฑํด์ผ ํ์ต๋๋ค.
useActionState
๋ ์ด๋ฅผ ๋จ์ํํฉ๋๋ค. - ๋ก๋ฉ ์ํ ๊ด๋ฆฌ: ์์ฒญ ์ค ์ํ๋ฅผ ์ฝ๊ฒ ํ์ ํ ์ ์์ด ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํฉ๋๋ค.
- ์ค๋ณต ๋ก์ง ์ ๊ฑฐ: ์์ฒญ ์ํ๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํ ๋ณ๋์ ๋ก์ง์ด ํ์ ์์ผ๋ฏ๋ก ์ฝ๋๊ฐ ๊ฐ๊ฒฐํด์ง๋๋ค.
3. useActionState์ ์ฌ์ฉ๋ฒ
import { useActionState } from 'react';
import { myAction } from './actions/myAction';
export default function MyComponent() {
const [isPending, action] = useActionState(myAction);
const handleClick = async () => {
const result = await action({ key: 'value' });
console.log(result);
};
return (
<div>
<button onClick={handleClick} disabled={isPending}>
{isPending ? 'Loading...' 'Call Action'}
</button>
</div>
);
}
๐ ๏ธ Server Action๊ณผ useActionState ์ฌ์ฉ์ ์์
์์ธํ ์ฝ๋๋ฅผ ํ์ธํ์๊ณ ์ถ์ผ์๋ฉด ์๋ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์!
https://github.com/lurgi/action-state-prac
1. Server Action ํจ์ ๋ง๋ค๊ธฐ
"use server";
// ...
export async function login(currentState: LoginReturn, formData: FormData): Promise<LoginReturn> {
const email = formData.get("email") as string;
const password = formData.get("password") as string;
if (email !== "lurgi@gmail.com" || password !== "qwer1234") {
return {
email,
password,
message: "fail",
isError: true,
};
}
return {
email,
password,
message: "success",
isError: false,
};
}
2. useActionState๋ก ๋ถ๋ฌ์ค๊ธฐ
"use client";
import { useActionState, useEffect, useState } from "react";
import { login } from "./action/login";
export default function Home() {
const [state, formAction, isPending] = useActionState(login, {
email: "",
password: "",
message: undefined,
isError: false,
});
const { email, password, message, isError } = state;
return (
<div className="grid place-items-center h-screen">
<form className="w-96" action={formAction}>
<label className="form-control w-full">
<div className="label">
<span className="label-text">Email</span>
</div>
<input
name="email"
type="email"
placeholder="Email"
className="input input-bordered w-full"
defaultValue={email}
/>
</label>
<label className="form-control w-full">
<div className="label">
<span className="label-text">Password</span>
</div>
<input
name="password"
type="password"
placeholder="Password"
className="input input-bordered w-full"
defaultValue={password}
/>
</label>
<div className="label">
<button className="btn btn-primary w-full" disabled={isPending}>
Login
</button>
</div>
</form>
</div>
);
}
3. ๋ก๋ฉ ์ํ(isPending) ์ปจํธ๋กค
isPending
์ ํ์ฉํด ๋ก๋ฉ ์ํ๋ฅผ ํ์ํ๊ณ , ์ฌ์ฉ์๊ฐ ์์ฒญ์ด ์๋ฃ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฌ๋๋ก ์๋ดํฉ๋๋ค. ์ด๋ฅผ ํตํด ๋ ๋์ UX๋ฅผ ์ ๊ณตํฉ๋๋ค. ์๋๋ isPending
์ ์ด์ฉํ์ฌ Toast
๋ฉ์ธ์ง๋ฅผ ์ปจํธ๋กค ํ๋ ์์์
๋๋ค.
export default function Home() {
const [state, formAction, isPending] = useActionState(login, {
email: "",
password: "",
message: undefined,
isError: false,
});
const [hasToast, setHasToast] = useState(false);
const { email, password, message, isError } = state;
useEffect(() => {
let timer: NodeJS.Timeout | undefined;
if (isPending) {
setHasToast(true);
}
if (!isPending) {
timer = setTimeout(() => setHasToast(false), 3000);
}
return () => {
if (timer) clearTimeout(timer);
};
}, [isPending, setHasToast]);
return (
{/*...*/}
<div className="toast toast-bottom toast-center">
{hasToast &&
message &&
(isError ? (
<div role="alert" className="alert alert-error">
<span>Error! {message}</span>
</div>
) : (
<div role="alert" className="alert alert-success">
<span>{message}</span>
</div>
))}
</div>
)
}
๐ ๋ณํํ๋ ๋ฐ์ดํฐ fetching๊ณผ mutate ์ํ๊ณ
Next.js์ Server Action๊ณผ React์ useActionState
๋ ํด๋ผ์ด์ธํธ-์๋ฒ ํต์ ์์ ๋ฐ์ํ๋ ๋ณต์ก์ฑ์ ํฌ๊ฒ ์ค์ฌ์ฃผ๋ ๋๊ตฌ์
๋๋ค. ์ด ๋ ๊ธฐ๋ฅ์ ์๋ฒ์์ ์คํ๋๋ ๋ก์ง๊ณผ ํด๋ผ์ด์ธํธ ์ํ ๊ด๋ฆฌ๋ฅผ ์์ฐ์ค๋ฝ๊ฒ ๊ฒฐํฉํ์ฌ ๋ฐ์ดํฐ ์์ฒญ๊ณผ ์ํ ๊ด๋ฆฌ๋ฅผ ํจ์ฌ ๊ฐ๋จํ๊ฒ ์ฒ๋ฆฌํ ์ ์๋ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค.
๊ธฐ์กด์ mutate ๋ฐฉ์๊ณผ์ ๋น๊ต
React ์ ํ๋ฆฌ์ผ์ด์
์์ ๋ฐ์ดํฐ๋ฅผ ๋ณํ(mutate)ํ๋ ๊ฐ์ฅ ์ผ๋ฐ์ ์ธ ๋ฐฉ์์ Tanstack Query์ useMutate
ํ
์ ์ฌ์ฉํ๋ ๊ฒ์ด์์ต๋๋ค. ์ด ๋ฐฉ์์ ๊ฐ๋ ฅํ ์บ์ฑ๊ณผ ๋ฐ์ดํฐ ๋๊ธฐํ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ฉฐ, ๋๊ท๋ชจ ์ ํ๋ฆฌ์ผ์ด์
์์๋ ์์ ์ ์ผ๋ก ์ฌ์ฉํ ์ ์๋ ๋๊ตฌ๋ก ์๋ฆฌ ์ก์์ต๋๋ค.
ํ์ง๋ง useActionState
๊ฐ ๋ฑ์ฅํ๋ฉด์ mutate ์ํ ๊ด๋ฆฌ๋ ์ด์ ๋ณด๋ค ํจ์ฌ ๊ฐ์ํ๋์์ต๋๋ค. useActionState
๋ ์๋ฒ์์ ์คํ๋๋ Server Action๊ณผ ์ง์ ํตํฉ๋์ด, ๋ฐ์ดํฐ๋ฅผ ์์ ํ๋ ๋ก์ง๊ณผ ์ํ ๊ด๋ฆฌ ๋ก์ง์ ํ ๊ณณ์ ๊ฒฐํฉํฉ๋๋ค. ์ด๋ฅผ ํตํด ๋ณ๋์ ์บ์ฑ์ด๋ ๋ก์ง ๋ถ๋ฆฌ๊ฐ ํ์ ์์ผ๋ฉฐ, ์ง๊ด์ ์ธ ์ฌ์ฉ ๋ฐฉ์ ๋๋ถ์ ์ฝ๋ ์์ฑ์ด ๊ฐ๊ฒฐํด์ก์ต๋๋ค.
๊ฒฐ๋ก ์ ์ผ๋ก, Next.js์ React๊ฐ ์ ๊ณตํ๋ ์ต์ ๋๊ตฌ๋ค์ ๋ฐ์ดํฐ ์์ฒญ๊ณผ ์ํ ๊ด๋ฆฌ์ ๋ฐฉ์์ ํฐ ๋ณํ๋ฅผ ๊ฐ์ ธ์ค๊ณ ์์ต๋๋ค. ์ด๋ก ์ธํด Tanstack Query์ ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋ ํนํ๋ ์์ญ์์ ํ์ฉ๋ ๊ฐ๋ฅ์ฑ์ด ๋์์ง๋ฉฐ, React์ ์ํ๊ณ๋ ์ ์ ๋ ๊ฐ๋จํ๊ณ ํจ์จ์ ์ธ ๋ฐฉํฅ์ผ๋ก ๋ฐ์ ํ ๊ฒ์ ๋๋ค.
์ถํ์ useLoaderState
์ ๊ฐ์ fetching์ ๋ด๋นํ๋ API๊ฐ ์ ๊ณต๋๋ค๋ฉด, Tanstack Query์ ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ์ด๋ค ์ญํ ๋ก ํ์ฉ๋ ๊น์?