[React] ๋ฆฌ์กํธ 19 ์ ๋ฐ์ดํธ ๋ด์ฉ ํบ์๋ณด๊ธฐ
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๊ฐ์ ์ค๋ณต๋ ๋ก๊ทธ๋ฅผ ์ถ๋ ฅํ์๋ค.
- ์๋ณธ ์๋ฌ ๋ก๊ทธ
- ๋ณต๊ตฌ ์คํจ๋ก ๋ฐ์ํ ๋์ผํ ์๋ฌ
- ์๋ฌ ๋ฐ์ ์์น๋ฅผ ํฌํจํ console.error ๋ฉ์์ง
๋ฆฌ์กํธ 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
๊ธ ์์ ์ฌํญ์ ๋ ธ์ ํ์ด์ง์ ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ๋ฐ์๋ฉ๋๋ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Next.js] Dynamic Routes ๋ค์ด๋๋ฏน ๋ผ์ฐํธ (0) | 2025.01.31 |
---|---|
[Next.js] App Router ๊ณต์ ํํ ๋ฆฌ์ผ ํบ์๋ณด๊ธฐ (1) | 2025.01.30 |
[Dev] ์๋งจํฑ ๋ฒ์ ๋ Semantic Versioning (0) | 2025.01.27 |
[React] ๋ฆฌ์กํธ์ ์ฌ๋ฐ๋ฅธ useEffect ์ฌ์ฉํ (2) | 2025.01.21 |
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ์ฝ๋ ์ต์ ํ ๊ธฐ๋ฒ ๋ชจ์ (23) | 2024.12.07 |
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[Next.js] Dynamic Routes ๋ค์ด๋๋ฏน ๋ผ์ฐํธ
[Next.js] Dynamic Routes ๋ค์ด๋๋ฏน ๋ผ์ฐํธ
2025.01.31 -
[Next.js] App Router ๊ณต์ ํํ ๋ฆฌ์ผ ํบ์๋ณด๊ธฐ
[Next.js] App Router ๊ณต์ ํํ ๋ฆฌ์ผ ํบ์๋ณด๊ธฐ
2025.01.30 -
[Dev] ์๋งจํฑ ๋ฒ์ ๋ Semantic Versioning
[Dev] ์๋งจํฑ ๋ฒ์ ๋ Semantic Versioning
2025.01.27 -
[React] ๋ฆฌ์กํธ์ ์ฌ๋ฐ๋ฅธ useEffect ์ฌ์ฉํ
[React] ๋ฆฌ์กํธ์ ์ฌ๋ฐ๋ฅธ useEffect ์ฌ์ฉํ
2025.01.21