서버 액션 및 데이터 수정
서버 액션은 서버에서 실행되는 비동기 함수입니다. Next.js 애플리케이션에서 폼 제출과 데이터 수정을 처리하기 위해 서버 및 클라이언트 컴포넌트에서 호출할 수 있습니다.
🎥 시청하기: 서버 액션을 사용한 변이에 대해 자세히 알아보기 → YouTube (10분).
규칙
서버 액션은 React "use server"
지시문으로 정의할 수 있습니다. async
함수의 맨 위에 지시문을 배치하여 해당 함수를 서버 액션으로 표시하거나, 별도의 파일 맨 위에 배치하여 해당 파일의 모든 내보내기를 서버 액션으로 표시할 수 있습니다.
서버 컴포넌트
서버 컴포넌트는 인라인 함수 수준 또는 모듈 수준 "use server"
지시문을 사용할 수 있습니다. 서버 액션을 인라인으로 사용하려면 함수 본문 맨 위에 "use server"
를 추가하세요:
export default function Page() {
// 서버 액션
async function create() {
"use server";
// 데이터 수정
}
return "...";
}
export default function Page() {
// 서버 액션
async function create() {
"use server";
// 데이터 수정
}
return "...";
}
클라이언트 컴포넌트
클라이언트 컴포넌트에서 서버 액션을 호출하려면 새 파일을 만들고 맨 위에 "use server"
지시문을 추가하세요. 파일 내의 모든 함수는 클라이언트 및 서버 컴포넌트 모두에서 재사용할 수 있는 서버 액션으로 표시됩니다:
"use server";
export async function create() {}
"use server";
export async function create() {}
"use client";
import { create } from "@/app/actions";
export function Button() {
return <Button onClick={create} />;
}
"use client";
import { create } from "@/app/actions";
export function Button() {
return <Button onClick={create} />;
}
액션을 props로 전달하기
서버 액션을 클라이언트 컴포넌트에 prop으로 전달할 수도 있습니다:
<ClientComponent updateItemAction={updateItem} />
"use client";
export default function ClientComponent({
updateItemAction,
}: {
updateItemAction: (formData: FormData) => void;
}) {
return <form action={updateItemAction}>{/* ... */}</form>;
}
"use client";
export default function ClientComponent({ updateItemAction }) {
return <form action={updateItemAction}>{/* ... */}</form>;
}
일반적으로 Next.js TypeScript 플러그인은 updateItemAction
을 client-component.tsx
에서 플래그를 지정합니다. 이는 일반적으로 클라이언트-서버 경계를 넘어 직렬화될 수 없는 함수이기 때문입니다.
그러나 action
이라는 이름이나 Action
으로 끝나는 props는 서버 액션을 받는 것으로 가정됩니다.
이는 TypeScript 플러그인이 실제로 서버 액션을 받는지 아니면 일반 함수를 받는지 알 수 없기 때문에 단지 추측일 뿐입니다.
런타임 타입 검사는 여전히 실수로 클라이언트 컴포넌트에 함수를 전달하지 않도록 보장합니다.
동작
- 서버 액션은
<form>
요소의action
속성을 사용하여 호출할 수 있습니다:- 서버 컴포넌트는 기본적으로 점진적 향상을 지원하므로, JavaScript가 아직 로드되지 않았거나 비활성화된 경우에도 폼이 제출됩니다.
- 클라이언트 컴포넌트에서 서버 액션을 호출하는 폼은 JavaScript가 아직 로드되지 않은 경우 제출을 큐에 넣어 클라이언트 하이드레이션을 우선시합니다.
- 하이드레이션 후에는 브라우저가 폼 제출 시 새로고침하지 않습니다.
- 서버 액션은
<form>
에만 국한되지 않으며 이벤트 핸들러,useEffect
, 서드파티 라이브러리 및 기타 폼 요소(예:<button>
)에서 호출할 수 있습니다. - 서버 액션은 Next.js의 캐싱 및 재검증 아키텍처와 통합됩니다. 액션이 호출되면 Next.js는 단일 서버 왕복에서 업데이트된 UI와 새 데이터를 모두 반환할 수 있습니다.
- 내부적으로 액션은
POST
메서드를 사용하며, 이 HTTP 메서드만 액션을 호출할 수 있습니다. - 서버 액션의 인수와 반환 값은 React에 의해 직렬화 가능해야 합니다. 직렬화 가능한 인수와 값의 목록은 React 문서를 참조하세요.
- 서버 액션은 함수입니다. 즉, 애플리케이션의 어디에서나 재사용할 수 있습니다.
- 서버 액션은 사용되는 페이지나 레이아웃에서 런타임을 상속받습니다.
- 서버 액션은
maxDuration
과 같은 필드를 포함하여 사용되는 페이지나 레이아웃에서 라우트 세그먼트 구성을 상속받습니다.
예시
폼
React는 HTML <form>
요소를 확장하여 action
prop으로 서버 액션을 호출할 수 있도록 합니다.
폼에서 호출될 때 액션은 자동으로 FormData
객체를 받습니다. 필드를 관리하기 위해 React useState
를 사용할 필요가 없으며, 대신 네이티브 FormData
메서드를 사용하여 데이터를 추출할 수 있습니다:
export default function Page() {
async function createInvoice(formData: FormData) {
"use server";
const rawFormData = {
customerId: formData.get("customerId"),
amount: formData.get("amount"),
status: formData.get("status"),
};
// 데이터 수정
// 캐시 재검증
}
return <form action={createInvoice}>...</form>;
}
export default function Page() {
async function createInvoice(formData) {
"use server";
const rawFormData = {
customerId: formData.get("customerId"),
amount: formData.get("amount"),
status: formData.get("status"),
};
// 데이터 수정
// 캐시 재검증
}
return <form action={createInvoice}>...</form>;
}
알아두면 좋은 점:
- 예시: 로딩 및 오류 상태가 있는 폼
- 많은 필드가 있는 폼을 다룰 때는
entries()
메서드와 JavaScript의Object.fromEntries()
를 사용하는 것을 고려해 볼 수 있습니다. 예를 들어:const rawFormData = Object.fromEntries(formData)
. 주의할 점은formData
에 추가적인$ACTION_
속성이 포함된다는 것입니다.- 자세한 내용은 React
<form>
문서를 참조하세요.
추가 인수 전달하기
JavaScript bind
메서드를 사용하여 서버 액션에 추가 인수를 전달할 수 있습니다.
"use client";
import { updateUser } from "./actions";
export function UserProfile({ userId }: { userId: string }) {
const updateUserWithId = updateUser.bind(null, userId);
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">사용자 이름 업데이트</button>
</form>
);
}
"use client";
import { updateUser } from "./actions";
export function UserProfile({ userId }) {
const updateUserWithId = updateUser.bind(null, userId);
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">사용자 이름 업데이트</button>
</form>
);
}
서버 액션은 폼 데이터 외에 userId
인수를 받게 됩니다:
"use server";
export async function updateUser(userId, formData) {}
알아두면 좋은 점:
- 대안으로 폼에서 숨겨진 입력 필드로 인수를 전달할 수 있습니다(예:
<input type="hidden" name="userId" value={userId} />
). 하지만 이 값은 렌더링된 HTML의 일부가 되며 인코딩되지 않습니다..bind
는 서버 및 클라이언트 컴포넌트 모두에서 작동합니다. 또한 단계적 기능 향상을 지원합니다.
중첩된 폼 요소
<form>
내부에 중첩된 요소에서도 서버 액션을 호출할 수 있습니다. 예를 들어 <button>
, <input type="submit">
, <input type="image">
와 같은 요소들은 formAction
prop이나 이벤트 핸들러를 받습니다.
이는 폼 내에서 여러 서버 액션을 호출하고 싶을 때 유용합니다. 예를 들어, 게시물 발행 외에도 게시물 초안을 저장하기 위한 특정 <button>
요소를 만들 수 있습니다. 자세한 내용은 React <form>
문서를 참조하세요.
프로그래밍 방식의 폼 제출
requestSubmit()
메서드를 사용하여 프로그래밍 방식으로 폼 제출을 트리거할 수 있습니다. 예를 들어, 사용자가 ⌘
+ Enter
키보드 단축키를 사용하여 폼을 제출할 때, onKeyDown
이벤트를 수신할 수 있습니다:
"use client";
export function Entry() {
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (
(e.ctrlKey || e.metaKey) &&
(e.key === "Enter" || e.key === "NumpadEnter")
) {
e.preventDefault();
e.currentTarget.form?.requestSubmit();
}
};
return (
<div>
<textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
</div>
);
}
"use client";
export function Entry() {
const handleKeyDown = (e) => {
if (
(e.ctrlKey || e.metaKey) &&
(e.key === "Enter" || e.key === "NumpadEnter")
) {
e.preventDefault();
e.currentTarget.form?.requestSubmit();
}
};
return (
<div>
<textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
</div>
);
}
이는 가장 가까운 <form>
조상의 제출을 트리거하여 서버 액션을 호출합니다.
서버 측 폼 유효성 검사
기본적인 클라이언트 측 폼 유효성 검사를 위해 required
와 type="email"
과 같은 HTML 속성을 사용할 수 있습니다.
더 고급 서버 측 유효성 검사를 위해 zod와 같은 라이브러리를 사용하여 데이터를 변경하기 전에 폼 필드를 검증할 수 있습니다:
"use server";
import { z } from "zod";
const schema = z.object({
email: z.string({
invalid_type_error: "Invalid Email",
}),
});
export default async function createUser(formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get("email"),
});
// 폼 데이터가 유효하지 않으면 일찍 반환
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}
// 데이터 변경
}
"use server";
import { z } from "zod";
const schema = z.object({
email: z.string({
invalid_type_error: "Invalid Email",
}),
});
export default async function createsUser(formData) {
const validatedFields = schema.safeParse({
email: formData.get("email"),
});
// 폼 데이터가 유효하지 않으면 일찍 반환
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}
// 데이터 변경
}
필드가 서버에서 검증되면 액션에서 직렬화 가능한 객체를 반환하고 React useActionState
훅을 사용하여 사용자에게 메시지를 표시할 수 있습니다.
useActionState
에 액션을 전달하면 액션의 함수 서명이 변경되어 첫 번째 인수로 새prevState
또는initialState
매개변수를 받습니다.useActionState
는 React 훅이므로 클라이언트 컴포넌트에서 사용해야 합니다.
알아두면 좋은 점: 이 예제들은 React 19 RC에서 사용 가능한 React의
useActionState
훅을 사용합니다. 이전 버전의 React를 사용 중이라면 대신useFormState
를 사용하세요. 자세한 내용은 React 문서를 참조하세요.
"use server";
import { redirect } from "next/navigation";
export async function createUser(prevState: any, formData: FormData) {
const res = await fetch("https://...");
const json = await res.json();
if (!res.ok) {
return { message: "Please enter a valid email" };
}
redirect("/dashboard");
}
"use server";
import { redirect } from "next/navigation";
export async function createUser(prevState, formData) {
const res = await fetch("https://...");
const json = await res.json();
if (!res.ok) {
return { message: "Please enter a valid email" };
}
redirect("/dashboard");
}
그런 다음 useActionState
훅에 액션을 전달하고 반환된 state
를 사용하여 오류 메시지를 표시할 수 있습니다.
"use client";
import { useActionState } from "react";
import { createUser } from "@/app/actions";
const initialState = {
message: "",
};
export function Signup() {
const [state, formAction] = useActionState(createUser, initialState);
return (
<form action={formAction}>
<label htmlFor="email">이메일</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button>가입하기</button>
</form>
);
}
"use client";
import { useActionState } from "react";
import { createUser } from "@/app/actions";
const initialState = {
message: "",
};
export function Signup() {
const [state, formAction] = useActionState(createUser, initialState);
return (
<form action={formAction}>
<label htmlFor="email">이메일</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button>가입하기</button>
</form>
);
}
알아두면 좋은 점:
- 데이터를 변경하기 전에 사용자가 해당 작업을 수행할 권한이 있는지 항상 확인해야 합니다. 인증 및 권한 부여를 참조하세요.
대기 상태
useActionState
훅은 액션이 실행되는 동안 로딩 표시기를 표시하는 데 사용할 수 있는 pending
상태를 노출합니다.
"use client";
import { useActionState } from "react";
import { createUser } from "@/app/actions";
const initialState = {
message: "",
};
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState);
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
<button aria-disabled={pending} type="submit">
{pending ? "Submitting..." : "Sign up"}
</button>
</form>
);
}
"use client";
import { useActionState } from "react";
import { createUser } from "@/app/actions";
const initialState = {
message: "",
};
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState);
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
<button aria-disabled={pending} type="submit">
{pending ? "Submitting..." : "Sign up"}
</button>
</form>
);
}
알아두면 좋은 점: 또는 특정 폼에 대한 대기 상태를 표시하기 위해
useFormStatus
훅을 사용할 수도 있습니다.
낙관적 업데이트
React useOptimistic
훅을 사용하여 서버 액션이 실행을 완료할 때까지 기다리지 않고 응답을 기다리는 대신 UI를 낙관적으로 업데이트할 수 있습니다:
"use client";
import { useOptimistic } from "react";
import { send } from "./actions";
type Message = {
message: string;
};
export function Thread({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic
Message[],
string
>(messages, (state, newMessage) => [...state, { message: newMessage }]);
const formAction = async (formData) => {
const message = formData.get("message") as string;
addOptimisticMessage(message);
await send(message);
};
return (
<div>
{optimisticMessages.map((m, i) => (
<div key={i}>{m.message}</div>
))}
<form action={formAction}>
<input type="text" name="message" />
<button type="submit">전송</button>
</form>
</div>
);
}
"use client";
import { useOptimistic } from "react";
import { send } from "./actions";
export function Thread({ messages }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { message: newMessage }]
);
const formAction = async (formData) => {
const message = formData.get("message");
addOptimisticMessage(message);
await send(message);
};
return (
<div>
{optimisticMessages.map((m) => (
<div>{m.message}</div>
))}
<form action={formAction}>
<input type="text" name="message" />
<button type="submit">전송</button>
</form>
</div>
);
}
이벤트 핸들러
서버 액션을 <form>
요소 내에서 사용하는 것이 일반적이지만, onClick
과 같은 이벤트 핸들러에서도 호출할 수 있습니다. 예를 들어, 좋아요 수를 증가시키는 경우:
"use client";
import { incrementLike } from "./actions";
import { useState } from "react";
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes);
return (
<>
<p>총 좋아요: {likes}</p>
<button
onClick={async () => {
const updatedLikes = await incrementLike();
setLikes(updatedLikes);
}}
>
좋아요
</button>
</>
);
}
"use client";
import { incrementLike } from "./actions";
import { useState } from "react";
export default function LikeButton({ initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
return (
<>
<p>총 좋아요: {likes}</p>
<button
onClick={async () => {
const updatedLikes = await incrementLike();
setLikes(updatedLikes);
}}
>
좋아요
</button>
</>
);
}
폼 요소에 이벤트 핸들러를 추가할 수도 있습니다. 예를 들어, onChange
에서 폼 필드를 저장하는 경우:
"use client";
import { publishPost, saveDraft } from "./actions";
export default function EditPost() {
return (
<form action={publishPost}>
<textarea
name="content"
onChange={async (e) => {
await saveDraft(e.target.value);
}}
/>
<button type="submit">발행</button>
</form>
);
}
이와 같이 여러 이벤트가 빠르게 발생할 수 있는 경우에는 불필요한 서버 액션 호출을 방지하기 위해 디바운싱을 사용하는 것을 권장합니다.
useEffect
React useEffect
훅을 사용하여 컴포넌트가 마운트되거나 의존성이 변경될 때 서버 액션을 호출할 수 있습니다. 이는 전역 이벤트에 의존하거나 자동으로 트리거해야 하는 변이에 유용합니다. 예를 들어, 앱 단축키를 위한 onKeyDown
, 무한 스크롤을 위한 교차 관찰자 훅, 또는 컴포넌트가 마운트될 때 조회 수를 업데이트하는 경우 등에 유용합니다:
"use client";
import { incrementViews } from "./actions";
import { useState, useEffect } from "react";
export default function ViewCount({ initialViews }: { initialViews: number }) {
const [views, setViews] = useState(initialViews);
useEffect(() => {
const updateViews = async () => {
const updatedViews = await incrementViews();
setViews(updatedViews);
};
updateViews();
}, []);
return <p>총 조회수: {views}</p>;
}
"use client";
import { incrementViews } from "./actions";
import { useState, useEffect } from "react";
export default function ViewCount({ initialViews }: { initialViews: number }) {
const [views, setViews] = useState(initialViews);
useEffect(() => {
const updateViews = async () => {
const updatedViews = await incrementViews();
setViews(updatedViews);
};
updateViews();
}, []);
return <p>총 조회수: {views}</p>;
}
useEffect
의 동작과 주의사항을 고려하는 것을 잊지 마세요.
오류 처리
오류가 발생하면 가장 가까운 error.js
또는 <Suspense>
경계에 의해 클라이언트에서 캐치됩니다. UI에서 처리할 오류를 반환하기 위해 try/catch
를 사용하는 것을 권장합니다.
예를 들어, 서버 액션이 새 항목 생성 시 오류를 처리하고 메시지를 반환할 수 있습니다:
"use server";
export async function createTodo(prevState: any, formData: FormData) {
try {
// 데이터 변경
} catch (e) {
throw new Error("할 일 생성 실패");
}
}
"use server";
export async function createTodo(prevState, formData) {
try {
// 데이터 변경
} catch (e) {
throw new Error("할 일 생성 실패");
}
}
알아두면 좋은 점:
- 오류를 던지는 것 외에도
useActionState
에서 처리할 객체를 반환할 수 있습니다. 서버 측 유효성 검사 및 오류 처리를 참조하세요.
데이터 재검증
서버 액션 내에서 revalidatePath
API를 사용하여 Next.js 캐시를 재검증할 수 있습니다:
"use server";
import { revalidatePath } from "next/cache";
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidatePath("/posts");
}
"use server";
import { revalidatePath } from "next/cache";
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidatePath("/posts");
}
또는 revalidateTag
를 사용하여 캐시 태그로 특정 데이터 가져오기를 무효화할 수 있습니다:
"use server";
import { revalidateTag } from "next/cache";
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidateTag("posts");
}
"use server";
import { revalidateTag } from "next/cache";
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidateTag("posts");
}
리디렉션
서버 액션 완료 후 사용자를 다른 경로로 리디렉션하려면 redirect
API를 사용할 수 있습니다. redirect
는 try/catch
블록 외부에서 호출해야 합니다:
"use server";
import { redirect } from "next/navigation";
import { revalidateTag } from "next/cache";
export async function createPost(id: string) {
try {
// ...
} catch (error) {
// ...
}
revalidateTag("posts"); // 캐시된 게시물 업데이트
redirect(`/post/${id}`); // 새 게시물 페이지로 이동
}
"use server";
import { redirect } from "next/navigation";
import { revalidateTag } from "next/cache";
export async function createPost(id) {
try {
// ...
} catch (error) {
// ...
}
revalidateTag("posts"); // 캐시된 게시물 업데이트
redirect(`/post/${id}`); // 새 게시물 페이지로 이동
}
쿠키
서버 액션 내에서 cookies
API를 사용하여 쿠키를 get
, set
, delete
할 수 있습니다:
"use server";
import { cookies } from "next/headers";
export async function exampleAction() {
// 쿠키 가져오기
const value = cookies().get("name")?.value;
// 쿠키 설정
cookies().set("name", "Delba");
// 쿠키 삭제
cookies().delete("name");
}
"use server";
import { cookies } from "next/headers";
export async function exampleAction() {
// 쿠키 가져오기
const value = cookies().get("name")?.value;
// 쿠키 설정
cookies().set("name", "Delba");
// 쿠키 삭제
cookies().delete("name");
}
서버 액션에서 쿠키를 삭제하는 추가 예제를 참조하세요.
보안
인증 및 권한 부여
서버 액션을 공개 API 엔드포인트처럼 취급하고 사용자가 해당 작업을 수행할 권한이 있는지 확인해야 합니다. 예를 들어:
"use server";
import { auth } from "./lib";
export function addItem() {
const { user } = auth();
if (!user) {
throw new Error("이 작업을 수행하려면 로그인해야 합니다");
}
// ...
}
클로저 및 암호화
컴포넌트 내에서 서버 액션을 정의하면 액션이 외부 함수의 범위에 접근할 수 있는 클로저가 생성됩니다. 예를 들어, publish
액션은 publishVersion
변수에 접근할 수 있습니다:
export default async function Page() {
const publishVersion = await getLatestVersion();
async function publish() {
"use server";
if (publishVersion !== await getLatestVersion()) {
throw new Error('게시 버튼을 누른 이후 버전이 변경되었습니다');
}
...
}
return (
<form>
<button formAction={publish}>게시</button>
</form>
);
}
export default async function Page() {
const publishVersion = await getLatestVersion();
async function publish() {
"use server";
if (publishVersion !== await getLatestVersion()) {
throw new Error('게시 버튼을 누른 이후 버전이 변경되었습니다');
}
...
}
return (
<form>
<button formAction={publish}>게시</button>
</form>
);
}
클로저는 렌더링 시점의 데이터 스냅샷(예: publishVersion
)을 캡처하여 나중에 액션이 호출될 때 사용할 수 있도록 하는 데 유용합니다.
그러나 이를 위해서는 캡처된 변수가 클라이언트로 전송되고 액션이 호출될 때 다시 서버로 전송되어야 합니다. 민감한 데이터가 클라이언트에 노출되는 것을 방지하기 위해 Next.js는 자동으로 클로저 변수를 암호화합니다. Next.js 애플리케이션이 빌드될 때마다 각 액션에 대해 새로운 개인 키가 생성됩니다. 이는 액션이 특정 빌드에 대해서만 호출될 수 있음을 의미합니다.
알아두면 좋은 점: 민감한 값이 클라이언트에 노출되는 것을 방지하기 위해 암호화에만 의존하는 것을 권장하지 않습니다. 대신 React taint APIs를 사용하여 특정 데이터가 클라이언트로 전송되는 것을 사전에 방지해야 합니다.
암호화 키 덮어쓰기 (고급)
Next.js 애플리케이션을 여러 서버에 자체 호스팅할 때, 각 서버 인스턴스가 서로 다른 암호화 키를 가질 수 있어 잠재적인 불일치를 유발할 수 있습니다.
이를 완화하기 위해 process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
환경 변수를 사용하여 암호화 키를 덮어쓸 수 있습니다. 이 변수를 지정하면 빌드 간에 암호화 키가 지속되고 모든 서버 인스턴스가 동일한 키를 사용하도록 보장합니다.
이는 여러 배포에서 일관된 암호화 동작이 애플리케이션에 중요한 고급 사용 사례입니다. 키 로테이션 및 서명과 같은 표준 보안 관행을 고려해야 합니다.
알아두면 좋은 점: Vercel에 배포된 Next.js 애플리케이션은 이를 자동으로 처리합니다.
허용된 출처 (고급)
서버 액션은 <form>
요소에서 호출될 수 있기 때문에 CSRF 공격에 노출될 수 있습니다.
내부적으로 서버 액션은 POST
메서드를 사용하며, 이 HTTP 메서드만 액션을 호출할 수 있습니다. 이는 SameSite 쿠키가 기본값인 최신 브라우저에서 대부분의 CSRF 취약점을 방지합니다.
추가적인 보호로, Next.js의 서버 액션은 Origin 헤더를 Host 헤더(또는 X-Forwarded-Host
)와 비교합니다. 이들이 일치하지 않으면 요청이 중단됩니다. 즉, 서버 액션은 호스팅하는 페이지와 동일한 호스트에서만 호출될 수 있습니다.
리버스 프록시나 다층 백엔드 아키텍처를 사용하는 대규모 애플리케이션(서버 API가 프로덕션 도메인과 다른 경우)의 경우, serverActions.allowedOrigins
옵션을 사용하여 안전한 출처 목록을 지정하는 것이 좋습니다. 이 옵션은 문자열 배열을 받습니다.
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
serverActions: {
allowedOrigins: ["my-proxy.com", "*.my-proxy.com"],
},
},
};
보안 및 서버 액션에 대해 자세히 알아보세요.
추가 리소스
서버 액션에 대한 자세한 정보는 다음 React 문서를 확인하세요: