목차
nextjs에서는 서버 액션을 따로 구분하는데 왜 그렇게 하는지 부터 이해하고 가면 좋을 것 같다.
javascript 로드 없이 form 실행
Server Components가 Progressive Enhancement(점진적 향상) 원칙을 지원한다는 뜻으로, 클라이언트 측의 JavaScript가 로드되지 않았거나 비활성화된 경우에도 서버와 상호작용이 가능하다는 장점을 설명한다.
이것을 가능하게 하는 것이 html의 form 태그이다.
HTML <form>
HTML <form> 요소는 브라우저에서 기본적으로 지원하는 기능이다. action 속성을 통해 폼 데이터를 서버로 전송하며, JavaScript 없이도 서버 요청이 가능하다.
아래에서 서버액션을 사용했을 때와 안했을 때의 차이점을 살펴보자
서버액션을 사용하지 않았을 경우
// server action을 사용하지 않는 경우
'use client';
export default function FormWithEnhancements() {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const response = await fetch("/api/submit", {
method: "POST",
body: formData,
});
console.log(await response.json());
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor="name">Name:</label>
<input id="name" name="name" type="text" />
<button type="submit">Submit</button>
</form>
);
}
이 방식은 브라우저에서 JavaScript가 반드시 활성화되어야 동작한다. JavaScript가 비활성화된 경우, 폼 제출 자체가 동작하지 않는다.
서버 액션을 사용했을 경우
// server action을 사용하는 경우
export default function Page() {
async function handleFormSubmit(formData: FormData) {
"use server";
const name = formData.get("name");
console.log(`Name submitted: ${name}`);
}
return (
<form action={handleFormSubmit}>
<label htmlFor="name">Name:</label>
<input id="name" name="name" type="text" />
<button type="submit">Submit</button>
</form>
);
}
Javascript가 로드되지 않았더라도 form의 action을 통해 form 제출이 가능하다.
그리고 handleFormSubmit함수는 브라우저가 아닌 Next 서버에서 실행됩니다. 위 함수가 서버에서 실행되기 때문에 브라우저에 Javascript가 로드되지 않더라도 사용할 수 있다.
이처럼 server action을 사용하면 Javascript가 로드되지 않더라도 폼 제출이 가능하다는 장점이 있다.
따라서 Next.js에서 Server Action을 폼에 연결하면, 브라우저의 기본 폼 제출 동작을 활용하여 데이터를 서버로 보낸다.
만약 클라이언트에서 JavaScript가 로드되지 않았거나 비활성화된 경우,Server Action을 통해 데이터를 처리하고, 응답을 반환할 수 있다.
그 외 장점들
- server action을 사용하면 UI를 초기 렌더링에 빠르게 보여줄 수 있다. 특정 UI를 클라이언트 컴포넌트로 사용하는 대신 서버 컴포넌트로 작성하여 페이지 로드시 UI는 빠르게 보여줄 수 있다.
- 폼 데이터를 처리하는 로직을 클라이언트에서 서버로 위임하면 컴포넌트가 UI에만 집중할 수 있게 해주고 클라이언트 번들 사이즈를 줄일 수도 있다.
useFormStatus
이 메서드를 통해서 제출하는 상태를 ui측에서 관리할 수 있다.
"use client";
import { useFormStatus } from "react-dom";
export default function FormSubmit() {
const status = useFormStatus();
console.log(status);
if (status.pending) {
return <p>Creating post...</p>;
}
return (
<>
<button type="reset">Reset</button>
<button>Create Post</button>
</>
);
}

useActionState
useActionState는 form action 결과에 기반하여 상태를 업데이트할 수 있는 hook이다. useState처럼 상태를 관리하는데 form과 관련된 상태라고 생각하면 쉬울 것 같다.
"use server";
import { redirect } from "next/navigation";
import { storePost, updatePostLikeStatus } from "@/lib/posts";
import { uploadImage } from "@/lib/upload";
import { revalidatePath } from "next/cache";
//이전 상태와 formData를 프롭스로 넘겨 받음
export async function createPost(prevState, formData) {
//클라이언트 폼 액션이 아니라 서버 액션임을 리액트에게 알림
const title = formData.get("title");
const image = formData.get("image");
const content = formData.get("content");
let errors = [];
if (!title || title.trim().length === 0) {
errors.push("Title is required.");
}
if (!content || content.trim().length === 0) {
errors.push("Content is required.");
}
if (!image || image.size === 0) {
errors.push("Image is required.");
}
if (errors.length > 0) {
return { errors };
}
let imageUrl;
try {
imageUrl = await uploadImage(image);
} catch (error) {
throw new Error("image upload failed...");
}
await storePost({
imageUrl,
title,
content,
userId: 1,
});
revalidatePath("/", "layout");
redirect("/feed");
}
아래 코드와 같이 form의 action에서 사용하고
입력안한 것에 대한 오류 메세지들이 표시되게 된다.
"use client";
import { useActionState } from "react";
import FormSubmit from "./form-submit";
export default function PostForm({ action }) {
//실행될 함수와 초기값을 넣어준다.
const [state, formAction] = useActionState(action, {});
return (
<>
<h1>Create a new post</h1>
<form action={formAction}>
<p className="form-control">
<label htmlFor="title">Title</label>
<input type="text" id="title" name="title" />
</p>
<p className="form-control">
<label htmlFor="image">Image</label>
<input
type="file"
accept="image/png, image/jpeg"
id="image"
name="image"
/>
</p>
<p className="form-control">
<label htmlFor="content">Content</label>
<textarea id="content" name="content" rows="5" />
</p>
<div className="form-actions">
<FormSubmit />
</div>
{state.errors && (
<ul className="form-errors">
{state.errors.map((error) => (
<li key={error}>{error}</li>
))}
</ul>
)}
</form>
</>
);
}

useOptimisticState
사용자가 폼을 제출할 때 서버의 응답을 기다리는 대신 ui를 기대하는 결과로 즉시 업데이트하여 유저에게 보여주고 싶을 때 사용한다.
export default function Posts({ posts }) {
//첫번째는 낙관적으로 업데이트될 데이터
//호출했을 때 낙관적 업데이트를 트리거하는 함수
//첫번재인자는 초기에 쓸 데이터
//두번째인자는 리액트가 호출할 함수
const [optimisticPosts, updateOptimisticPosts] = useOptimistic(
posts,
(prevPosts, updatedPostId) => {
const updatedPostIndex = prevPosts.findIndex(
(post) => post.id === updatedPostId
);
if (updatedPostIndex === -1) {
return prevPosts;
}
const updatedPost = { ...prevPosts[updatedPostIndex] };
updatedPost.likes = updatedPost.likes + (updatedPost.isLiked ? -1 : 1);
updatedPost.isLiked = !updatedPost.isLiked;
const newPosts = [...prevPosts];
newPosts[updatedPostIndex] = updatedPost;
return newPosts;
}
);
if (!optimisticPosts || optimisticPosts.length === 0) {
return <p>There are no posts yet. Maybe start sharing some?</p>;
}
async function updatePost(postId) {
updateOptimisticPosts(postId);
await togglePostLikeStatus(postId);
}
return (
<ul className="posts">
{optimisticPosts.map((post) => (
<li key={post.id}>
<Post key={post.id} post={post} action={updatePost} />
</li>
))}
</ul>
);
}
'코딩 정보 > NextJs' 카테고리의 다른 글
| [NextJs] 이미지 최적화에 대해 알아보자 (0) | 2025.04.16 |
|---|---|
| [NextJs] 캐싱에 대해 더 자세히 알아보자 (0) | 2025.04.15 |
| [NextJs] 서버 컴포넌트를 통한 데이터 가져오기 (0) | 2025.04.05 |
| [NextJs] 라우터 기능에 대해 빠르게 알아보자 (0) | 2025.04.03 |
| [NextJs] 핵심 기능 정리 (1) | 2025.03.12 |