안녕하세요. 이번에는 nextjs14에서 server action을 통해 Data fetching을 하고 이것이 적절하게 이루어지면 그에 맞는 object를 받는 것을 한번 해보도록 하겠습니다.
개인 프로젝트를 진행하는 과정에서 Nextjs14를 도입하게 되었고, Nextjs14하면 또 유명한게 server action이지 않습니까! 이를 적절하게 잘 사용하고 싶었습니다. 그러나 생각보다 수월하진 않았습니다. 당시 nextjs14와 관련하여 업데이트가 된지 얼마 안됐고, 이에 따라 한국 블로그에서는 자료가 거의 없었습니다. 하지만 엄청난 구글링을 통해 원하는 결과를 이끌어 낼 수 있었고, 저처럼 잘 모르시는 분들을 위함과 저의 공부 기록을 위해 글을 작성하게 되었습니다.
저는 현재 zod + react hook form을 통해 login을 하려는 form을 생성하였습니다. 더불어 이를 component에 적용하는 과정에서 shadcn이라는 것을 사용하였음을 먼저 알고 가셨으면 좋겠습니다.
(해당 내용을 몰라도 제가 앞으로 설명하는 내용을 이해하는데 전혀 어려움이 없으실거라 생각합니다.)
이후에 이 게시글의 조회수가 나쁘지 않으면 zod, react hook form, shadcn없이 server action을 사용하는 글도 올리도록 하겠습니다.
더불어 지금은 server action이 무엇인지, zod가 무엇인지, react hook form이 무엇인지, shadcn가 무엇인지는 크게 다루지 않도록 하겠습니다.
"use client";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormLabel,
FormMessage,
FormField,
FormItem,
} from "@/components/ui/form";
import * as z from "zod";
import { modalStore } from "@/store/modal-store";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { useFormState } from "react-dom";
import { Button } from "../ui/button";
import { Label } from "../ui/label";
import { Input } from "../ui/input";
import {LoginAdmin} from "@/app/actions/loginAdmin";
import { useEffect } from "react";
import { userStore } from "@/store/user-store";
const formSchema = z.object({
id: z.string().nonempty("Id is required"),
password: z.string().nonempty("Password is required"),
});
const initFormState = {
success : false,
message : "",
}
export default function LoginModal() {
const {isOpen, onOpen, onClose} = modalStore();
const {loginAdmin} = userStore();
const [formState, formAction] = useFormState(LoginAdmin, initFormState);
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues : {
id : "",
password : "",
},
})
useEffect(()=>{
if(formState.success) {
form.reset();
onClose();
loginAdmin();
}
}, [formState])
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="bg-white text-black p-0 overflow-hidden pb-8">
<DialogHeader className="pt-8 px-6">
<DialogTitle className="text-2xl text-center font-bold">Are you SangEok?</DialogTitle>
</DialogHeader>
<Form {...form}>
<form action={formAction} className="space-y-8">
<div className="space-y-8 px-6">
<FormField
control={form.control}
name="id"
render={({field})=>(
<FormItem>
<FormControl>
<Input
placeholder="Id"
{...field}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({field})=>(
<FormItem>
<FormControl>
<Input
placeholder="Password"
{...field}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<DialogFooter className="px-6">
<Button variant="custom">
Login
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
)
}
"use server"
import db from "@/lib/db";
import { revalidatePath } from "next/cache";
interface FormState {
success : boolean
message : string
}
export const LoginAdmin = async (
preState : FormState,
formData : FormData,
) : Promise<FormState> => {
const id = formData.get("id") as string;
const password = formData.get("password") as string;
const admin = await db.admin.findFirst({
where : {
userId : id,
password : password
}
})
if(admin) {
console.log("Login Success");
return {
success : true,
message : "Login Success"
}
} else {
console.log("Login Failed");
return {
success : false,
message : "Login Failed"
}
}
}
우선 전체만 본다면 어질어질 하실 수도 있습니다. 그래도 전체를 먼저 보여드리고 중요한 일부분을 설명하는 것이 맞다고 판단하여 전체적인 소스코드를 먼저 올렸습니다.
import {LoginAdmin} from "@/app/actions/loginAdmin";
const initFormState = {
success : false,
message : "",
}
const [formState, formAction] = useFormState(LoginAdmin, initFormState);
이는 LoginModal.tsx의 일부분 입니다. 우선 form 제출 후, Server Action을 통해 object값을 return 받고 싶으시면 useFormState를 사용하시는 것이 적절합니다.
(다른 더 좋은 방법이 있다면 알려주세요!!!)
간단하게 useFormState에 대해 소개해드리겠습니다.
첫번째 인자로 제가 만든 server action인 "LoginAdmin"을 넣어줬습니다.
그 후로는 server action을 통해 return 받는 object를 저장할 공간인 formState를 초기화 할 initFormState를 넣어줍니다.
(간단히 이야기하면 formState의 값을 우선적으로 initFormState로 초기화 해주는 것을 의미합니다.)
이후 form을 통해 formAction(LoginAdmin)이 호출되고 그 후에 서버 단위에서 object값을 return해주면 formState에 저장이 되는 것입니다.
<form action={formAction} className="space-y-8">
<div className="space-y-8 px-6">
<FormField
control={form.control}
name="id"
render={({field})=>(
<FormItem>
<FormControl>
<Input
placeholder="Id"
{...field}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({field})=>(
<FormItem>
<FormControl>
<Input
placeholder="Password"
{...field}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<DialogFooter className="px-6">
<Button variant="custom">
Login
</Button>
</DialogFooter>
</form>
우선은 전체적인 form을 가져왔다고 볼 수 있지만 사실 중요한 것은
<form action={formAction} className="space-y-8">
이 부분입니다.
Login버튼을 누르면 formAction이 실행되는 것이죠.
formAction이 실행된다는 말은 제가
따로 만든 loginAdmin이라는 server action이 호출된다는 것입니다.
"use server"
import db from "@/lib/db";
import { revalidatePath } from "next/cache";
interface FormState {
success : boolean
message : string
}
export const LoginAdmin = async (
preState : FormState,
formData : FormData,
) : Promise<FormState> => {
const id = formData.get("id") as string;
const password = formData.get("password") as string;
const admin = await db.admin.findFirst({
where : {
userId : id,
password : password
}
})
if(admin) {
console.log("Login Success");
return {
success : true,
message : "Login Success"
}
} else {
console.log("Login Failed");
return {
success : false,
message : "Login Failed"
}
}
}
loginAdmin은 다음과 같습니다. 제 db의 admin 아이디 패스워드와 일치하면 성공 여부를 객체로 return하고 그렇지 않다면 실패했음을 객체로 return합니다.
중요한 것은
interface FormState {
success : boolean
message : string
}
export const LoginAdmin = async (
preState : FormState,
formData : FormData,
) : Promise<FormState> => {
이 부분입니다. 이전에 앞서 사용했던 useFormState를 사용하기 위해선 다음과 같이 작성하는 것이 필수입니다.
useFormState를 사용한 server action을 호출하면 총 2가지 인자인 formState와 사용자가 입력한 formData가 넘어오기 때문이죠.
이상으로 제가 소개해드리고자 했던 부분은 설명이 끝났습니다.
저는 이것을 알고나서 되게 유익했는데 여러분도 유익하셨으면 좋겠습니다.
피드백은 언제나 환영입니다.
코린이라 딥한 질문은 답변이 어려울 수 있음을 양해바랍니다.