* 이글은 What's new in React 19를 번역하였습니다.
Sever Components
서버 컴포넌트는 React가 10년 전 처음 출시된 이후 가장 큰 변화 중 하나입니다. 이들은 React 19의 새로운 기능들의 기반 역할을 하며 다음과 같은 개선을 제공합니다:
- 초기 페이지 로드 시간: 서버에서 컴포넌트를 렌더링하여 클라이언트로 전송되는 JavaScript의 양을 줄임으로써 초기 로드 속도를 빠르게 합니다. 또한, 페이지가 클라이언트에 전송되기 전에 서버에서 데이터 쿼리를 시작할 수 있습니다.
- 코드 이식성: 서버 컴포넌트를 사용하면 서버와 클라이언트 모두에서 실행 가능한 컴포넌트를 작성할 수 있어 코드 중복을 줄이고 유지보수성을 개선하며 코드베이스 전체에서 논리를 더 쉽게 공유할 수 있습니다.
- SEO(검색 엔진 최적화): 컴포넌트의 서버 측 렌더링은 검색 엔진과 대형 언어 모델(LLM)이 콘텐츠를 더 효과적으로 크롤링하고 인덱싱할 수 있게 하여 검색 엔진 최적화를 향상시킵니다.
서버 컴포넌트의 중요성을 이해하기 위해 React 렌더링의 발전 과정을 간략히 살펴보겠습니다.
React는 처음에 클라이언트 사이드 렌더링(CSR)으로 시작하여 사용자에게 최소한의 HTML을 제공했습니다.
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="/static/js/bundle.js"></script>
</body>
</html>
연결된 스크립트에는 React, 서드파티 의존성, 그리고 모든 애플리케이션 코드가 포함됩니다. 애플리케이션이 커질수록 번들 크기도 함께 커졌습니다. JavaScript가 다운로드되고 파싱된 후, React는 빈 div 요소에 DOM 요소들을 로드합니다. 이 과정이 진행되는 동안 사용자가 보는 것은 빈 페이지뿐입니다.
초기 UI가 마침내 표시되더라도 페이지 콘텐츠는 여전히 비어있어, 로딩 스켈레톤이 인기를 얻게 되었습니다. 그 후 데이터가 페치되고, UI가 두 번째로 렌더링되어 로딩 스켈레톤을 실제 콘텐츠로 대체하게 됩니다.
React는 서버 사이드 렌더링(SSR)을 통해 개선되었으며, 첫 번째 렌더링을 서버로 옮겼습니다. 사용자에게 제공되는 HTML이 더 이상 빈 페이지가 아니었으며, 사용자가 초기 UI를 더 빨리 볼 수 있게 되었습니다. 그러나 실제 콘텐츠를 표시하기 위해 여전히 데이터를 페치해야 합니다.
export default async function Page() {
const res = await fetch("https://api.example.com/products");
const products = res.json();
return (
<>
<h1>Products</h1>
{products.map((product) => (
<div key={product.id}>
<h2>{product.title}</h2>
<p>{product.description}</p>
</div>
))}
</>
);
}
사용자에게 제공되는 HTML은 첫 번째 렌더링 시 실제 콘텐츠로 완전히 채워져 있으며, 추가로 데이터를 가져오거나 두 번째 렌더링을 할 필요가 없습니다.
서버 컴포넌트는 속도와 성능 면에서 큰 진전을 이루었으며, 개발자와 사용자 모두에게 더 나은 경험을 제공합니다.
New directives
디렉티브는 React 19의 기능은 아니지만, 관련이 있습니다. React 서버 컴포넌트의 도입으로 인해 번들러는 컴포넌트와 함수가 실행되는 위치를 구분해야 합니다. 이를 위해 React 컴포넌트를 만들 때 알아두어야 할 두 가지 새로운 디렉티브가 있습니다:
- 'use client'는 클라이언트에서만 실행되는 코드를 표시합니다. 서버 컴포넌트가 기본값이므로, 상호작용과 상태 관리를 위한 훅을 사용하는 클라이언트 컴포넌트에는 'use client'를 추가해야 합니다.
- 'use server'는 클라이언트 측 코드에서 호출할 수 있는 서버 측 함수를 표시합니다. 서버 컴포넌트에는 'use server'를 추가할 필요는 없으며, 서버 액션에만 추가하면 됩니다(아래에서 더 자세히 설명). 특정 코드가 서버에서만 실행되도록 하려면 server-only npm 패키지를 사용할 수 있습니다.
Actions
React 19는 액션(Actions)을 도입했습니다. 이 함수들은 이벤트 핸들러 사용을 대체하며, React 전환 및 동시성 기능과 통합됩니다.
액션은 클라이언트와 서버 모두에서 사용할 수 있습니다. 예를 들어, 클라이언트 액션을 사용하여 폼의 onSubmit을 대체할 수 있습니다.
이벤트를 파싱할 필요 없이, 액션은 FormData를 직접 전달받습니다.
import { useState } from "react";
export default function TodoApp() {
const [items, setItems] = useState([
{ text: "My first todo" },
]);
async function formAction(formData) {
const newItem = formData.get("item");
// Could make a POST request to the server to save the new item
setItems((items) => [...items, { text: newItem }]);
}
return (
<>
<h1>Todo List</h1>
<form action={formAction}>
<input type="text" name="item" placeholder="Add todo..." />
<button type="submit">Add</button>
</form>
<ul>
{items.map((item, index) => (
<li key={index}>{item.text}</li>
))}
</ul>
</>
);
}
Sever Actions
서버 액션(Server Actions)을 통해 클라이언트 컴포넌트가 서버에서 실행되는 비동기 함수를 호출할 수 있습니다. 이를 통해 파일 시스템을 읽거나 데이터베이스에 직접 호출하는 등의 추가적인 이점을 제공하며, UI를 위한 별도의 API 엔드포인트를 만들 필요가 없어집니다.
액션은 'use server' 디렉티브로 정의되며, 클라이언트 측 컴포넌트와 통합됩니다.
클라이언트 컴포넌트에서 서버 액션을 호출하려면 새 파일을 생성하고 이를 가져옵니다.
'use server'
export async function create() {
// Insert into database
}
"use client";
import { create } from "./actions";
export default function TodoList() {
return (
<>
<h1>Todo List</h1>
<form action={create}>
<input type="text" name="item" placeholder="Add todo..." />
<button type="submit">Add</button>
</form>
</>
);
}
New hooks
액션을 보완하기 위해, React 19는 상태, 상태 확인, 시각적 피드백을 더 쉽게 처리할 수 있도록 세 가지 새로운 훅을 도입했습니다. 이러한 훅들은 특히 폼 작업에 유용하지만, 버튼과 같은 다른 요소에도 사용할 수 있습니다.
useActionState
이 훅은 폼 상태와 폼 제출 관리를 간소화합니다. 액션을 사용하여 폼 입력 데이터를 캡처하고, 유효성 검사 및 오류 상태를 처리하며, 사용자 정의 상태 관리 로직의 필요성을 줄입니다. useActionState 훅은 또한 액션이 실행되는 동안 로딩 표시기를 보여줄 수 있는 대기 상태(pending state)를 노출합니다.
"use client";
import { useActionState } from "react";
import { createUser } from "./actions";
const initialState = {
message: "",
};
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState);
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
{state?.message && <p aria-live="polite">{state.message}</p>}
<button aria-disabled={pending} type="submit">
{pending ? "Submitting..." : "Sign up"}
</button>
</form>
);
}
useFormStatus
이 훅은 마지막 폼 제출의 상태를 관리하며, 반드시 폼 안에 있는 컴포넌트 내부에서 호출되어야 합니다.
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>
);
}
useActionState 훅에 내장된 대기 상태(pending status)가 있지만, useFormStatus는 다음과 같은 경우에 독립적으로 유용합니다
- 폼 상태가 없는 경우
- 공유되는 폼 컴포넌트를 만드는 경우
- 동일한 페이지에 여러 폼이 있는 경우 useFormStatus는 부모 폼에 대한 상태 정보만 반환합니다.
useOptimistic
이 훅은 서버 액션이 완료되기 전에 UI를 낙관적으로 업데이트할 수 있게 해주며, 응답을 기다리지 않아도 됩니다. 비동기 액션이 완료되면 서버로부터 최종 상태를 받아 UI가 업데이트됩니다.
다음 예시는 새로운 메시지를 스레드에 즉시 추가하면서 동시에 해당 메시지를 서버 액션으로 보내어 영구적으로 저장하는 과정을 낙관적으로 처리하는 방법을 보여줍니다.
"use client";
import { useOptimistic } from "react";
import { send } from "./actions";
export function Thread({ messages }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { message: newMessage }],
);
const formAction = async (formData) => {
const message = formData.get("message") as string;
addOptimisticMessage(message);
await send(message);
};
return (
<div>
{optimisticMessages.map((m, i) => (
<div key={i}>{m.message}</div>
))}
<form action={formAction}>
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
</div>
);
}
New API : use
use 함수는 렌더링 중에 프라미스와 컨텍스트에 대한 일급 지원을 제공합니다. 다른 React 훅과는 달리, use는 반복문, 조건문, 그리고 조기 반환 구문 내에서 호출될 수 있습니다. 오류 처리와 로딩은 가장 가까운 Suspense 경계에서 처리됩니다.
다음 예시는 장바구니 항목의 프라미스가 해결되는 동안 로딩 메시지를 표시하는 방법을 보여줍니다.
import { use } from "react";
function Cart({ cartPromise }) {
// `use` will suspend until the promise resolves
const cart = use(cartPromise);
return cart.map((item) => <p key={item.id}>{item.title}</p>);
}
function Page({ cartPromise }) {
return (
/*{ ... }*/
// When `use` suspends in Cart, this Suspense boundary will be shown
<Suspense fallback={<div>Loading...</div>}>
<Cart cartPromise={cartPromise} />
</Suspense>
);
}
이 기능을 사용하면 모든 컴포넌트의 데이터가 준비되었을 때만 컴포넌트들을 함께 렌더링할 수 있도록 그룹화할 수 있습니다.
Preloading resources
React 19는 스크립트, 스타일시트, 폰트와 같은 리소스를 로드 및 프리로드하여 페이지 로드 성능과 사용자 경험을 개선하는 여러 새로운 API를 추가했습니다.
- prefetchDNS: 연결할 것으로 예상되는 DNS 도메인 이름의 IP 주소를 미리 가져옵니다.
- preconnect: 요청할 리소스가 명확하지 않더라도, 연결할 것으로 예상되는 서버에 미리 연결합니다.
- preload: 사용할 것으로 예상되는 스타일시트, 폰트, 이미지 또는 외부 스크립트를 미리 가져옵니다.
- preloadModule: 사용할 것으로 예상되는 ESM 모듈을 미리 가져옵니다.
- preinit: 외부 스크립트를 가져와 평가하거나 스타일시트를 가져와 삽입합니다.
- preinitModule: ESM 모듈을 가져와 평가합니다.
예를 들어, 다음 React 코드는 다음과 같은 HTML 출력 결과를 제공합니다. 링크와 스크립트는 React에서 사용되는 순서가 아닌, 얼마나 빨리 로드되어야 하는지에 따라 우선순위가 매겨집니다.
// React code
import { prefetchDNS, preconnect, preload, preinit } from "react-dom";
function MyComponent() {
preinit("https://.../path/to/some/script.js", { as: "script" });
preload("https://.../path/to/some/font.woff", { as: "font" });
preload("https://.../path/to/some/stylesheet.css", { as: "style" });
prefetchDNS("https://...");
preconnect("https://...");
}
<!-- Resulting HTML -->
<html>
<head>
<link rel="prefetch-dns" href="https://..." />
<link rel="preconnect" href="https://..." />
<link rel="preload" as="font" href="https://.../path/to/some/font.woff" />
<link
rel="preload"
as="style"
href="https://.../path/to/some/stylesheet.css"
/>
<script async="" src="https://.../path/to/some/script.js"></script>
</head>
<body>
<!-- ... -->
</body>
</html>
React 프레임워크는 자주 이러한 리소스 로딩을 자동으로 처리해 주기 때문에, 직접 이러한 API를 호출할 필요가 없을 수도 있습니다.
Other improvements
ref as a prop
더 이상 forwardRef를 사용할 필요가 없습니다. React는 전환을 쉽게 하기 위해 코드 변환 도구(codemod)를 제공할 예정입니다.
function CustomInput({ placeholder, ref }) {
return <input placeholder={placeholder} ref={ref} />;
}
// ...
<CustomInput ref={ref} />;
ref callbacks
ref를 props로 전달하는 것 외에도, refs는 정리 작업을 위한 콜백 함수도 반환할 수 있습니다. 컴포넌트가 언마운트될 때, React는 이 정리 작업을 위한 콜백 함수를 호출합니다.
<input
ref={(ref) => {
// ref created
// Return a cleanup function to reset
// ref when element is removed from DOM.
return () => {
// ref cleanup
};
}}
/>;
Context as a provider
이제 <Context.Provider>를 사용할 필요가 없습니다. 대신 <Context>를 직접 사용할 수 있습니다. React는 기존의 Provider를 변환할 수 있는 코드 변환 도구(codemod)를 제공할 예정입니다.
const ThemeContext = createContext("");
function App({ children }) {
return <ThemeContext value="dark">{children}</ThemeContext>;
}
useDeferredValue initial value
useDeferredValue에 initialValue 옵션이 추가되었습니다. 이 옵션이 제공되면, useDeferredValue는 초기 렌더링에 해당 값을 사용하고, 백그라운드에서 다시 렌더링을 예약한 후 deferredValue를 반환합니다.
function Search({ deferredValue }) {
// On initial render the value is ''.
// Then a re-render is scheduled with the deferredValue.
const value = useDeferredValue(deferredValue, "");
return <Results value={value} />;
}
Document metadata support
React 19는 타이틀, 링크, 메타 태그를 네이티브로 올리고 렌더링하며, 심지어 중첩된 컴포넌트에서도 가능합니다. 이제 이러한 태그를 관리하기 위해 서드파티 솔루션을 사용할 필요가 없습니다.
function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<title>{post.title}</title>
<meta name="author" content="Jane Doe" />
<link rel="author" href="https://x.com/janedoe" />
<meta name="keywords" content={post.keywords} />
<p>...</p>
</article>
);
}
Stylesheet support
React 19는 precedence(우선순위)를 사용하여 스타일시트 로딩 순서를 제어할 수 있습니다. 이를 통해 컴포넌트 근처에 스타일시트를 배치하는 것이 더 쉬워지며, React는 해당 스타일시트가 사용될 때만 로드합니다.
몇 가지 중요한 점을 기억해야 합니다:
- 애플리케이션 내에서 동일한 컴포넌트를 여러 위치에 렌더링할 경우, React는 스타일시트를 중복 제거하여 문서에 한 번만 포함합니다.
- 서버 사이드 렌더링 시, React는 스타일시트를 head에 포함시킵니다. 이를 통해 브라우저가 로드될 때까지 페인트를 하지 않도록 보장합니다.
- 스트리밍이 시작된 후 스타일시트가 발견되면, React는 Suspense 경계를 통해 스타일시트에 의존하는 콘텐츠를 노출하기 전에 클라이언트에서 head에 스타일시트를 삽입합니다.
- 클라이언트 사이드 렌더링 중, React는 새로 렌더링된 스타일시트가 로드될 때까지 렌더링을 커밋하지 않고 기다립니다.
function ComponentOne() {
return (
<Suspense fallback="loading...">
<link rel="stylesheet" href="one" precedence="default" />
<link rel="stylesheet" href="two" precedence="high" />
<article>...</article>
</Suspense>
);
}
function ComponentTwo() {
return (
<div>
<p>...</p>
{/* Stylesheet "three" below will be inserted between "one" and "two" */}
<link rel="stylesheet" href="three" precedence="default" />
</div>
);
}
Async scripts support
비동기 스크립트를 모든 컴포넌트에서 렌더링할 수 있습니다. 이를 통해 컴포넌트 근처에 스크립트를 배치하는 것이 더 쉬워지며, React는 해당 스크립트가 사용될 때만 로드합니다.
몇 가지 유의할 점은 다음과 같습니다:
- 애플리케이션 내에서 동일한 컴포넌트를 여러 위치에 렌더링할 경우, React는 스크립트를 중복 제거하여 문서에 한 번만 포함합니다.
- 서버 사이드 렌더링 시, 비동기 스크립트는 head에 포함되며 스타일시트, 폰트, 이미지 프리로드와 같은 페인트를 차단하는 더 중요한 리소스 뒤에 우선순위가 지정됩니다.
function Component() {
return (
<div>
<script async={true} src="..." />
// ...
</div>
);
}
function App() {
return (
<html>
<body>
<Component>
// ...
</Component> // Won't duplicate script in the DOM
</body>
</html>
);
}
Custom Elements support
커스텀 엘리먼트(Custom Elements)는 개발자가 웹 컴포넌트 사양의 일환으로 자체 HTML 요소를 정의할 수 있게 해줍니다. 이전 버전의 React에서는 React가 인식하지 못한 props를 속성으로 처리하기 때문에 커스텀 엘리먼트를 사용하는 것이 어려웠습니다.
React 19는 커스텀 엘리먼트에 대한 완전한 지원을 추가했으며, Custom Elements Everywhere의 모든 테스트를 통과했습니다.
Better error reporting
오류 처리 기능이 개선되어 중복된 오류 메시지가 제거되었습니다.
서드파티 스크립트 및 브라우저 확장 프로그램을 사용할 때 발생하는 하이드레이션 오류도 개선되었습니다. 이전에는 서드파티 스크립트나 브라우저 확장 프로그램에 의해 삽입된 요소가 불일치 오류를 유발했으나, React 19에서는 head와 body에서 예상치 못한 태그가 발견될 경우 이를 건너뛰고 오류를 발생시키지 않습니다.
마지막으로, React 19는 기존의 onRecoverableError에 더해 두 가지 새로운 루트 옵션을 추가하여 오류가 발생한 이유를 더 명확하게 제공합니다.
- onCaughtError는 React가 오류 경계(Error Boundary) 내에서 오류를 포착했을 때 트리거됩니다.
- onUncaughtError는 오류가 발생했지만 오류 경계에서 포착되지 않았을 때 트리거됩니다.
- onRecoverableError는 오류가 발생하고 자동으로 복구되었을 때 트리거됩니다.
< 참고자료 >
[사이트] #Vercel
<React> What's new in React19?
'Language & Framework & Library > React' 카테고리의 다른 글
You might not need an effect (0) | 2024.04.22 |
---|---|
왜 ReactHooks일까? 그리고 어떻게 여기까지 왔을까? (0) | 2024.04.13 |
react useState에 대한 공통적인 실수 (1) | 2024.03.17 |
React Render Props and HOC 이해하기 (0) | 2023.11.26 |
React useEffect: 개발자가 알아야 4가지 팁 (0) | 2023.11.17 |