์๋ฌธ: https://marmelab.com/blog/2024/01/23/react-19-new-hooks.html
์ผ๋ฐ์ ์ธ ์๊ฐ๊ณผ ๋ฌ๋ฆฌ React coreํ์ ๋ฆฌ์กํธ ์๋ฒ ์ปดํฌ๋ํธ์ Next.js์๋ง ์ค์ง ์ด์ ์ ๋์ง ์๋๋ค. ์๋ก์ด client-side ํ ๋ค์ด ๋ค์ React ๋ฉ์ธ ๋ฒ์ ์ธ 19๋ฒ์ ์ ๋์ ๋๋ค. ๊ทธ๋ค์ ๋ฆฌ์กํธ์ ๋ ๊ฐ์ง ์ฃผ์ ๋ฌธ์ ์ , ์ฆ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ(data fetching)๊ณผ ํผ(form)์ ์ด์ ์ ๋ง์ถ๊ณ ์๋ค. ์ด ํ ๋ค์ ์ฑ๊ธ ํ์ด์ง ์ดํ๋ฆฌ์ผ์ด์ ์์ ์ ํ๋ ๊ฐ๋ฐ์๋ฅผ ํฌํจํด React ๊ฐ๋ฐ์๋ค์ ์์ฐ์ฑ์ ํฅ์์์ผ์ค ๊ฒ์ด๋ค.
๋ ์ด์ ๊ณ ๋ฏผํ์ง ๋ง๊ณ , ์๋ก์ด ํ ๋ค์ ์ดํด๋ณด์!
- use(Promise)
- use(Context)
- Form actions
- useFormState
- useFormStatus
- useOptimistic
- Bonus: Async transitions
์ฃผ๋ชฉ: ์ด ํ ๋ค์ 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>
{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 ๋ฆด๋ฆฌ์ฆ์ ์ด ๊ธฐ๋ฅ๋ค์ด ๋ณด์ด๊ธธ ๊ธฐ๋ํ๋ค!