React 19 새로운 기능 총정리 — 뭐가 바뀌었고 뭘 써야 하나
React 19의 핵심 변경사항 정리. Actions, useActionState, use(), ref 개선, 서버 컴포넌트까지 실무에서 알아야 할 것들.

React 19가 정식 출시된 지 좀 됐는데, 아직 18에 머물러 있는 프로젝트가 많다. 바꿀 이유가 뭔지, 바꾸면 뭐가 달라지는지 감이 안 오니까. 이 글에서는 "실무에서 실제로 영향을 주는 변경사항" 위주로 정리한다. 내부 아키텍처 변경 같은 건 빼고, 코드를 쓸 때 체감되는 것들만.
Actions — 폼 처리의 패러다임 전환
React 19에서 가장 큰 변화를 꼽으라면 이거다. 폼 제출과 데이터 변경을 다루는 방식이 근본적으로 바뀌었다.
기존에 폼을 처리하려면 이런 보일러플레이트가 필요했다:
// React 18 방식
function LoginForm() {
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);
async function handleSubmit(e) {
e.preventDefault();
setIsPending(true);
setError(null);
try {
await login(formData);
} catch (err) {
setError(err.message);
} finally {
setIsPending(false);
}
}
return <form onSubmit={handleSubmit}>...</form>;
}
로딩 상태, 에러 상태, try-catch... 매번 같은 패턴의 반복. 폼이 10개면 이 보일러플레이트도 10번 반복된다. React 19에서는 action prop으로 이걸 깔끔하게 처리한다:
// React 19 방식
function LoginForm() {
async function loginAction(formData: FormData) {
"use server"; // 서버에서 실행 (Next.js 등)
await login(formData);
}
return <form action={loginAction}>...</form>;
}
<form action>에 비동기 함수를 넘기면 React가 자동으로 pending 상태를 관리한다. 훨씬 적은 코드로 같은 결과를 얻는다. e.preventDefault()를 직접 호출할 필요도 없어졌다.
useActionState — 폼 상태 관리의 정석
Actions와 세트로 쓰는 훅이다. 폼의 상태(결과, 에러, 로딩)를 한번에 관리해준다.
import { useActionState } from "react";
function ContactForm() {
const [state, submitAction, isPending] = useActionState(
async (prevState, formData: FormData) => {
const result = await sendMessage(formData);
if (!result.success) return { error: result.message };
return { success: true };
},
null // 초기 상태
);
return (
<form action={submitAction}>
<input name="message" />
<button disabled={isPending}>
{isPending ? "전송 중..." : "전송"}
</button>
{state?.error && <p className="text-red-500">{state.error}</p>}
{state?.success && <p className="text-green-500">전송 완료</p>}
</form>
);
}
isPending이 자동으로 관리되니까 useState로 로딩 상태를 따로 관리할 필요가 없다. 에러 상태도 반환값으로 처리하면 돼서 try-catch 보일러플레이트가 사라진다. 이전에는 서드파티 라이브러리(React Hook Form 등)에 의존하던 패턴이 이제 React 자체에서 지원되는 거다.
useOptimistic — 낙관적 업데이트
좋아요 버튼을 누르면 서버 응답을 기다리지 않고 UI를 먼저 업데이트하는 패턴. 사용자 체감 속도를 높이는 대표적인 기법인데, 이전에는 직접 구현해야 했다. 이제 훅으로 제공된다.
import { useOptimistic } from "react";
function LikeButton({ count, onLike }) {
const [optimisticCount, addOptimistic] = useOptimistic(
count,
(current, increment: number) => current + increment
);
return (
<form action={async () => {
addOptimistic(1); // 즉시 UI 업데이트
await onLike(); // 서버 요청은 백그라운드에서
}}>
<button>좋아요 {optimisticCount}</button>
</form>
);
}
서버 요청이 실패하면 자동으로 이전 상태로 롤백된다. 별도의 롤백 로직을 안 짜도 되는 거다. SNS 피드, 댓글, 투표 같은 인터랙션이 많은 UI에서 유용하다.
use() — Promise와 Context를 직접 읽기
새로운 API use()는 컴포넌트 안에서 Promise를 직접 "읽을" 수 있게 해준다.
import { use } from "react";
function UserProfile({ userPromise }) {
const user = use(userPromise); // Suspense가 로딩 처리
return <h1>{user.name}</h1>;
}
useEffect + useState로 비동기 데이터를 로드하던 패턴을 대체한다. Suspense와 결합하면 로딩 상태도 자동으로 처리된다. 컴포넌트 코드가 훨씬 간결해진다.
use()는 다른 훅들과 달리 조건부로 호출할 수 있다. if 문 안에서 쓸 수 있다는 뜻이다. 이건 기존 훅 규칙의 예외인데, use()가 훅이 아니라 API라서 가능한 거다. 조건에 따라 다른 데이터를 읽어야 하는 상황에서 유연하게 쓸 수 있다.
Context도 use()로 읽을 수 있다:
const theme = use(ThemeContext); // useContext(ThemeContext) 대체
useContext가 사라지는 건 아니지만, 조건부 호출이 필요한 경우에는 use()가 유용하다.
ref가 간단해졌다
React 18까지는 함수 컴포넌트에 ref를 전달하려면 forwardRef로 감싸야 했다. 보일러플레이트가 귀찮아서 ref 전달을 꺼리는 경우도 있었다.
// React 18 — forwardRef 필요
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <input ref={ref} {...props} />;
});
React 19에서는 ref가 일반 prop으로 전달된다:
// React 19 — 그냥 prop
function Input({ ref, ...props }: InputProps & { ref?: Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
forwardRef 래퍼가 필요 없다. 코드가 간결해지고, 타입 추론도 깔끔해진다. forwardRef는 아직 작동하지만 향후 deprecated될 예정이다. 새로 작성하는 코드에서는 prop으로 받는 방식을 쓰는 게 맞다.
메타데이터 네이티브 지원
<title>, <meta>, <link> 같은 메타데이터 태그를 컴포넌트 안에서 직접 렌더링하면 React가 자동으로 <head>에 호이스팅해준다.
function BlogPost({ post }) {
return (
<>
<title>{post.title}</title>
<meta name="description" content={post.description} />
<article>{post.content}</article>
</>
);
}
이전에는 react-helmet이나 Next.js의 Head 컴포넌트가 필요했던 부분이다. React 자체에서 지원하니까 별도 라이브러리 의존성이 줄어든다. 특히 서드파티 메타데이터 라이브러리의 버전 충돌이나 SSR 호환성 문제를 겪었던 사람이라면 반가운 변경이다.
스타일시트 로딩 순서 관리(<link rel="stylesheet" precedence="high">)도 추가되어서, 컴포넌트 단위로 CSS를 로드할 때 순서 충돌을 방지할 수 있다.
그 외 실용적인 변경들
ref 콜백 클린업 — ref 콜백에서 클린업 함수를 반환할 수 있게 됐다. DOM 요소가 언마운트될 때 정리 작업을 하기 편해졌다. 예를 들어 IntersectionObserver를 ref 콜백 안에서 연결하고, 클린업에서 해제하는 패턴이 깔끔해진다.
에러 리포팅 개선 — onCaughtError, onUncaughtError 콜백이 추가되어 에러 바운더리의 동작을 더 세밀하게 제어할 수 있다. 에러 발생 시 로깅이나 모니터링 서비스에 보내는 처리가 편해졌다.
<Context> 직접 사용 — <Context.Provider>대신 <Context>를 바로 쓸 수 있다. 작은 변화지만 코드가 한 줄 짧아진다. Provider를 감싸는 네스팅이 줄어드는 것도 가독성 측면에서 좋다.
업그레이드해야 하나
신규 프로젝트라면 React 19로 시작하면 된다. Actions, useActionState, ref 개선 등이 개발 경험을 확실히 좋게 만든다.
기존 프로젝트는 상황에 따라 다르다. React 19에는 breaking change가 일부 있다 — propTypes 런타임 체크 제거, 기존의 ReactDOM.render 완전 제거, 일부 내부 API 변경 등. 서드파티 라이브러리 호환성도 확인해야 한다. 핵심 UI 라이브러리(Material UI, Ant Design, Chakra UI 등)가 React 19를 지원하는지 먼저 체크하고 마이그레이션 계획을 세우는 게 안전하다.
급하지 않다면 생태계가 안정될 때까지 기다려도 된다. React 18로도 프로덕션 앱 만드는 데 아무 문제 없으니까. 다만 신규 기능을 쓰고 싶은 유혹은 점점 커질 거다. 라이브러리들이 React 19를 기본으로 가정하고 튜토리얼도 19 기준으로 나오기 시작하면, 그때는 마이그레이션 압력이 본격적으로 올 것이다.
마이그레이션 팁
실제로 업그레이드한다면 몇 가지 주의할 점이 있다.
먼저 React 18.3으로 올리자. React 18.3에는 19에서 제거될 API에 대한 deprecation 경고가 포함되어 있다. 이 경고를 전부 해결한 다음에 19로 올리면 깨지는 부분이 줄어든다.
서드파티 라이브러리 호환성을 체크하자. UI 라이브러리(MUI, Ant Design 등), 상태 관리(Zustand, Jotai 등), 폼 라이브러리(React Hook Form, Formik) 등이 React 19를 지원하는지 확인해야 한다. 핵심 라이브러리가 아직 지원하지 않으면 마이그레이션을 미루는 게 맞다.
점진적으로 새 API를 도입하자. 한번에 모든 폼을 Actions로 바꾸거나 모든 forwardRef를 제거할 필요는 없다. 새로 작성하는 코드부터 19 방식으로 쓰고, 기존 코드는 여유 있을 때 리팩토링하면 된다. React 19는 이전 API와의 호환성을 상당히 잘 유지하고 있어서, 점진적 전환이 가능하다.
TypeScript 사용자는 타입 업데이트에 주의하자. @types/react 버전을 올리면 일부 타입이 바뀌어서 컴파일 에러가 날 수 있다. 특히 forwardRef 관련 타입, 이벤트 핸들러 타입 등에서 변경이 있다. 타입 에러가 많이 나면 당황하지 말고 하나씩 수정하면 된다.
전체적으로 보면, React 19는 기존의 복잡하고 반복적이던 패턴을 프레임워크 레벨에서 간소화한 릴리즈다. 폼 처리, 비동기 데이터 로딩, ref 전달 등 매번 보일러플레이트를 써야 했던 부분이 크게 줄었다. 당장의 마이그레이션 부담보다 장기적인 개발 생산성 향상이 더 큰 업데이트라고 볼 수 있다.