Next.js에서 Server Action은 서버에서 직접 실행되는 함수를 의미한다. 이 함수들은 클라이언트에서 직접 호출할 수 없고, 서버에서만 실행된다. Server Action은 주로 데이터베이스 작업, API 호출, 인증 처리 등 보안이 중요한 작업을 수행하는 데 사용된다.
Next.js 13 버전부터는 app 디렉토리와 함께 서버 컴포넌트(Server Components)를 도입하면서 서버에서 실행되는 코드와 클라이언트에서 실행되는 코드를 명확하게 분리할 수 있게 되었다. 이에 따라 Server Action을 더 효율적으로 사용할 수 있게 되었다.
nextjs-sever-action 폴더생성 후 터미널에서 npx create-next-app@latest ./
next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true // beta이기때문에 이렇게 설정
}
};
export default nextConfig;
서버컴포넌트에서 사용할수있고
재사용을 위해 분리된 파일에서 사용할 수도 있다.
함수 본문 상단에 "use server"지시문을 사용하여 비동기 함수를 정의하여 서버작업을 만든다. "use server"는 이 기능이 서버에서만 실행되도록 한다.
export default function ServerCompoenet() {
async function myAction() {
"use server";
// ...
}
return <div></div>
}
파일 상단에 "use server"지시문을 사용하여 별도의 파일에 서버 작업을 만든다.(재사용가능) 그런 다음 서버작업을 클라이언트 구성요소로 가져온다.
src/lib/actions.ts
'use server';
export async function myAction() {
//..
}
src/components/ClientComponents.tsx
import { myAction } from '@/lib/actions'
import React from 'react'
const ClientComponents = () => {
return (
<form action={myAction}>
<button type="submit">Add to Cart</button>
</form>
)
}
export default ClientComponents
action
을 사용: form에 action속성에 넣으면 호출 할수있음form
앨리먼트에서 리액트의 action prop은 Server Action을 부를 수 있음formAction
prop을 사용startTransition
과 함께 커스텀 호출src/db.json
{
"todos": [
{
"userId": 1,
"title": "밥먹기",
"completed": false,
"id": 3
},
{
"userId": 2,
"title": "운동하기",
"completed": false,
"id": 4
},
{
"userId": 3,
"title": "공부하기",
"completed": false,
"id": 5
}
]
}
json서버돌리기
npm install -g json-server
json-server --watch db.json -p 3001
components/Form.tsx
import { addTodo } from '@/lib/actions'
import React from 'react'
const Form = () => {
return (
<form action={addTodo} className="flex items-center gap-2">
<input
type="text"
name="title"
className="flex-grow w-full p-1 text-xl border border-gray-400 rounded-lg"
placeholder="새로운 할일을 생성하세요."
autoFocus
/>
<button type="submit" className="border border-gray-400 p-2 rounded-lg">
Submit
</button>
</form>
);
}
export default Form
TodoItem.tsx
import React from 'react'
import { Todo } from './TodoList'
import Link from 'next/link'
import { deleteTodo } from '@/lib/actions'
import Checkbox from './Checkbox'
const TodoItem = (todo: Todo) => {
return (
<form className="my-4 flex justify-between items-center">
<div className="flex items-center gap-4">
<Checkbox />
<label htmlFor="completed" className="text-2xl hover:underline">
<Link href={`/edit/${todo.id}`}>{todo.title}</Link>
</label>
</div>
<button>
X
</button>
</form>
);
}
export default TodoItem
TodoList.tsx
import React from 'react'
import TodoItem from './TodoItem';
interface Todo {
userId: number;
id: number;
title: string;
completed: boolean;
}
async function fetchTodos() {
try {
const res = await fetch("http://localhost:3001/todos");
const todos: Todo[] = await res.json();
return todos;
} catch(error) {
if (error instanceof Error) {
console.log(error.stack);
}
}
}
const TodoList = async() => {
const todos = await fetchTodos();
let content;
if(!todos || todos.length === 0) {
content = <p>Todo 리스트가 없습니다. </p>
} else {
const sortedTodos = todos.reverse();
content = (
<>
{sortedTodos.map((todo) => (
<TodoItem key={todo.id} {...todo} />
))}
</>
)
}
return content;
}
export default TodoList
fetchTodos()
에서 데이터 받고
TodoList에서 정렬후 map으로 item뿌려줌
actions.ts
export async function addTodo(data: FormData) {
const title = data.get('title');
await fetch('http://localhost:3001/todos',{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId: 1, title, completed: false
})
})
// 데이터 추가시 바로 보여주기
// 캐시된 데이터를 purge함
revalidatePath('/');
}
javascript없이 form이 작동. (POST)
javascript disable 해도 잘 되는것을 확인할 수 있다.
추가 후 input value하는 방법은.. ?
이건 formAction
속성을 이용하는 방법으로 사용해본다.
<form>
안의 <button>
, <input type="submit">
, <input type="image">
엘리멘트에 사용할 수 있다.
TodoItem.tsx
<button
formAction={async () => {
"use server";
await deleteTodo(todo)
}}
>X</button>
actions.ts
export async function deleteTodo(todo: Todo) {
const res = await fetch(`http://localhost:3001/todos/${todo.id}`,{
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: todo.id
})
})
await res.json();
revalidatePath('/');
}
세번째방법인 startTransition
을 사용한다.
Checkbox.tsx
'use client';
import React, { useTransition } from 'react'
import { Todo } from './TodoList';
import { updateTodo } from '@/lib/actions';
const Checkbox = ({todo}: {todo: Todo}) => {
const [isPending, startTransition] = useTransition();
// isPending은 데이터를 수정중에 true가 됨
return (
<input
type='checkbox'
checked={todo.completed}
id="completed"
name="completed"
disabled={isPending}
onChange={() => startTransition(() => updateTodo(todo))}
className='min-w-[2rem] min-h-[2rem]'
/>
)
}
export default Checkbox
체크박스 컴포넌트를 생성해주고 TodoItem 컴포넌트에 넣어준다.
actions.ts
export async function updateTodo(todo: Todo) {
const res = await fetch(`http://localhost:3001/todos/${todo.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...todo, completed: !todo.completed
})
})
await res.json();
await sleep(2000); // 2초 멈춘 후 revalidate하기 위해 지연시킴
revalidatePath('/');
}
lib/sleep.ts
// 비동기요청을 할때 지연이 되게 함
export default function sleep(ms: number) {
return new Promise((r) => setTimeout(r, ms))
}
2초 멈춘 후 revalidate하기 위해 지연시킴
이렇게 데이터가 업데이트 되는 동안(백엔드 처리중) disable되는게 너무 보이니까!
실제로 요청이 완료되기까지 300ms가 걸린다.
useOptimistic을 이용하면 response를 기다리지 않고 다끝나기전에 변화된걸 보여줄 수 있다.
Checkbox.tsx
'use client';
import React, { useTransition, useOptimistic } from 'react'
import { Todo } from './TodoList';
import { updateTodo } from '@/lib/actions';
const Checkbox = ({todo}: {todo: Todo}) => {
const [optimisticTodo, addOptimisticTodo] = useOptimistic(todo,
(state: Todo, completed: boolean) => ({ ...state, completed})
)
// const [isPending, startTransition] = useTransition();
// isPending은 데이터를 수정중에 true가 됨
return (
<input
type='checkbox'
checked={optimisticTodo.completed}
id="completed"
name="completed"
// disabled={isPending}
onChange={async () => {
addOptimisticTodo(!todo.completed)
await updateTodo(todo)
// startTransition(() => updateTodo(todo));
}}
className='min-w-[2rem] min-h-[2rem]'
/>
)
}
export default Checkbox
isPending을 사용하지 X
작업이 끝나기전에 UI가 변화하는것을 확인할 수 있다.