Nextjs
의 서버 액션과 Route handler
의 간단한 사용 방법에 대해 알아봅시다.
먼저 서버 액션이 무엇인지 간단하게 이해하고 넘어가볼까요? 아래는 Nextjs 공식문서에서 서버 액션에 대해 설명하는 글 입니다.
Server Actions are asynchronous functions that are executed on the server. They can be used in Server and Client Components to handle form submissions and data mutations in Next.js applications.
공식문서는 서버 액션을 서버에서 작동하는 비동기 함수
라고 설명하고 있습니다. 개발자들은 이를 사용해 form의 제출이나 mutation을 처리할 수 있습니다.
함수를 서버 액션으로 만드는 방법은 아주 간단합니다. use server
지시어를 붙여주기만 하면 되는데요, 아래 예제 코드를 통해 확인해봅시다.
"use server"; // 지시어 추가
import prisma from "@/app/shared/lib/prisma";
import { getServerSession } from "@/app/shared/utils";
export const likeReview = async (
formData: FormData,
): Promise<void> => {
const session = await getServerSession();
if (!session) return;
const userId = session.user?.id as string;
const reviewId = formData.get("reviewId") as string;
await prisma.reviewLike.create({
data: {
userId,
reviewId,
},
});
};
// form
import { likeReview } from '@/apis';
export const LikeButton = ({ id }: { id: string }) => {
return (
<form action={likeReview}> // form의 action 속성에 서버 액션 전달
<input type='hidden' name='reviewId' value={id}/>
<button type='submit'>
<Heart size={24} className='...' />
</button>
</form>
)
};
위 코드는 서버 액션으로 작성한 간단한 좋아요 함수입니다. likeReview
함수는 formData
를 인자로 받아 전달된 formData에서 필요한 reviewId를 추출하고, Prisma를 통해 db에 접근하여 좋아요 로직을 처리합니다.
함수를 사용하는 LikeButton 컴포넌트는 form의 action 속성에 해당 서버 액션 함수를 전달하기만 하면, reviewId라는 name을 가진 input의 value 값이 formData로 전달됩니다.
이렇게 하면 별도의 state로 input value를 관리할 필요 없이 폼을 관리할 수 있게됩니다.
서버 액션 함수는 클라이언트 컴포넌트에서 사용하기 위해선 props로 전달하거나, 직접 import 해서 사용할 수 있습니다.
// some component
<ClientComponent updateLike={likeReview} />
// client component
'use client'
export default function ClientComponent({ updateLike }) {
return (
<form action={updateLike}>{/* ... */}</form>
)
}
서버 액션 함수에 추가 인수를 전달할 땐 어떻게 해야할까요? 예를 들어, 어떤 영화에 새로운 리뷰를 추가한다고 할 때 movieId와 같은 값을 인자로 넘겨주어야 할겁니다. 이를 처리하는 방법은 크게 두 가지가 있습니다.
// form
import { likeReview } from '@/apis';
interface Props {
id: string;
movieId: string;
}
export const LikeButton = ({ id, movieId }: Props) => {
return (
<form action={likeReview}>
<input type='hidden' name='reviewId' value={id}/>
<input type='hidden' name='movieId' value={movieId} />
<button type='submit'>
<Heart size={24} className='...' />
</button>
</form>
)
};
예제 코드 처럼 type 속성에 hidden을 부여한 input을 하나 더 추가해 formData에 movieId를 추가할 수 있습니다.
bind
메소드 사용하기// form
import { likeReview } from '@/apis';
interface Props {
id: string;
movieId: string;
}
export const LikeButton = ({ id, movieId }: Props) => {
const likeReviewWithMovieId = likeReview.bind(null, movieId); // bind로 movieId 주입
return (
<form action={likeReviewWithMovieId} />
<input type='hidden' name='reviewId' value={id}/>
<button type='submit'>
<Heart size={24} className='...' />
</button>
</form>
)
};
두 번째 방법은 자바스크립트의 bind 메소드를 사용하는 것입니다. 이 방법을 통해 불필요한 input이 HTML DOM 요소로 추가되는 것을 방지하고, 서버와 클라이언트 측에서 모두 사용할 수 있기 때문에 공식문서는 bind를 사용할 것을 권장하고 있습니다.
개발 진행 중 서버 액션 함수를 조건부로 호출해야 할 상황이 있었습니다. 모달이 열렸을 때에만 데이터를 가져오게 하기 위함이었는데요.
처음엔 useEffect를 사용해 조건부로 호출하려고 했으나, 이런 에러가 발생했습니다. Error: PrismaClient is unable to be run in the browser.
Prisma 함수는 서버 사이드에서만 작동하도록 설계되어 브라우저 상에서 호출했을 때 에러를 발생시키는 것이었습니다.
이럴 때 사용할 수 있는 방법이 바로 Route handler를 활용하는 것 입니다.
Nextjs 에서 백엔드 코드를 다룰 때 주로 서버 액션과 라우트 핸들러를 사용할 수 있습니다.
Route handler는 기존 pages 라우터의 API route
와 동일합니다.
더 자세한 사항은 공식 문서에서 확인하실 수 있습니다.
app/api
경로에 route.ts
파일을 만들어 줍니다. 저는 review와 관련된 api이기 때문에 app/api/reviews
경로에 만들었습니다.
// app/api/review/route.ts
import { getReviewsByMovie } from "@/app/features/review/apis";
import { NextRequest } from "next/server";
export async function GET(
req: NextRequest, // eslint-disable-line
{ params }: { params: { id: string } },
): Promise<Response> {
const movieId = params.id;
const reviews = await getReviewsByMovie(movieId);
return Response.json({ reviews });
}
위 예제에서 getReviewsByMovie
함수는 인자로 movieId를 받아 db에서 해당 영화의 리뷰 데이터를 받아오는 서버 액션 함수입니다.
Route handler 함수의 두 번째 매개변수를 통해 url params
에 접근할 수 있습니다. 이를 사용하여 url에서 movieId 값을 가져온 뒤, Reponse.json으로 데이터를 파싱하고 리턴합니다.
클라이언트 컴포넌트에서 아래와 같이 사용할 수 있습니다.
// client component
useEffect(() => {
if (isOpen) {
const fetchReviews = async () => {
setIsFetching(true);
const response = await fetchAPI.get(`/api/reviews/${params.id}`);
setReviews(response.reviews);
setIsFetching(false);
};
fetchReviews();
}, [isOpen]);
if (isFetching) return <>loading...</>;
return (
// review data 렌더링 ..
)