1. REST API를 활용한 동작 방법을 먼저 배워보자.
이 방법(route handler)은, 웹 개발만 할 것이라면 추천하지는 않는다. 앱 개발을 하려면 백엔드가 필요한데, 이따 공용으로 사용하는 백엔드를 위해서는 필요하다. 그리고 이 방법외에도 graphql로도 개발 가능하긴 하다.
*www/users/routes.ts
import { NextRequest } from "next/server";
export async function GET(request: NextRequest) {
console.log(request);
return Response.json({
ok: true,
});
}
export async function POST(request: NextRequest) {
const data = await request.json();
console.log("log the user in!!!");
return Response.json(data);
}
1) routes 파일을 nextjs에서 읽을 수 있다. 해당 파일명은 고유파일명이다. 그리고 GET과 POST도 고정으로 사용해야 한다.
*login/page.tsx
"use client";
import FormButton from "@/components/form-btn";
import FormInput from "@/components/form-input";
import SocialLogin from "@/components/social-login";
export default function LogIn() {
const onClick = async () => {
const response = await fetch("/www/users", {
method: "POST",
body: JSON.stringify({
username: "nico",
password: "1234",
}),
});
console.log(await response.json());
};
return (
<div className="flex flex-col gap-10 py-8 px-6">
<div className="flex flex-col gap-2 *:font-medium">
<h1 className="text-2xl">안녕하세요!</h1>
<h2 className="text-xl">Log in with email and password.</h2>
</div>
<form className="flex flex-col gap-3">
<FormInput type="email" placeholder="Email" required errors={[]} />
<FormInput
type="password"
placeholder="Password"
required
errors={[]}
/>
</form>
<span onClick={onClick}>
<FormButton loading={false} text="Log in" />
</span>
<SocialLogin />
</div>
);
}
1) 위와 같이 세팅하면 www/users폴더에 있는 route 파일을 자동으로 검색하여, 데이터 송신을 진행하게 된다.
2) 이때 추가적으로 form과 관련된 useState등와 같은 것들을 추가적으로 세팅해야 한다.
2. Server Actions
이번에는, 쉽게 POST 를 보내는 방법으로 진행해보자.
먼저 Form 파일에 name을 필수로 지정해 주어야한다. 기존 nextjs 버전이나 react사용을 하게 되면 해당 name은 크게 의미가 없는데 nextjs14 server action을 활용하기위해서는 필수다.
*components/form-input.tsx
interface FormInputProps {
type: string;
placeholder: string;
required: boolean;
errors: string[];
name: string;
}
export default function FormInput({
type,
placeholder,
required,
errors,
name,
}: FormInputProps) {
return (
<div className="flex flex-col gap-2">
<input
name={name}
className="bg-transparent rounded-md w-full h-10 focus:outline-none ring-2 focus:ring-4 transition ring-neutral-200 focus:ring-orange-500 border-none placeholder:text-neutral-400"
type={type}
placeholder={placeholder}
required={required}
/>
{errors.map((error, index) => (
<span key={index} className="text-red-500 font-medium">
{error}
</span>
))}
</div>
);
}
1) name 추가
*login/page.tsx
import FormButton from "@/components/form-btn";
import FormInput from "@/components/form-input";
import SocialLogin from "@/components/social-login";
export default function LogIn() {
async function handleForm(formData: FormData) {
"use server";
console.log(formData.get("email"), formData.get("password"));
console.log("i run in the server baby!");
}
return (
<div className="flex flex-col gap-10 py-8 px-6">
<div className="flex flex-col gap-2 *:font-medium">
<h1 className="text-2xl">안녕하세요!</h1>
<h2 className="text-xl">Log in with email and password.</h2>
</div>
<form action={handleForm} className="flex flex-col gap-3">
<FormInput
name="email"
type="email"
placeholder="Email"
required
errors={[]}
/>
<FormInput
name="password"
type="password"
placeholder="Password"
required
errors={[]}
/>
<FormButton loading={false} text="Log in" />
</form>
<SocialLogin />
</div>
);
}
1)위와 같은 세팅으로 쉽게 post 데이터를 보낼 수 있다.
2) handleForm 인자값의 타입은 FormData가 필수, 반드시 최상단에 "use server" 입력필요
3) 하지만 이렇게 구현하면 해당 form의 결과 값이 error가 있는지 알 수 없다. 그리고 formbutton의 loading부분이 바뀔려면 use server를 사용 할 수 없다. 이러한 문제를 해결 하기 위해 아래 경계선 밑에 다시 방법을 설명 할 것이다.
3. useFormState (버튼 로딩 구현)
*components/form-btn.tsx
"use client";
import { useFormStatus } from "react-dom";
interface FormButtonProps {
text: string;
}
export default function FormButton({ text }: FormButtonProps) {
const { pending } = useFormStatus();
return (
<button
disabled={pending}
className="primary-btn h-10 disabled:bg-neutral-400 disabled:text-neutral-300 disabled:cursor-not-allowed"
>
{pending ? "로딩 중" : text}
</button>
);
}
1) "use client" 사용해야함
2) useFormStatus를 통해 pending 상태 확인
*login/user/page.tsx
import FormButton from "@/components/form-btn";
import FormInput from "@/components/form-input";
import SocialLogin from "@/components/social-login";
export default function LogIn() {
async function handleForm(formData: FormData) {
"use server";
await new Promise((resolve) => setTimeout(resolve, 5000));
console.log("logged in!");
}
return (
<div className="flex flex-col gap-10 py-8 px-6">
<div className="flex flex-col gap-2 *:font-medium">
<h1 className="text-2xl">안녕하세요!</h1>
<h2 className="text-xl">Log in with email and password.</h2>
</div>
<form action={handleForm} className="flex flex-col gap-3">
<FormInput
name="email"
type="email"
placeholder="Email"
required
errors={[]}
/>
<FormInput
name="password"
type="password"
placeholder="Password"
required
errors={[]}
/>
<FormButton text="Log in" />
</form>
<SocialLogin />
</div>
);
}
1) FormButton 컴포넌트 사용
1. 최종적으로 아래와 같이 구현을 해야 한다.
*login/page.tsx
"use client";
import FormButton from "@/components/form-btn";
import FormInput from "@/components/form-input";
import SocialLogin from "@/components/social-login";
import { useFormState } from "react-dom";
import { handleForm } from "./actions";
export default function LogIn() {
const [state, action] = useFormState(handleForm, null);
return (
<div className="flex flex-col gap-10 py-8 px-6">
<div className="flex flex-col gap-2 *:font-medium">
<h1 className="text-2xl">안녕하세요!</h1>
<h2 className="text-xl">Log in with email and password.</h2>
</div>
<form action={action} className="flex flex-col gap-3">
<FormInput
name="email"
type="email"
placeholder="Email"
required
errors={[]}
/>
<FormInput
name="password"
type="password"
placeholder="Password"
required
errors={state?.errors ?? []}
/>
<FormButton text="Log in" />
</form>
<SocialLogin />
</div>
);
}
1) useFormState를 사용한다. 여기서 state에 결과 값이 담기고(data, error 등), action을 통해 handleForm을 불러온다. useFormState의 두번째 인자는 초기값이다.
2) FormButton 로딩 버튼을 위해, 그리고 결과값을 받은 것을 ui에 업데이트 해주기 위해 "use client"를 사용한다.
*login/actions.ts
"use server";
export async function handleForm(prevState: any, formData: FormData) {
console.log(prevState);
await new Promise((resolve) => setTimeout(resolve, 5000));
return {
errors: ["wrong password", "password too short"],
};
}
1) 별도로 actions 파일을 생성하고, "use server"를 사용한다. 해당 결과값을 action을 호출한 곳에 보내준다.
2) 이때 리턴값은 현재 값과 prevState, 즉 이전 상태값을 전달해주는데 이건 추후에 다시 다뤄보자. 여러 절차가 있는 form을 만들 때 유용하게 쓰인다고 함. (sms 인증) / 여러개의 action을 만들 필요가 없음