React
React 19 is now stable!
한국 시각으로 2024년 12월 6일, React 19가 정식 출시되었습니다.
React 19의 주요 변경점과 새롭게 출시된 사항에 대해서 알아보고 기록해보려고 합니다.
아쉽게도 React Compiler는 이번에 소식이 없었습니다.
최적화 관련된 내용과 여러가지 피드백들을 더 많이 수용하여 신중하게 출시하려나 봅니다 :)
React 19의 새로운 기능
Actions
React 앱에서 일반적인 훅들의 사용 사례는 mutation을 수행하고 응답에서 상태를 업데이트하는 것입니다. 예를 들어, 사용자가 "닉네임"을 변경하는 폼을 제출하면, API 요청을 생성하고 그 응답을 받아서 처리합니다. 이 과정에서 개발자는 요청의 Pending 상태, Error 처리, Optimistic Update(낙관적 업데이트), Sequential Request(순차적 요청) 등을 수동으로 관리해야 했습니다.
예를 들어 useState에서 Pending, Error를 다음과 같이 관리해야 합니다:
// 이전 Actions 관리
function UpdateNickname({}) {
const [name, setName] = useState("");
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);
const handleSubmit = async () => {
setIsPending(true);
const error = await updateName(name);
setIsPending(false);
if (error) {
setError(error);
return;
}
redirect("/path");
};
return (
<div>
<input value={name} onChange={(event) =>
setName(event.target.value)} />
<button onClick={handleSubmit} disabled={isPending}>
Update
</button>
{error && <p>{error}</p>}
</div>
);
}
React 19에서는 Pending, Error, Form, Optimistic Update를 자동으로 처리하기 위해 transition에서 비동기 함수를 사용할 수 있는 기능을 지원합니다.
예를 들어, useTransition를 사용하여 Pending 상태를 처리할 수 있습니다:
// useTransition hook을 사용한 actions 관리
function UpdateName({}) {
const [name, setName] = useState("");
const [error, setError] = useState(null);
const [isPending, startTransition] = useTransition();
const handleSubmit = () => {
startTransition(async () => {
const error = await updateName(name);
if (error) {
setError(error);
return;
}
redirect("/path");
})
};
return (
<div>
<input value={name} onChange={(event) =>
setName(event.target.value)} />
<button onClick={handleSubmit} disabled={isPending}>
Update
</button>
{error && <p>{error}</p>}
</div>
);
}
비동기 함수는 즉시 isPending 상태를 true로 설정하고 비동기 함수를 요청합니다. 그리고 transition 이후에 isPending 상태를 false로 변경합니다. 이를 통해 데이터가 변경되는 동안 현재 UI를 반응성 있고 상호작용할 수 있도록 유지됩니다.
메모
비동기 transition을 수행하는 함수를 "Action"이라고 부릅니다.
이러한 작업은 데이터 제출을 자동으로 관리하여 개발자의 수고를 덜어줍니다. Pending 상태: Actions는 요청이 시작되면 Pending 상태로 전환되며, 최종 상태 업데이트가 완료되면 자동으로 재설정됩니다.
Optimistic Update: 새로운useOptimistic훅을 통해 요청이 진행되는 동안 사용자에게 즉각적인 피드백을 제공할 수 있습니다.
Error Handling: Actions는 Error Handling을 지원하여 요청 실패 시 Error Boundaries를 표시하고, Optimistic Update를 원래 값으로 되돌릴 수 있습니다.
Forms:<form>요소는 이제 action 및 formAction props를 통해 함수를 전달할 수 있으며, action props에 함수를 전달하면 기본적으로 Actions를 사용하고 submit 후 자동으로 form을 재설정합니다. React 19에서는 Actions을 기반으로 Optimistic Update를 처리하기 위한useOptimistic훅과, Actions에서 공통적인 케이스를 다루기 위한 새로운 훅인React.useActionState를 제공합니다. 또한,react-dom에서는 자동으로 폼을 관리하기 위해<form>에 Actions를 추가하였습니다. 이와 함께, 폼 내에서 액션의 공통 케이스를 지원하기 위해useFormStatus훅도 새롭게 도입되었습니다.
// <form> Actions, useActionState 사용 예시
function ChangeName({ name, setName }) {
const [error, submitAction, isPending] = useActionState(async (previousState, formData) => {
const error = await updateName(formData.get("name"))
if (error) {
return error
}
redirect("/path")
return null
}, null)
return (
<form action={submitAction}>
<input type="text" name="name" />
<button type="submit" disabled={isPending}>
Update
</button>
{error && <p>{error}</p>}
</form>
)
}
React 19의 새로운 Action 기능들을 자세히 살펴보겠습니다.
useActionState
일반적인 케이스에서 useTransition을 사용하는 것보다 Actions를 비교적 더 쉽게 사용하기 위해 useActionState 훅을 사용합니다.
const [error, submitAction, isPending] = useActionState(async (previousState, newName) => {
const error = await updateName(newName)
if (error) {
// action의 어떠한 결과도 반환할 수 있습니다.
// 이 스코프에서는 오직 에러만 반환합니다.
return error
}
// 요청 성공 로직
return null
}, null)
useActionState는 파라미터로 function('action')을 받습니다. 그리고 이 Action을 호출하는 Wrapper Action을 반환합니다. Wrapper Action이 호출될 . 때 useActionState는 Action의 마지막 결과로 data를 반환하고, Action의 Pending 상태인 pending을 반환합니다.
useActionState는 이전에 Canary Release에서useFormState로 불렸습니다. 하지만useFormState를 deprecated하고 더 이상 사용되지 않습니다.
자세한 내용은 #28491에서 확인할 수 있습니다.useActionState에 대해서 더 많은 정보를 확인하고 싶으시다면 여기를 확인하세요
Form Actions
Actions는 React 19의 새로운 <form>기능과 통합되었습니다. Actions를 사용하여 자동으로 <form>을 submit하기 위해 action props로 함수를 넘겨 Actions와 함께 submit할 수 있도록 지원합니다.
<form action={actionFunction}></form>
<form>의 Action이 성공했을 때, React는 uncontrolled 요소에 대해 자동으로 form을 재설정합니다. 만약 수동으로 재설정하려면, 새로운 React DOM API requestFormReset을 호출하면 됩니다.
더 많은 정보를 확인하고 싶으시다면 react-dom 공식 문서에서<form>, <input> 그리고 <button>을 확인하세요.
useFormStatus
<form> 디자인 시스템에서는 props를 컴포넌트까지 파고들지 않고도 <form>에 대한 정보에 접근해야하는 경우가 일반적입니다. 이 과정은 Context를 통해 구현할 수 있지만, 더 쉽게 작업하기 위해 useFormStatus라는 새로운 Hook이 등장했습니다.
const { pending, data, method, action } = useFormStatus();
import { useFormStatus } from 'react-dom';
// 자식 컴포넌트
function SubmitButton() {
const { pending } = useFormStatus();
return <button type='submit' disabled={pending} />
}
// 부모 컴포넌트
function Form({ action }) {
return (
<form action={action}>
<SubmitButton />
</form>
)
}
useFormStatus는 Context.Provider와 유사하게 부모 <form>의 상태를 읽습니다.
이전에는 위와 같은 작업을 하기 위해서 Context.Provider를 사용하여 자식 컴포넌트에서는 Context에 접근하여 <form>에 대한 상태를 가져왔습니다.
type FormStatus = {
pending: boolean;
...
}
const FormStatusContext = createContext<FormStatus | null>(null)
// 자식 컴포넌트
function SubmitButton() {
const { pending } = useContext(FormStatusContext);
return <button type='submit' disabled={pending} />
}
// 부모 컴포넌트
function Form() {
return (
<FormStatusContext.Provider value={formStatus}>
<form>
<SubmitButton />
</form>
</FormStatusContext.Provider>
)
}
이렇게 비교하니 DX가 많이 좋아진 것이 한 눈에 보이는 것 같습니다.
useFormStatus에 대해서 더 많은 정보를 확인하고 싶으시다면 여기를 확인하세요
useOptimistic
Data mutation을 수행할 때 많이 사용되는 또 다른 패턴은 비동기 요청이 진행되는 동안 최종 상태를 낙관적으로 보여주는 것입니다. Reat 19에서는 이를 더 쉽게 하기 위해 useOptimistic이라는 새로운 Hook이 등장했습니다.
낙관적인(Optimistic) 가 생소하신 분들을 위해서 UI를 인스타그램의 좋아요로 예를 들어보겠습니다.
ㄴ 좋아요 버튼 클릭: 사용자가 좋아요 버튼을 클릭
ㄴ UI 업데이트: 클릭 즉시 UI가 업데이트되어 하트 아이콘이 채워짐(Optimistic Update)
ㄴ 서버에 전송: 좋아요 요청을 서버로 전송
ㄴ 서버 응답: 서버에서 좋아요 처리가 정상적으로 완료되면 UI는 변경된 상태를 유지하고, 오류가 발생하면 UI는 이전 상태로 돌아감. 이름을 변경하는 Action으로 예시를 보겠습니다:
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 훅은 updateName 요청이 진행되는 동안 optimisticName을 즉시 렌더링합니다. 업데이트가 완료되거나 오류가 발생하면 React는 자동으로 currentName 값으로 되돌립니다.
import { useOptimistic } from "react"
function AppContainer() {
const [optimisticState, addOptimistic] = useOptimistic(
state,
// 업데이트 함수
(currentState, optimisticValue) => {
//optimistic 값과 merge 및 새로운 상태 반환
}
)
}
useOptimistic에 대해서 더 많은 정보를 확인하고 싶으시다면 여기를 확인하세요
new api use
React 19에서는 렌더링 중에 리소스를 읽을 수 있는 새로운 API인 use를 추가했습니다.
예를 들어, use를 사용하여 Promise를 읽을 수 있고, Promise가 resolve될 때까지 Suspend할 수 있습니다.
// use를 사용한 비동기 요청
import { use } from "react";
function Comments({ commentsPromise }) {
// use는 promise가 resolve될 때까지 Suspend합니다.
const comments = use(commentsPromise)
return comments.map((comment) => <p key={comment.id}>{comment}</p>)
}
function Page({ commentsPromise }) {
// `use`가 Comments를 Suspend할 때, Suspense Boundary가 보여집니다.
return (
<Suspense fallback={<div>Loading...</div>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
)
}
// 기존 비동기 요청
const getComments = async () => {
return new Promise<string[]>((resolve) => {
setTimeout(() => {
resolve(['hello', 'world']);
}, 2000)
})
}
function App() {
const [comments, setComments] = useState<string[]>([]);
const getCommentList = async () => {
const comments = await getComments();
setComments(comments);
};
useEffect(() => {
getCommentList();
}, [])
return <div>{ comments ? <Comment comments={comments} /> : "Loading..." }</div>;
}
function Comment({ comments }: { comments: string[] }) {
return (
<ul>
{comments.map((comment, index) => (
<li key={index}>{comment}</li>
))}
</ul>
);
}
더 이상 비동기 데이터에 대해 useEffect를 이용한 패턴을 사용하지 않아도 되겠네요!
메모
plain text useplain text use
또한 Context를 읽을 때도 use를 사용할 수 있습니다. 컴포넌트 최상단에 return문을 사용해 (ealry return) 조건부로 Context를 읽는 것도 가능합니다.
import { use } from 'react';
import ThemeContext from './ThemeContext'
function Heading({children}) {
if (children == null) {
return null;
}
// 만약 그냥 useContext를 사용한다면 React Hook 특성상
// 조건문이 최상단에 있어서 동작하지 않는다.
const theme = use(ThemeContext);
return (
<h1 style={{color: theme.color}}>
{children}
</h1>
);
}
use는 hook과 비슷하게 렌더링 중에 호출될 수 있습니다. 그런데 다른 hook과 달리, use는 조건부로 호출될 수 있습니다. 앞으로 use를 사용하여 렌더링 중에 리소스를 소비하는 더 많은 방법을 지원할 계획이라고 합니다.
use에 대해서 더 많은 정보를 확인하고 싶으시다면 여기를 확인하세요
Static APIs
정적 사이트 생성을 위해 React 19에서는 react-dom/static에 새로운 두가지 API를 추가했습니다.
- prerender
- prerenderToNodeStream
이 새로운 API는 정적 HTML 생성을 위해 데이터가 로드될 때까지 기다리는 방식으로
renderToString을 개선합니다. Node.js 스트림 및 웹 스트림과 같은 스트리밍 환경에서 작동하도록 설계되었습니다.
예를 들어, 웹 스트림 환경에서는 prerender를 사용하여 정적 HTML에 React 트리를 미리 렌더링할 수 있습니다:
import { prerender } from 'react-dom/static';
async function handler(request) {
const {prelude} = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}
prerender는 모든 데이터가 로드될 때까지 기다렸다가 정적 HTML 스트림을 반환합니다. 스트림은 문자열로 변환하거나 스트리밍 응답과 함께 전송할 수 있습니다. 기존 React DOM 서버 렌더링 API에서 지원하는 콘텐츠가 로드되는 동안의 스트리밍은 지원하지 않습니다.
자세한 내용은 React DOM Static APIs를 참조하세요.
RSC
Server Components
서버 컴포넌트는 번들링 전에 클라이언트 애플리케이션 또는 SSR 서버와 분리된 환경에서 컴포넌트를 미리 렌더링할 수 있는 새로운 옵션입니다. 이 별도의 환경이 React Server Component에서 “server”입니다. 서버 컴포넌트는 CI 서버에서 빌드 시 한 번 실행하거나 웹 서버를 사용하여 각 요청에 대해 실행할 수 있습니다.
React 19에는 Canary에 포함된 모든 React Server Components 기능이 포함되어 있습니다. 즉, 서버 컴포넌트와 함께 제공되는 라이브러리는 이제 풀스택 React 아키텍처를 지원하는 프레임워크(Next.js, Remix, ...)에서 사용하기 위해 react-server 조건부 export으로 React 19를 peer dependency로 타겟팅할 수 있습니다.
서버 컴포넌트를 지원하도록 만드려면?
React 19의 React 서버 컴포넌트는 안정적이며 주요 버전 간에 중단되지 않지만, React Server Components 번들러 또는 프레임워크를 구현하는 데 사용되는 기본 API는 셈버를 따르지 않으며 React 19.x의 마이너 버전 간에 중단될 수 있습니다.
번들러 또는 프레임워크로서 React Server Components를 지원하려면 특정 React 버전에 고정하거나 Canary 릴리스를 사용하는 것이 좋습니다. 앞으로도 번들러 및 프레임워크와 협력하여 React Server Components를 구현하는 데 사용되는 API를 안정화하기 위해 노력할 것입니다. 자세한 내용은 React Server Components 문서를 참조하세요.
Server Actions
서버 액션을 사용하면 클라이언트 컴포넌트가 서버에서 실행되는 비동기 함수를 호출할 수 있습니다.
서버 액션이 "use server" 지시어로 정의되면 프레임워크는 자동으로 서버 함수에 대한 참조를 생성하고 해당 참조를 클라이언트 컴포넌트에 전달합니다. 클라이언트에서 해당 함수가 호출되면 React는 서버에 함수를 실행하라는 요청을 보내고 결과를 반환합니다.
서버 컴포넌트에 대한 지시어는 없습니다.
흔히 오해하는 것은 서버 컴포넌트는"use server"으로 표시되지만 서버 컴포넌트에 대한 지시어는 없다는 것입니다."use server"지시어는 Server Actions에 사용됩니다. Server Actions는 Server Components에서 생성하여 Client Components에 props로 전달하거나 Client Components에서 가져와서 사용할 수 있습니다.
자세한 내용은 React Server Actions 문서를 참조하세요.
React 19 개선점
ref prop
React 19에서 함수 컴포넌트에서 ref prop을 사용할 수 있게 되었습니다.
나이스!
function MyInput({placeholder, ref}) {
return <input placeholder={placeholder} ref={ref} />
}
function Form() {
const ref = useRef()
return <MyInput ref={ref} />
}
새로운 함수 컴포넌트는 이제 forwardRef가 필요하지 않습니다!
새로운 ref prop을 사용하기 위해 codemod를 사용하여 자동으로 업데이트 할 수 있도록 제공할 것이라고 하네요
앞으로 forwardRef가 deprecate될 예정입니다.
메모
클래스 컴포넌트에서 전달하는 refs는 해당 컴포넌트 인스턴스를 참조하므로 prop으로로 전달되지 않습니다.
Hydration 에러에 대한 Diff 추가
React 19에서는 react-dom hydration에 대한 에러도 개선되었습니다.
예를 들어, 이전 개발 모드에서 mismatch에 대한 어떠한 에러도 없이 다수의 에러가 발생했습니다:

이제 mismatch한 정보가 하나의 메시지로 표시됩니다:

Context Provider
React 19에서는 이제 <Context.Provider>를 사용하지 않고<Context>를 바로 사용할 수 있습니다.
const ThemeContext = createContext("")
function App({ children }) {
return <ThemeContext value="dark">{children}</ThemeContext>
}
기존 <Context.Provider>를 새롭게 변경하는 codemod를 제공할 예정이라고 합니다.
앞으로는, <Context.Provider>는 deprecated될 예정입니다.
refs Cleanup Functions
ref 콜백 함수에서 cleanup function을 반환할 수 있도록 지원합니다.
<input
ref={(ref) => {
// ref 생성
// 엘리먼트가 DOM에서 제거될 때 ref를 리셋하기 위한
// cleanup function을 반환합니다.
return () => {
// ref cleanup
}
}}
/>
컴포넌트가 언마운트되면 React는 콜백 함수에서 반환된 cleanup function을 호출합니다. 이는 DOM ref, class components, useImerativeHandle에서 모두 동일하게 동작합니다.
메모
이전에는 React는 컴포넌트가 언마운트 할 때ref함수를null로 호출했습니다.ref함수가 cleanup function을 반환한다면, 이제 React는null로 호출하는 단계를 건너뜁니다.refcleanup function이 도입되어ref콜백에서 다른 것을 반환하는 것은 이제 TypeScript에서 에러가 날 수 있습니다.
해결책은 일반적으로 암묵적 반환(() => ())을 사용하지 않는 것입니다:
- <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />
원래 코드는 HTMLDivElement 인스턴스를 반환했고 TypeScript는 이것이 cleanup function을 반환하려는 것인지 아닌지 알 수 없습니다.
이 패턴은 no-implicit-ref-callback-return을 사용하여 수정할 수 있습니다.
useDeferredValue initial value
useDeferredValue의 initialValue 옵션을 추가하였습니다.
function Search({ deferredValue }) {
// 초기 렌더링 값이 '' 으로 설정된 후,
// 그런 다음 deferredValue와 함께 리렌더링으로 예약됩니다.
const value = useDeferredValue(deferredValue, "")
return <Results query={value} />
}
initialValue가 제공되면 useDeferredValue는 컴포넌트의 초기 값으로 value를 반환하고, 반환된 deferredValue로 백그라운드에서 다시 렌더링하도록 예약합니다.
자세한 정보는 useDeferredValue를 참조하세요.
Document Metadata 지원
HTML에서 <title>, <link>, <meta>와 같은 문서 메타데이터 태그는 문서의 <head> 섹션에 배치하기 위해 예약되어 있습니다. React에서는 앱에 적합한 메타데이터를 결정하는 컴포넌트가 <head>를 렌더링하는 위치에서 매우 멀리 떨어져 있거나 React가 <head>를 전혀 렌더링하지 않을 수 있습니다. 과거에는 이러한 엘리먼트를 effect를 통해서 수동으로 삽입하거나 react-helmet과 같은 라이브러리를 통해 삽입해야 했고, 서버에서 React 애플리케이션을 렌더링할 때 세심한 처리가 필요했습니다.
이제는 이러한 메타데이터 태그를 컴포넌트에서 native로 렌더링할 수 있도록 지원합니다:
function BlogPost({post}) {
return (
<article>
<h1>{post.title}</h1>
<title>{post.title}</title>
<meta name="author" content="Josh" />
<link rel="author" href="<https://twitter.com/joshcstory/>" />
<meta name="keywords" content={post.keywords} />
<p>
Eee equals em-see-squared...
</p>
</article>
);
}
위 컴포넌트를 렌더링할 때 <title>, <link>, <meta> 태그를 확인하고, 자동으로 <head> 섹션으로 호이스트 합니다. native로 이러한 메타데이터 태그를 지원함으로써 client-only app, streaming ssr, server components에서 모두 잘 동작합니다.
아직 메타데이터 라이브러리가 필요할 수도 있어요
간단한 사용 사례의 경우 Document 메타데이터를 렌더링하는 것이 적합할 수 있지만 라이브러리는 현재 라우트 기반으로 일반적인 메타데이터를 특정 메타데이터로 재정의하는 것과 같은 보다 강력한 기능을 제공합니다.react-helmet과 같은 라이브러리나 프레임워크를 통해서 이러한 기능을 사용하면 보다 더 쉽게 지원할 수도 있습니다. 자세한 정보는<title>,<link>,<meta>를 참조하세요.
StyleSheet 지원
스타일시트는 외부 링크(<link rel=“stylesheet” href=“...”>)와 인라인(<style>...</style>) 모두 스타일 우선순위 규칙으로 인해 DOM에서 신중하게 배치해야 합니다. 컴포넌트 내에서 구성성을 허용하는 스타일시트 기능을 구축하는 것은 어렵기 때문에 사용자는 모든 스타일을 의존할 수 있는 컴포넌트에서 멀리 떨어진 곳에 로드하거나 이러한 복잡성을 캡슐화하는 스타일 라이브러리를 사용하는 경우가 많습니다.
React 19에서는 이러한 복잡성을 해결하고 스타일시트를 기본 지원하여 클라이언트의 동시 렌더링과 서버의 스트리밍 렌더링에 더욱 긴밀하게 통합할 수 있게 되었습니다. React에 스타일시트의 우선순위를 지정하면 DOM에서 스타일시트의 삽입 순서를 관리하고 해당 스타일 규칙에 의존하는 콘텐츠를 표시하기 전에 스타일시트(외부인 경우)가 로드되는지 확인합니다.
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>
<link rel="stylesheet" href="baz" precedence="default" /> {/* <-- foo & bar 사이에 추가될 것입니다 */}
</div>
)
}
서버 사이드 렌더링 중에 React는 <head>에 스타일시트를 포함시켜 브라우저가 로드될 때까지 페인팅하지 않도록 합니다. 이미 스트리밍을 시작한 후 스타일시트가 늦게 발견되는 경우, React는 스타일시트가 클라이언트의 <head>에 삽입되었는지 확인한 후 해당 스타일시트에 의존하는 Suspense Boundary의 콘텐츠를 표시합니다.
클라이언트 사이드 렌더링 도중 React는 렌더링을 커밋하기 전에 새로 렌더링된 스타일시트가 로드될 때까지 기다립니다. 애플리케이션 내 여러 위치에서 이 컴포넌트를 렌더링하는 경우 React는 스타일시트를 문서에 한 번만 포함합니다:
function App() {
return (
<>
<ComponentOne />
...
<ComponentOne /> {/* DOM에 스타일 링크가 중복으로 추가되지 않습니다 */}
</>
)
}
스타일시트를 수동으로 로드하는 데 익숙한 사용자에게는 스타일시트에 의존하는 컴포넌트와 함께 해당 스타일시트를 찾을 수 있어 로컬 추론(Local Reasoning)이 향상되고 실제로 의존하는 스타일시트만 로드하기가 더 쉬워질 수 있는 기회입니다.
스타일 라이브러리 및 번들러와의 스타일 통합도 이 새로운 기능을 채택할 수 있으므로 직접 스타일시트를 렌더링하지 않더라도 이 기능을 사용하도록 도구가 업그레이드되면 이점을 누릴 수 있습니다.
자세한 내용은 <link>, <style>를 참조하세요.
async scripts 지원
HTML에서 일반 스크립트(<script src=“...”>)와 지연 스크립트(<script defer=“” src=“...”>)는 문서 순서대로 로드되므로 이러한 종류의 스크립트를 컴포넌트 트리의 깊은 곳에 렌더링하기가 어렵습니다. 그러나 비동기 스크립트(<script async=“” src=“...”>)는 임의의 순서로 로드됩니다.
React 19에서는 스크립트 인스턴스 재배치 및 중복 제거를 관리할 필요 없이 컴포넌트 트리의 어느 곳에서나 스크립트에 실제로 의존하는 컴포넌트 내에서 렌더링할 수 있도록 비동기 스크립트에 대한 지원이 개선되었습니다.
function MyComponent() {
return (
<div>
<script async={true} src="..." />
Hello World
</div>
)
}
function App() {
<html>
<body>
<MyComponent />
...
<MyComponent /> {/* DOM에 script가 중복으로 추가되지 않습니다 */}
</body>
</html>
}
모든 렌더링 환경에서 비동기 스크립트는 중복이 제거되어 여러 다른 컴포넌트에서 렌더링되더라도 React가 스크립트를 한 번만 로드하고 실행합니다.
서버 측 렌더링에서 비동기 스크립트는 <head>에 포함되며 스타일시트, 글꼴 및 이미지 프리로드와 같이 페인트를 차단하는 더 중요한 리소스보다 우선순위가 지정됩니다.
자세한 내용은 <script>를 참조하세요.
preloading 리소스 지원
초기 문서 로드 시와 클라이언트 측 업데이트 시 브라우저에 가능한 한 빨리 로드해야 할 리소스에 대해 알려주면 페이지 성능에 큰 영향을 미칠 수 있습니다.
React 19에는 브라우저 리소스를 로드하고 미리 로드하기 위한 새로운 API가 다수 포함되어 있어 비효율적인 리소스 로딩으로 인해 지연되지 않는 훌륭한 경험을 최대한 쉽게 구축할 수 있습니다.
import { preconnect, prefetchDNS, preinit, preload } from "react-dom"
function MyComponent() {
preinit("https://.../path/to/some/script.js", { as: "script" }) // 열심히 이 sciprt를 로드하고 실행합니다.
preload("https://.../path/to/font.woff", { as: "font" }) // 이 font를 preload합니다.
preload("https://.../path/to/stylesheet.css", { as: "style" }) // 이 스타일시트를 preload합니다.
prefetchDNS("https://...") // 이 호스트에서 실제로 아무것도 요청하지 않을 때
preconnect("https://...") // 무언가를 요청할 것이지만 확실하지 않은 경우
}
<!-- 위 결과는 아래와 같은 DOM/HTML이 생성됩니다. -->
<html>
<head>
<!-- links/scripts는 호출 순서가 아닌 early loading 유틸리티에 따라 우선순위가 지정됩니다. -->
<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>
이러한 API를 사용하면 글꼴과 같은 추가 리소스의 검색을 스타일시트 로딩에서 제외하여 초기 페이지 로드를 최적화할 수 있습니다. 또한 예상 탐색에 사용되는 리소스 목록을 미리 가져온 다음 클릭 또는 마우스오버 시 해당 리소스를 미리 로드하여 클라이언트 업데이트 속도를 높일 수 있습니다.
자세한 정보는 Resource Preloading APIs를 참조하세요.
third-party script와 확장 프로그램 호환성
third-party 및 브라우저 확장 프로그램을 고려하도록 hydration을 개선했습니다.
hydrating 시 클라이언트에서 렌더링되는 element가 서버의 HTML에서 찾은 element와 일치하지 않으면 React는 클라이언트에서 강제로 다시 렌더링하여 콘텐츠를 수정합니다. 이전에는 third-pary script나 브라우저 확장 프로그램에 의해 element가 삽입된 경우 불일치 오류가 발생하고 클라이언트 렌더링이 트리거되었습니다.
React 19에서는 <head>와 <body>의 예기치 않은 태그를 건너뛰어서 불일치 오류를 방지합니다. 관련 없는 hydration 불일치로 인해 React가 전체 document를 렌더링해야 하는 경우, third-pary 및 브라우저 확장 프로그램에 의해 삽입된 스타일시트를 그대로 유지합니다.
더 나은 에러
React 19에서는 중복을 제거하고 caught 오류와 uncaught 오류를 처리하는 옵션을 제공하기 위해 오류 처리를 개선했습니다.
예를 들어, 렌더링에서 Error Boundary에 caught 오류가 발생하면 이전에는 React가 오류를 두 번(원래 오류에 대해 한 번, 자동 복구에 실패한 후 한 번) 발생시킨 다음 오류가 발생한 위치에 대한 정보와 함께 console.error를 호출했습니다.
그 결과 오류가 발견될 때마다 3개의 오류가 발생했습니다:

React 19에서는 모든 오류 정보가 포함된 단일 오류를 기록합니다:

또한, onRecoverableError를 보완하기 위해 두 가지 새로운 루트 옵션을 추가했습니다:
- onCaughtError: React가 에러 바운더리에서 에러를 포착할 때 호출됩니다.
- onUncaughtError: 에러가 발생했지만 에러 바운더리에서 잡히지 않았을 때 호출됩니다.
- onRecoverableError: 에러가 발생하고 자동으로 복구될 때 호출됩니다.
자세한 정보와 예제는
createRoot및hydrateRoot문서를 참조하세요.
커스텀 엘리먼트에 대한 지원
React 19는 Custom Element에 대한 완전한 지원을 추가하고
Custom Element Everywhere에 대한 테스트를 통과했습니다.
이전 버전에서는 React에서 Custom Element를 사용하는 것이 어려웠는데, 그 이유는 React가 인식되지 않는 prop을 property가 아닌 attribute로 취급했기 때문입니다. React 19에서는 다음과 같은 전략으로 클라이언트에서 작동하는 property와 SSR 중에 작동하는 property에 대한 지원을 추가했습니다:
- 서버 사이드 렌더링: 사용자 정의 엘리먼트에 전달된 property의 type이
string,number와 같은 primitive 값이거나 값이true인 경우 attribute로 렌더링됩니다.object,symbol,function또는 값false와 같이 primitive가 아닌 타입의 prop은 생략됩니다. - 클라이언트 사이드 렌더링: Custom Element 인스턴스의 property와 일치하는 property는 그대로 props로 할당되며, 그렇지 않은 경우 attribute으로 할당됩니다.
React 19 업그레이드 방법
React 19의 업그레이드 방법은 React 19 Upgrade Guide를 참고하세요.
마무리하며
React 19가 Beta 버전을 거쳐 드디어 안정적인 상태로 출시되었습니다.
새로운 hook이 추가 되고 native API도 추가되면서 개발 편의성이 크게 향상되었습니다.
포스트 작성 시간을 기준으로 하루 전, NextJS 15.1 또한 React 19를 안정적으로 지원하는 버전이 공개되었습니다. 이제 본격적으로 React 19와 그를 지원하는 NextJS 15.1 버전을 공부할 수 있을 것 같습니다.
