React v.19 - Mai-Nova/AIssue-Overview GitHub Wiki

React v.19 ๊ณผ v.18 ๋น„๊ต

ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์˜ react version:

  • react: 19.1.0
  • react-dom: 19.1.0
  • react-router-dom: 7.6.0
  • vite: 6.3.5
  • @vitejs/plugin-react-swc": 3.9.0

version update Reference

1. Create React App ์ข…๋ฃŒ

  • ๊ธฐ์กด ์•ฑ์—์„œ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•˜๊ฑฐ๋‚˜ Vite, Parcel, RSBuild ์™€ ๊ฐ™์€ ๋นŒ๋“œ ๋„๊ตฌ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ถŒ์žฅ

  • Next.js , React Router , Expo ์™€ ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ์— ๊ถŒ์žฅ

  • ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

    • ํด๋ผ์ด์–ธํŠธ์ธก CSR, SPA ์ง€์›

2. React Router

์„ ์–ธ์ (declarative)/ ๋ฐ์ดํ„ฐ(data) / framework ์„ธ๊ฐ€์ง€๊ฐ€ ์žˆ๋‹ค.

framework/library

  1. declarative
import { BrowserRouter } from "react-router";

ReactDOM.createRoot(root).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);
  1. data
import { createBrowserRouter, RouterProvider } from "react-router";

let router = createBrowserRouter([
  {
    path: "/",
    Component: Root,
    loader: loadRootData,
  },
]);

ReactDOM.createRoot(root).render(<RouterProvider router={router} />);
  1. framework mode : Vite ํ”Œ๋Ÿฌ๊ทธ์ธ
  • ํƒ€์ž…์„ธ์ดํ”„ href
  • ์œ ํ˜• ์•ˆ์ „ ๊ฒฝ๋กœ ๋ชจ๋“ˆ API
  • ์ง€๋Šฅํ˜• ์ฝ”๋“œ ๋ถ„ํ• 
  • SPA, SSR ๋ฐ ์ •์  ๋ Œ๋”๋ง ์ „๋žต
// routes.ts
import { index, route } from "@react-router/dev/routes";

export default [index("./home.tsx"), route("products/:pid", "./product.tsx")];
//product.tsx
import { index, route } from "@react-router/dev/routes";

export default [index("./home.tsx"), route("products/:pid", "./product.tsx")];
import {RouterProvider, createBrowserRouter} from 'react-router';

import Home from './Home';
import Dashboard from './Dashboard';

// โœ… Each route has it's own URL
const router = createBrowserRouter([
  {path: '/', element: <Home />},
  {path: '/dashboard', element: <Dashboard />}
]);

export default function App() {
  return (
    <RouterProvider value={router} />
  )

3. Network Waterfalls ํ•ด๊ฒฐ์„ ์œ„ํ•ด React Query, SWR, Apollo, Relay ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ

  • Network Waterfalls : ์ฝ”๋“œ ๋‹ค์šด๋กœ๋“œ ์ค‘์— ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ‘๋ ฌ๋กœ ๊ฐ€์ ธ์˜ค๋Š” ๋Œ€์‹  ์•ฑ์ด ๋ Œ๋”๋ง๋  ๋•Œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒฝ์šฐ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.
  • ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋˜๊ธฐ ์ „์— ์š”์ฒญ์ด ์‹œ์ž‘๋˜๋„๋ก ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ€์ ธ์˜ค๋Š” ์˜ต์…˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ฒฝ๋กœ ์ˆ˜์ค€์—์„œ ๋ฐ์ดํ„ฐ ์ข…์†์„ฑ์„ ์ง€์ •ํ•˜๊ธฐ ์œ„ํ•ด ๋ผ์šฐํŒ… "๋กœ๋”" ํŒจํ„ด๊ณผ ํ†ตํ•ฉํ•  ๋•Œ ๊ฐ€์žฅ ์ž˜ ์ž‘๋™ํ•˜๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด ๋ผ์šฐํ„ฐ๊ฐ€ ๋ฐ์ดํ„ฐ ํŒจ์น˜๋ฅผ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
export default function Dashboard() {
  const [data, setData] = useState(null);

  // โŒ Fetching data in a component causes network waterfalls
  useEffect(() => {
    fetch("/api/data")
      .then((response) => response.json())
      .then((data) => setData(data));
  }, []);

  return (
    <div>
      {data.map((item) => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}
export async function loader() {
  const response = await fetch(`/api/data`);
  const data = await response.json();
  return data;
}

// โœ… Fetching data in parallel while the code is downloading
export default function Dashboard({ loaderData }) {
  return (
    <div>
      {loaderData.map((item) => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}
  • ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ ๋ผ์šฐํ„ฐ๋Š” ๊ฒฝ๋กœ๊ฐ€ ๋ Œ๋”๋ง๋˜๊ธฐ ์ง์ „์— ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ์„ ํƒ์ƒ‰ํ•  ๋•Œ ๋ผ์šฐํ„ฐ๋Š” ๋ฐ์ดํ„ฐ์™€ ๊ฒฝ๋กœ๋ฅผ ๋™์‹œ์— ๊ฐ€์ ธ์™€์„œ ๊ฐ€์ ธ์˜ค๊ธฐ ์ž‘์—…์„ ๋ณ‘๋ ฌํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ํ™”๋ฉด์— ์ฝ˜ํ…์ธ ๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๋ฐ ๊ฑธ๋ฆฌ๋Š” ์‹œ๊ฐ„์„ ์ค„์ด๊ณ  ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์•ฑ์˜ ๋กœ๋”๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑํ•ด์•ผ ํ•˜๋ฉฐ ๋ณต์žก์„ฑ์„ ๊ฐ์ˆ˜ํ•ด์•ผ ์„ฑ๋Šฅ์ด ์ €ํ•˜๋ฉ๋‹ˆ๋‹ค.

+ ์ฝ”๋“œ ๋ถ„ํ• 

  • ์ฝ”๋“œ๊ฐ€ ๋‹ค์šด๋กœ๋“œ๋˜๋Š” ๋™์•ˆ ๋ณ‘๋ ฌ๋กœ ์ฝ”๋“œ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ผ์šฐํ„ฐ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, React Router๋Š” lazy๊ฒฝ๋กœ๊ฐ€ ๋กœ๋“œ๋  ๋•Œ ์ฝ”๋“œ ๋ถ„ํ•  ๋ฐ ์ตœ์ ํ™”๋˜๋„๋ก ์ง€์ •ํ•˜๋Š” ์˜ต์…˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
import Home from "./Home";
import Dashboard from "./Dashboard";

// โœ… Routes are downloaded before rendering
const router = createBrowserRouter([
  { path: "/", lazy: () => import("./Home") },
  { path: "/dashboard", lazy: () => import("Dashboard") },
]);
  • ๋ผ์šฐํ„ฐ ๋ฐ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์†”๋ฃจ์…˜๊ณผ ํ†ตํ•ฉํ•˜์—ฌ ์บ์‹ฑ์„ ๊ทน๋Œ€ํ™”ํ•˜๊ณ , ํŽ˜์น˜๋ฅผ ๋ณ‘๋ ฌํ™”ํ•˜๋ฉฐ, "์ƒํ˜ธ ์ž‘์šฉ ์‹œ ๊ฐ€์ ธ์˜ค๊ธฐ" ํŒจํ„ด์„ ์ง€์›ํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ํšจ๊ณผ์ ์ž…๋‹ˆ๋‹ค.

4. useTransition

  • React Hook
  • v.18์ด๋ž‘ ์กฐ๊ธˆ ๋‹ค๋ฆ„
  • UI์˜ ์ผ๋ถ€๋ฅผ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ Œ๋”๋ง ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ๋Š” React Hook - UI ๋ฐ˜์‘์„ฑ์„ ์œ ์ง€(state)
  • ์šฐ์„ ์ˆœ์œ„๋ฅผ ์ค˜์„œ startTransition
const [isPending, startTransition] = useTransition()
  • ์‚ฌ์šฉ๋ฒ•
import { useTransition } from 'react';

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  // ...
}
  1. isPending ํ”Œ๋ž˜๊ทธ๋Š” ๋Œ€๊ธฐ ์ค‘์ธ Transition ์ด ์žˆ๋Š”์ง€ ์•Œ๋ ค์ค๋‹ˆ๋‹ค.
  2. startTransition ํ•จ์ˆ˜๋Š” ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ Transition ์œผ๋กœ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }
  // ...
}
  • startTransition ๋‚ด์—์„œ ํ˜ธ์ถœ๋˜๋Š” ํ•จ์ˆ˜๋ฅผ โ€œActionsโ€์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์‹œ์ž‘ ํŠธ๋žœ์ง€์…˜ ๋‚ด์—์„œ ํ˜ธ์ถœ๋˜๋Š” ๋ชจ๋“  ์ฝœ๋ฐฑ(์˜ˆ: ์ฝœ๋ฐฑ ํ”„๋กœํผํ‹ฐ)์˜ ์ด๋ฆ„์€ action์ด๊ฑฐ๋‚˜ โ€œActionโ€ ์ ‘๋ฏธ์‚ฌ๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ์ง„ํ–‰ ์ค‘์ธ Transition์— ๋Œ€ํ•ด ์‚ฌ์šฉ์ž์—๊ฒŒ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด isPending ์ƒํƒœ๋Š” startTransition์„ ์ฒ˜์Œ ํ˜ธ์ถœํ•  ๋•Œ true๋กœ ์ „ํ™˜๋˜๋ฉฐ, ๋ชจ๋“  Action์ด ์™„๋ฃŒ๋˜์–ด ์ตœ์ข… ์ƒํƒœ๊ฐ€ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘œ์‹œ๋  ๋•Œ๊นŒ์ง€ true ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  • Transition์ด ์ง„ํ–‰ ์ค‘์ผ ๋•Œ useOptimistic์„ ์‚ฌ์šฉํ•˜์—ฌ ์ฆ‰๊ฐ์ ์ธ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
function SubmitButton({ submitAction }) {
  const [isPending, startTransition] = useTransition();

  return (
    <button
      disabled={isPending}
      onClick={() => {
        startTransition(() => {
          submitAction();
        });
      }}
    >
      Submit
    </button>
  );
}

์ฐธ๊ณ -Action๊ณผ ์ผ๋ฐ˜ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ์˜ ์ฐจ์ด์ 

5. useActionState

  • ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋„ ์šฐ์„  ์ˆœ์œ„๋ฅผ ์ค€๋‹ค
const [state, formAction, isPending] = useActionState(fn, initialState, permalink?);
  • useActionState๋ฅผ ์ปดํฌ๋„ŒํŠธ์˜ ์ตœ์ƒ์œ„ ๋ ˆ๋ฒจ์—์„œ ํ˜ธ์ถœํ•˜์—ฌ ํผ ์•ก์…˜์ด ์‹คํ–‰๋  ๋•Œ ์—…๋ฐ์ดํŠธ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ State๋ฅผ ์ƒ์„ฑํ•˜์„ธ์š”. useActionState๋Š” ๊ธฐ์กด์˜ ํผ ์•ก์…˜ ํ•จ์ˆ˜์™€ ์ดˆ๊ธฐ State๋ฅผ ์ „๋‹ฌ๋ฐ›๊ณ , ํผ์—์„œ ์‚ฌ์šฉํ•  ์ƒˆ๋กœ์šด ์•ก์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ์ตœ์‹  ํผ State์™€ ์•ก์…˜์ด ๋Œ€๊ธฐ ์ค‘์ธ์ง€ ์—ฌ๋ถ€isPending๋„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ ์ตœ์‹  ํผ State๋Š” useActionState์— ์ „๋‹ฌํ•œ ํ•จ์ˆ˜์—๋„ ํ•จ๊ป˜ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.

  • ๋ฐ˜ํ™˜๊ฐ’: ์„ธ๊ฐ€์ง€ ๊ฐ’์„ ๋‹ด์€ ๋ฐฐ์—ด

  • 1.ํ˜„์žฌ State์ž…๋‹ˆ๋‹ค. ์ฒซ ๋ Œ๋”๋ง ์‹œ์—๋Š” initialState์™€ ์ผ์น˜ํ•˜๋ฉฐ, ์•ก์…˜์ด ์‹คํ–‰๋œ ํ›„์—๋Š” ํ•ด๋‹น ์•ก์…˜์ด ๋ฐ˜ํ™˜ํ•œ ๊ฐ’๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

    1. form ์ปดํฌ๋„ŒํŠธ์˜ action Prop์ด๋‚˜, ํผ ๋‚ด๋ถ€ button ์ปดํฌ๋„ŒํŠธ์˜ formAction Prop์— ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋Š” ์ƒˆ ์•ก์…˜์ž…๋‹ˆ๋‹ค. ์ด ์•ก์…˜์€ startTransition ๋‚ด์—์„œ ์ˆ˜๋™์œผ๋กœ ํ˜ธ์ถœํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
    1. ํ˜„์žฌ Transition์ด ๋Œ€๊ธฐ ์ค‘์ธ์ง€ ์•Œ๋ ค์ฃผ๋Š” isPending ํ”Œ๋ž˜๊ทธ์ž…๋‹ˆ๋‹ค.
import { useActionState } from 'react';
import { action } from './actions.js';

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

useActionState๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฐ์—ด์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์š”์†Œ๋ฅผ ๊ฐ–์Šต๋‹ˆ๋‹ค.

  1. ํผ์˜ ํ˜„์žฌ State๋Š”, ์ฒ˜์Œ์—๋Š” ์ „๋‹ฌํ•œ ์ดˆ๊ธฐ State(null)๋กœ ์„ค์ •๋˜๋ฉฐ, ํผ์ด ์ œ์ถœ๋œ ํ›„์—๋Š” ์ „๋‹ฌํ•œ ์•ก์…˜์˜ ๋ฐ˜ํ™˜๊ฐ’์œผ๋กœ ์„ค์ •๋ฉ๋‹ˆ๋‹ค.
  2. <form>์˜ action Prop์— ์ „๋‹ฌํ•˜๊ฑฐ๋‚˜ startTransition ์•ˆ์—์„œ ์ง์ ‘ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š”์ƒˆ๋กœ์šด ์•ก์…˜์ž…๋‹ˆ๋‹ค.(formAction)
  3. ์•ก์…˜์ด ์ฒ˜๋ฆฌ๋˜๋Š” ๋™์•ˆ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋Œ€๊ธฐ Pending State์ž…๋‹ˆ๋‹ค.
  • ํผ์ด ์ œ์ถœ๋˜๋ฉด, ์ œ๊ณตํ•œ ์•ก์…˜ ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉฐ, ํ•ด๋‹น ํ•จ์ˆ˜์˜ ๋ฐ˜ํ™˜๊ฐ’์ด ์ƒˆ๋กœ์šด ํ˜„์žฌ State๋กœ ์„ค์ •๋ฉ๋‹ˆ๋‹ค.
  • ์ด ์•ก์…˜ ํ•จ์ˆ˜๋Š” ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ ํ˜„์žฌ State๋ฅผ ์ถ”๊ฐ€๋กœ ์ „๋‹ฌ๋ฐ›์Šต๋‹ˆ๋‹ค. ์ฒ˜์Œ ์ œ์ถœ๋  ๋•Œ๋Š”์ดˆ๊ธฐ State๊ฐ€ ์ „๋‹ฌ๋˜๋ฉฐ, ์ดํ›„ ์ œ์ถœ๋ถ€ํ„ฐ๋Š” ์ง์ „ ํ˜ธ์ถœ ์‹œ ๋ฐ˜ํ™˜๋œ ๊ฐ’์ด ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. ๋‚˜๋จธ์ง€ ์ธ์ˆ˜๋“ค์€ useActionState๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•˜์„ ๋•Œ์™€ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ์‹œ)

//App.js
import { useActionState } from "react";
import { addToCart } from "./actions.js";

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

export default function App() {
  return (
    <>
      <AddToCartForm itemID="1" itemTitle="JavaScript: The Definitive Guide" />
      <AddToCartForm itemID="2" itemTitle="JavaScript: The Good Parts" />
    </>
  );
}
//actions.js
"use server";

export async function addToCart(prevState, queryData) {
  const itemID = queryData.get('itemID');
  if (itemID === "1") {
    return "Added to cart";
  } else {
    // Add a fake delay to make waiting noticeable.
    await new Promise(resolve => {
      setTimeout(resolve, 2000);
    });
    return "Couldn't add to cart: the item is sold out.";
  }
}

6. useOptimistic

  • useOptimistic ๋Š” UI๋ฅผ ๋‚™๊ด€์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” React Hook์ž…๋‹ˆ๋‹ค.
const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);
  • React Hook์œผ๋กœ, ๋น„๋™๊ธฐ ์ž‘์—…์ด ์ง„ํ–‰ ์ค‘์ผ ๋•Œ ๋‹ค๋ฅธ ์ƒํƒœ๋ฅผ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค. ์ธ์ž๋กœ ์ฃผ์–ด์ง„ ์ผ๋ถ€ ์ƒํƒœ๋ฅผ ๋ฐ›์•„, ๋„คํŠธ์›Œํฌ ์š”์ฒญ๊ณผ ๊ฐ™์€ ๋น„๋™๊ธฐ ์ž‘์—… ๊ธฐ๊ฐ„ ๋™์•ˆ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ๋Š” ๊ทธ ์ƒํƒœ์˜ ๋ณต์‚ฌ๋ณธ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์ƒํƒœ์™€ ์ž‘์—…์˜ ์ž…๋ ฅ์„ ์ทจํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•˜๊ณ , ์ž‘์—…์ด ๋Œ€๊ธฐ ์ค‘์ผ ๋•Œ ์‚ฌ์šฉํ•  ๋‚™๊ด€์ ์ธ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • ์ด ์ƒํƒœ๋Š” โ€œ๋‚™๊ด€์ โ€ ์ƒํƒœ๋ผ๊ณ  ๋ถˆ๋ฆฌ๋Š”๋ฐ, ์‹ค์ œ๋กœ ์ž‘์—…์„ ์™„๋ฃŒํ•˜๋Š” ๋ฐ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ๋”๋ผ๋„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ฆ‰์‹œ ์ž‘์—…์˜ ๊ฒฐ๊ณผ๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด ์ผ๋ฐ˜์ ์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

ex) ์ข‹์•„์š” user๊ฐ€ ์ข‹์•„์š” ๋ˆŒ๋ €์„๋•Œ ? : frontend > backend > db๊นŒ์ง€๊ฐ€์ง€ ์•Š๊ณ  useOptimistic์„ ์จ์„œ ๋‚™๊ด€์  ์ƒํƒœ๋กœ ํ‘œ์‹œํ•˜๊ณ  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ๋งค๊ฐœ๋ณ€์ˆ˜

    • state: ์ž‘์—…์ด ๋Œ€๊ธฐ ์ค‘์ด์ง€ ์•Š์„ ๋•Œ ์ดˆ๊ธฐ์— ๋ฐ˜ํ™˜๋  ๊ฐ’์ž…๋‹ˆ๋‹ค.
    • updateFn(currentState, optimisticValue): ํ˜„์žฌ ์ƒํƒœ์™€ addOptimistic์— ์ „๋‹ฌ๋œ ๋‚™๊ด€์ ์ธ ๊ฐ’์„ ์ทจํ•˜๋Š” ํ•จ์ˆ˜๋กœ, ๊ฒฐ๊ณผ์ ์ธ ๋‚™๊ด€์ ์ธ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ˆœ์ˆ˜ ํ•จ์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. updateFn์€ ๋‘ ๊ฐœ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์ทจํ•ฉ๋‹ˆ๋‹ค. currentState์™€ optimisticValue. ๋ฐ˜ํ™˜ ๊ฐ’์€ currentState์™€ optimisticValue์˜ ๋ณ‘ํ•ฉ๋œ ๊ฐ’์ž…๋‹ˆ๋‹ค.
  • ๋ฐ˜ํ™˜๊ฐ’

    • optimisticState: ๊ฒฐ๊ณผ์ ์ธ ๋‚™๊ด€์ ์ธ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ์ž‘์—…์ด ๋Œ€๊ธฐ ์ค‘์ด์ง€ ์•Š์„ ๋•Œ๋Š” state์™€ ๋™์ผํ•˜๋ฉฐ, ๊ทธ๋ ‡์ง€ ์•Š์€ ๊ฒฝ์šฐ updateFn์—์„œ ๋ฐ˜ํ™˜๋œ ๊ฐ’๊ณผ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.
    • addOptimistic: addOptimistic๋Š” ๋‚™๊ด€์ ์ธ ์—…๋ฐ์ดํŠธ๊ฐ€ ์žˆ์„ ๋•Œ ํ˜ธ์ถœํ•˜๋Š” dispatch ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ์–ด๋– ํ•œ ํƒ€์ž…์˜ optimisticValue๋ผ๋Š” ํ•˜๋‚˜์˜ ์ธ์ž๋ฅผ ์ทจํ•˜๋ฉฐ, state์™€ optimisticValue๋กœ updateFn์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

7. useFormState

  • useActionState๋Š” ํผ ์•ก์…˜์˜ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ State๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋„๋ก ์ œ๊ณตํ•˜๋Š” Hook์ž…๋‹ˆ๋‹ค.
import { useFormStatus } from "react-dom";
import action from './actions';

function Submit() {
  const status = useFormStatus();
  return <button disabled={status.pending}>Submit</button>
}

export default function App() {
  return (
    <form action={action}>
      <Submit />
    </form>
  );
}
  • ์ƒํƒœ ์ •๋ณด๋ฅผ ์ œ๊ณต๋ฐ›๊ธฐ ์œ„ํ•ด Submit ์ปดํฌ๋„ŒํŠธ๋ฅผ <form> ๋‚ด๋ถ€์— ๋ Œ๋”๋งํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด Hook์€ ํผ์ด ํ˜„์žฌ ์ œ์ถœํ•˜๊ณ  ์žˆ๋Š” ์ƒํƒœ์ธ์ง€๋ฅผ ์˜๋ฏธํ•˜๋Š” pending ํ”„๋กœํผํ‹ฐ์™€ ๊ฐ™์€ ์ƒํƒœ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

  • useFormStatus Hook์€ form ๋‚ด๋ถ€์— ๋ Œ๋”๋งํ•œ ์ปดํฌ๋„ŒํŠธ์—์„œ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  • useFormStatus๋Š” ์˜ค์ง ์ƒ์œ„ form์— ๋Œ€ํ•œ ์ƒํƒœ ์ •๋ณด๋งŒ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋™์ผํ•œ ์ปดํฌ๋„ŒํŠธ๋‚˜ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ Œ๋”๋งํ•œ form์˜ ์ƒํƒœ ์ •๋ณด๋Š” ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

  • ๋ฐ˜ํ™˜๊ฐ’

    • pending: ๋ถˆ๋ฆฌ์–ธ ๊ฐ’์ž…๋‹ˆ๋‹ค. true๋ผ๋ฉด ์ƒ์œ„ form์ด ์•„์ง ์ œ์ถœ ์ค‘์ด๋ผ๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด false์ž…๋‹ˆ๋‹ค.

    • data: FormData ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ ๊ฐ์ฒด๋กœ, ์ƒ์œ„ form์ด ์ œ์ถœํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ํ™œ์„ฑํ™”๋œ ์ œ์ถœ์ด ์—†๊ฑฐ๋‚˜ ์ƒ์œ„์— form์ด ์—†๋Š” ๊ฒฝ์šฐ์—๋Š” null์ž…๋‹ˆ๋‹ค.

    • method: 'get' ๋˜๋Š” 'post' ์ค‘ ํ•˜๋‚˜์˜ ๋ฌธ์ž์—ด ๊ฐ’์ž…๋‹ˆ๋‹ค. ์ด ํ”„๋กœํผํ‹ฐ๋Š” ์ƒ์œ„ form์ด GET ๋˜๋Š” POST HTTP ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ œ์ถœ๋˜๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ form์€ GET ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ method ํ”„๋กœํผํ‹ฐ๋ฅผ ํ†ตํ•ด ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    • action: ์ƒ์œ„ form์˜ action Prop์— ์ „๋‹ฌํ•œ ํ•จ์ˆ˜์˜ ๋ ˆํผ๋Ÿฐ์Šค์ž…๋‹ˆ๋‹ค. ์ƒ์œ„

      ์ด ์—†๋Š” ๊ฒฝ์šฐ์—๋Š” ์ด ํ”„๋กœํผํ‹ฐ๋Š” null์ž…๋‹ˆ๋‹ค. action Prop์— URI ๊ฐ’์ด ์ œ๊ณต๋˜์—ˆ๊ฑฐ๋‚˜ action prop๋ฅผ ์ง€์ •ํ•˜์ง€ ์•Š์•˜์„ ๊ฒฝ์šฐ์—๋Š” status.action์€ null์ž…๋‹ˆ๋‹ค.
import { useFormStatus } from "react-dom";
import { submitForm } from "./actions.js";

function Submit() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? "Submitting..." : "Submit"}
    </button>
  );
}

function Form({ action }) {
  return (
    <form action={action}>
      <Submit />
    </form>
  );
}

export default function App() {
  return <Form action={submitForm} />;
}

8. cache

cache

  • React 19์—์„œ ์ƒˆ๋กญ๊ฒŒ ์ œ๊ณต๋˜๋Š” cache() API๋ฅผ ํ™œ์šฉํ•ด ์ด๋ฅผ ์ตœ์†Œํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์†Œ๊ฐœํ•œ๋‹ค. ์ค‘๋ณต ์š”์ฒญ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์บ์‹ฑ์„ ํ™œ์šฉํ•ด ๋ถˆํ•„์š”ํ•œ ๋Œ€๊ธฐ ์‹œ๊ฐ„์„ ์ค„์ž„์œผ๋กœ์จ, ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ์„ฑ๋Šฅ์„ ํ•œ์ธต ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์ด ํ•ต์‹ฌ์ด๋‹ค.
  • ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ์ดํ„ฐ ์š”์ฒญ์ด ๋ถ€๋ชจโ†’์ž์‹์œผ๋กœ ์ˆœ์ฐจ์ ์œผ๋กœ ์ด๋ค„์ ธ ์ „์ฒด ๋ Œ๋”๋ง์„ ์ง€์—ฐ์‹œํ‚ค๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š”๋ฐ, ์ด๋ฅผ Waterfall Fetching์ด๋ผ ๋ถ€๋ฅธ๋‹ค.
  • React 19 cache()API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ ๋˜๋Š” ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ์บ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด API๋Š” React Server Components์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
const getUser = cache(async (userId: string) => {
  return db.getUser(userId);
})
  • Next.js์˜ ํ”„๋ฆฌ๋กœ๋“œํŒจํ„ด์„ ์‚ฌ์šฉํ•ด ๋ฐ์ดํ„ฐ ๋ฏธ๋ฆฌ ๋กœ๋“œํ•˜๋Š” ๊ฒƒ ๋ฐฉ์ง€

Reference :

โš ๏ธ **GitHub.com Fallback** โš ๏ธ