노트

useActionState와 폼 액션

useActionState

프론트엔드#react · 연결된 개념 7개

쉽게 말하면

useActionState는 폼을 제출했을 때 '처리 중'인지와 마지막 결과를 한 번에 챙겨 주는 훅이에요. 접수 창구에 서류를 내면 처리 중 표시가 켜지고 결과 쪽지가 돌아오는 것과 비슷해서, 로딩 플래그를 따로 안 짜도 돼요.

비유의 한계 창구 앞에서 멍하니 기다리는 것과 달리 액션은 transition 안에서 실행돼요. 서버 액션과 함께 쓰면 JS가 로드되기 전에도 폼이 제출돼요. 액션은 이전 상태와 FormData를 받아 다음 상태를 돌려줘요.

React 19에서는 <form action={함수}>에 함수를 바로 넘길 수 있고, useActionState가 그 액션의 대기 상태와 마지막 결과를 한 훅으로 관리한다. useState + onSubmit + 로딩 플래그로 짜던 패턴을 대체한다.

'use client'
const [state, formAction, isPending] = useActionState(updateName, null)
// updateName(prevState, formData) — 서버 함수여도 된다
 
<form action={formAction}>
  <input name="name" disabled={isPending} />
  <button disabled={isPending}>저장</button>
  {state?.error && <p>{state.error}</p>}
</form>
  • 액션은 이전 상태와 FormData를 받아 다음 상태(에러 메시지, 결과)를 반환한다. 서버 쪽 검증 결과를 그대로 화면에 보여 주기 좋다(서버 함수와 서버 액션)
  • form action으로 넘긴 함수는 자동으로 transition 안에서 실행된다(useTransition)
  • 점진적 향상(Progressive Enhancement): 서버 액션과 함께 쓰면 JS(JavaScript)가 로드되기 전에도 폼이 제출되고, 하이드레이션 전 제출은 나중에 다시 실행된다(하이드레이션)
  • useFormStatus: 폼 안쪽의 자식 컴포넌트(제출 버튼)가 props를 받지 않고도 상위 폼의 제출 상태를 읽는다
  • useOptimistic: 서버 확정 전에 결과를 미리 화면에 반영한다. 아래 절을 본다

입력값을 state로 들지 않는 비제어 입력과 잘 맞는다. 같은 일을 클라이언트 캐시 쪽에서 하는 방법은 TanStack Query의 useMutation이다.

useOptimistic

useOptimistic(value, reducer?)는 [optimisticState, setOptimistic]을 돌려준다. Action이 진행 중일 때만 낙관적 값이 렌더되고, 진행 중인 Action이 없으면 value가 그대로 렌더된다.

// todos: 부모가 저장 성공 후 갱신하는 실제 목록
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
  todos,
  (currentTodos, newTodo) => [...currentTodos, { ...newTodo, pending: true }] // reducer
)
 
function handleAdd(text: string) {
  const newTodo = { id: crypto.randomUUID(), text }
  startTransition(async () => {
    addOptimisticTodo(newTodo)   // pending 항목이 바로 보인다
    await addTodoAction(newTodo) // 성공하면 부모가 todos를 갱신한다
  })
}
  • reducer는 순수 함수여야 한다. 저장 중에 todos가 바뀌어도 최신 목록 위에 임시 항목을 얹어 준다
  • setter는 Action이나 startTransition 안에서 부른다. 밖에서 부르면 경고가 뜨고 낙관적 값이 잠깐만 보인다. form action처럼 Action으로 넘긴 함수는 이미 transition 안이라 그대로 불러도 된다
  • 따로 "되돌리는" 코드는 없다. Transition이 끝나면 그 시점의 value가 렌더된다. 성공하면 부모가 갱신한 실제 값과 같은 렌더에서 합쳐지고, 실패하면 value가 그대로라서 원래 화면으로 돌아간다

출처: React — useOptimistic · React — 목록에 낙관적으로 추가하기 · React — setOptimistic 주의 사항

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • useReducer

    useReducer는 "이전 상태와 액션을 받아 새 상태를 돌려주는" 리듀서(Reducer) 함수로 상태를 관리하는 훅이다. 이름처럼 Array.prototype.reduce의 아이디어를 시간축으로 늘린 것이다. 배열 요소 대신 시간순으로 들어오는 액션을 누적한다.

  • 트랜잭션·트랜지션·뮤테이션 구분

    세 단어는 이름이 비슷하지만 다루는 층이 다르다. 뮤테이션(mutation)은 데이터를 바꾸는 작업, 트랜잭션(transaction)은 여러 변경을 전부 반영하거나 전혀 반영하지 않게 묶는 단위, React의 트랜지션(transition)은 화면 업데이트의 우선순위를 낮추는 표시다.

  • React 상태 갱신

    React는 상태를 직접 고치지 않고 새 값을 setState로 넘겨야 다시 그린다. 이전 값과 Object.is로 비교하므로, 같은 객체를 고친 뒤 넘기면 바뀐 줄 모른다.

  • 상태 패턴

    객체의 행동이 현재 상태에 따라 달라질 때, 상태마다 별도 객체를 두고 현재 상태 객체에 행동을 맡기는 패턴. 상태별 조건문이 메서드마다 반복되는 문제를 푼다.

  • Activity와 숨겨진 라우트 보존

    Activity는 React 19.2의 컴포넌트로, UI를 언마운트하지 않고 display: none으로 숨기면서 state와 DOM(Document Object Model)을 보존한다. 숨기는 동안 Effect는 정리(cleanup)된다. 예전 이름은 Offscreen이다.

보기 옵션