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... 매번 같은 패턴의 반복. 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 상태를 관리한다. 훨씬 적은 코드로 같은 결과를 얻는다.
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 보일러플레이트가 사라진다.
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>
);
}
서버 요청이 실패하면 자동으로 이전 상태로 롤백된다. 별도의 롤백 로직을 안 짜도 되는 거다.
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될 예정이다.
메타데이터 네이티브 지원
<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 자체에서 지원하니까 별도 라이브러리 의존성이 줄어든다.
스타일시트 로딩 순서 관리(<link rel="stylesheet" precedence="high">)도 추가되어서, 컴포넌트 단위로 CSS를 로드할 때 순서 충돌을 방지할 수 있다.
그 외 실용적인 변경들
ref 콜백 클린업 — ref 콜백에서 클린업 함수를 반환할 수 있게 됐다. DOM 요소가 언마운트될 때 정리 작업을 하기 편해졌다.
에러 리포팅 개선 — onCaughtError, onUncaughtError 콜백이 추가되어 에러 바운더리의 동작을 더 세밀하게 제어할 수 있다.
<Context> 직접 사용 — <Context.Provider>대신 <Context>를 바로 쓸 수 있다. 작은 변화지만 코드가 한 줄 짧아진다.
업그레이드해야 하나
신규 프로젝트라면 React 19로 시작하면 된다. Actions, useActionState, ref 개선 등이 개발 경험을 확실히 좋게 만든다.
기존 프로젝트는 상황에 따라 다르다. React 19에는 breaking change가 일부 있다 — propTypes 런타임 체크 제거, 기존의 ReactDOM.render 완전 제거, 일부 내부 API 변경 등. 서드파티 라이브러리 호환성도 확인해야 한다. 핵심 UI 라이브러리가 React 19를 지원하는지 먼저 체크하고 마이그레이션 계획을 세우는 게 안전하다.
급하지 않다면 생태계가 안정될 때까지 기다려도 된다. React 18로도 프로덕션 앱 만드는 데 아무 문제 없으니까. 다만 신규 기능을 쓰고 싶은 유혹은 점점 커질 거다.