React 19: Actions, 한층 쉬워진 Form 관리

시소·2024년 5월 8일
post-thumbnail

들어가며

지난 4월 React Blog에 React 19 Beta에 대한 소개글이 업로드되었다. 해당 글에는 React 19의 새로운 기능에 대한 개요와 이를 채택하는 방법에 대해 설명하고 있다.

그중에서도 오늘 포스팅에서는 Form과 관련된 작업을 훨씬 쉽게 만들어 주는 새로운 기능 "Actions"와 이와 관련 있는 새로운 훅들에 대해 소개하려 한다.


Actions

HTML <form> 그리고 Actions

React 19의 "Actions"는 클라이언트와 서버 간 폼 데이터 제출을 한층 쉽게 만들어 주는 새로운 기능이다. HTML Form 에서 폼 데이터를 제출을 처리할 때 사용되는 action 속성과도 연관이 있다.

Actions는 <form>과 같은 DOM 요소에 특정 작업을 처리하는 함수를 직접 전달할 수 있도록 하며, 이런 작업들의 처리를 자동으로 관리하며 새로운 훅과 같이 유용한 도구와 함께 제공된다.
이를 통해 사용자 입력 후 폼 제출에 따른 서버와의 통신을 좀 더 간단하게 처리할 수 있게 되었다.

그래서, Actions가 뭔가요?

리액트 측에서는 관례적으로 비동기 트랜지션을 사용하는 함수를 "Actions"로 정의한다.
액션은 제출된 데이터에 대해 아래와 같이 기존에 수동으로 진행해왔던 여러 작업을 단순화하는 제공하는 도구로 볼 수 있다.

React 19에서는 기존에 폼 제출 시 수동으로 처리해야 했거나 서드 파티 라이브러리의 도움을 받아 처리해왔던 일련의 작업들에 대해 아래 소개되는 것 처럼 간단하게 관리할 수 있는 기능이 제공된다.

  • Pending state: 요청 시에 시작되고 최종 상태 업데이트가 커밋되면 자동으로 재설정되는 보류 상태를 제공한다.
  • Optimistic updates: 새로운 useOptimistic 훅을 지원해, 요청이 제출되는 동안 사용자에게 즉각적인 피드백을 표시할 수 있다.
  • Error handling: 오류 처리를 제공해, 요청이 실패했을 때 에러 경계(Error Boundaries)를 표시하고 자동으로 낙관적 업데이트를 원래의 값으로 되돌린다.
  • Forms: <form> 요소가 이제 actionformAction props에 함수 전달을 지원하기 때문에, <form> 액션이 성공하면 비제어 컴포넌트의 경우 폼 제출 후 자동으로 폼이 재설정된다.

잠깐, "낙관적 업데이트"란 무엇일까?

간단히 말하면, 사용자가 어떤 작업을 요청할 때, 그 작업이 완료될 때까지 기다리지 않고 즉시 UI를 업데이트 하는 기술이다. 예를 들어 일반적으로는 데이터를 수정하고 저장 버튼을 누르면, 서버와 통신이 완료될 때까지 기다려야 한다. 이 때 UI가 반응하지 않거나 사용자가 대기 상태에 놓여 사용자 경험이 저하될 가능성이 있다.

낙관적 업데이트는 이와 다르게 저장 버튼을 누르면 즉시 UI를 업데이트 하고, 동시에 백그라운드에서 서버와 통신을 시작한다. 이렇게 하면 사용자는 즉시 결과를 볼 수 있고 앱이 빠르고 반응적이라 느낄 수 있다.
만약 통신이 실패하거나 에러 발생 시, 사용자에 적절한 에러 메시지를 제공하고 UI를 이전 상태로 되돌릴 수 있다.


관련 Hooks

아래와 같은 새로운 훅을 사용해 데이터 제출과 관련된 라이프사이클 관리를 단순화 할 수 있다.

  • useActionState: 폼과 관련해 비동기 작업을 처리하고 상태를 관리하는 데 도움
  • useFormStatus: 폼 안의 다양한 컴포넌트에서 폼의 상태에 전역적으로 액세스
  • useOptimistic: 비동기 요청이 진행되는 동안 낙관적인 UI 업데이트

useActionState

Form action의 결과에 따라 상태를 업데이트 할 수 있는 훅이다. 함수(액션)을 받아들이고 호출될 수 있는 래핑된 액션으로 반환한다. 이 래핑된 액션이 호출되면, 액션의 마지막 결과를 데이터로 반환하고 보류 상태를 pending 으로 반환한다.

// form action function
async function increment(prevState, formData) {
  return prevState + 1 // next state
}

// Usage of useActionState hooks
function StatefulForm({}) {
  const [state, formAction] = useActionState(increment, 0)
  
  return (
    <form>
      {state}
      <button formAction={formAction}>Increment</button>
    </form>
  )
}

훅에 액션 함수(increment)와 초기 상태(0)를 전달하면, 폼의 상태(state)와 함께 폼에서 사용되도록 래핑된 액션 함수(formAction)가 반환된다.

state는 폼이 마지막으로 제출되었을 때 폼 액션에 의해 반환된 값이다.


useFormStatus

마지막으로 제출된 Form의 상태 정보를 제공하는 훅이다. 폼의 정보에 접근하기 위해 해당 정보를 여러 컴포넌트에 드릴다운 하는 건 굉장히 번거롭다. 이를 해결하려 Context를 사용할 수 있었지만, 일반적인 경우에 보다 간편하게 처리하기 위해 새로 등장한 훅이다.

function Submit() {
  const status = useFormStatus()
  
  // 폼이 제출되는 동안 버튼 클릭 비활성화
  return <button disabled={status.pending}>Submit</button>
}

export default function App() {
  return (
    <form action={action}>
      <Submit />
    </form>
  )
}

상태 정보를 얻기 위해 Submit 컴포넌트가 <form> 안에 렌더링 되어야 한다.

이 훅이 반환하는 status 객체는 폼과 관련된 다음과 같은 데이터를 가진다.

  • pending: 제출 중인지 여부를 나타내는 불리언 값
  • data: 제출하는 데이터가 포함된 FormData 인터페이스를 구현하는 객체
  • method: GET 또는 POST HTTP 메서드로 제출하는지 나타내는 문자열 값
  • action: action prop에 전달된 함수에 대한 참조

주의 사항:

  • 이 훅은 <form> 내부에 렌더링되는 컴포넌트에서 호출되어야 한다.
  • 이 훅은 상위 <form>에 대한 상태 정보만 반환한다. 동일한 컴포넌트나 하위 컴포넌트에 렌더링 된 <form>에 대해서는 정보를 반환하지 않는다.

useOptimistic

UI를 낙관적으로 업데이트 할 수 있는 훅이다. 사용자가 폼 제출 시 서버의 응답이 변경 사항을 반영할 때까지 기다리는 대신, 예상되는 결과로 UI를 즉시 업데이트한다.

function AppContainer() {
  const [optimisticState, addOptimistic] = useOptimistic(
    state,
    (currState, optimisticValue) => { // updateFn
       // merge and return new state with optimistic value
    }
  )
}

Parameters:

  • state: 처음에 반환될 값, pending 중인 작업이 없을 때마다 반환되는 값
  • updateFn: 현재 상태와 addOptimistic에 전달된 낙관적 값을 가져와 결과 낙관적 상태를 반환하는 함수. 반환하는 값은 currStateoptimisticValue가 병합된 값

Returns:

  • optimisticState: 결과 낙관적 상태. pending 중인 경우를 제외하고 이 값은 state와 동일
  • addOptimistic: 낙관적 업데이트가 있을 때 호출할 디스패치 함수.

예제 코드

일반적인 사용 사례로 사용자가 이름을 변경하기 위해 입력 폼에 데이터를 입력하고 제출했을 때, API를 호출한 다음 요청에 대한 응답을 처리하는 사례가 있다.

아래에서 Actions를 사용하지 않은 코드와 사용한 코드를 비교해서 살펴보자.

Before Actions:

이 코드는, 사용자의 입력을 받아 비동기로 업데이트를 처리할 때 흔하게 작성해왔던 코드이다.

function UpdateName() {
  const [name, setName] = useState("")
  const [error, setError] = useState(null)
  const [isPending, setIsPending] = useState(false)
  
  const handleSubmit = async () => {
    setIsPending(true)
    const error = await updateName(name)
    setIsPending(false)
    if (error) {
      setError(error)
      return
    }
    // ...
  }
  
  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        Update
      </button>
      {error && <p>{error}</p>}
    </div>
  )
}

After Actions ①: useTransition 을 사용해 보류 상태 처리

아래 코드는 비동기 트랜지션이 isPending 상태를 즉시 true로 설정하고, 비동기 요청을 만든 후에, 모든 트랜지션이 끝나면 isPendingfalse로 전환한다.

function UpdateName() {
  const [name, setName] = useState("")
  const [error, setError] = useState(null)
  const [isPending, startTransition] = useTransition()
  
  const handleSubmit = () => {
    startTransition(async () => {
      const error = await updateName(name)
      if (error) {
        setError(error)
        return
      }
      // ...
    })
  }
  
  // ...
}

After Actions ②: useActionState 활용

async function changeName(prevState, formData) {
  const newName = formData.get("name")
  const error = await updateName(newName) 
  if (error) {
    return { success: false, message: error.message }
  } else {
    return { success: true } 
  }
}

function ChangeName() {
  const [formState, formAction, isPending] = useActionState(changeName, {})
  
  return (
    <form action={formAction}>
      <input name="name" />
      <button type="submit" disabled={isPending}>Update</button>
      {formState?.success &&
        <p>Successfully changed.</p>
      }
      {formState?.success === false && 
        <p>Failed to change: {formState.message}</p>
      }
    </form>
  )
}

마치며

Form을 다루기 위해 이제 과거에 react-hook-form이나 Context API를 기반으로 작성했던 여러 명시적인 코드를 이제 새로운 훅을 통해 좀 더 깨끗하고 효율적으로 코드로 뒤바꿈할 수 있지 않을까 하고 기대가 된다.

이들을 활용해 코드를 간결하고 더 가독성 높게 작성할 수 있을 것 같고, 오류 처리나 상태 관리 같은 부분에서 일관된 형식을 유지할 수 있는데도 도움이 될 것 같다.

다만 아직은 Stable 버전이 발표된 것은 아니기 때문에 관심을 가지고 꾸준히 지켜 봐야 할 것 같다.


참고

profile
배우고 익힌 것을 나만의 언어로 정리하는 공간 ..🛝

0개의 댓글