woolta

24년 react 소식 ( React 컴파일러, useFormStatus, useFormState, useOptimistic ...)

wooltaUserImgb00032 | React | 2024-03-02

이번에 올라온 react 업데이트 소식에 꽤나 재미있는 내용들이 몇개 보여서 이에 대해 간단하게 공유하고자 가져왔습니다. 보다 자세한 내용은 2024-02-15 react-lab-blog 에서 확인 가능합니다.

React 컴파일러

React만을 위한 컴파일러를 개발중이리고 합니다. 실제로 instagram 에서는 이미 React 컴파일러를 사용 하였다고 하고 조만간 오픈소스로 출시 준비중이라고 말하는것으로 보아 머지않은 시점에 공개하지 않을까 생각됩니다. 가장 큰 장점으로 생각되는건 useMemo, useCallback,memo 와 같은 메모이지에이션을 사용하지 않아도 자동으로 최적화 해준다는 부분인데요. 그동안 React를 통해 개발하면서 위의 훅들을 개발하는 개발자가 적절한 판단을 하여 최적화가 필요한 곳에 사용하였는데 사람마다 그 최적화의 기준이 다르기도 하고 일일히 수동 최적화를 해주어야 하는 부분이 상당히 피로도가 많았는데 아주 행복한 소식이라고 생각합니다. (나중엔 react 컴파일러가 아닌 react 만 사용해도 자동으로 최적화 되었으면 더욱 좋을것 같습니다.)

useOptimistic

useOptimisticoptimistic update 를 편하게 지원하게 나온 훅으로 간단하게 optimistic update 에 대한 예시를 보면 좋아요 액션에 대한 예시가 적절한 예시로 생각됩니다.

optimistic update 예시 흐름

  • 사용자가 좋아요 버튼 누름
  • 좋아요 처리에 대한 http 네트워크 호출과 동시에 성공 응답으로 좋아요에 대한 상태를 true로 변경하여 좋아요 상태 즉각 반영
  • 이후 네트워크 처리에서 에러가 발생할 경우 좋아요에 대한 상태를 다시 원복

이러한 데이터 처리에 대해 편하게 제어할수있도록 useOptimistic 가 새롭게 추가되었습니다.

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

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

function Thread({ messages, sendMessage }) {
  const formRef = useRef();
  async function formAction(formData) {
    addOptimisticMessage(formData.get("message"));
    formRef.current.reset();
    await sendMessage(formData);
  }
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state,
      {
        text: newMessage,
        sending: true
      }
    ]
  );

  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} />;
}
  • 네트워크 전송중인 sending 시점에 응답에 대한 데이터를 미리 보여주는것을 볼 수 있습니다. https://image.woolta.com/3fe383a9bd5d3359.gif

Form 개선 훅 useFormStatus, useFormState

그동안 React 에서 가장 힘든 처리중 하나가 Form 데이터에 대한 처리였습니다. React로만 작업하려면 보통 아래 케이스 처럼 개발하였습니다.

  • form 속성마다 useState 추가해서 사용
  • useState에 1개의 객체를 넣어서 처리
  • useReducer를 통한 처리
  • 복잡도 관리를 편하게 하기 위해 context를 활용
  • 최적화를 위해 비제어 컴포넌트 활용

그러나 어떠한 방법을 사용하더라도 React를 통한 form 데이터 처리는 상당히 귀찮고 최적화하기도 힘들었습니다.

때문에 보통 잘 구현된 라이브러리 (react-hook-form 등.) 를 많이 사용하곤 했습니다. 이러한 불편함을 해결해주기 위해 Form 관련 지원 hook 들이 추가되었습니다.

  • useFormStatus: 폼 상태의 정보 (폼이 제출완료되었는지, 유효성검사가 끝난거지, 폼처리중 상태인것인지) 와 같은 부분에 사용합니다.
  • useFormState: 폼데이터에 대한 데이터 관리 (입력값에 대한 제어, 유효성 검사) 등에 사용합니다.

이번글에선 간단한 사용 예시만 보여드리고 다음 글에서 해당 훅들에 대한 여러 예시를 살펴볼 예정입니다.

useFormStatus

./actions.js
"use server";

export async function addToCart(prevState, queryData) {
  const itemID = queryData.get('itemID');
  if (itemID === "1") {
    return "Added to cart";
  } else {
    return "Couldn't add to cart: the item is sold out.";
  }
}

------------------------------------------------------------------------------------

import { useState } from "react";
import { useFormState } from "react-dom";
import { addToCart } from "./actions.js";

function AddToCartForm({itemID, itemTitle}) {
  const [message, formAction] = useFormState(addToCart, null);
  return (
    <form action={formAction}>
      <h2>{itemTitle}</h2>
      <input type="hidden" name="itemID" value={itemID} />
      <button type="submit">Add to Cart</button>
      {message}
    </form>
  );
}

export default function App() {
  return (
    <>
      <AddToCartForm itemID="1" itemTitle="JavaScript: The Definitive Guide" />
      <AddToCartForm itemID="2" itemTitle="JavaScript: The Good Parts" />
    </>
  )
}
  • 위의 From 에서 add to cart 버튼 클릭시 addToCart 함수가 호출되며 각 input값에 대해 노출되는것이 아래 이미지를 통해 확인됩니다.

useFormStatus

./aptions.js

import UsernameForm from './UsernameForm';
import { submitForm } from "./actions.js";
import {useRef} from 'react';

export default function App() {
  const ref = useRef(null);
  return (
    <form ref={ref} action={async (formData) => {
      await submitForm(formData);
      ref.current.reset();
    }}>
      <UsernameForm />
    </form>
  );
}

-------------------------------------------
import {useState, useMemo, useRef} from 'react';
import {useFormStatus} from 'react-dom';

export default function UsernameForm() {
  const {pending, data} = useFormStatus();

  return (
    <div>
      <h3>Request a Username: </h3>
      <input type="text" name="username" disabled={pending}/>
      <button type="submit" disabled={pending}>
        Submit
      </button>
      <br />
      <p>{data ? `Requesting ${data?.get("username")}...`: ''}</p>
    </div>
  );
}
  • action 시 이에 대한 loading 상태 제어 케이스 예시

"use client", "use server" 의 추가

이제 nextjs가 아닌 react에서도 동일하게 서버컴포넌트를 사용할수 있게 되도록 업데이트 예정입니다.

메타 태그 지원

React Helmet 을 통해 하던 메타 태그에 대한 작업을 자체적으로 지원하도록 추가될 예정 입니다. 물론 클라이언트,SSR , RSC를 포함한 모든 환경 전부 동일하게 작동합니다.

asset 데이터 로딩 처리

style, link, script 태그를 사용하여 폰트, css 와 같은 에셋 데이터 들에 대해서도 완전히 로드될떄까지 이에 대한 로딩 처리를 지원 하도록 preload, preinit 와 같은 api가 새로 추가됩니다.

  • example
import { preload } from 'react-dom';

function AppRoot() {
  preload("https://example.com/font.woff2", {as: "font"});

}

참조

Copyright © 2018 woolta.com

gommpo111@gmail.com