๋ฐ˜์‘ํ˜•

2024๋…„ 12์›” React 19์˜ stable ๋ฒ„์ „์ด ์ถœ์‹œ๋๋‹ค. ๋ฆฌ์•กํŠธ ๊ณต์‹ ๋ธ”๋กœ๊ทธ๋ฅผ ์ฐธ๊ณ ํ•ด์„œ React 19์˜ ์ฃผ์š” ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ •๋ฆฌํ•ด ๋ดค๋‹ค. ์ƒˆ๋กญ๊ฒŒ ์„ ๋ณด์ธ ํ›…์€ ๊ฐ์ข… ๋ฌธ์„œ์™€ ์˜ˆ์ œ๋ฅผ ์ฐธ๊ณ ํ•ด์„œ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๋„๋ก ๋ถ€์—ฐ ์„ค๋ช…์„ ๋ง๋ถ™์˜€๋‹ค.

 

๊ณต์‹๋ฌธ์„œ์— ๊ธฐ์กด ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ๋„ ์ž์„ธํ•˜๊ฒŒ ๋‚˜์™€์žˆ์œผ๋‹ˆ ์ฐธ๊ณ ํ•˜์ž.

 

 

useTransition


useTransition์€ ์ฃผ๋กœ ๋ฌด๊ฑฐ์šด ์ž‘์—…์˜ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„๋กœ ์ง€์ •ํ•˜์—ฌ UI ๋ธ”๋กœํ‚น์„ ๋ฐฉ์ง€ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค. React 18 ๋ฒ„์ „๊นŒ์ง€ startTransition ์ฝœ๋ฐฑ์€ ํ•ญ์ƒ ๋™๊ธฐ์ ์ด์–ด์•ผ ํ•˜๋Š” ์ œ์•ฝ์ด ์žˆ์—ˆ๋‹ค. ๋•Œ๋ฌธ์— ์ฝœ๋ฐฑ ์•ˆ์—์„œ ๋น„๋™๊ธฐ ํ˜ธ์ถœ ๊ฐ™์€ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์—†์—ˆ๋‹ค.

 

React 19๋ถ€ํ„ด startTransition ์ฝœ๋ฐฑ ์•ˆ์—์„œ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋„ ๊ฐ€๋Šฅํ•ด์กŒ๋‹ค. ๋•๋ถ„์— ๋ณ„๋„ ์ƒํƒœ๋ฅผ ์ •์˜ํ•˜์ง€ ์•Š๊ณ ๋„ useTransition ํ›…์ด ๋ฐ˜ํ™˜ํ•˜๋Š” isPending์„ ์ด์šฉํ•ด ๋กœ๋”ฉ ์ƒํƒœ๊นŒ์ง€ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋๋‹ค.

// ์ฝ”๋“œ ์ถœ์ฒ˜: ๋ฆฌ์•กํŠธ ๊ณต์‹ ๋ฌธ์„œ
function UpdateName({}) {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);
  const [isPending, startTransition] = useTransition();

  const handleSubmit = () => {
    startTransition(async () => {
      const error = await updateName(name);
      // ...
    });
  };

  return (
    <div>
      <input value={name} onChange={(event) => setName(event.target.value)} />
      {/* updateName ๋น„๋™๊ธฐ ํ˜ธ์ถœ์ด ์‹œ์ž‘๋˜๋ฉด isPending = true */}
      <button onClick={handleSubmit} disabled={isPending}>
        Update
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

 

 

useActionState


useFormState ํ›…์€ deprecated ๋๋‹ค.

 

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

useActionState(action, initialState, permalink?)

 

// ํผ ์ œ์ถœ ํ˜น์€ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ ํ˜ธ์ถœํ•  ํ•จ์ˆ˜(์•ก์…˜)
async function increment(previousState, formData) {
  return previousState + 1;
}

function StatefulForm({}) {
  // state: ์ฒซ ๋ Œ๋”๋ง์—์„  initialState, ์•ก์…˜ ์‹คํ–‰ ํ›„๋ถ€ํ„ด ์•ก์…˜(increment ํ•จ์ˆ˜) ๋ฐ˜ํ™˜๊ฐ’
  // formAction: form.action ํ˜น์€ button.formAction์— ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ž˜ํ•‘๋œ ์•ก์…˜
  const [state, formAction] = useActionState(increment, 0);
  return (
    <form>
      {state}
      <button formAction={formAction}>Increment</button>
    </form>
  );
}

 

์•„๋ž˜ ์˜ˆ์‹œ์—์„œ ํผ์„ ์ œ์ถœํ•˜๋ฉด useActionState ํ›… ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ ์ „๋‹ฌํ•œ ํ•จ์ˆ˜(์•ก์…˜)๊ฐ€ ์‹คํ–‰๋˜๊ณ , ํ•ด๋‹น ํ•จ์ˆ˜์˜ 2๋ฒˆ์งธ ์ธ์ž๋ฅผ ํ†ตํ•ด FormData ๊ฐ์ฒด๊ฐ€ ์ „๋‹ฌ๋œ๋‹ค. ์ด๋•Œ ์•ก์…˜์ด ์ฒ˜๋ฆฌ ์ค‘์ž„์„ ์•Œ๋ฆฌ๋Š” isPending ์ƒํƒœ๋Š” true๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค.

function ChangeName({ name, setName }) {
  // ์ดˆ๊ธฐ ์ƒํƒœ๋ฅผ null๋กœ ์„ค์ •ํ–ˆ์œผ๋ฏ€๋กœ error์˜ ์ดˆ๊ธฐ๊ฐ’ ์—ญ์‹œ null
  const [error, submitAction, isPending] = useActionState(
    // ๋น„๋™๊ธฐ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์•ก์…˜ (useActionState ํ›… ์ฒซ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ)
    async (previousState, formData) => {
      const error = await updateName(formData.get("name"));
      if (error) return error; // ์š”์ฒญ ์‹คํŒจ ์‹œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฐ˜ํ™˜

      // ์„ฑ๊ณต ์ฒ˜๋ฆฌ
      redirect("/path");
      return null;
    },
    null, // ์ดˆ๊ธฐ ์ƒํƒœ (useActionState ํ›… ๋‘๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ)
  );

  return (
    // useActionState ํ›…์ด ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ž˜ํ•‘๋œ ์•ก์…˜์„ action ์†์„ฑ์— ํ• ๋‹น
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>
        Update
      </button>
      {error && <p>{error}</p>}
    </form>
  );
}

 

์•ก์…˜(ํ•จ์ˆ˜)์€ actions/ ๊ฐ™์€ ํด๋”๋ฅผ ๋งŒ๋“ค์–ด์„œ ๋ณ„๋„๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

 

ํ•œํŽธ ๋ฆฌ์•กํŠธ <form> ์ปดํฌ๋„ŒํŠธ์˜ action ์†์„ฑ์€ URL ํ˜น์€ ํ•จ์ˆ˜(์•ก์…˜)๋ฅผ ๋ชจ๋‘ ์ง€์›ํ•œ๋‹ค. URL์„ ์ „๋‹ฌํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ณธ ํผ ์ œ์ถœ ๋™์ž‘์— ๋”ฐ๋ผ ํ•ด๋‹น URL๋กœ ์š”์ฒญ์ด ์ „์†ก๋œ๋‹ค.

 

ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ–ˆ์„ ๋• ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ณธ ํผ ์ œ์ถœ ๋™์ž‘์„ ์ƒ๋žตํ•˜๊ณ  ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค. ์ด๋•Œ ํ•จ์ˆ˜๋Š” FormData ๊ฐ์ฒด๋ฅผ ์ฒซ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๊ณ , HTTP ๋ฉ”์„œ๋“œ๋Š” method ํ”„๋กœํผํ‹ฐ์™€ ๊ด€๊ณ„์—†์ด ํ•ญ์ƒ POST ๋ฉ”์„œ๋“œ๋กœ ์ฒ˜๋ฆฌ๋œ๋‹ค. ์ฐธ๊ณ ๋กœ <form> ํƒœ๊ทธ์˜ method ์†์„ฑ ๊ธฐ๋ณธ๊ฐ’์€ get์ด๋‹ค(MDN).

// CodeSandbox: https://codesandbox.io/p/sandbox/6ndp8g
export default function Search() {
  function search(formData) {
    const query = formData.get("query");
    // input์— ๊ฐ’ ์ž…๋ ฅ ํ›„ Search ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์ž…๋ ฅํ•œ ๊ฐ’ ์ฝ˜์†” ์ถœ๋ ฅ
    console.log(`You searched for '${query}'`);
  }
  return (
    <form action={search}>
      <input name="query" />
      <button type="submit">Search</button>
    </form>
  );
}

 

useActionState ํ›…์ด ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ž˜ํ•‘ ๋œ ์•ก์…˜์„ action ์†์„ฑ์— ํ• ๋‹นํ–ˆ์„ ๋•Œ์™€, ์ผ๋ฐ˜ ์•ก์…˜์„ ํ• ๋‹นํ–ˆ์„ ๋•Œ ๊ฐ ์•ก์…˜ ํ•จ์ˆ˜์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋‹ค๋ฅธ ์ ์— ์ฃผ์˜ํ•˜์ž.

 

 

useFormStatus


useFormStatus๋Š” ๋งˆ์ง€๋ง‰ ํผ ์ œ์ถœ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์กฐํšŒํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ํ›…์ด๋‹ค. ์ด ํ›…์„ ์‚ฌ์šฉํ•˜๋ฉด Context API๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ ๋„ ๋ถ€๋ชจ <form>์˜ ์ƒํƒœ๋ฅผ ์ง์ ‘ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ ํผ ์ •๋ณด๋ฅผ ์ฐธ์กฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณ„๋„๋กœ props drilling์„ ํ•  ํ•„์š”๊ฐ€ ์—†์–ด์ง„๋‹ค.

// UsernameForm.js
import { useFormStatus } from "react-dom";

export default function UsernameForm() {
  // pending: ํผ ์ œ์ถœ์ค‘ ์—ฌ๋ถ€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ถˆ๋ฆฌ์–ธ ๊ฐ’. ์ œ์ถœ์ค‘์ผ ๋• true
  // data: ์ƒ์œ„ <form>์ด ์ œ์ถœํ•˜๊ณ  ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์€ FormData.
  // ์ œ์ถœ์ค‘์ธ ํผ์ด ์—†๊ฑฐ๋‚˜ ์ƒ์œ„์— <form>์ด ์—†์œผ๋ฉด null
  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>
  );
}
// App.js
// CodeSandbox: https://codesandbox.io/p/sandbox/4zcn6k
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); // Promise๋ฅผ ์ด์šฉํ•ด 2์ดˆ๊ฐ„ wait
        ref.current.reset(); // ํผ ์ดˆ๊ธฐํ™”
      }}
    >
      <UsernameForm />
    </form>
  );
}

 

useFormStatus ํ›…์€ ์•„๋ž˜ status ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

  • pending: ์ƒ์œ„ ํผ์ด ์ œ์ถœ ์ค‘์ธ์ง€ ๋‚˜ํƒ€๋‚ด๋Š” ๋ถˆ๋ฆฌ์–ธ ๊ฐ’. ์ œ์ถœ ์ค‘์ผ ๋•Œ true.
  • data: ์ œ์ถœ ์ค‘์ธ ์ƒ์œ„ ํผ์˜ FormData ๊ฐ์ฒด. ์ œ์ถœ ์ค‘์ธ ํผ์ด ์—†๊ฑฐ๋‚˜ ์ƒ์œ„ ํผ์ด ์—†์œผ๋ฉด null.
  • method: ์ƒ์œ„ ํผ์˜ HTTP ๋ฉ”์„œ๋“œ('get' ํ˜น์€ 'post').
  • action: ์ƒ์œ„ ํผ action ์†์„ฑ์— ์ „๋‹ฌํ•œ ํ•จ์ˆ˜์˜ ๋ ˆํผ๋Ÿฐ์Šค. URI ๋ฌธ์ž์—ด์ด๊ฑฐ๋‚˜ ์ƒ์œ„ ํผ์ด ์—†์œผ๋ฉด null.

 

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

function Form() {
  // <form>๊ณผ ๋™์ผํ•œ ์ปดํฌ๋„ŒํŠธ์—์„œ useFormStatus ํ›…์„ ์‚ฌ์šฉํ–ˆ์œผ๋ฏ€๋กœ ์ƒํƒœ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๋Š”๋‹ค.
  // ๋”ฐ๋ผ์„œ pending ๊ฐ’์€ ํ•ญ์ƒ false
  const { pending } = useFormStatus();
  return <form action={submit}></form>;
}

 

 

useOptimistic


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

 

useOptimistic(state, [updateFn])

state: ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๊ฐ€ ์ด๋ฃจ์–ด์ง€๊ธฐ ์ „ ์ดˆ๊ธฐ ์ƒํƒœ
updateFn(currentState, optimisticValue): ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜. state๊ฐ€ ๋‹จ์ˆœํ•œ ๊ฐ’์ผ ๊ฒฝ์šฐ updateFn์„ ๋”ฐ๋กœ ์ •์˜ํ•˜์ง€ ์•Š์•„๋„ ๋ฌด๋ฐฉ(useState์˜ ์ƒํƒœ ์„ค์ • ํ•จ์ˆ˜์™€ ์œ ์‚ฌ)

 

์•„๋ž˜ ์˜ˆ์ œ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ํผ์„ ์ œ์ถœํ•˜์—ฌ updateName ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๋™์•ˆ optimisticName ๊ฐ’์„ ์ฆ‰์‹œ ๋ Œ๋”๋งํ•œ๋‹ค. ์š”์ฒญ์ด ์™„๋ฃŒ๋˜๊ฑฐ๋‚˜ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด React๋Š” ์ž๋™์œผ๋กœ currentName ๊ฐ’์œผ๋กœ ์ƒํƒœ๋ฅผ ์ „ํ™˜ํ•œ๋‹ค.

function ChangeName({ currentName, onUpdateName }) {
  const [optimisticName, setOptimisticName] = useOptimistic(currentName);

  const submitAction = async (formData) => {
    const newName = formData.get("name");
    setOptimisticName(newName); // ์ž…๋ ฅํ•œ ๊ฐ’์œผ๋กœ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ํ›„ ์ฆ‰์‹œ ๋ Œ๋”๋ง
    const updatedName = await updateName(newName); // ๋น„๋™๊ธฐ ํ˜ธ์ถœ(์ด๋ผ๊ณ  ๊ฐ€์ •)
    onUpdateName(updatedName); // ๋น„๋™๊ธฐ ํ˜ธ์ถœ ์„ฑ๊ณต ํ›„ ์•ก์…˜ ์ˆ˜ํ–‰
  };

  return (
    <form action={submitAction}>
      <p>Your name is: {optimisticName}</p>
      <p>
        <label>Change Name:</label>
        <input
          type="text"
          name="name"
          // ์„œ๋ฒ„ ์‘๋‹ต ๋Œ€๊ธฐ ์ค‘์ผ ๋• ์ž…๋ ฅ ๋ฐฉ์ง€
          disabled={currentName !== optimisticName}
        />
      </p>
    </form>
  );
}

 

์•„๋ž˜๋Š” ๊ณต์‹ ๋ฌธ์„œ์—์„œ ์ œ๊ณตํ•œ ์ฝ”๋“œ๋ฅผ ์ผ๋ถ€ ์ˆ˜์ •ํ•œ ์˜ˆ์ œ. useOptimistic ํ›…์„ ํ†ตํ•œ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋Š” ์•„๋ž˜ ๋‹จ๊ณ„๋ฅผ ํ†ตํ•ด ์ง„ํ–‰๋œ๋‹ค.

 

โ‘ Input ํ•„๋“œ์— ๊ฐ’ ์ž…๋ ฅ ํ›„ Send ๋ฒ„ํŠผ ํด๋ฆญ โ‘ก์ž…๋ ฅํ•œ ๊ฐ’์œผ๋กœ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ํ›„ ๋ Œ๋”๋ง โ‘ขsendMessage ์š”์ฒญ ์„ฑ๊ณต ํ›„ <App /> ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์—์„œ messages ์ƒํƒœ ์—…๋ฐ์ดํŠธ โ‘ฃ<Thread /> ์ž์‹ ์ปดํฌ๋„ŒํŠธ์—์„œ ์—…๋ฐ์ดํŠธ๋œ messages ์ƒํƒœ๋ฅผ ๋ฐ›์•„ ๋ฆฌ๋ Œ๋”๋ง ๋˜๊ณ , messages ์ƒํƒœ๋Š” useOptimistic ํ›…์œผ๋กœ ์ „๋‹ฌ โ‘คํ›… ๋‚ด๋ถ€์—์„œ ๊ธฐ์กด ๋‚™๊ด€์  ์ƒํƒœ(optimisticMessages)๋ฅผ ์—…๋ฐ์ดํŠธ๋œ messages ๊ฐ’์œผ๋กœ ๊ต์ฒด(๋™๊ธฐํ™”) → โ‘ฅ๋ฆฌ๋ Œ๋”๋ง

// CodeSandbox: https://codesandbox.io/p/sandbox/react-dev-forked-hnxdnv

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

const createMessage = (text, sending = false) => ({ text, sending });

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, createMessage(newMessage, true)],
  );

  // ์ฒซ ๋ Œ๋”๋ง: true
  // ์š”์ฒญ(sendMessage) ์ง„ํ–‰ ์ค‘: false
  // ์š”์ฒญ(sendMessage) ์™„๋ฃŒ ํ›„: false
  // ๋‚™๊ด€์  ์ƒํƒœ๋ฅผ ์ƒˆ๋กœ์šด messages ๊ฐ’์œผ๋กœ ๊ต์ฒด ํ›„: true
  console.log(messages === optimisticMessages);

  return (
    <>
      {optimisticMessages.map(({ text, sending }, index) => (
        <div key={index}>
          {text}
          {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([createMessage("Hello There!")]);

  async function sendMessage(formData) {
    // 1์ดˆ ๋Œ€๊ธฐ ํ›„ ์ธ์ž๋กœ ์ „๋‹ฌ๋ฐ›์€ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š” Fake API
    const sentMessage = await deliverMessage(formData.get("message"));
    // ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์ฃผ์„ ์ฒ˜๋ฆฌํ•˜๋ฉด ๋‚™๊ด€์  ์ƒํƒœ๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค๊ฐ€ ๋‹ค์‹œ ์›๋ž˜ messages ์ƒํƒœ๋กœ ๋กค๋ฐฑ๋œ๋‹ค
    setMessages((messages) => [...messages, createMessage(sentMessage)]);
  }

  return <Thread messages={messages} sendMessage={sendMessage} />;
}

 

 

use


use๋Š” Promise, Context ๊ฐ™์€ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฝ์„ ์ˆ˜ ์žˆ๋Š” API๋‹ค. ํ•ญ์ƒ ์ปดํฌ๋„ŒํŠธ์˜ ์ตœ์ƒ์œ„์—์„œ ์‚ฌ์šฉํ•ด์•ผ ํ–ˆ๋˜ ๋ฆฌ์•กํŠธ Hook๊ณผ ๋‹ฌ๋ฆฌ use๋Š” if ๊ฐ™์€ ์กฐ๊ฑด๋ฌธ์ด๋‚˜ ๋ฐ˜๋ณต๋ฌธ ์•ˆ์—์„œ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, Suspense/Error Boundary์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

use๋Š” ์ปดํฌ๋„ŒํŠธ, ํ›… ์•ˆ์—์„œ๋งŒ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๊ณ  try-catch ๋ธ”๋ก์—์„  ํ˜ธ์ถœํ•  ์ˆ˜ ์—†๋Š” ์ œ์•ฝ์ด ์žˆ๋‹ค. ๋Œ€์‹  ErrorBoundary๋กœ ๊ฐ์‹ธ๊ฑฐ๋‚˜ promise.catch ํ›„์† ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

import { use } from "react";

function Comments({ commentsPromise }) {
  // ํ”„๋กœ๋ฏธ์Šค๊ฐ€ resolve๋  ๋•Œ๊นŒ์ง€ ์ปดํฌ๋„ŒํŠธ ์ง€์—ฐ
  const comments = use(commentsPromise);
  return comments.map((comment) => <p key={comment.id}>{comment}</p>);
}

function Page({ commentsPromise }) {
  // use๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ Suspense๋กœ ๊ฐ์‹ธ๋ฉด ํ”„๋กœ๋ฏธ์Šค๊ฐ€ resolve๋  ๋•Œ๊นŒ์ง€ fallback ํ‘œ์‹œ
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  );
}

 

Early Return์€ ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋ฉด ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•˜์—ฌ ์ดํ›„ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜์ง€ ์•Š๋Š” ํŒจํ„ด์ด๋‹ค. ๋ฆฌ์•กํŠธ Hook์€ ์กฐ๊ฑด๋ถ€ ํ˜ธ์ถœ์ด ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ทœ์น™ ๋•Œ๋ฌธ์— ์•„๋ž˜์™€ ๊ฐ™์€ Early Return ๋’ค์— ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค.

import ThemeContext from "./ThemeContext";

function Heading({ children }) {
  if (!children) return null; // Early return

  const theme = useContext(ThemeContext); // โŒ ์˜ค๋ฅ˜!

  // ...
}

 

๋ฐ˜๋ฉด, use ํ›…์€ ์กฐ๊ฑด๋ถ€ ํ˜ธ์ถœ์ด ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ ์•„๋ž˜์ฒ˜๋Ÿผ Early Return ๋’ค์—์„œ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

import ThemeContext from "./ThemeContext";

function Heading({ children }) {
  if (!children) return null; // Early return

  const theme = use(ThemeContext); // โœ… OK

  // ...
}

 

์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ํ”„๋กœ๋ฏธ์Šค(Promise)๋ฅผ ์ƒ์„ฑํ•œ ํ›„ Prop์œผ๋กœ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌํ•˜๋ฉด, ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ŠคํŠธ๋ฆฌ๋ฐ ํ•  ์ˆ˜ ์žˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„  ๋ Œ๋”๋ง ํ•  ๋•Œ๋งˆ๋‹ค ํ”„๋กœ๋ฏธ์Šค๊ฐ€ ๋‹ค์‹œ ์ƒ์„ฑ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ํ”„๋กœ๋ฏธ์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ์‹์„ ๋” ๊ถŒ์žฅํ•˜๊ณ  ์žˆ๋‹ค(์ฐธ๊ณ ).

์ŠคํŠธ๋ฆฌ๋ฐ: ๋ฐ์ดํ„ฐ๋ฅผ ์กฐ๊ฐ(์ฒญํฌ) ๋‹จ์œ„๋กœ ์ „์†กํ•ด์„œ, ์ „์ฒด ๋ฐ์ดํ„ฐ๊ฐ€ ์™„์ „ํžˆ ์ค€๋น„๋˜์ง€ ์•Š์•„๋„ ์ ์ง„์ ์œผ๋กœ ์†Œ๋น„ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ์‹.

 

์•„๋ž˜ ์˜ˆ์‹œ์—์„œ <Message /> ์ปดํฌ๋„ŒํŠธ๋ฅผ Suspense๊ฐ€ ๊ฐ์‹ธ๊ณ  ์žˆ์œผ๋ฏ€๋กœ ํ”„๋กœ๋ฏธ์Šค๊ฐ€ resolve ๋  ๋•Œ๊นŒ์ง€ fallback์„ ํ‘œ์‹œํ•œ๋‹ค. fetchMessage ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์„œ๋ฒ„ ์š”์ฒญ์ด ์‹œ์ž‘๋˜๊ณ , ๋ฐ˜ํ™˜๋œ ํ”„๋กœ๋ฏธ์Šค๋Š” ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌ๋œ๋‹ค. ์„œ๋ฒ„๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ฒญํฌ ๋‹จ์œ„๋กœ ์ŠคํŠธ๋ฆฌ๋ฐ ํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‘ ์ˆ˜์‹ ํ•ด์„œ ํ”„๋กœ๋ฏธ์Šค๊ฐ€ resolve ๋˜๋ฉด, use ํ›…์„ ํ†ตํ•ด ๊ฐ’์„ ์ฐธ์กฐํ•˜์—ฌ ๋ Œ๋”๋ง ํ•œ๋‹ค(์ฐธ๊ณ ).

// App.js ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ
import { fetchMessage } from './lib.js';
import { Message } from './message.js';

export default function App() {
  const messagePromise = fetchMessage();
  return (
    <Suspense fallback={<p>waiting for message...</p>}>
      <Message messagePromise={messagePromise} />
    </Suspense>
  );
}

// message.js ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ
'use client';

import { use } from 'react';

export function Message({ messagePromise }) {
  const messageContent = use(messagePromise);
  return <p>Here is the message: {messageContent}</p>;
}

 

์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ํ”„๋กœ๋ฏธ์Šค๋ฅผ ์ „๋‹ฌํ•  ๋•Œ, ํ”„๋กœ๋ฏธ์Šค๊ฐ€ resolve ํ•  ๊ฐ’์€ ๋ฐ˜๋“œ์‹œ ์ง๋ ฌํ™”ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ํ•จ์ˆ˜๋Š” ์ง๋ ฌํ™”ํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ํ”„๋กœ๋ฏธ์Šค์˜ resolve ๊ฐ’์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค.

 

ํ•œํŽธ, ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ await๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ”„๋กœ๋ฏธ์Šค๋ฅผ resolve ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์ง€๋งŒ, await๋ฌธ์„ ์™„๋ฃŒํ•  ๋•Œ๊นŒ์ง€ ๋ Œ๋”๋ง์ด ์ฐจ๋‹จ๋˜๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค.

 

 

Server Components


๏ฟญ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ์„œ๋ฒ„์—์„œ ํ•œ ๋ฒˆ๋งŒ ๋ Œ๋”๋ง ๋œ๋‹ค(๋ฆฌ๋ Œ๋”๋ง ์—†์Œ).
๏ฟญ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋Š” ์„œ๋ฒ„/ํด๋ผ์ด์–ธํŠธ ์–‘์ชฝ์—์„œ ๋ Œ๋”๋ง ๋œ๋‹ค.
๏ฟญ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋Š” JS ๋ฒˆ๋“ค์— ํฌํ•จ๋œ๋‹ค.
๏ฟญ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„  useState, useEffect ๊ฐ™์€ ํด๋ผ์ด์–ธํŠธ ํ›…์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค.
๏ฟญ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์— async๋ฅผ ๋ถ™์—ฌ์„œ ๋น„๋™๊ธฐ๋กœ ๋™์ž‘ํ•˜๋„๋ก ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

 

์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ํด๋ผ์ด์–ธํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด๋‚˜ SSR ์„œ๋ฒ„์™€ ๋ถ„๋ฆฌ๋œ ํ™˜๊ฒฝ์—์„œ ๋ฒˆ๋“ค๋ง ์ „์— ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฏธ๋ฆฌ ๋ Œ๋”๋ง ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ด๋‹ค. ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” CI ์„œ๋ฒ„์—์„œ ๋นŒ๋“œ ํƒ€์ž„์— ํ•œ ๋ฒˆ ์‹คํ–‰๋˜๊ฑฐ๋‚˜, ์›น ์„œ๋ฒ„๋ฅผ ํ†ตํ•ด ๊ฐ ์š”์ฒญ๋งˆ๋‹ค ์‹คํ–‰๋  ์ˆ˜ ์žˆ๋‹ค.

 

React 19์—์„œ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ธฐ๋ณธ๊ฐ’์ด๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ฌด๋Ÿฐ ์ง€์‹œ๋ฌธ์ด ์—†์œผ๋ฉด ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋œ๋‹ค. ํŒŒ์ผ ์ตœ์ƒ๋‹จ์— 'use client' ์ง€์‹œ๋ฌธ์„ ์ถ”๊ฐ€ํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋œ๋‹ค(ํด๋ผ์ด์–ธํŠธ์—์„œ ์ฝ”๋“œ ์‹คํ–‰). 'use server' ์ง€์‹œ๋ฌธ๋Š” ํด๋ผ์ด์–ธํŠธ์—์„œ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ์„œ๋ฒ„ ํ•จ์ˆ˜๋ฅผ ์ง€์ •ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.

 

Server Components without a Server

๋งŒ์•ฝ ํด๋ผ์ด์–ธํŠธ์— ์ œ๊ณตํ•  ์ฝ˜ํ…์ธ ๊ฐ€ ์ •์ ์ด๊ฑฐ๋‚˜ ํŒŒ์ผ ์‹œ์Šคํ…œ์—์„œ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๋‹ค๋ฉด ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ด์šฉํ•ด ๋นŒ๋“œ ํƒ€์ž„์— ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด ๊ฒฝ์šฐ ๋ณ„๋„์˜ ์›น ์„œ๋ฒ„๋Š” ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค.

 

๋งŒ์•ฝ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋ผ๋ฉด useEffect ์•ˆ์—์„œ ์ •์  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•œ API ํ˜ธ์ถœ์ด ํ•„์š”ํ•˜๊ณ , marked ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ถ”๊ฐ€์ ์œผ๋กœ ๋‹ค์šด๋กœ๋“œํ•œ ๋’ค ์ฝ˜ํ…์ธ ๋ฅผ ํŒŒ์‹ฑ ํ•˜๋Š” ์ž‘์—…์ด ํ•„์š”ํ•˜๋‹ค.

// bundle.js
import marked from "marked"; // 35.9K (11.2K gzipped)
import sanitizeHtml from "sanitize-html"; // 206K (63.3K gzipped)

function Page({ page }) {
  const [content, setContent] = useState("");
  // ์ฒซ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง ์ดํ›„์— ๋ฐ์ดํ„ฐ ๋กœ๋“œ
  useEffect(() => {
    fetch(`/api/content/${page}`).then((data) => {
      setContent(data.content);
    });
  }, [page]);

  return <div>{sanitizeHtml(marked(content))}</div>;
}

 

์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋Ÿฌํ•œ ์ž‘์—…์„ ๋นŒ๋“œ ํƒ€์ž„์— ์ฒ˜๋ฆฌํ•˜์—ฌ ์ •์  ์ฝ˜ํ…์ธ ๋ฅผ ํ•œ ๋ฒˆ๋งŒ ๋ Œ๋”๋ง ํ•˜๊ณ  ํด๋ผ์ด์–ธํŠธ๋กœ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ Œ๋”๋ง ๊ฒฐ๊ณผ๋Š” SSR์„ ํ†ตํ•ด HTML๋กœ ๋ณ€ํ™˜ํ•ด์„œ CDN์— ์—…๋กœ๋“œํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

import marked from 'marked'; // ๋ฒˆ๋“ค์— ํฌํ•จ ์•ˆ๋จ
import sanitizeHtml from 'sanitize-html'; // ๋ฒˆ๋“ค์— ํฌํ•จ ์•ˆ๋จ

async function Page({page}) {
  // ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๋นŒ๋“œ๋  ๋•Œ ๋ Œ๋”๋ง ๋„์ค‘์— ๋ฐ์ดํ„ฐ ๋กœ๋“œ
  const content = await file.readFile(`${page}.md`);

  return <div>{sanitizeHtml(marked(content))}</div>;
}

 

์œ„์ฒ˜๋Ÿผ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ Œ๋”๋ง์— ํ•„์š”ํ•œ ๋ฌด๊ฑฐ์šด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋ฒˆ๋“ค์— ํฌํ•จ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ฒˆ๋“ค ํฌ๊ธฐ๊ฐ€ ์ค„์–ด๋“ค์–ด ๋กœ๋“œ ์‹œ๊ฐ„์„ ๋‹จ์ถ•ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ดˆ๊ธฐ HTML์„ ์ƒ์„ฑํ•˜๋ ค๋ฉด ์—ฌ์ „ํžˆ SSR์— ์˜์กดํ•ด์•ผ ํ•œ๋‹ค. ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ์„œ๋ฒ„์—์„œ ์‹คํ–‰ํ•˜์—ฌ ๋ Œ๋”๋ง ํ•œ ๋ฆฌ์•กํŠธ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ JSON ํ˜•์‹์œผ๋กœ ์ง๋ ฌํ™”ํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ๋กœ ์ „๋‹ฌํ•˜๋ฉฐ, ํด๋ผ์ด์–ธํŠธ๋Š” ์ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„ HTML์„ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ ์—…๋ฐ์ดํŠธํ•œ๋‹ค.

 

Server Components with a Server

์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ์›น ์„œ๋ฒ„์—์„œ ์‹คํ–‰๋˜์–ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฐ™์€ ์„œ๋ฒ„ ์ธก ๋ฆฌ์†Œ์Šค์— ์ง์ ‘ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ๋ณ„๋„์˜ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋งŒ๋“ค์ง€ ์•Š์•„๋„ ๋œ๋‹ค. ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋ง ๋œ JSX(UI)์™€ ๋ฐ์ดํ„ฐ๋ฅผ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์˜ Props๋กœ ์ „๋‹ฌํ•˜๋ฉด, ํด๋ผ์ด์–ธํŠธ๋Š” ์ถ”๊ฐ€ ์ž‘์—… ์—†์ด ํ™”๋ฉด์— ๋ Œ๋”๋ง ํ•  ์ˆ˜ ์žˆ๋‹ค.

import db from "./database";

async function Note({ id }) {
  // ๋ Œ๋”๋ง ๋„์ค‘์— ๋ฐ์ดํ„ฐ ๋กœ๋“œ
  const note = await db.notes.get(id);
  return (
    <div>
      <Author id={note.authorId} />
      <p>{note}</p>
    </div>
  );
}

async function Author({ id }) {
  // <Note /> ์ดํ›„์— ๋ฐ์ดํ„ฐ ๋กœ๋“œ
  // ๋ฐ์ดํ„ฐ๊ฐ€ ๊ฐ™์€ ์œ„์น˜(co-located)์— ์žˆ๋‹ค๋ฉด ๋” ๋น ๋ฅด๊ฒŒ ๋กœ๋“œ
  const author = await db.authors.get(id);
  return <span>By: {author.name}</span>;
}

 

์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ๋ Œ๋”๋ง ๋„์ค‘ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ , ๋™์ ์ธ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์™€ ๊ฒฐํ•ฉํ•˜์—ฌ ์ตœ์ ํ™”๋œ ๋ฒˆ๋“ค์„ ์ƒ์„ฑํ•œ๋‹ค. ์ด ๊ณผ์ •์—์„œ SSR์„ ์‚ฌ์šฉํ•ด ํŽ˜์ด์ง€์˜ ์ดˆ๊ธฐ HTML์„ ์ƒ์„ฑํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

 

์ด์ฒ˜๋Ÿผ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ์„œ๋ฒ„์—์„œ ์‹คํ–‰๋ผ์„œ ๋ Œ๋”๋ง ํ•œ ๊ฒฐ๊ณผ๋งŒ ํด๋ผ์ด์–ธํŠธ๋กœ ์ „๋‹ฌํ•˜๋ฏ€๋กœ, ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ๊ตฌ์กฐ๋‚˜ ์ฝ”๋“œ๋Š” ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜์ง€ ์•Š๋Š”๋‹ค.

 

Adding interactivity to Server Components

์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹คํ–‰๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ƒํƒœ ๊ด€๋ฆฌ๋‚˜ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ํฌํ•จํ•  ์ˆ˜ ์—†๋‹ค. ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ์„ ์ฒ˜๋ฆฌํ•˜๋ ค๋ฉด, ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ƒํ˜ธ์ž‘์šฉ ์ฝ”๋“œ๋ฅผ ํฌํ•จํ•œ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค.

 

์•„๋ž˜ ์˜ˆ์‹œ์—์„œ Notes๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์ด๋ฏ€๋กœ ์„œ๋ฒ„์—์„œ๋งŒ ์‹คํ–‰๋˜๊ณ  ๋ฒˆ๋“ค์—๋Š” ํฌํ•จ๋˜์ง€ ์•Š๋Š”๋‹ค. ๋ฐ˜๋ฉด, ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹คํ–‰์‹œ์ผœ์•ผ ํ•˜๋ฏ€๋กœ, ๋ฒˆ๋“ค๋Ÿฌ๋Š” Expandable ์ปดํฌ๋„ŒํŠธ๋ฅผ bundle.js ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฒˆ๋“ค ํŒŒ์ผ์— ํฌํ•จ์‹œํ‚จ๋‹ค.

// Server Component
import Expandable from "./Expandable";

async function Notes() {
  const notes = await db.notes.getAll();

  return (
    <div>
      {notes.map((note) => (
        <Expandable key={note.id}>
          <p>{note.content}</p>
        </Expandable>
      ))}
    </div>
  );
}
// Client Component
"use client";

export default function Expandable({ children }) {
  const [expanded, setExpanded] = useState(false);
  const toggle = () => setExpanded((prev) => !prev);

  return (
    <div>
      <button onClick={toggle}>Toggle</button>
      {expanded && children}
    </div>
  );
}

 

๋ธŒ๋ผ์šฐ์ €๋Š” ์„œ๋ฒ„์—์„œ ์ƒ์„ฑ๋œ ์ถœ๋ ฅ์„ ๋ฐ›์•„ ํ™”๋ฉด์— ๋ Œ๋”๋ง ํ•œ ํ›„, ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ๋ฒˆ๋“ค์„ ๋‹ค์šด๋ฐ›์•„ ์‹คํ–‰ํ•œ๋‹ค. ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ™œ์„ฑํ™”๋˜๋ฉด, Expandable ์ปดํฌ๋„ŒํŠธ๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ์ถœ๋ ฅ์„ Prop์œผ๋กœ ์ „๋‹ฌ๋ฐ›์•„ ์ƒํ˜ธ์ž‘์šฉ์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์ƒํƒœ์— ๋”ฐ๋ผ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•œ๋‹ค.

<head>
  <!-- ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ๋ฒˆ๋“ค -->
  <script src="bundle.js"></script>
</head>
<body>
  <!-- ์„œ๋ฒ„์—์„œ ์ƒ์„ฑ๋œ ์ดˆ๊ธฐ ์ถœ๋ ฅ -->
  <div>
    <div>
      <button>Toggle</button>
      <!-- ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ Œ๋”๋งํ•  children ์˜์—ญ -->
    </div>
    <div>
      <button>Toggle</button>
      <!-- ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ Œ๋”๋งํ•  children ์˜์—ญ -->
    </div>
  </div>
</body>

 

 

Server Actions


์„œ๋ฒ„ ํ•จ์ˆ˜๋ฅผ action ์†์„ฑ์— ์ „๋‹ฌํ•˜๊ฑฐ๋‚˜ action ๋‚ด๋ถ€์—์„œ ํ˜ธ์ถœํ•˜๋ฉด ์„œ๋ฒ„ ์•ก์…˜์ด๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค.

 

์„œ๋ฒ„ ์•ก์…˜์€ ํด๋ผ์ด์–ธํŠธ์—์„œ ์„œ๋ฒ„์— ์žˆ๋Š” ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์œผ๋กœ, ํด๋ผ์ด์–ธํŠธ๋Š” ์„œ๋ฒ„ ์•ก์…˜์˜ ๊ฒฐ๊ณผ๋งŒ ๋ฐ›์•„์„œ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋‹ค. ์„œ๋ฒ„ ์•ก์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„์˜ ์ž‘์—…์„ ๋ช…ํ™•ํžˆ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๊ณ , ๋ณด์•ˆ์ด ์ค‘์š”ํ•œ ๋ฐ์ดํ„ฐ๋‚˜ ๋กœ์ง์„ ์„œ๋ฒ„์—์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.

 

์„œ๋ฒ„ ํ•จ์ˆ˜๋Š” 'use server' ์ง€์‹œ์–ด๋กœ ์ •์˜ํ•œ๋‹ค. ์„œ๋ฒ„ ํ•จ์ˆ˜๋Š” ์„œ๋ฒ„์—์„œ๋งŒ ์‹คํ–‰๋˜๋ฏ€๋กœ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„  ์ •์˜ํ•  ์ˆ˜ ์—†๋‹ค. ๋Œ€์‹  ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ •์˜ํ•œ ์„œ๋ฒ„ ํ•จ์ˆ˜๋ฅผ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ import ํ•˜๊ฑฐ๋‚˜ Prop์œผ๋กœ ๋ฐ›์•„์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

import Button from "./Button"; // Button ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ

// EmptyNote ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ
function EmptyNote() {
  async function createNote() {
    // ์„œ๋ฒ„ ํ•จ์ˆ˜ (ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ฒ„ํŠผ ํด๋ฆญ์‹œ ์„œ๋ฒ„์—์„œ createNote ํ•จ์ˆ˜ ์‹คํ–‰)
    "use server";
    await db.notes.create();
  }

  return <Button onClick={createNote} />;
}

 

ํŒŒ์ผ ์ตœ์ƒ๋‹จ์— 'use server' ์ง€์‹œ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ•ด๋‹น ํŒŒ์ผ์—์„œ ๋‚ด๋ณด๋‚ธ ๋ชจ๋“  ํ•จ์ˆ˜๊ฐ€ ์„œ๋ฒ„ ํ•จ์ˆ˜๋กœ ์ •์˜๋œ๋‹ค.

"use server";

export async function createNote() {
  await db.notes.create();
}

 

์„œ๋ฒ„ ํ•จ์ˆ˜๋Š” ํด๋ผ์ด์–ธํŠธ ์•ก์…˜(ํด๋ผ์ด์–ธํŠธ์—์„œ ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜/๋กœ์ง)๊ณผ ํ•จ๊ป˜ ๊ตฌ์„ฑํ•˜๊ฑฐ๋‚˜, <form />์˜ action ์†์„ฑ๊ณผ ํ•จ๊ป˜ ๋™์ž‘ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

"use server";

export async function updateName(name) {
  if (!name) {
    return {error: 'Name is required'};
  }
  await db.users.updateName(name);
}
"use client";

import { updateName } from "./actions";

function UpdateName() {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);

  const [isPending, startTransition] = useTransition();

  // ํด๋ผ์ด์–ธํŠธ ์•ก์…˜
  const submitAction = async () => {
    // startTransition ๋‚ด๋ถ€ ์ž‘์—…์ด ์‹œ์ž‘๋  ๋•Œ isPending=true, ์™„๋ฃŒ์‹œ isPending=false
    startTransition(async () => {
      // ์„œ๋ฒ„ ํ•จ์ˆ˜ ํ˜ธ์ถœ (updateName ํ•จ์ˆ˜๋Š” ์„œ๋ฒ„์—์„œ ์ฒ˜๋ฆฌ๋˜๊ณ  ๊ฒฐ๊ณผ๋งŒ ํด๋ผ์ด์–ธํŠธ๋กœ ๋ฐ˜ํ™˜)
      const { error } = await updateName(name);
      setError(error ?? "");
    });
  };

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

 

 

Improvements


deprecate(์‚ฌ์šฉ ์ค‘๋‹จ) ์˜ˆ์ •์ธ API๋Š” ์ถ”ํ›„ ๊ณต๊ฐœํ•  codemode(๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋„๊ตฌ)๋ฅผ ํ†ตํ•ด ์—…๋ฐ์ดํŠธ๋œ ์ฝ”๋“œ๋กœ ์ž๋™ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

ref as a prop

ref๋ฅผ prop์œผ๋กœ ๋„˜๊ธธ ์ˆ˜ ์žˆ๊ฒŒ ๋๋‹ค. forwardRef() ํ•จ์ˆ˜๋Š” deprecate ๋  ์˜ˆ์ •์ด๋‹ค.

function MyInput({ placeholder, ref }) {
  return <input placeholder={placeholder} ref={ref} />;
}

 

Diffs for hydration errors

ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š๋Š” ๋ถ€๋ถ„์„ ๋‹จ์ผ ๋ฉ”์‹œ์ง€์™€ ํ•จ๊ป˜ ํ‘œ์‹œํ•ด ์ค€๋‹ค.

 

<Context> as a provider

createContext() ํ•จ์ˆ˜๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” Context๋ฅผ Provider๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋๋‹ค. <Context.Provider> ๋ฐฉ์‹์€ deprecate ๋  ์˜ˆ์ •์ด๋‹ค.

const ThemeContext = createContext("");

function App({ children }) {
  return <ThemeContext value="dark">{children}</ThemeContext>;
}

 

Cleanup functions for refs

ref ์ฝœ๋ฐฑ์—์„œ๋„ ํด๋ฆฐ์—… ํ•จ์ˆ˜๋ฅผ ์ง€์›ํ•œ๋‹ค. ์ปดํฌ๋„ŒํŠธ๋ฅผ ์–ธ๋งˆ์šดํŠธํ•˜๋ฉด ref ์ฝœ๋ฐฑ์—์„œ ๋ฐ˜ํ™˜ํ•œ ํด๋ฆฐ์—… ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค. ์ด๋Ÿฌํ•œ ๋ฐฉ์‹์€ DOM ref, ํด๋ž˜์Šค ์ปดํฌ๋„ŒํŠธ ref, useImperativeHandle() ์—๋„ ๋ชจ๋‘ ์ ์šฉ๋œ๋‹ค.

<input
  ref={(ref) => {
    // ref ์ƒ์„ฑ์‹œ ํ˜ธ์ถœ

    // ํด๋ฆฐ์—… ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด DOM์—์„œ ์—˜๋ฆฌ๋จผํŠธ๊ฐ€ ์ œ๊ฑฐ๋  ๋•Œ ref๋ฅผ reset ํ•  ์ˆ˜ ์žˆ์Œ
    return () => {
      console.log("Ref cleanup:", ref);
    };
  }}
/>;

 

useDeferredValue initial value

useDeferredValue ๋‘ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ initialValue๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ฒŒ ๋๋‹ค. ์ปดํฌ๋„ŒํŠธ ์ดˆ๊ธฐ ๋ Œ๋”๋ง์—” initialValue๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , ์ดํ›„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ deferredValue ๊ฐ’์œผ๋กœ ๋ฆฌ๋ Œ๋”๋ง์„ ์˜ˆ์•ฝํ•œ๋‹ค.

function Search({deferredValue}) {
  // ์ดˆ๊ธฐ ๋ Œ๋”๋ง ๊ฐ’์€ '', ๊ทธ ํ›„ deferredValue ๊ฐ’์œผ๋กœ ๋ฆฌ๋ Œ๋”๋ง ์˜ˆ์•ฝ
  const value = useDeferredValue(deferredValue, '');

  return (
    <Results query={value} />
  );
}

 

Support for Document Metadata

๋ฆฌ์•กํŠธ 19๋ถ€ํ„ฐ ์ปดํฌ๋„ŒํŠธ ๋‚ด์—์„œ <title>, <link>, <meta> ๊ฐ™์€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํƒœ๊ทธ๋ฅผ ์ง€์›ํ•œ๋‹ค. ์ปดํฌ๋„ŒํŠธ ๋‚ด์—์„œ ์ •์˜ํ•œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํƒœ๊ทธ๋Š” ๋„ํ๋จผํŠธ์˜ <head> ์˜์—ญ์œผ๋กœ ํ˜ธ์ด์ŠคํŒ… ๋œ๋‹ค.

function BlogPost({post}) {
  return (
    <article>
      <h1>{post.title}</h1>
      <title>{post.title}</title>
      <meta name="author" content="Josh" />
      <link rel="author" href="..." />
      <meta name="keywords" content={post.keywords} />
      <p>
        Eee equals em-see-squared...
      </p>
    </article>
  );
}

 

Support for stylesheets

์ปดํฌ๋„ŒํŠธ ๋‹จ์œ„๋กœ ์Šคํƒ€์ผ์‹œํŠธ(Stylesheets)๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ด ๋„์ž…๋๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์Šคํƒ€์ผ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์ง€์ •ํ•˜๊ณ , ์Šคํƒ€์ผ์‹œํŠธ๊ฐ€ ๋กœ๋“œ๋œ ํ›„ ์ฝ˜ํ…์ธ ๊ฐ€ ํ‘œ์‹œ๋˜๋„๋ก ๋ณด์žฅํ•  ์ˆ˜ ์žˆ๋‹ค. CSS๋Š” Cascading ๊ทœ์น™ ๋•Œ๋ฌธ์— ์Šคํƒ€์ผ์ด ๋กœ๋“œ๋˜๋Š” ์ˆœ์„œ๊ฐ€ ์ค‘์š”ํ•˜๋ฏ€๋กœ, React์—์„œ ์ด๋ฅผ ๋ช…ํ™•ํžˆ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง€์›ํ•œ๋‹ค.

 

  • ์šฐ์„ ์ˆœ์œ„ ๊ด€๋ฆฌ: <link>์˜ precedence ์†์„ฑ์„ ํ†ตํ•ด ์Šคํƒ€์ผ ์‚ฝ์ž… ์ˆœ์„œ ์ œ์–ด
  • ์ค‘๋ณต ๋ฐฉ์ง€: ๋™์ผํ•œ ์Šคํƒ€์ผ์‹œํŠธ๋ฅผ ์—ฌ๋Ÿฌ ๊ณณ์—์„œ ๋ Œ๋”๋งํ•ด๋„ DOM์— ์ค‘๋ณต ์‚ฝ์ž… ์•ˆ๋จ
  • SSR: ์„œ๋ฒ„์—์„œ ์Šคํƒ€์ผ์‹œํŠธ๋ฅผ <head>์— ํฌํ•จ์‹œ์ผœ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์Šคํƒ€์ผ ๋กœ๋“œ ํ›„ ์ฝ˜ํ…์ธ ๋ฅผ ๋ Œ๋”๋ง ํ•˜๋„๋ก ๋ณด์žฅ
  • CSR: ์Šคํƒ€์ผ์‹œํŠธ๋ฅผ ๋กœ๋“œํ•  ๋•Œ๊นŒ์ง€ ๋ Œ๋”๋ง์„ ์ง€์—ฐ์‹œ์ผœ ์Šคํƒ€์ผ ์ ์šฉ ๋ณด์žฅ.

 

function ComponentOne() {
  return (
    <Suspense fallback="loading...">
      {/* ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์„์ˆ˜๋ก ๋’ค์— ์‚ฝ์ž…๋œ๋‹ค */}
      <link rel="stylesheet" href="foo" precedence="default" />
      <link rel="stylesheet" href="bar" precedence="high" />
      <article class="foo-class bar-class">{/* ... */}</article>
    </Suspense>
  );
}

function ComponentTwo() {
  return (
    <div>
      <p>{/* ... */}</p>
      {/* ์•„๋ž˜ ์Šคํƒ€์ผ์€ foo์™€ bar ์Šคํƒ€์ผ์‹œํŠธ ์‚ฌ์ด์— ์‚ฝ์ž…๋จ */}
      <link rel="stylesheet" href="baz" precedence="default" />
    </div>
  );
}

 

Support for async scripts

defer ์Šคํฌ๋ฆฝํŠธ๋Š” ๋ฌธ์„œ ๋กœ๋“œ๋ฅผ ์™„๋ฃŒํ•œ ๋’ค์— ์‹คํ–‰๋œ๋‹ค. ์ŠคํŠธ๋ฆฌ๋ฐ ๋ฐฉ์‹์˜ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” defer์™€ ํ˜ธํ™˜๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ async ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

 

๋ฆฌ์•กํŠธ 19๋ถ€ํ„ฐ ๋น„๋™๊ธฐ ์Šคํฌ๋ฆฝํŠธ(async scripts)๋ฅผ ์ง€์›ํ•œ๋‹ค. ์ปดํฌ๋„ŒํŠธ ํŠธ๋ฆฌ ์–ด๋””์„œ๋“  ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋ Œ๋”๋ง ํ•  ์ˆ˜ ์žˆ๊ณ  ์ค‘๋ณต ๋กœ๋“œ ์—†์ด ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋œ๋‹ค(๋™์ผํ•œ src๋ฉด ์ค‘๋ณต ์ œ๊ฑฐ). SSR์—์„œ๋Š” ๋น„๋™๊ธฐ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ <head>์— ํฌํ•จ๋˜๋ฉฐ, ์Šคํƒ€์ผ/ํฐํŠธ ๋“ฑ ํŽ˜์ธํŠธ๋ฅผ ๋ธ”๋กœํ‚นํ•  ์ˆ˜ ์žˆ๋Š” ์ค‘์š”ํ•œ ๋ฆฌ์†Œ์Šค ๋ณด๋‹ค ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„๋กœ ์„ค์ •๋œ๋‹ค.

function MyComponent() {
  return (
    <div>
      <script async={true} src="..." />
      Hello World
    </div>
  );
}

function App() {
  return (
    <html>
      <body>
        <MyComponent />
        {/* ์Šคํฌ๋ฆฝํŠธ๊ฐ€ DOM์— ์ค‘๋ณต์œผ๋กœ ์ถ”๊ฐ€๋˜์ง€ ์•Š์Œ */}
        <MyComponent />
      </body>
    </html>
  );
}

 

Support for preloading resources

๋ธŒ๋ผ์šฐ์ € ๋ฆฌ์†Œ์Šค ๋กœ๋”ฉ์„ ์ตœ์ ํ™”ํ•˜๊ธฐ ์œ„ํ•œ ๋‹ค์–‘ํ•œ API๊ฐ€ ๋„์ž…๋๋‹ค. ํฐํŠธ/์Šคํƒ€์ผ์‹œํŠธ/์Šคํฌ๋ฆฝํŠธ ๋“ฑ์˜ ๋ฆฌ์†Œ์Šค๋ฅผ ๋ฏธ๋ฆฌ ๋กœ๋“œํ•˜๊ฑฐ๋‚˜, ์„œ๋ฒ„ ์—ฐ๊ฒฐ์„ ์‚ฌ์ „์— ์„ค์ •ํ•˜์—ฌ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„์™€ ํŽ˜์ด์ง€ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

import { prefetchDNS, preconnect, preload, preinit } from "react-dom";

function Component() {
  // ์™ธ๋ถ€ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋ฏธ๋ฆฌ ๋กœ๋“œํ•˜๊ณ  ์ฆ‰์‹œ ์‹คํ–‰ (๋‹ค์šด๋กœ๋“œ/์‹คํ–‰)
  preinit("https://.../path/to/some/script.js", { as: "script" });
  // ํฐํŠธ ๋ฏธ๋ฆฌ ๋‹ค์šด๋กœ๋“œ
  preload("https://.../path/to/font.woff", { as: "font" });
  // ์Šคํƒ€์ผ์‹œํŠธ ๋ฏธ๋ฆฌ ๋‹ค์šด๋กœ๋“œ
  preload("https://.../path/to/stylesheet.css", { as: "style" }); 
  // ๋ฆฌ์†Œ์Šค๋ฅผ ์š”์ฒญํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋˜๋Š” ๋„๋ฉ”์ธ์˜ DNS ์ •๋ณด ๋ฏธ๋ฆฌ ์กฐํšŒ
  prefetchDNS("https://...");
  // ๋ฆฌ์†Œ์Šค๋ฅผ ์š”์ฒญํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋˜๋Š” ์„œ๋ฒ„์™€์˜ ์—ฐ๊ฒฐ์„ ๋ฏธ๋ฆฌ ์„ค์ •
  preconnect("https://...");
}

 

์œ„ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ์•„๋ž˜์™€ ๊ฐ™์€ HTML์ด ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค. link/script ํƒœ๊ทธ์˜ ๋กœ๋”ฉ ์šฐ์„ ์ˆœ์œ„๋Š” ์ฝ”๋“œ ์ˆœ์„œ๊ฐ€ ์•„๋‹Œ, ๋ธŒ๋ผ์šฐ์ €์˜ ๋กœ๋”ฉ ํšจ์œจ์„ฑ ๊ธฐ์ค€์— ๋”ฐ๋ผ ๊ฒฐ์ •๋œ๋‹ค. ์ฆ‰, ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์†๋„์— ๊ฐ€์žฅ ํฐ ์˜ํ–ฅ์„ ์ฃผ๋Š” ๋ฆฌ์†Œ์Šค๋ฅผ ์šฐ์„ ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.

<html>
  <head>
    <!-- link/script๋Š” ์ฝ”๋“œ ์ˆœ์„œ์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋กœ๋”ฉ ํšจ์œจ์„ฑ์„ ๊ธฐ์ค€์œผ๋กœ ์ฒ˜๋ฆฌ -->
    <link rel="prefetch-dns" href="https://...">
    <link rel="preconnect" href="https://...">
    <link rel="preload" as="font" href="https://.../path/to/font.woff">
    <link rel="preload" as="style" href="https://.../path/to/stylesheet.css">
    <script async="" src="https://.../path/to/some/script.js"></script>
  </head>
  <body>
    <!-- ... -->
  </body>
</html>

 

์ด๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์Šคํƒ€์ผ์‹œํŠธ ๋กœ๋”ฉ์— ์˜์กดํ•˜์ง€ ์•Š๊ณ  ๋ฆฌ์†Œ์Šค๋ฅผ ๋ณ„๋„๋กœ ๋กœ๋“œํ•˜์—ฌ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„๋ฅผ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

  • ๊ธฐ์กด ๋ฐฉ์‹: HTML ํŒŒ์‹ฑ → CSS ๋กœ๋“œ → ํฐํŠธ/์ด๋ฏธ์ง€ ๋ฐœ๊ฒฌ → ๋ฆฌ์†Œ์Šค ๋กœ๋“œ ์‹œ์ž‘
  • ์‹ ๊ทœ ๋ฐฉ์‹: HTML ํŒŒ์‹ฑ ์‹œ์ ์— preload ์„ ์–ธ์ด ์žˆ๋Š” ๋ฆฌ์†Œ์Šค ๋ฏธ๋ฆฌ ๋กœ๋“œ → ์Šคํƒ€์ผ ์‹œํŠธ์™€ ๋™์‹œ์— ๋ Œ๋”๋ง

 

๋˜ํ•œ, ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋™ํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์€ ํŽ˜์ด์ง€์˜ ๋ฆฌ์†Œ์Šค๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ณ (prefetch), ๋งˆ์šฐ์Šค ํด๋ฆญ ํ˜น์€ ํ˜ธ๋ฒ„ ํ–ˆ์„ ๋•Œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฆ‰์‹œ ๋กœ๋“œ(preload)ํ•˜์—ฌ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์‹œ๊ฐ„์„ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค.

Preloading API์—๋Š” eagerly๋ผ๋Š” ๋‹จ์–ด๊ฐ€ ์ž์ฃผ ๋“ฑ์žฅํ•œ๋‹ค. eagerly๋Š” ์‚ฌ์ „์ ์œผ๋กœ "์—ด์‹ฌํžˆ, ๊ฐ„์ ˆํžˆ"๋ผ๋Š” ๋œป์ด์ง€๋งŒ, ๋ฌธ๋งฅ์ƒ "๊ฐ€๋Šฅํ•œ ๋น ๋ฅด๊ฒŒ" ๋˜๋Š” "์ฆ‰์‹œ" ์ •๋„๋กœ ํ•ด์„ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

Compatibility with third-party scripts and extensions

๋ฆฌ์•กํŠธ 19๋ถ€ํ„ฐ ์„œ๋“œํŒŒํ‹ฐ ์Šคํฌ๋ฆฝํŠธ๋‚˜ ๋ธŒ๋ผ์šฐ์ € ํ™•์žฅํ”„๋กœ๊ทธ๋žจ์ด <head> ํ˜น์€ <body> ๋‚ด๋ถ€์— ์‚ฝ์ž…ํ•œ ํƒœ๊ทธ๋ฅผ ๊ฑด๋„ˆ๋›ฐ๋Š” ๋ฐฉ์‹์œผ๋กœ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๋ถˆ์ผ์น˜ ์˜ค๋ฅ˜๋ฅผ ๋ฐฉ์ง€ํ•œ๋‹ค. ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์ง€๋งŒ ์„œ๋“œํŒŒํ‹ฐ ์š”์†Œ์™€ ์ง์ ‘์ ์ธ ๊ด€๋ จ์ด ์—†๋Š” ๊ฒฝ์šฐ ๋ฆฌ์•กํŠธ๋Š” ์ „์ฒด ๋ฌธ์„œ๋ฅผ ๋‹ค์‹œ ๋ Œ๋”๋ง ํ•  ๋•Œ ์„œ๋“œํŒŒํ‹ฐ ์Šคํƒ€์ผ์‹œํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•จ์œผ๋กœ์จ ์„œ๋“œํŒŒํ‹ฐ์˜ ์Šคํƒ€์ผ๊ณผ ๊ธฐ๋Šฅ์ด ์†์ƒ๋˜์ง€ ์•Š๋„๋ก ์ฒ˜๋ฆฌํ•œ๋‹ค.

 

Better error reporting

์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์ด ๊ฐœ์„ ๋˜์–ด ์ค‘๋ณต ์—๋Ÿฌ ๋กœ๊ทธ๊ฐ€ ์ œ๊ฑฐ๋˜๊ณ , caught, uncaught ์—๋Ÿฌ ์ฒ˜๋ฆฌ์— ๋Œ€ํ•œ ์ƒˆ๋กœ์šด ์˜ต์…˜์ด ์ถ”๊ฐ€๋๋‹ค. ์ด์ „ ๋ฒ„์ „์—์„  Error Boundary์—์„œ ์—๋Ÿฌ๋ฅผ ์บ์น˜ํ–ˆ์„ ๋•Œ 3๊ฐœ์˜ ์ค‘๋ณต๋œ ๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅํ–ˆ์—ˆ๋‹ค.

 

  1. ์›๋ณธ ์—๋Ÿฌ ๋กœ๊ทธ
  2. ๋ณต๊ตฌ ์‹คํŒจ๋กœ ๋ฐœ์ƒํ•œ ๋™์ผํ•œ ์—๋Ÿฌ
  3. ์—๋Ÿฌ ๋ฐœ์ƒ ์œ„์น˜๋ฅผ ํฌํ•จํ•œ console.error ๋ฉ”์‹œ์ง€

 

๋ฆฌ์•กํŠธ 19๋ถ€ํ„ด ์ด๋Ÿฌํ•œ ์—๋Ÿฌ ๋กœ๊ทธ๋ฅผ ๋‹จ์ผ ๋ฉ”์‹œ์ง€๋กœ ํ†ตํ•ฉํ•˜์—ฌ ํ•œ ๋ฒˆ์— ๋ชจ๋“  ์ •๋ณด๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

๋ฆฌ์•กํŠธ 19 ์ด์ „ ๋ฒ„์ „์˜ ์—๋Ÿฌ ๋กœ๊ทธ. ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ด 3๊ฐœ์˜ ๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค.
๋ฆฌ์•กํŠธ 19 ๋ฒ„์ „์˜ ์—๋Ÿฌ ๋กœ๊ทธ. ๋‹จ์ผ ๋ฉ”์‹œ์ง€์— ๋ชจ๋“  ์—๋Ÿฌ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ๋‹ค.

๋˜ํ•œ createRoot, hydrateRoot ๋ฃจํŠธ ์ƒ์„ฑ ํ•จ์ˆ˜์— ์ƒˆ๋กœ์šด ์—๋Ÿฌ ๊ด€๋ จ ์˜ต์…˜์ด ์ถ”๊ฐ€๋๋‹ค.

  • onCaughtError: Error Boundary์—์„œ ์—๋Ÿฌ๋ฅผ ์บ์น˜ํ–ˆ์„ ๋•Œ ํ˜ธ์ถœ
  • onUncaughtError: Error Boundary๋กœ ์บ์น˜๋˜์ง€ ์•Š์€ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ํ˜ธ์ถœ
  • onRecoverableError: ๋ฐœ์ƒํ•œ ์—๋Ÿฌ๋ฅผ ์ž๋™์œผ๋กœ ๋ณต๊ตฌํ–ˆ์„ ๋•Œ ํ˜ธ์ถœ

 

Support for Custom Elements

๏ฟญ ์–ดํŠธ๋ฆฌ๋ทฐํŠธ(Attribute): HTML ์š”์†Œ์˜ ์†์„ฑ. ์ดˆ๊ธฐ๊ฐ’์„ ์ง€์ •ํ•˜๋Š” ์ •์ ์ธ ๊ฐ’.
๏ฟญ ํ”„๋กœํผํ‹ฐ(Property): DOM ๊ฐ์ฒด์˜ ์†์„ฑ. ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋ฐ˜์˜ํ•˜๋Š” ๋™์ ์ธ ๊ฐ’.

 

์ด์ „ ๋ฒ„์ „์˜ ๋ฆฌ์•กํŠธ์—์„  ํ‘œ์ค€ HTML ์†์„ฑ(value, className ๋“ฑ)์ด๋‚˜ React ๊ณ ์œ  ์†์„ฑ(key, ref ๋“ฑ)์ด ์•„๋‹ˆ๋ฉด ๋ชจ๋‘ ์–ดํŠธ๋ฆฌ๋ทฐํŠธ๋กœ ์ทจ๊ธ‰ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์–ด๋ ค์› ๋‹ค. ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์„ ๋• ๊ฐ์ฒด, ๋ฐฐ์—ด ๊ฐ™์€ ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๋Š” ํ”„๋กœํผํ‹ฐ๋ฅผ ์ฃผ๋กœ ์‚ฌ์šฉํ•˜์ง€๋งŒ, ์–ดํŠธ๋ฆฌ๋ทฐํŠธ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฌธ์ž์—ด๋งŒ ํ—ˆ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ(๋ฌธ์ž์—ด์ด ์•„๋‹Œ ๊ฐ’์„ ํ• ๋‹นํ•˜๋ฉด ๋‚ด๋ถ€์ ์œผ๋กœ toString()์„ ํ˜ธ์ถœํ•ด์„œ ๋ฌธ์ž์—ด๋กœ ์ž๋™ ๋ณ€ํ™˜๋จ).

 

๋ฆฌ์•กํŠธ 19 ๋ฒ„์ „๋ถ€ํ„ด CSR ๋ฐ SSR์—์„œ ์–ดํŠธ๋ฆฌ๋ทฐํŠธ์™€ ํ”„๋กœํผํ‹ฐ๋ฅผ ์ ์ ˆํžˆ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ ๋๋‹ค.

 

  • SSR: ์›์‹œ ํƒ€์ž…์ด๋‚˜ ๊ฐ’์ด true์ธ props๋Š” ์–ดํŠธ๋ฆฌ๋ทฐํŠธ๋กœ ๋ Œ๋”๋ง. ๋น„์›์‹œํƒ€์ž…์ด๋‚˜ ๊ฐ’์ด false์ด๋ฉด ์ƒ๋žต.
  • CSR: ์ปค์Šคํ…€ ์—˜๋ฆฌ๋จผํŠธ ์ธ์Šคํ„ด์Šค์˜ ํ”„๋กœํผํ‹ฐ์™€ ์ผ์น˜ํ•˜๋Š” props๋Š” ํ”„๋กœํผํ‹ฐ๋กœ, ๊ทธ ์™ธ๋Š” ์–ดํŠธ๋ฆฌ๋ทฐํŠธ๋กœ ์„ค์ •.

 

 

๋ ˆํผ๋Ÿฐ์Šค


 

React v19 – React

The library for web and native user interfaces

react.dev

 


๊ธ€ ์ˆ˜์ •์‚ฌํ•ญ์€ ๋…ธ์…˜ ํŽ˜์ด์ง€์— ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”
 
๋ฐ˜์‘ํ˜•