React 19가 안정 버전으로 나왔다. 체감상 가장 큰 변화들을 실제로 써보면서 정리했다.
use() 훅
가장 인상적이었던 건 use() 훅이다. 기존에 useEffect + useState로 처리하던 비동기 데이터 페칭을 훨씬 간결하게 쓸 수 있게 됐다.
import { use, Suspense } from 'react';
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <div>{user.name}</div>;
}
function App() {
const userPromise = fetchUser(userId);
return (
<Suspense fallback={<Spinner />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
Promise를 직접 컴포넌트에 넘기고, Suspense로 로딩 상태를 처리하는 방식이다. Context를 조건부로 읽을 수도 있는데, 이건 기존 useContext에서는 안 됐던 거라 꽤 유용하다. if 블록 안에서 use(MyContext)를 써도 Rules of Hooks를 위반하지 않는다.
useOptimistic
낙관적 업데이트를 공식 API로 지원하기 시작했다. 기존에는 직접 상태 롤백 로직을 구현해야 했다.
function LikeButton({ postId, initialLikes }: Props) {
const [likes, setLikes] = useState(initialLikes);
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(state, amount: number) => state + amount
);
async function handleLike() {
addOptimisticLike(1);
await likePost(postId);
setLikes(prev => prev + 1);
}
return <button onClick={handleLike}>{optimisticLikes} likes</button>;
}
네트워크 요청이 실패하면 자동으로 이전 상태로 돌아간다. 직접 try/catch에서 rollback 처리하던 시절에 비하면 훨씬 깔끔하다.
Server Actions
'use server' 디렉티브를 붙인 함수는 서버에서 실행된다. 폼 제출이나 데이터 mutation에 자주 쓴다.
// actions/post.ts
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
await db.post.create({ data: { title } });
revalidatePath('/posts');
}
// components/CreatePostForm.tsx
import { createPost } from '@/actions/post';
export function CreatePostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="제목 입력" />
<button type="submit">작성</button>
</form>
);
}
API 라우트를 따로 안 만들어도 된다는 게 편하다. 다만 Server Actions는 POST로만 호출되니까 GET 데이터 페칭에는 못 쓴다. 그리고 useFormStatus로 제출 중 상태를 읽을 수 있다.
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? '처리 중...' : '제출'}</button>;
}
forwardRef 사라짐
React 19부터 ref를 일반 prop처럼 바로 받을 수 있다. forwardRef로 감싸는 보일러플레이트가 없어졌다.
// 이제 이렇게 쓸 수 있다
function Input({ ref, ...props }: React.ComponentProps<'input'>) {
return <input ref={ref} {...props} />;
}
마이그레이션 가이드에 자동 변환 codemod도 제공하니까, 기존 코드 올릴 때 참고하면 된다.
큰 변화처럼 보여도 실제로 마이그레이션 부담은 크지 않았다. use()와 useOptimistic은 바로 써먹을 수 있고, Server Actions는 Next.js 앱에서 이미 쓰던 방식이라 자연스럽게 연결됐다. forwardRef 정리가 제일 반가웠다.