본문 바로가기
Frontend2026년 8월 20일7분 읽기

useActionState — React 19 폼 상태와 서버 액션을 한 훅으로

YS
김영삼
조회 4
useActionState — React 19 폼 상태와 서버 액션을 한 훅으로

useActionStateReact 19에 정식으로 들어온 훅으로, 폼 제출 같은 액션을 실행하면서 그 결과 상태와 진행 중 여부(pending)를 한 번에 관리해 주는 도구다. 쉽게 말해 "폼 액션을 실행하고, 반환값을 상태로 받고, 처리 중인지도 알려주는" 세 가지를 하나로 묶어준다.

React 18 시절 useFormState를 써봤다면 거의 그대로 옮겨온 느낌일 것이다. 실제로 이름만 바뀐 게 아니라, 세 번째 반환값으로 isPending이 추가되면서 로딩 스피너 붙이려고 따로 useState 만들던 수고가 사라졌다. 나는 이 하나 때문에 기존 폼 코드를 꽤 갈아엎었다.

왜 이게 필요한가

전통적인 React 폼은 대충 이런 패턴이었다. onSubmit에서 preventDefault하고, setLoading(true) 걸고, fetch 던지고, 성공하면 상태 갱신, 실패하면 에러 상태 세팅, finally에서 setLoading(false). 상태만 서너 개다. 폼이 열 개면 이 보일러플레이트가 열 번 반복된다.

솔직히 처음엔 "그냥 커스텀 훅 하나 만들면 되잖아" 싶었다. 그런데 서버 컴포넌트, 서버 액션과 엮이기 시작하면 얘기가 달라진다. useActionState는 서버 액션을 그대로 액션으로 받아서, 폼이 제출될 때 서버 함수를 호출하고 그 반환값을 클라이언트 상태로 되돌려준다. JS가 로드되기 전에도 폼이 동작하는 점진적 향상(progressive enhancement)이 공짜로 따라온다.

기본 사용법

시그니처는 이렇게 생겼다. 첫 인자는 액션 함수, 둘째는 초기 상태, 셋째(선택)는 permalink다.

import { useActionState } from "react";

function updateName(previousState, formData) {
  const name = formData.get("name");
  if (!name) return { error: "이름을 입력하세요" };
  // ... 저장 로직 (서버 액션이면 여기가 서버에서 실행)
  return { success: true, name };
}

function NameForm() {
  const [state, formAction, isPending] = useActionState(updateName, {});

  return (
    <form action={formAction}>
      <input name="name" />
      <button disabled={isPending}>
        {isPending ? "저장 중..." : "저장"}
      </button>
      {state.error && <p>{state.error}</p>}
      {state.success && <p>저장됨: {state.name}</p>}
    </form>
  );
}

핵심은 액션 함수의 시그니처다. 첫 번째 인자가 이전 상태, 두 번째가 FormData다. 예전 이벤트 핸들러 습관대로 첫 인자를 event나 formData로 착각하면 값이 계속 이상하게 들어온다. 나도 처음에 여기서 한참 헤맸다.

서버 액션과 함께

액션 함수 맨 위에 "use server"를 붙이거나, 별도 서버 파일에서 임포트하면 그대로 서버에서 실행된다. 클라이언트는 상태만 받는다. 폼 데이터 검증을 서버에서 하고, 그 결과 메시지를 상태로 되돌리는 흐름이 아주 자연스럽다.

useFormState에서 뭐가 바뀌었나

항목useFormState (18)useActionState (19)
임포트 위치react-domreact
반환값 개수2개 (state, action)3개 (state, action, isPending)
pending 처리useFormStatus 별도세 번째 반환값으로 바로

useFormState는 아직 동작하지만 deprecated 경고가 뜬다. 새 코드라면 그냥 useActionState로 시작하자.

흔한 함정

  • 상태를 통째로 갈아치운다: 반환값이 곧 새 상태다. 이전 상태의 일부만 유지하려면 return { ...previousState, ... } 처럼 직접 펼쳐줘야 한다.
  • isPending은 낙관적 UI가 아니다: 화면을 미리 바꾸고 싶으면 useOptimistic과 조합해야 한다. isPending은 단순히 "처리 중"만 알려준다.
  • form 밖의 버튼 상태: 제출 버튼이 별도 컴포넌트로 빠져 있으면 useFormStatus를 써야 한다. useActionStateisPending은 그 훅을 호출한 컴포넌트 스코프에서만 유효하다.

자주 묻는 질문

useActionState와 useFormStatus의 차이는 무엇인가요?

useActionState는 액션을 실행하고 그 결과 상태와 pending을 반환하는 훅이고, useFormStatus는 상위 form의 제출 상태를 자식 컴포넌트에서 읽는 훅입니다. 제출 버튼을 별도 컴포넌트로 분리했다면 useFormStatus로 pending을 읽고, 폼 로직 전체를 한 곳에서 다루면 useActionState의 세 번째 반환값을 씁니다.

서버 액션 없이 클라이언트에서만 써도 되나요?

네. 액션 함수를 일반 클라이언트 함수로 두어도 됩니다. 서버 액션은 선택이고, 클라이언트 검증이나 상태 전이만 처리해도 useActionState의 장점(상태와 pending 통합 관리)은 그대로 얻습니다.

비동기 함수를 액션으로 넘길 수 있나요?

가능합니다. 액션 함수가 Promise를 반환하면 React가 완료될 때까지 isPending을 true로 유지하고, resolve된 값을 새 상태로 반영합니다. 그래서 fetch 호출을 액션 안에서 그대로 await 하면 됩니다.

초기 상태로 null을 줘도 되나요?

됩니다. 다만 렌더링에서 state를 참조할 때 옵셔널 체이닝이나 기본값 처리를 해두는 편이 안전합니다. 저는 보통 초기값을 빈 객체 {}로 두고 error, success 필드를 조건부로 읽습니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록