React19 변경점 살펴보기
늦게나마..
React 19.2의<Activity />와useEffectEvent부터 React 19.0의 Actions와 제거된 API까지, 최신 버전부터 순서대로 살펴봅니다.
1. 들어가며
React 19는 2024년 12월 5일 정식으로 출시됐다. React 18이 동시성 렌더링의 기반을 마련한 버전이었다면, React 19는 그 기반 위에서 데이터 변경, 폼 제출, 비동기 상태, 리소스 로딩을 React의 API 안으로 가져온 버전에 가깝다.
새 기능만 추가된 것은 아니다. 오랫동안 deprecated 상태였던 ReactDOM.render, 문자열 ref, 레거시 Context 같은 API가 실제로 제거됐다. 반면 forwardRef처럼 더 이상 권장되지 않지만 아직 제거되지 않은 API도 있다.
이 글에서는 가장 최근에 추가된 기능부터 볼 수 있도록 React 19.2, 19.1, 19.0 순서로 내려간다. 이후 React 19에서 제거된 API와 마이그레이션 항목을 정리한다.
| 버전 | 출시일 | 주요 추가 기능 |
|---|---|---|
| React 19.2 | 2025-10-01 | <Activity />, useEffectEvent, cacheSignal, Partial Pre-rendering |
| React 19.1 | 2025-03-28 | Owner Stack, captureOwnerStack |
| React 19.0 | 2024-12-05 | Actions, use, ref prop, 문서·리소스 API |
2. React 19.2: Activity와 Effect Event
React 19.2는 2025년 10월 1일 출시됐다. 작성 시점의 공식 최신 마이너 버전이며, React 19.0에서 시작된 비동기 렌더링과 서버 렌더링의 방향을 이어간다.
2.1. <Activity />
<Activity>는 UI를 제거하지 않고 숨기면서 상태를 유지한다.
import { Activity } from "react";
<Activity mode={isEditorOpen ? "visible" : "hidden"}>
<Editor />
</Activity>hidden 상태에서는 자식의 Effect를 정리하고 업데이트의 우선순위를 낮춘다. 다시 visible로 전환하면 기존 입력 상태 등을 유지한 채 화면을 보여줄 수 있다.
2.2. useEffectEvent
Effect의 연결 조건과 Effect 안에서 발생하는 이벤트를 분리한다.
import { useEffect, useEffectEvent } from "react";
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification("연결했습니다.", theme);
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on("connected", onConnected);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
}theme이 바뀌어도 채팅 연결을 다시 만들 필요는 없지만, 알림은 최신 theme을 사용한다. useEffectEvent는 의존성 경고를 숨기는 용도가 아니라 Effect에서 발생하는 이벤트를 표현할 때 사용해야 한다.
2.3. 서버와 성능 도구
cacheSignal: Server Components에서cache()의 수명이 끝날 때 작업 취소- React Performance Tracks: Chrome DevTools에서 Scheduler와 컴포넌트 작업 확인
- Partial Pre-rendering: 정적 shell을 먼저 만들고 나중에 렌더링 재개
- Node.js에서 Web Streams 기반 SSR API 지원
이 기능들은 앱 코드보다 프레임워크와 렌더링 인프라에서 먼저 활용될 가능성이 크다.
3. React 19.1: Owner Stack
React 19.1은 2025년 3월 28일 출시됐다. 애플리케이션 코드에서 바로 사용하는 기능보다는 개발 도구와 오류 화면을 개선하기 위한 Owner Stack이 핵심 변화다.
Owner Stack은 특정 컴포넌트를 누가 만들었는지 보여준다. 오류가 발생한 위치에서 부모로 올라가는 Component Stack과 달리, JSX를 통해 해당 컴포넌트를 직접 생성한 컴포넌트를 추적한다.
captureOwnerStack을 사용하면 렌더링, Effect, React 이벤트 핸들러, React 오류 핸들러 안에서 현재 Owner Stack을 읽을 수 있다.
import * as React from "react";
function DebugPanel() {
if (process.env.NODE_ENV !== "production") {
const ownerStack = React.captureOwnerStack();
console.log("Owner Stack", ownerStack);
}
return <section>Debug</section>;
}captureOwnerStack은 개발 빌드에서만 제공되며 프로덕션에서는 사용할 수 없다. 일반적인 화면 기능보다 커스텀 오류 오버레이나 디버깅 도구를 만들 때 유용한 API다.
4. React 19.0: Actions와 폼
React 19에서 가장 큰 변화는 Actions다. 여기서 Action은 별도의 컴포넌트나 단일 API 이름이 아니다. transition 안에서 실행되는 비동기 함수와, 그 함수를 사용하는 React의 비동기 처리 방식을 가리키는 표현이다.
Actions는 다음 상태를 React의 렌더링 흐름 안에서 다룬다.
- 요청이 진행 중인지 나타내는 pending 상태
- 요청이 끝나기 전 결과를 먼저 보여주는 optimistic state
- 요청 실패와 Error Boundary 연동
- 폼 제출과 성공 후 초기화
4.1. useActionState
useActionState는 Action의 마지막 결과와 pending 상태를 관리한다. <form action>과 함께 사용하면 제출 이벤트를 직접 연결하지 않아도 된다.
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
type FormState = {
message: string;
error?: string;
};
const initialState: FormState = { message: "" };
async function updateProfile(
_previousState: FormState,
formData: FormData,
): Promise<FormState> {
const displayName = String(formData.get("displayName"));
const result = await saveDisplayName(displayName);
if (!result.ok) {
return { message: "", error: "저장하지 못했습니다." };
}
return { message: "저장했습니다." };
}
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "저장 중" : "저장"}
</button>
);
}
export default function ProfileForm() {
const [state, formAction] = useActionState(
updateProfile,
initialState,
);
return (
<form action={formAction}>
<input name="displayName" />
<SubmitButton />
{state.error && <p>{state.error}</p>}
{state.message && <p>{state.message}</p>}
</form>
);
}useFormStatus는 자신을 감싸는 부모 <form>의 제출 상태를 읽는다. 성공한 form Action은 uncontrolled input을 자동으로 초기화한다.
4.2. useOptimistic
useOptimistic은 서버 응답을 기다리는 동안 성공한 것처럼 UI를 먼저 갱신할 때 사용한다.
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(currentTodos, title: string) => [
...currentTodos,
{ id: "pending", title },
],
);Action이 완료되면 실제 상태로 전환되고, 실패하면 optimistic state를 원래 상태로 되돌릴 수 있다. 직접 임시 상태와 rollback 로직을 따로 관리하던 코드를 줄이는 데 의미가 있다.
4.3. Action과 Server Action은 같은 말이 아니다
Action은 클라이언트에서도 사용할 수 있는 비동기 처리 방식이다. Server Action은 "use server"로 표시된 함수를 프레임워크가 서버에서 실행하도록 연결한 경우를 말한다.
모든 Action이 서버에서 실행되는 것은 아니다. Server Action을 실제로 사용할 수 있는지는 Next.js 같은 프레임워크와 번들러의 지원 여부에 달려 있다.
5. React 19.0: 렌더링 중 리소스를 읽는 use
use는 렌더링 중 Promise나 Context 같은 리소스를 읽는다. Promise가 아직 끝나지 않았다면 가장 가까운 Suspense 경계로 렌더링을 넘긴다.
import { Suspense, use } from "react";
type CommentItem = {
id: string;
body: string;
};
function Comments({
commentsPromise,
}: {
commentsPromise: Promise<CommentItem[]>;
}) {
const comments = use(commentsPromise);
return (
<ul>
{comments.map((comment) => (
<li key={comment.id}>{comment.body}</li>
))}
</ul>
);
}
function CommentSection({
commentsPromise,
}: {
commentsPromise: Promise<CommentItem[]>;
}) {
return (
<Suspense fallback={<p>댓글을 불러오는 중...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
);
}일반 Hook과 달리 use는 조건문 안에서 호출할 수 있다. 다만 컴포넌트 렌더링 중 새 Promise를 만들어 바로 전달하면 안 된다. Suspense를 지원하는 프레임워크나 캐시 계층에서 만든 Promise를 전달해야 한다.
use(Context)도 지원하므로 early return 이후처럼 useContext를 호출하기 어려운 위치에서 Context를 조건부로 읽을 수 있다.
6. React 19.0: 단순해진 ref와 Context
6.1. 함수 컴포넌트에서 ref를 prop으로 받는다
React 18까지 함수 컴포넌트에 ref를 전달하려면 forwardRef가 필요했다.
const SearchInput = forwardRef(function SearchInput(props, ref) {
return <input {...props} ref={ref} />;
});React 19부터는 ref를 일반 prop처럼 받을 수 있다.
import type { ComponentPropsWithRef } from "react";
type SearchInputProps = ComponentPropsWithRef<"input">;
function SearchInput({ ref, ...props }: SearchInputProps) {
return <input {...props} ref={ref} />;
}새 컴포넌트에서는 forwardRef를 사용할 이유가 줄었다. 하지만 forwardRef 자체가 React 19에서 제거된 것은 아니다.
6.2. ref callback에서 정리 함수를 반환한다
ref callback은 Effect처럼 정리 함수를 반환할 수 있다.
<div
ref={(node) => {
if (!node) return;
observer.observe(node);
return () => {
observer.unobserve(node);
};
}}
/>이 변화로 TypeScript에서는 값을 암시적으로 반환하던 ref callback이 오류가 될 수 있다.
// 이전 코드: 대입 결과를 반환한다.
<div ref={(node) => (instance = node)} />
// React 19 타입에 맞는 코드
<div ref={(node) => { instance = node; }} />6.3. <Context.Provider>를 줄여 쓴다
Context 객체 자체를 Provider로 렌더링할 수 있다.
const ThemeContext = createContext("light");
function App() {
return (
<ThemeContext value="dark">
<Dashboard />
</ThemeContext>
);
}<ThemeContext.Provider>도 여전히 동작한다. React 팀은 향후 버전에서 이를 deprecated 처리할 계획이라고 밝혔지만 React 19에서 제거하지는 않았다.
7. React 19.0: 문서 메타데이터와 리소스 관리
React 19에서는 컴포넌트가 <title>, <meta>, <link>를 직접 렌더링할 수 있다. React는 이 요소들을 문서의 <head>로 이동한다.
function ProductPage({ product }) {
return (
<article>
<title>{product.name}</title>
<meta name="description" content={product.summary} />
<h1>{product.name}</h1>
</article>
);
}단순한 메타데이터는 별도 Effect 없이 처리할 수 있고, Client Rendering, Streaming SSR, Server Components에서도 같은 방식으로 사용할 수 있다.
스타일시트와 async script 처리도 React의 렌더링 흐름에 들어왔다.
<link rel="stylesheet" precedence="...">로 스타일 우선순위 관리- 컴포넌트 내부에서 렌더링한 async script의 이동과 중복 제거
preload,preinit,preconnect,prefetchDNS같은 리소스 API
import { preconnect, preload } from "react-dom";
function ProductLink() {
preconnect("<https://cdn.example.com>");
preload("/fonts/brand.woff2", {
as: "font",
type: "font/woff2",
crossOrigin: "",
});
return <a href="/products">상품 보기</a>;
}프레임워크가 리소스 로딩을 관리한다면 이 API를 직접 호출할 일은 많지 않다. 직접 SSR 환경이나 라우팅 계층을 구성할 때 더 의미가 크다.
8. React 19.0: 서버 렌더링과 개발 경험
React 19에는 Canary 채널에서 제공되던 Server Components 기능이 포함됐다. Server Components 자체는 React 19에서 안정화됐지만, 이를 프레임워크나 번들러에 통합하는 내부 API는 19.x 마이너 버전 사이에서도 바뀔 수 있다.
정적 생성에는 react-dom/static의 prerender, prerenderToNodeStream이 추가됐다. 기존 renderToString과 달리 데이터가 준비될 때까지 기다린 뒤 정적 HTML을 반환한다.
개발 과정에서 눈에 띄는 개선도 있다.
- hydration mismatch를 여러 경고 대신 하나의 diff로 표시
createRoot와hydrateRoot에onCaughtError,onUncaughtError추가- Suspense가 중단된 트리의 리소스를 미리 준비하는 방식 개선
- Custom Elements의 property와 attribute 처리 개선
렌더링 오류를 수집하는 방식도 바뀌었다. React 19는 렌더링 오류를 이전처럼 다시 throw하지 않는다. 오류 수집 도구가 재throw 동작에 의존했다면 root error callback으로 연결해야 한다.
9. React 19.0에서 없어진 것
제거된 기능 대부분은 React 19에서 갑자기 사라진 API가 아니다. 여러 해 동안 deprecated 상태였고 React 18.3에서 마이그레이션 경고가 추가됐던 항목들이다.
| 제거된 API 또는 방식 | 바꿀 방향 |
|---|---|
propTypes 검사 | TypeScript 등 정적 타입 도구 |
함수 컴포넌트의 defaultProps | 매개변수 기본값 |
contextTypes, getChildContext | createContext, contextType |
문자열 ref와 this.refs | ref 객체 또는 callback ref |
| Module pattern factory | 일반 함수 컴포넌트 |
React.createFactory | JSX |
react-test-renderer/shallow | Testing Library 권장 |
ReactDOM.render | createRoot(...).render(...) |
ReactDOM.hydrate | hydrateRoot(...) |
unmountComponentAtNode | root.unmount() |
findDOMNode | DOM ref |
renderToNodeStream | renderToPipeableStream 등 |
renderToStaticNodeStream | prerenderToNodeStream 등 |
| UMD 빌드 | 빌드 도구 또는 ESM CDN |
react-dom/test-utils의 act는 react 패키지로 이동했다. 나머지 저수준 test-utils 함수는 제거됐거나 호출 시 오류가 발생하므로 사용자 관점의 테스트 도구로 옮기는 편이 낫다.
// Before
import { act } from "react-dom/test-utils";
// After
import { act } from "react";루트 렌더링 코드는 다음처럼 바꾼다.
// Before
import { render } from "react-dom";
render(<App />, document.getElementById("root"));
// After
import { createRoot } from "react-dom/client";
const container = document.getElementById("root");
if (container) {
createRoot(container).render(<App />);
}10. 제거와 deprecated를 구분하기
React 19 관련 글에서 자주 섞이는 항목들이 있다.
| 항목 | React 19에서의 상태 |
|---|---|
forwardRef | 아직 사용 가능. 향후 제거 계획 |
<Context.Provider> | 아직 사용 가능. 향후 deprecated 계획 |
react-test-renderer | deprecated. 패키지 자체는 남아 있음 |
react-test-renderer/shallow | 제거됨 |
element.ref 접근 | deprecated. element.props.ref 사용 |
| Create React App | 별도 결정으로 deprecated. React 19에서 제거된 것은 아님 |
특히 forwardRef와 <Context.Provider>는 “이제 없어졌다”가 아니라 “새 코드에서 대체 문법을 사용할 수 있다”가 정확한 설명이다.
11. 마이그레이션 체크리스트
React 팀은 먼저 18.3으로 올려 deprecated 경고를 확인한 뒤 19로 이동하는 순서를 권장한다.
npm install react@19 react-dom@19
npm install --save-dev @types/react@19 @types/react-dom@19공식 migration recipe와 TypeScript codemod도 제공된다.
npx codemod@latest react/19/migration-recipe
npx types-react-codemod@latest preset-19 ./src업그레이드 전에 다음 항목을 확인한다.
- 새 JSX Transform을 사용하고 있는가?
ReactDOM.render,hydrate,findDOMNode가 남아 있는가?- 함수 컴포넌트에서
propTypes나defaultProps를 사용하는가? react-dom/test-utils나 shallow renderer에 의존하는 테스트가 있는가?- ref callback이 대입 결과를 암시적으로 반환하는가?
- 인자 없는
useRef()호출이 있는가? - 오류 수집 코드가 render error의 재throw에 의존하는가?
- React 내부 API를 직접 참조하는 라이브러리가 있는가?
React Server Components를 사용하는 앱은 초기 19.x 패키지에 공개된 보안 취약점도 확인해야 한다. 최초 릴리스에 머무르지 말고 사용하는 19.x 라인의 최신 patch 버전으로 올리는 것이 안전하다.
12. 마치며
React 19를 살펴보면 새 Hook 몇 개가 추가된 것보다 React가 책임지는 범위가 넓어진 점이 더 크게 보인다. 폼 제출과 비동기 상태, optimistic UI, 문서 메타데이터, 리소스 로딩이 React의 렌더링 모델 안으로 들어왔다.
없어진 API는 대부분 오래된 렌더링 방식과 레거시 escape hatch다. React 18에서 이미 createRoot, 현대적인 Context, callback ref를 사용하고 있었다면 마이그레이션의 크기는 크지 않을 수 있다. 반대로 오래된 테스트 도구나 클래스 기반 레거시 API가 남아 있다면 기능 도입보다 제거된 API를 정리하는 일이 먼저다.
개인적으로 React 19의 핵심은 새로운 문법보다 “비동기 작업을 UI 상태와 어떻게 연결할 것인가”에 대한 React의 답이 더 구체적으로 바뀌었다는 점이라고 생각한다.