본문 바로가기
Front-end

React 19에 대하여

by ghDev 2024. 12. 9.
React가 19버전으로 변경되며 적용했을때 생기는 변경점과 현재 제 개발 스펙에 어떤것들을 적용할 수 있는지 적어보았습니다.

1. useTrasition

 

기존 useTrasition은 React 18에 도입된 Hook으로 UI를 차단하지 않으면서 상태 업데이트를 효율적으로 처리하는데 도움을 주나, 기존 useTrasition은 비동기 작업과 함께 사용하는데 제약이 있었습니다.
React 19부턴 이제 비동기 함수와 함께 사용할 수 있습니다.

 

// Using pending state from Actions
// example
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;
      } 
      redirect("/path");
    })
  };
  return (
    <div>
      <input value={name} onChange={(event) => setName(event.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        Update
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

 

1-2. useActionState

useActionState는 form의 Action 결과를 기반으로 상태를 업데이트 할 수 있도록 제공되는 Hook으로 기존 useFormState가 useActionState로 대체되었습니다.

 

RSC에서 사용할 경우 Hydration이 완료되기 전에 form을 상호작용 가능한 상태로 유지할 수 있고, RSC를 사용하지 않는다면 일반적인 CSR의 state와 동일하게 동작합니다.

 

// App.js
"use client";

import {updateName} from './actions';

function UpdateName() {
	// updateName은 action. {error: null} 은 초기 값.
  const [state, submitAction, isPending] = useActionState(updateName, {error: null});

  return (
    <form action={submitAction}>
      <input type="text" name="name" disabled={isPending}/>
      {state.error && <span>Failed: {state.error}</span>}
    </form>
  );
}

// actions.js
"use server";

export async function updateName(name) {
  if (!name) {
	  // 이 상황에서 return 값은 useActionState의 새로운 state가 된다.
    return {error: 'Name is required'}; 
  }
  await db.users.updateName(name);
}

 

React 19부터는 form의 action 속성에 실제 action이 이루어질 함수를 지정하여 수행 할 수 있고 반복적인 상태관리 로직과 pending 상태를 더 쉽게 관리할 수 있게 해줍니다.

현재는 React Hook Form을 사용하고 있고 간단한 폼이나 서버 컴포넌트를 활용해야 할 때 이점이 있습니다.

1-3. useFormStatus

useFormStatus는 하위 컴포넌트에서 상위 form의 상태를 읽을 수 있도록 해주는 Hook입니다.

이 Hook을 사용하면 상위 form의 유효성 검사나 제출 상태를 쉽게 확인 할 수 있습니다.

 

import {useFormStatus} from 'react-dom';
function DesignButton() {
  const {pending} = useFormStatus();
  return <button type="submit" disabled={pending} />
}

 

이 Hook 또한 React Hook Form을 사용하지 않는다면 적용을 고려해볼만 합니다.

1-4. useOptimistic

useOptimistic은 UI를 낙관적으로 업데이트할 수 있는 Hook으로 API 요청 후 실제 응답이 오기 전에, 마치 응답이 이미 완료 된 것처럼 UI를 업데이트 할 수 있습니다.

 

// App.js
import { useOptimistic, useState, useRef } from "react";
import { deliverMessage } from "./actions.js";

function Thread({ messages, sendMessage }) {
  const formRef = useRef();
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    // state는 currentState를 의미한다.
    // newMessage는 optimisticValue를 의미한다.
    (state, newMessage) => [ 
      ...state,
	    // 여기서 사용자가 입력한 optimistic value인 newMessage가 설정되고, sending 상태가 된다.
      {
        text: newMessage,
        sending: true
      }
    ]
  );
  
  async function formAction(formData) {
	  // 사용자가 입력한 value를 가지고 addOptimisticMessage를 호출한다.
	  // 넘기는 파라미터는 위 useOptimistic callback에서 newMessage에 해당되게 된다.
    addOptimisticMessage(formData.get("message"));
    formRef.current.reset();
    await sendMessage(formData);
  }

  return (
    <>
      {optimisticMessages.map((message, index) => (
        <div key={index}>
          {message.text}
          {!!message.sending && <small> (Sending...)</small>}
        </div>
      ))}
      <form action={formAction} ref={formRef}>
        <input type="text" name="message" placeholder="Hello!" />
        <button type="submit">Send</button>
      </form>
    </>
  );
}

export default function App() {
  const [messages, setMessages] = useState([
    { text: "Hello there!", sending: false, key: 1 }
  ]);
  async function sendMessage(formData) {
    const sentMessage = await deliverMessage(formData.get("message"));
    setMessages((messages) => [...messages, { text: sentMessage }]);
  }
  return <Thread messages={messages} sendMessage={sendMessage} />;
}

// actions.js
export async function deliverMessage(message) {
  await new Promise((res) => setTimeout(res, 1000));
  return message;
}

 

마지막은 새로 추가된 React API인 use로 Promise나 context 같은 리소스의 값을 읽을수 있게 해줍니다.

 

import {use} from 'react';

function Comments({commentsPromise}) {
  // commentsPromise가 resolve 되기 전 까지 suspend 상태가 된다.
  const comments = use(commentsPromise);
  return comments.map(comment => <p key={comment.id}>{comment}</p>);
}

function Page({commentsPromise}) {
  // promise가 resolve 되기 전까지 Suspense의 fallback이 표시된다.
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  )
}

use는 조금 더 알아보고 테스트 해봐야 활용할 수 있을 것 같습니다.

 

Hook 뿐만아니라 편의선 개선 사항들도 있고 이중 Next.js를 사용하며 유용한 것들에 대해 기재합니다.

 

2-1. ref


기존에 ref를 하위 컴포넌트에 전달하기 위해선 반드시 forwardRef를 사용해야 했습니다. React 19 부턴 props로 ref를 전달할 수 있어 훨씬 코드가 간결해지고, 전달방식이 더 직관적이게 개선되었습니다.

 

2-2. Context

 

React 19 부턴 <Context.Provider> 대신 <Context>를 provider로 직접 사용할 수 있어 context의 사용 방식이 더 간결해지고 불필요한 반복을 줄일 수 있습니다.

 

이외에도

  • Hydration 에러
  • Metadata
  • Stylesheet precedence 속성

 

등이 있습니다.

 

요약

useActionState, useOptimistic 등과 같은 훅들은 React Hook form, Tanstack Query를 사용하고 있어 당장 적용하기엔 무리가 있지만

상황에 따라 충분히 활용 할 만해 보입니다. 그 외에 useTrasition은 개인적으로 되게 유용하게 사용할 수 있을것 같고 ref props 등의 편의성 개선도 눈에 띕니다.

 

 

참고문서