useActionState Hook을 활용하여 회원가입 폼 만들기

2025. 5. 27. 00:51·FE
728x90

글을 시작하며

리액트 공식문서를 끝까지 읽으며 공부한 적이 없는 것 같아서 더 깊이 있게 공부하고자 공식문서를 공부하게 되었습니다. 학부생으로서 막학기를 지내며 이력서, 포트폴리오, 코딩테스트 등등 할 일이 많지만 차근차근 학습해 나가도록 하겠습니다!

이번 시간에는 useActionState를 공부하며 직접 코드로 구현해보는 시간을 가져보려고 합니다.

useActionState란

React 19버전에 도입되었으며, 폼 액션의 결과를 기반으로 state를 업데이트할 수 있도록 제공하는 Hook입니다.

이전 버전에서는 비동기 작업의 상태를 추적하기 위해 여러 개의 useState를 사용해 각각의 상태를 따로 관리해야 했습니다.

하지만 useActionState를 사용하면, 별도의 상태 훅 없이도 form 액션의 결과와 처리 상태를 한 번에 관리할 수 있습니다.

const [state, formAction, isPending] = useActionState(fn, initialState, permalink?);

fn : 폼에서 실행할 액션 함수

initialState: 초기 state 값

permalink (Optional): 상태 동기화를 위한 참조값

자바스크립트가 로드되어 폼이 완전히 활성화된 상태(hydrated)가 되면 permalink는 더 이상 필요하지 않음

ref> hydrate: 렌더링된 HTML 마크업에 기반하여 클라이언트 측에서 자바스크립트 이벤트와 상태를 연결하는 과정

 

이 훅은 다음 3가지를 반환합니다.

  1. state : 액션 함수 실행 결과 상태
  2. formAction : form action={formAction} 형태로 연결되는 핸들러
  3. isPending :현재 Transition이 대기 중인지 알려주는 플래그 (true/false)

useActionState 실제로 활용하기

Next.js 환경에서 회원가입 폼을 서버로 전송하는 예시를 만들어 보겠습니다.

 

src/actions/registerUser.tsx

"use server";

import { validateSignupForm } from "@/utils/validateSignupForm";

interface RegisterFormData {
  name: string;
  phone: string;
  email: string;
  password: string;
}

interface FormState {
  success: boolean;
  message: string;
  errors?: {
    name?: string;
    phone?: string;
    email?: string;
    password?: string;
    message?: string;
  };
}

export async function registerUser(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  const data: RegisterFormData = {
    name: formData.get("name") as string,
    phone: formData.get("phone") as string,
    email: formData.get("email") as string,
    password: formData.get("password") as string,
  };

  const errors = validateSignupForm(data);

  if (Object.keys(errors).length > 0) {
    return {
      success: false,
      message: "에러 고치기",
      errors,
    };
  }

  console.log("회원가입 요청", data);

  await new Promise((resolve) => setTimeout(resolve, 4000));

  return {
    success: true,
    message: "회원가입 완료",
  };
}

서버 액션을 정의하여 폼 데이터를 추출하고 (formData.get…) 유효성 검사를 실시해서(validateSignupForm) setTimeout으로 대기하여 폼 처리 시뮬레이션을 구현하고 성공 또는 실패를 반환하도록 구현했습니다.

 

src/app/signup/page.tsx

"use client";

import { registerUser } from "@/actions/registerUser";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";

const initialState = {
  success: false,
  message: "",
  errors: {},
};

function FormSubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button
      type="submit"
      disabled={pending}
      className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
    >
      {pending ? "가입중..." : " 회원가입"}
    </button>
  );
}

export default function SignupForm() {
  const [state, formAction, isPending] = useActionState(
    registerUser,
    initialState
  );

  return (
    <form
      action={formAction}
      className="space-y-4 max-w-md mx-auto p-6 bg-white shadow rounded"
    >
      <h2 className="text-xl font-bold">회원가입</h2>
      <div>
        <label htmlFor="name">이름</label>
        <input
          id="name"
          name="name"
          type="text"
          required
          className={`w-full mt-1 p-2 border rounded ${
            state.errors?.name ? "border-red-400" : "border-gray-300"
          }`}
        />
        {state.errors?.name && (
          <p className="text-sm text-red-600">{state.errors.name}</p>
        )}
      </div>

      <div>
        <label htmlFor="phone">전화번호</label>
        <input
          id="phone"
          name="phone"
          type="tel"
          required
          className="w-full mt-1 p-2 border rounded border-gray-300"
        />
        {state.errors?.phone && (
          <p className="text-sm text-red-600">{state.errors.phone}</p>
        )}
      </div>

      <div>
        <label htmlFor="email">이메일</label>
        <input
          id="email"
          name="email"
          type="email"
          required
          className={`w-full mt-1 p-2 border rounded ${
            state.errors?.email ? "border-red-400" : "border-gray-300"
          }`}
        />
        {state.errors?.email && (
          <p className="text-sm text-red-600">{state.errors.email}</p>
        )}
      </div>

      <div>
        <label htmlFor="password">비밀번호</label>
        <input
          id="password"
          name="password"
          type="password"
          required
          className={`w-full mt-1 p-2 border rounded ${
            state.errors?.password ? "border-red-400" : "border-gray-300"
          }`}
        />
        {state.errors?.password && (
          <p className="text-sm text-red-600">{state.errors.password}</p>
        )}
      </div>

      <FormSubmitButton />
      {isPending && <p>회원가입 처리중...</p>}

      {state.message && (
        <p
          className={`mt-4 p-2 text-center rounded ${
            state.success
              ? "text-green-700 bg-green-100"
              : "text-red-700 bg-red-100"
          }`}
        >
          {state.message}
        </p>
      )}
    </form>
  );
}

useFormStatus는 React에서 <form> 내부의 폼 전송 상태를 감지하고 현재 폼이 제출 중인지 여부를 확인할 수 있도록 도와주는 훅입니다.

주로 button, form 등 폼 내부 하위 컴포넌트에서 제출 상태에 따른 UI 변화를 줄 때 사용합니다.

 

 

src/utils/validateSignupForm.tsx

export function validateSignupForm(data: {
  name: string;
  phone: string;
  email: string;
  password: string;
}) {
  const errors: Record<string, string> = {};

  if (!data.name || data.name.length < 2) {
    errors.name = "이름은 최소 2자 이상이어야 합니다.";
  }

  if (!data.email || !data.email.includes("@")) {
    errors.email = "올바른 이메일 주소를 입력해주세요.";
  }

  if (!data.password || data.password.length < 6) {
    errors.password = "비밀번호는 최소 6자 이상이어야 합니다.";
  }

  return errors;
}

각 필드에 대해 간단한 유효성 검사를 진행합니다.

 

 

글을 마치며

기존에 form 제출 시 새로고침이 발생하지 않아 사용자 경험을 향상할 수 있겠다는 생각이 들었고,

UI로직에서 추가적으로 상태 변수 컴포넌트를 만들어서 사용했었는데 폼 상태와 함께 관리하기 때문에 코드 복잡성이 줄어들고 가독성이 높아짐을 알 수 있었습니다.

긴 글 읽으시느라 고생 많으셨습니다.
위에서 다뤘던 useActionState 개념과 특징들을 정리하면서 글을 마치도록 하겠습니다 😀

  • 서버 액션 함수의 결과 상태 + 로딩상태를 통합적으로 관리할 수 있게 해주는 훅
  • 기존의 useState, useTransition 없이도 폼 제출의 흐름을 간편하게 구현
  • state, formAction, isPending을 한 번에 관리가 가능하고, 새로고침없이 서버 동작 연결도 가능
  • 서버에 폼을 제출하거나 유효성을 검사할 때 유용
728x90

'FE' 카테고리의 다른 글

HTML과 React의 이벤트 처리 차이점  (0) 2024.10.18
[Type Challenges] Pick 풀이  (1) 2024.07.03
프론트에서 Transaction을 ?  (0) 2024.05.23
[ Jest ] toBeCalledWith vs toEqual  (1) 2023.11.07
[Error : Warning: Each child in a list should have a unique "key" prop ]  (0) 2023.09.21
'FE' 카테고리의 다른 글
  • HTML과 React의 이벤트 처리 차이점
  • [Type Challenges] Pick 풀이
  • 프론트에서 Transaction을 ?
  • [ Jest ] toBeCalledWith vs toEqual
Yura 🌼
Yura 🌼
만나서 반갑습니다. 어려운 내용을 쉽게 설명하고 싶은 프론트엔드 개발자 김유라입니다.
  • Yura 🌼
    쉽게 설명할 수 없으면, 아는 것이 아니다 ✍️
    Yura 🌼
    • Yura (72)
      • FE (6)
      • BE (14)
      • 코테 (52)
  • 최근 글

  • 링크

    • Github
    • LinkedIn
  • 전체
    오늘
    어제
  • hELLO· Designed By정상우.v4.10.3
Yura 🌼
useActionState Hook을 활용하여 회원가입 폼 만들기
상단으로

티스토리툴바