์›๋ฌธ: https://marmelab.com/blog/2024/01/23/react-19-new-hooks.html

 

 

์ผ๋ฐ˜์ ์ธ ์ƒ๊ฐ๊ณผ ๋‹ฌ๋ฆฌ React coreํŒ€์€ ๋ฆฌ์•กํŠธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์™€ Next.js์—๋งŒ ์˜ค์ง ์ดˆ์ ์„ ๋‘์ง„ ์•Š๋Š”๋‹ค. ์ƒˆ๋กœ์šด client-side ํ›…๋“ค์ด ๋‹ค์Œ React ๋ฉ”์ธ ๋ฒ„์ „์ธ 19๋ฒ„์ „์— ๋„์ž…๋œ๋‹ค. ๊ทธ๋“ค์€ ๋ฆฌ์•กํŠธ์˜ ๋‘ ๊ฐ€์ง€ ์ฃผ์š” ๋ฌธ์ œ์ , ์ฆ‰ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ(data fetching)๊ณผ ํผ(form)์— ์ดˆ์ ์„ ๋งž์ถ”๊ณ  ์žˆ๋‹ค. ์ด ํ›…๋“ค์€ ์‹ฑ๊ธ€ ํŽ˜์ด์ง€ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ž‘์—…์„ ํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋ฅผ ํฌํ•จํ•ด React ๊ฐœ๋ฐœ์ž๋“ค์˜ ์ƒ์‚ฐ์„ฑ์„ ํ–ฅ์ƒ์‹œ์ผœ์ค„ ๊ฒƒ์ด๋‹ค.

 

๋” ์ด์ƒ ๊ณ ๋ฏผํ•˜์ง€ ๋ง๊ณ , ์ƒˆ๋กœ์šด ํ›…๋“ค์„ ์‚ดํŽด๋ณด์ž!

 

 

์ฃผ๋ชฉ: ์ด ํ›…๋“ค์€ React์˜ Canary์™€ ์‹คํ—˜์  ์ฑ„๋„์—์„œ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด ํ›…๋“ค์€ ๊ณง ์ถœ์‹œ๋  ๋ฆฌ์•กํŠธ 19์— ํฌํ•จ๋˜์ง€๋งŒ, API๋Š” ์ตœ์ข… ์ถœ์‹œ ์ „์— ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ๋‹ค.

 

 

use(Promise)

 

์ด ์ƒˆ๋กœ์šด ํ›…์€ ํด๋ผ์ด์–ธํŠธ์˜ 'suspending'์„ ์œ„ํ•œ ๊ณต์‹ API์ด๋‹ค. ๋‹น์‹ ์€ ๊ทธ๊ฒƒ์„ promise๋กœ ๋„˜๊ธธ ์ˆ˜ ์žˆ๊ณ  ๋ฆฌ์•กํŠธ๋Š” ๊ทธ๊ฒƒ์ด ํ•ด๊ฒฐ๋  ๋•Œ๊นŒ์ง€ ๊ทธ๊ฒƒ์„ ์ค‘๋‹จ์‹œํ‚ฌ ๊ฒƒ์ด๋‹ค. React use ๋ฌธ์„œ์—์„œ ๊ฐ€์ ธ์˜จ ๊ธฐ๋ณธ ๊ตฌ๋ฌธ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

 

import { use } from 'react';

function MessageComponent({ messagePromise }) {
    const message = use(messagePromise);
    // ...
}

 

์ข‹์€ ์†Œ์‹์€ ์ด ํ›…์ด ๋ฐ์ดํ„ฐ ํŒจ์นญ์— ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ์—ฌ๊ธฐ ๋งˆ์šดํŠธ์‹œ์™€ ๋ฒ„ํŠผ ํด๋ฆญ์‹œ ๋ฐ์ดํ„ฐ ํŒจ์นญ์„ ํ•˜๋Š” ๊ตฌ์ฒด์ ์ธ ์˜ˆ์‹œ๊ฐ€ ์žˆ๋‹ค. ์ด ์ฝ”๋“œ๋Š” ๋‹จ์ผ useEffect๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค:

 

 

 

<Suspense> ๋ฌธ์„œ์˜ ์ด ๊ฒฝ๊ณ ๋ฅผ ๊ธฐ์–ตํ•˜๋Š”๊ฐ€?

 

ํ”„๋ ˆ์ž„์›Œํฌ์˜ ์˜๊ฒฌ ์—†๋Š” Suspense ์ง€์› ๋ฐ์ดํ„ฐ ํŒจ์นญ์€ ์•„์ง ์ง€์›๋˜์ง€ ์•Š๋Š”๋‹ค.

 

์ด๋Š” React19์—์„œ๋Š” ๋” ์ด์ƒ ์‚ฌ์‹ค์ด ์•„๋‹ˆ๋‹ค.

 

์ƒˆ๋กœ์šด use ํ›…์€ ์ˆจ๊ฒจ์ง„ ํž˜์ด ์žˆ๋‹ค: ๋‹ค๋ฅธ ๋ชจ๋“  React Hook๋“ค๊ณผ ๋‹ฌ๋ฆฌ, use๋Š” loop์™€ if์™€ ๊ฐ™์€ ์กฐ๊ฑด ์ ˆ์—์„œ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ด ๋ง์ด ์ฆ‰, ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด TanStack Query์™€ ๊ฐ™์€ ์„œ๋“œํŒŒํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋” ์ด์ƒ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์—†์Œ์„ ์˜๋ฏธํ• ๊นŒ? TanStack Query๊ฐ€ ๋‹จ์ˆœํžˆ Promise๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๊ฒƒ ์ด์ƒ์˜ ์—ญํ• ์„ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋” ๋‘๊ณ  ๋ด์•ผ ํ•œ๋‹ค.

 

ํ•˜์ง€๋งŒ ๊ทธ๊ฒƒ์€ ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉํ–ฅ์œผ๋กœ ๋‚˜์•„๊ฐ€๋Š” ํ›Œ๋ฅญํ•œ ๋‹จ๊ณ„์ด๋ฉฐ REST ๋˜๋Š” GraphQL API๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ์‹ฑ๊ธ€ ํŽ˜์ด์ง€ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋‹ค. ๋‚˜๋Š” ์ด ์ƒˆ๋กœ์šด ํ›…์— ๋งค์šฐ ์—ด๊ด‘ํ•œ๋‹ค!

 

React ๋ฌธ์„œ์—์„œ use(Promise) ํ›…์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์„ ์ฝ์–ด๋ณด์•„๋ผ.

 

use(Context)

React Context๋ฅผ ์ฝ๋Š”๋ฐ ๊ฐ™์€ use ํ›…์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฃจํ”„ ์•ˆ์—์„œ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ if์™€ ๊ฐ™์€ ์กฐ๊ฑด๋ฌธ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์„ ์ œ์™ธํ•˜๋ฉด ์ด๋Š” ์ •ํ™•ํžˆ useContext์™€ ๊ฐ™๋‹ค.

 

 

import { use } from 'react';

function HorizontalRule({ show }) {
    if (show) {
        const theme = use(ThemeContext);
        return <hr className={theme} />;
    }
    return false;
}

 

๋ฃจํ”„ ํ˜น์€ ์กฐ๊ฑด์ ˆ์—์„œ ์ปจํ…์ŠคํŠธ๋ฅผ ์ฝ๋Š” ์œ ์ผํ•œ ๋ฐฉ๋ฒ•์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‘˜๋กœ ๋‚˜๋ˆ„๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด๊ฒƒ์€ ์ผ๋ถ€ ์‚ฌ์šฉ ์‚ฌ๋ก€์— ๋Œ€ํ•œ ์ปดํฌ๋„ŒํŠธ ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ๋‹จ์ˆœํ™”ํ•  ๊ฒƒ์ด๋‹ค.

 

์ปจํ…์ŠคํŠธ๊ฐ€ ๋ฐ”๋€Œ์—ˆ๋”๋ผ๋„ ์ปดํฌ๋„ŒํŠธ์˜ ๋ฆฌ๋ Œ๋”๋ง์„ ์กฐ๊ฑด์ ์œผ๋กœ ๊ฑด๋„ˆ๋›ธ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์„ฑ๋Šฅ ์ธก๋ฉด์—์„œ๋„ ํฐ ํ˜๋ช…์ด ๋  ์ˆ˜ ์žˆ๋‹ค.

 

React ๋ฌธ์„œ์—์„œ use(Context) ํ›…์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์„ ์ฝ์–ด๋ณด์•„๋ผ.

 

Form Actions

 

์ด ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ฉด ํ•จ์ˆ˜๋ฅผ <form>์˜ ์•ก์…˜ ํ”„๋กญ์— ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ค. ํผ์ด ์ œ์ถœ๋˜๋ฉด React๋Š” ์ด ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค:

 

 

 

 

๋งŒ์•ฝ ๋‹น์‹ ์ด <form action> ํ”„๋กญ์„ React18์—์„œ ์ถ”๊ฐ€ํ•œ๋‹ค๋ฉด ๋‹น์‹ ์€ ๊ฒฝ๊ณ (warning)๋ฅผ ๋ฐ›๊ฒŒ ๋  ๊ฒƒ์„ ๊ธฐ์–ตํ•˜๋ผ:

 

Warning: Invalid value for prop action on <form> tag. Either remove it from the element or pass a string or number value to keep it in the DOM.

 

๋‹ค์Œ๊ณผ ๊ฐ™์€ form์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋Š” React 19์—์„œ ์ด ๊ฒฝ๊ณ ๋Š” ๋” ์ด์ƒ ์‚ฌ์‹ค์ด ์•„๋‹ˆ๋‹ค.

 

import { useState } from 'react';

const AddToCartForm = ({ id, title, addToCart }) => {
  const formAction = async (formData) => {
    try {
      await addToCart(formData, title);
    } catch (e) {
      // show error notification
    }
  };

  return (
    <form action={formAction}>
      <h2>{title}</h2>
      <input type="hidden" name="itemID" value={id} />
      <button type="submit">Add to Cart</button>
    </form>
  );
};

type Item = {
  id: string;
  title: string;
};

const Cart = ({ cart }: { cart: Item[] }) => {
  if (cart.length == 0) {
    return null;
  }
  return (
    <>
      Cart content:
      <ul>
        {cart.map((item, index) => (
          <li key={index}>{item.title}</li>
        ))}
      </ul>
      <hr />
    </>
  );
};

export const App = () => {
  const [cart, setCart] = useState<Item[]>([]);

  const addToCart = async (formData: FormData, title) => {
    const id = String(formData.get('itemID'));
    // simulate an AJAX call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    setCart((cart: Item[]) => [...cart, { id, title }]);

    return { id };
  };

  return (
    <>
      <Cart cart={cart} />
      <AddToCartForm
        id="1"
        title="JavaScript: The Definitive Guide"
        addToCart={addToCart}
      />
      <AddToCartForm
        id="2"
        title="JavaScript: The Good Parts"
        addToCart={addToCart}
      />
    </>
  );
};

 

accToCart ํ•จ์ˆ˜๋Š” ์„œ๋ฒ„ Action์ด ์•„๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ํ˜ธ์ถœ๋˜๋ฉฐ ๋น„๋™๊ธฐ ๊ธฐ๋Šฅ์ผ ์ˆ˜ ์žˆ๋‹ค.

 

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฆฌ์•กํŠธ์—์„œ ๊ฒ€์ƒ‰ ํผ๊ณผ ๊ฐ™์€ AJAX ํผ์˜ ์ฒ˜๋ฆฌ๊ฐ€ ํฌ๊ฒŒ ๊ฐ„๋‹จํ•ด์งˆ ๊ฒƒ์ด๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด๊ฒƒ์€ ์–‘์‹ ์ œ์ถœ(์œ ํšจ์„ฑ ํ™•์ธ, ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ ๋“ฑ)์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ ์ด์ƒ์˜ ์ผ์„ ํ•˜๋Š” React Hook Form๊ณผ ๊ฐ™์€ ์„œ๋“œํŒŒํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์—†์•จ๋งŒํผ ์ถฉ๋ถ„ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค. 

 

Tip: ๋‹น์‹ ์€ ์œ„ ์˜ˆ์ œ์—์„œ ๋ช‡๋ช‡ ์‚ฌ์šฉ์„ฑ ๋ฌธ์ œ๋ฅผ ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ๋‹ค. (submit์„ ์ง„ํ–‰ํ•  ๋•Œ submit ๋ฒ„ํŠผ์ด ํ™œ์„ฑํ™”๋˜์ง€ ์•Š์Œ, ํ™•์ธ ๋ฉ”์„ธ์ง€ ๋ˆ„๋ฝ, ๋Šฆ์€ ์นดํŠธ ์—…๋ฐ์ดํŠธ). ๋‹คํ–‰ํžˆ๋„ ๋” ๋งŽ์€ ํ›…๋“ค์ด ์ด๋Ÿฌํ•œ ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ๋•๊ธฐ ์œ„ํ•ด ์ œ๊ณต๋˜๊ณ  ์žˆ๋‹ค. ๊ณ„์† ์ฝ์–ด๋ณด๊ธธ!

 

 

React ๋ฌธ์„œ์—์„œ the <form action> prop์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์„ ์ฝ์–ด๋ณด์•„๋ผ.

 

 

useFormState

 

์ด ์ƒˆ๋กœ์šด ํ›…์€ ์•„๋ž˜ ๋ฌ˜์‚ฌํ•œ ๊ฒƒ์ฒ˜๋Ÿผ ๋น„๋™๊ธฐ ํผ ์•ก์…˜์„ ๋•๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. useFormState๋ฅผ ํ˜ธ์ถœํ•ด ํผ์ด ์ œ์ถœ๋œ ๋งˆ์ง€๋ง‰ ์‹œ์ ๋ถ€ํ„ฐ ์•ก์…˜์˜ ๋ฐ˜ํ™˜ ๊ฐ’์— ์ ‘๊ทผํ•œ๋‹ค.

 

import { useFormState } from 'react-dom';
import { action } from './action';

function MyComponent() {
    const [state, formAction] = useFormState(action, null);
    // ...
    return <form action={formAction}>{/* ... */}</form>;
}

 

 

์˜ˆ๋ฅผ ๋“ค์–ด, ์ด๊ฒƒ์€ ๋‹น์‹ ์ด ํผ ์•ก์…˜์—์„œ ๋ฐ˜ํ™˜๋œ ํ™•์ธ ํ˜น์€ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๊ฒŒ ํ•œ๋‹ค.

 

import { useState } from 'react';
import { useFormState } from 'react-dom';

const AddToCartForm = ({ id, title, addToCart }) => {
  const addToCartAction = async (prevState, formData) => {
    try {
      await addToCart(formData, title);
      return 'Added to cart';
    } catch (e) {
      return "Couldn't add to cart: the item is sold out.";
    }
  };

  const [message, formAction] = useFormState(addToCartAction, null);

  return (
    <form action={formAction}>
      <h2>{title}</h2>
      <input type="hidden" name="itemID" value={id} />
      <button type="submit">Add to Cart</button>&nbsp;
      {message}
    </form>
  );
};

type Item = {
  id: string;
  title: string;
};

export const App = () => {
  const [cart, setCart] = useState<Item[]>([]);

  const addToCart = async (formData: FormData, title) => {
    const id = String(formData.get('itemID'));
    // simulate an AJAX call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    if (id === '1') {
      setCart((cart: Item[]) => [...cart, { id, title }]);
    } else {
      throw new Error('Unavailable');
    }

    return { id };
  };

  return (
    <>
      <AddToCartForm
        id="1"
        title="JavaScript: The Definitive Guide"
        addToCart={addToCart}
      />
      <AddToCartForm
        id="2"
        title="JavaScript: The Good Parts"
        addToCart={addToCart}
      />
    </>
  );
};

 

 

์ฃผ๋ชฉ: useFormState๋Š” react๊ฐ€ ์•„๋‹ˆ๋ผ react-dom์—์„œ ๋ถ€ํ„ฐ import๋˜์–ด์•ผ ํ•œ๋‹ค.

 

 

React ๋ฌธ์„œ์—์„œ useFormState์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์„ ์ฝ์–ด๋ณด์•„๋ผ.

 

 

 

useFormStatus

 

useFormStatus๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ถ€๋ชจ <form>์ด ํ˜„์žฌ ์ œ์ถœ ์ค‘์ด๊ฑฐ๋‚˜ ์„ฑ๊ณต์ ์œผ๋กœ ์ œ์ถœํ–ˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ๋‹ค. ํผ์˜ ์ž์‹์—์„œ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ๋‹ค์Œ ์†์„ฑ์„ ๊ฐ€์ง„ ๊ฐœ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค:

 

const { pending, data, method, action } = useFormStatus();

 

 

๋‹น์‹ ์€ ๋ฐ์ดํ„ฐ ์†์„ฑ์„ ํ†ตํ•ด ์œ ์ €๊ฐ€ ์ œ์ถœ ์ค‘์ธ ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋‹ค. ํผ์ด ์ œ์ถœ๋˜๋Š” ๋™์•ˆ ๋ฒ„ํŠผ์ด ๋น„ํ™œ์„ฑํ™”๋œ pending ์ƒํƒœ ๋˜ํ•œ ์•„๋ž˜ ์˜ˆ์‹œ์™€ ๊ฐ™์ด ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋‹ค:

 

import { useState } from 'react';
import { useFormStatus } from 'react-dom';

const AddToCartForm = ({ id, title, addToCart }) => {
  const formAction = async (formData) => {
    try {
      await addToCart(formData, title);
    } catch (e) {
      // show error notification
    }
  };

  return (
    <form action={formAction}>
      <h2>{title}</h2>
      <input type="hidden" name="itemID" value={id} />
      <SubmitButton />
    </form>
  );
};

const SubmitButton = () => {
  const { pending } = useFormStatus();
  return (
    <button disabled={pending} type="submit">
      Add to Cart
    </button>
  );
};

type Item = {
  id: string;
  title: string;
};

const Cart = ({ cart }: { cart: Item[] }) => {
  if (cart.length == 0) {
    return null;
  }
  return (
    <>
      Cart content:
      <ul>
        {cart.map((item, index) => (
          <li key={index}>{item.title}</li>
        ))}
      </ul>
      <hr />
    </>
  );
};

export const App = () => {
  const [cart, setCart] = useState<Item[]>([]);

  const addToCart = async (formData: FormData, title) => {
    const id = String(formData.get('itemID'));
    // simulate an AJAX call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    setCart((cart: Item[]) => [...cart, { id, title }]);

    return { id };
  };

  return (
    <>
      <Cart cart={cart} />
      <AddToCartForm
        id="1"
        title="JavaScript: The Definitive Guide"
        addToCart={addToCart}
      />
      <AddToCartForm
        id="2"
        title="JavaScript: The Good Parts"
        addToCart={addToCart}
      />
    </>
  );
};

 

 

์ฃผ๋ชฉ: useFormStatus๋Š” react๊ฐ€ ์•„๋‹Œ react-dom์—์„œ importํ•ด์•ผ ํ•œ๋‹ค. ๋˜ํ•œ ๋ถ€๋ชจ ํผ์ด ์œ„์—์„œ ์„ค๋ช…ํ•œ action prop์„ ์‚ฌ์šฉํ•  ๋•Œ๋งŒ ์ž‘๋™ํ•œ๋‹ค.

 

์ด ํ›…์€ useFormState์™€ ํ•จ๊ป˜ ๋ถˆํ•„์š”ํ•œ ์ปจํ…์ŠคํŠธ ํšจ๊ณผ๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์–ด์ˆ˜์„ ํ•˜๊ฒŒ ๋งŒ๋“ค์ง€ ์•Š๊ณ  ํด๋ผ์ด์–ธํŠธ ์ธก ํผ์˜ ์œ ์ € ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ฌ ๊ฒƒ์ด๋‹ค. useFormStatus ํ›„ํฌ์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ React ๋ฌธ์„œ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

useOptimistic

 

์ด ์ƒˆ๋กœ์šด ํ›…์€ actioin์ด ์ œ์ถœ๋˜๋Š” ๋™์•ˆ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ตœ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค. 

 

 

import { useOptimistic } from 'react';

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

 

 

์œ„ ์นดํŠธ ์˜ˆ์ œ์—์„œ ์šฐ๋ฆฌ๋Š” AJAX ํ˜ธ์ถœ์ด ์™„๋ฃŒ๋˜๊ธฐ ์ „์— ์ƒˆ ํ•ญ๋ชฉ์ด ์ถ”๊ฐ€๋œ ์นดํŠธ๋ฅผ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋‹ค: 

 

import { useState, useOptimistic } from 'react';

const AddToCartForm = ({ id, title, addToCart, optimisticAddToCart }) => {
  const formAction = async (formData) => {
    optimisticAddToCart({ id, title });
    try {
      await addToCart(formData, title);
    } catch (e) {
      // show error notification
    }
  };

  return (
    <form action={formAction}>
      <h2>{title}</h2>
      <input type="hidden" name="itemID" value={id} />
      <button type="submit">Add to Cart</button>
    </form>
  );
};

type Item = {
  id: string;
  title: string;
};

const Cart = ({ cart }: { cart: Item[] }) => {
  if (cart.length == 0) {
    return null;
  }
  return (
    <>
      Cart content:
      <ul>
        {cart.map((item, index) => (
          <li key={index}>{item.title}</li>
        ))}
      </ul>
      <hr />
    </>
  );
};

export const App = () => {
  const [cart, setCart] = useState<Item[]>([]);

  const [optimisticCart, optimisticAddToCart] = useOptimistic<Item[], Item>(
    cart,
    (state, item) => [...state, item]
  );

  const addToCart = async (formData: FormData, title) => {
    const id = String(formData.get('itemID'));
    // simulate an AJAX call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    setCart((cart: Item[]) => [...cart, { id, title }]);

    return { id };
  };

  return (
    <>
      <Cart cart={optimisticCart} />
      <AddToCartForm
        id="1"
        title="JavaScript: The Definitive Guide"
        addToCart={addToCart}
        optimisticAddToCart={optimisticAddToCart}
      />
      <AddToCartForm
        id="2"
        title="JavaScript: The Good Parts"
        addToCart={addToCart}
        optimisticAddToCart={optimisticAddToCart}
      />
    </>
  );
};

 

 

UI๋ฅผ ์ตœ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์€ ์›น ์•ฑ์˜ ์œ ์ € ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ์ข‹์€ ๋ฐฉ๋ฒ•์ด๋‹ค. ์ด ํ›…์€ ์ด ์‚ฌ์šฉ ์‚ฌ๋ก€์— ๋งŽ์€ ๋„์›€์ด ๋œ๋‹ค. useOptimistic์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ React ๋ฌธ์„œ์—์„œ ์ฝ์„ ์ˆ˜ ์žˆ๋‹ค.

 

 

๋ณด๋„ˆ์Šค: ๋น„๋™๊ธฐ ์ „ํ™˜

React์˜ Transition API๋Š” UI ์ฐจ๋‹จ์—†์ด ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž๊ฐ€ ๋งˆ์Œ์„ ๋ฐ”๊ฟ€ ๊ฒฝ์šฐ ์ด์ „ ์ƒํƒœ ๋ณ€๊ฒฝ์„ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ด ์•„์ด๋””์–ด๋Š” startTransition๋ฅผ ํ˜ธ์ถœํ•ด ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ๊ฐ์‹ผ๋‹ค.

 

function TabContainer() {
    const [isPending, startTransition] = useTransition();
    const [tab, setTab] = useState('about');

    function selectTab(nextTab) {
        // instead of setTab(nextTab), put the state change in a transition
        startTransition(() => {
            setTab(nextTab);
        });
    }
    // ...
}

 

๋‹ค์Œ ์˜ˆ๋Š” ์ด Transition API๋ฅผ ์‚ฌ์šฉํ•œ ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜์„ ๋ณด์—ฌ์ค€๋‹ค. "Posts"์„ ํด๋ฆญํ•œ ๋‹ค์Œ ์ฆ‰์‹œ "Contact"๋ฅผ ํด๋ฆญํ•œ๋‹ค. ์ด๋กœ ์ธํ•ด "Posts"์˜ ๋Š๋ฆฐ ๋ Œ๋”๋ง์ด ์ค‘๋‹จ๋œ๋‹ค. "Contact" ํƒญ์ด ์ฆ‰์‹œ ํ‘œ์‹œ๋œ๋‹ค. ์ด ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋Š” ํŠธ๋žœ์ง€์…˜์œผ๋กœ ํ‘œ์‹œ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋Š๋ฆฐ ๋ฆฌ๋ Œ๋”(re-render)๋Š” ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ต์ œํ•˜์ง€ ์•Š๋Š”๋‹ค.

 

import { useState, useTransition } from 'react';
import TabButton from './TabButton';
import AboutTab from './AboutTab';
import PostsTab from './PostsTab';
import ContactTab from './ContactTab';

export function App() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <>
      <TabButton isActive={tab === 'about'} onClick={() => selectTab('about')}>
        About
      </TabButton>
      <TabButton isActive={tab === 'posts'} onClick={() => selectTab('posts')}>
        Posts (slow)
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => selectTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </>
  );
}

 

 

React 18.2์—์„œ๋Š” Transition ํ›…์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. React 19์˜ ์ƒˆ๋กœ์šด ์ ์€ ์ด์ œ ๋น„๋™๊ธฐ ๊ธฐ๋Šฅ์„ ์ง€๋‚˜ Transition์„ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, React๋Š” Transition์„ ์‹œ์ž‘ํ•˜๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๊ณ  ์žˆ๋‹ค.

 

์ด๋Š” AJAX ํ˜ธ์ถœ์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์ œ์ถœํ•˜๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ํŠธ๋žœ์ง€์…˜์œผ๋กœ ๋ Œ๋”๋งํ•˜๋Š” ๋ฐ ์œ ์šฉํ•˜๋‹ค. ํŠธ๋žœ์ง€์…˜์˜ pending ์ƒํƒœ๋Š” ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ์ œ์ถœ๊ณผ ํ•จ๊ป˜ ์‹œ์ž‘๋œ๋‹ค. ์ด๋Š” ์œ„์—์„œ ์„ค๋ช…ํ•œ ํผ actions ๊ธฐ๋Šฅ์—์„œ ์ด๋ฏธ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ๋‹ค. React๊ฐ€ startTransition์œผ๋กœ ๊ฐ์‹ธ์ง„ <form action> ํ•ธ๋“ค๋Ÿฌ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํ˜„์žฌ ํŽ˜์ด์ง€๋ฅผ ์ฐจ๋‹จํ•˜์ง€ ์•Š๋Š”๋‹ค.

 

 

์ด ๊ธฐ๋Šฅ์€ ์•„์ง React ์„ค๋ช…์„œ์— ๋ฌธ์„œํ™”๋˜์–ด ์žˆ์ง€ ์•Š์ง€๋งŒ pull request์—์„œ ์ด ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์„ ์ฝ์„ ์ˆ˜ ์žˆ๋‹ค.

 

๊ฒฐ๋ก 

์ด๋Ÿฌํ•œ ๋ชจ๋“  ๊ธฐ๋Šฅ๋“ค์€ ํด๋ผ์ด์–ธํŠธ ์ „์šฉ์ธ ๋ฆฌ์•กํŠธ ์•ฑ์—์„œ ์ž‘๋™ํ•˜๋ฉฐ, Vite์™€ ํ•จ๊ป˜ ๋ฒˆ๋“ค๋œ ์•ฑ์—์„œ ์ž‘๋™ํ•œ๋‹ค. ๋น„๋ก ์„œ๋ฒ„ ํ†ตํ•ฉํ˜• ๋ฆฌ์•กํŠธ ์•ฑ์—์„œ๋„ ์ด๊ฒƒ์ด ์ž‘๋™ํ•˜์ง€๋งŒ ์ด๊ฒƒ์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด Next ํ˜น์€ Remix์™€ ๊ฐ™์€ SSR ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š”๋Š” ์—†๋‹ค. 

 

์ด๋Ÿฌํ•œ ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ๋ฆฌ์•กํŠธ์—์„œ ๋ฐ์ดํ„ฐ ํŒจ์นญ๊ณผ ํผ์„ ๊ตฌํ˜„ํ•˜๊ธฐ๊ฐ€ ํ›จ์”ฌ ์‰ฌ์›Œ์ง„๋‹ค. ํ•˜์ง€๋งŒ ํ›Œ๋ฅญํ•œ ์œ ์ € ๊ฒฝํ—˜์„ ๋งŒ๋“ค๋ ค๋ฉด ์ด ๋ชจ๋“  ํ›…๋ฅผ ํ†ตํ•ฉํ•ด์•ผ ํ•˜๋Š”๋ฐ, ์ด๋Š” ๋ณต์žกํ•  ์ˆ˜ ์žˆ๋‹ค. ๋˜๋Š” ์ตœ์ ์˜ ๋นŒํŠธ์ธ ์—…๋ฐ์ดํŠธ๋ฅผ ์ง€์›ํ•˜๋Š” ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ํผ์„ ๊ฐ€์ง„ react-admin๊ณผ ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ด ๊ธฐ๋Šฅ๋“ค์€ ์™œ React 18.3์ด ์•„๋‹Œ React 19์— ๋„์ž…๋˜๋Š” ๊ฒƒ์ผ๊นŒ? ์ด ๊ธฐ๋Šฅ๋“ค์—๋Š” ์•ฝ๊ฐ„์˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ํฌํ•จ๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— 18.3 release์—๋Š” ์—†์„ ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค.

 

React 19๋Š” ์–ธ์ œ ์˜ฌ ๊ฒƒ์ธ๊ฐ€ ? ์•„์ง ETA๋Š” ์—†์ง€๋งŒ ์ด ๊ฒŒ์‹œ๋ฌผ์— ์–ธ๊ธ‰๋œ ๋ชจ๋“  ๊ธฐ๋Šฅ์€ ์ด๋ฏธ ์ž‘๋™ํ•˜๊ณ  ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์•„์ง ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•˜๊ณ  ์‹ถ์ง€๋Š” ์•Š๋‹ค. (Next.js๊ฐ€ ๊ทธ๋ ‡๊ฒŒ ํ•œ๋‹ค๊ณ  ํ•ด๋„) canary release๋ฅผ ํ”„๋กœ๋•ํŠธ์— ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ์ข‹์€ ์ƒ๊ฐ์ด ์•„๋‹ˆ๋‹ค.

 

๋ฆฌ์•กํŠธ ์ฝ”์–ด ํŒ€์ด SSR ์•ฑ์—์„œ ์ผํ•˜๋Š” ์‚ฌ๋žŒ๋“ค๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋ชจ๋“  ๋ฆฌ์•กํŠธ ๊ฐœ๋ฐœ์ž๋“ค์˜ ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด ๋…ธ๋ ฅํ•˜๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์€ ์ •๋ง ๊ธฐ์œ ์ผ์ด๋‹ค. ๋˜ํ•œ ๊ทธ๋“ค์€ ์ปค๋ฎค๋‹ˆํ‹ฐ ํ”ผ๋“œ๋ฐฑ์— ๊ท€๋ฅผ ๊ธฐ์šธ์ด๊ณ  ์žˆ๋Š” ๊ฒƒ ๊ฐ™๋‹ค - ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋ฐ ํผ ํ•ธ๋“ค๋ง์€ ๋งค์šฐ ํ”ํ•œ ๋ฌธ์ œ์ด๋‹ค.

 

๋‚˜๋Š” React์˜ Stable ๋ฆด๋ฆฌ์ฆˆ์— ์ด ๊ธฐ๋Šฅ๋“ค์ด ๋ณด์ด๊ธธ ๊ธฐ๋Œ€ํ•œ๋‹ค!

 

+ Recent posts