

이 코드는 사용자에게 어떤 양식을 입력받는 form 태그를 반환하고 있다.
양식을 입력하고, submit 버튼이 입력되면.. action 옵션으로 삽입된 saveName 함수가 실행된다.
그런데 'use server'라는 지시자를 입력해주면, 이 함수는 Next 서버에서만 실행되는 서버 액션으로 간주된다.



API를 사용해야 했던 브라우저 - 서버 사이의 데이터를 오가는 작업을 서버 액션만으로 수행할 수 있게 된다.
특정 form이 제출되거나 했을 때, 서버 측에서 실행되는 기능 함수들을 클라이언트에서 실행할 수 있게 된다.
function ReviewEditor() {
async function createReviewAction(formData: FormData) {
"use server";
const content = formData.get("content")?.toString();
const author = formData.get("author")?.toString();
console.log(content, author);
}
return (
<section>
<form action={createReviewAction}>
<input name="content" placeholder="리뷰 내용" />
<input name="author" placeholder="작성자" />
<button type="submit">작성하기</button>
</form>
</section>
);
}
export default function Page({ params }: { params: { id: string } }) {
return (
<div className={style.container}>
<BookDetail bookId={params.id} />
<ReviewEditor />
</div>
);
}
"use server"; 지시자를 사용하여 서버 액션이 적용된 createReviewAction 함수를 작성하였다.
책에 대한 리뷰 내용을 작성하여, Submit을 실행한다. 서버 액션에 의해 createReviewAction 함수에서는 formData Props로 리뷰 데이터들을 받아오게 된다.


const content = formData.get("content")?.toString();
const author = formData.get("author")?.toString();
서버 액션 함수 내부에서 받아온 데이터를 가공한다던지, 데이터베이스 관련 로직을 처리한다던지 등의 작업을 수행할 수 있다.
서버 액션 함수 내부에서의 formData는 FormDataEntryValue라는 고유 타입으로 간주된다. 따라서 TypeScript 환경에서는 데이터의 타입을 올바르게 바꿔야할 수도 있다.
-> 학습 프로젝트의 경우 String 타입이어야 하므로, toString()함수로 형변환을 실행해주었다.
일단 코드가 간결하다. 다만 간단한 수준의 기능에는 적합하지만 복잡한 기능들에는 조금 사용하기가 어렵다.
API를 이용하는 경우에는 별도의 파일 생성, 요청 처리, 예외 처리 등의 작업들이 필요하다.
또한 사용시 참고. 서버 액션은 서버에서만 실행될 뿐, 클라이언트에서는 호출할 수만 있다. 따라서 보안에서 민감한 데이터 등을 다루는게 유용하게 사용될 수 있다.

"use server";
export async function createReviewAction(formData: FormData) {
const bookId = formData.get("bookId")?.toString();
const content = formData.get("content")?.toString();
const author = formData.get("author")?.toString();
if (!bookId || !content || !author) {
return;
}
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_SERVER_URL}/review`,
{
method: "POST",
body: JSON.stringify({ bookId, content, author }),
}
);
console.log(response.status);
} catch (err) {
console.error(err);
return;
}
}
당연하게도 서버 액션은 별도의 파일로 분리할 수 있다. 서버 액션이 별도 파일로 분리될 경우에는 "use server"; 지시자는 함수 내부보다는 파일 최상단에 위치하는 것이 좋다.
학습 프로젝트 단계에서는 데이터베이스까지 다루기에는 필요한 패키지, 설정 등이 많아서 진행하기가 어렵다. 대신 백엔드단과 데이터를 주고받은 수준에서 서버 액션의 실습을 진행하려고 한다.
예외 처리를 잊지 말 것! 특히 데이터가 제대로 들어오지 않았거나, 존재하지 않을 경우에는 후속 기능이 동작하지 않아야 한다.
<input required name="content" placeholder="리뷰 내용" />
입력값의 유효성 검사도 필수. input 태그의 경우 required 옵션만 걸어도 프론트단에서 한번 빈값의 입력을 방지할 수 있다.
-> 유효성 검사는 프론트에서 1차, 2차로 실시하고 백엔드에서도 진행해야한다. 양 쪽에서 검사를 진행한다는 것은 검사의 신뢰성이 높아진다는 뜻.
JSON.stringify? 프론트엔드의 객체 데이터를 그대로 백엔드에 넘겨줄 수는 없다. 표준 데이터 형식인 JSON으로 변환해주어야 하고, 또 문자 형태로 직렬화 해줘야 한다.

<form action={createReviewAction}>
<input name="bookId" value={bookId} hidden />
<input required name="content" placeholder="리뷰 내용" />
<input required name="author" placeholder="작성자" />
<button type="submit">작성하기</button>
</form>
form + 서버 액션으로 데이터를 다루게 될 때, id 값이 포함되어야 하는 것은 당연한 이야기.
그런데 사용자 입력 UI에 id 값을 입력하게 하는 것은 다소 이상한 방법.
이럴 때에는 hidden 옵션을 적용한 input 태그에 id 값을 value로 넣어 사용할 수 있다.
사용자에게 보이진 않지만, form 내부에 포함되어 있으니 서버 액션에서도 정상적으로 값을 받아 사용할 수 있다.
async function ReviewList({ bookId }: { bookId: string }) {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_SERVER_URL}/review/book/${bookId}`
);
if (!response.ok) {
throw new Error(`Review fetch failed : ${response.statusText}`);
}
const reviews: ReviewData[] = await response.json();
return (
<section>
{reviews.map((review) => (
<ReviewItem key={`review-item-${review.id}`} {...review} />
))}
</section>
);
}
export default function Page({ params }: { params: { id: string } }) {
return (
<div className={style.container}>
<BookDetail bookId={params.id} />
<ReviewEditor bookId={params.id} />
<ReviewList bookId={params.id} />
</div>
);
}
import { ReviewData } from "@/types";
import style from "./review-item.module.css";
export default function ReviewItem({
id,
content,
author,
createdAt,
bookId,
}: ReviewData) {
return (
<div className={style.container}>
<div className={style.author}>{author}</div>
<div className={style.content}>{content}</div>
<div className={style.bottom_container}>
<div className={style.date}>
{new Date(createdAt).toLocaleString()}
</div>
<div className={style.delete_btn}>삭제하기</div>
</div>
</div>
);
}
id 값을 받아 특정 서적의 리뷰 데이터만 조회한 다음, 화면에 렌더링하는 컴포넌트 ReviewList. 그리고 리뷰 내용이 렌더링될 ReviewItem 컴포넌트.
에러 처리의 경우 throw new Error로 에러를 throw 해주기만 하면, error 핸들링을 위한 에러 컴포넌트로 자동으로 넘어가진다.
백엔드에서 넘어오는 데이터는 json() 메소드로 변환해주는 것이 좋다. 어떤 형식으로 넘어올지 모르기 때문.
