React 19 새 기능 실전 가이드 — use, Actions, Server Components 통합
React 19의 use hook, Actions, useActionState, useOptimistic 등 핵심 변경사항과 마이그레이션 팁.
한 줄 요약: React 19는 use 훅, Actions, form actions, 개선된 Server Components 통합으로 비동기 상태 관리와 서버-클라이언트 데이터 흐름을 크게 단순화했다. 기존 useEffect + useState 패턴 상당 부분을 더 선언적인 방식으로 대체할 수 있다.
2024년 12월 정식 출시된 React 19는 단순한 성능 업데이트가 아니라 비동기 처리와 서버-클라이언트 아키텍처에 대한 React 팀의 새로운 접근 방식을 담고 있다. use 훅으로 프로미스를 직접 읽고, Actions로 폼 제출과 낙관적 업데이트를 선언적으로 처리할 수 있다. 이 가이드는 실전 코드 중심으로 React 19의 핵심 변경사항을 정리한다.
use 훅 — 프로미스와 컨텍스트를 직접 읽기
use는 React 19에서 도입된 새로운 훅으로, 다른 훅과 달리 조건문 안에서도 호출 가능하다. 프로미스를 받으면 Suspense와 연동해 자동으로 로딩 상태를 처리하고, Context를 받으면 useContext처럼 동작한다.
TypeScript/React — use 훅으로 프로미스 읽기import { use, Suspense } from 'react' // 서버나 상위 컴포넌트에서 프로미스를 생성 async function fetchUser(id: string) { const res = await fetch(`/api/users/${id}`) if (!res.ok) throw new Error('사용자를 찾을 수 없습니다') return res.json() as Promise<{ name: string; email: string }> } // 클라이언트 컴포넌트에서 use로 프로미스 읽기 function UserProfile({ userPromise }: { userPromise: Promise<{ name: string; email: string }> }) { // Suspense 경계 안에서 자동으로 로딩 처리 const user = use(userPromise) return ( <div> <h2>{user.name}</h2> <p>{user.email}</p> </div> ) } // 상위 컴포넌트에서 Suspense로 감싸기 export function UserPage({ id }: { id: string }) { const userPromise = fetchUser(id) return ( <Suspense fallback={<div>사용자 정보 로딩 중...</div>}> <UserProfile userPromise={userPromise} /> </Suspense> ) }
TypeScript/React — 조건부 use (기존 훅과 다른 점)import { use, createContext } from 'react' const ThemeContext = createContext<'light' | 'dark'>('light') function ThemedButton({ show }: { show: boolean }) { // use는 조건문 안에서도 사용 가능 (useContext는 불가) if (!show) return null const theme = use(ThemeContext) // Context도 읽을 수 있음 return ( <button style={{ background: theme === 'dark' ? '#333' : '#fff' }}> 테마 버튼 </button> ) }
use에 전달할 때 컴포넌트 함수 내부에서 직접 fetch()를 호출하면 렌더링마다 새 프로미스가 생성돼 무한 루프가 발생한다. 프로미스는 반드시 컴포넌트 외부, 서버 컴포넌트, 또는 useMemo로 안정화해서 전달해야 한다.Actions — 폼 제출과 비동기 상태 관리
React 19의 Actions는 비동기 상태 전환을 처리하는 새로운 패러다임이다. useTransition의 startTransition에 async 함수를 전달하거나, form의 action prop에 함수를 직접 전달할 수 있다.
기존 패턴 vs Actions 패턴을 비교하면 차이가 명확해진다.
TypeScript/React — 기존 패턴 vs Actions 패턴 비교// ===== 기존 패턴 (React 18 이하) ===== import { useState } from 'react' function OldForm() { const [name, setName] = useState('') const [error, setError] = useState<string | null>(null) const [isPending, setIsPending] = useState(false) async function handleSubmit(e: React.FormEvent) { e.preventDefault() setIsPending(true) setError(null) try { await updateName(name) } catch (err) { setError(err instanceof Error ? err.message : '알 수 없는 오류') } finally { setIsPending(false) } } return ( <form onSubmit={handleSubmit}> <input value={name} onChange={(e) => setName(e.target.value)} /> {error && <p style={{ color: 'red' }}>{error}</p>} <button disabled={isPending}>{isPending ? '저장 중...' : '저장'}</button> </form> ) } // ===== React 19 Actions 패턴 ===== import { useActionState } from 'react' async function updateNameAction(prevState: string | null, formData: FormData) { const name = formData.get('name') as string try { await updateName(name) return null // 에러 없음 } catch (err) { return err instanceof Error ? err.message : '알 수 없는 오류' } } function NewForm() { const [error, submitAction, isPending] = useActionState(updateNameAction, null) return ( <form action={submitAction}> <input name='name' /> {error && <p style={{ color: 'red' }}>{error}</p>} <button disabled={isPending}>{isPending ? '저장 중...' : '저장'}</button> </form> ) }
useOptimistic으로 낙관적 업데이트 구현
useOptimistic은 비동기 작업이 완료되기 전에 UI를 즉시 업데이트하고, 작업 실패 시 자동으로 롤백하는 낙관적 업데이트 패턴을 선언적으로 구현한다.
TypeScript/React — useOptimistic 낙관적 좋아요 버튼import { useOptimistic, useTransition } from 'react' interface Post { id: string title: string likes: number liked: boolean } function LikeButton({ post }: { post: Post }) { const [isPending, startTransition] = useTransition() // 낙관적 상태: 실제 서버 응답 전 UI 선반영 const [optimisticPost, updateOptimistic] = useOptimistic( post, (currentPost, liked: boolean) => ({ ...currentPost, liked, likes: liked ? currentPost.likes + 1 : currentPost.likes - 1, }) ) function handleLike() { startTransition(async () => { // UI 즉시 업데이트 updateOptimistic(!optimisticPost.liked) try { // 서버 요청 (실패 시 optimisticPost는 원래 post 값으로 자동 복원) await toggleLike(post.id) } catch (err) { console.error('좋아요 처리 실패:', err) // useOptimistic이 자동으로 롤백 } }) } return ( <button onClick={handleLike} disabled={isPending}> {optimisticPost.liked ? '♥' : '♡'} {optimisticPost.likes} </button> ) } async function toggleLike(postId: string) { const res = await fetch(`/api/posts/${postId}/like`, { method: 'POST' }) if (!res.ok) throw new Error('좋아요 처리 실패') }
Server Components 통합과 실전 패턴
React 19에서 Server Components(RSC)는 Next.js App Router와의 통합이 더 안정화됐다. 서버에서 데이터를 직접 fetch하고, 직렬화된 결과를 클라이언트에 스트리밍하는 패턴이 실무 표준으로 자리잡았다.
TypeScript/React — Server Component + Client Component 분리 패턴// app/posts/page.tsx — Server Component (기본값) // 서버에서 직접 DB/API 접근, 클라이언트에 JS 번들 없음 import { Suspense } from 'react' import { PostList } from './PostList' async function getPosts() { // 서버에서 직접 DB 쿼리 가능 const res = await fetch('https://api.example.com/posts', { next: { revalidate: 60 } // ISR: 60초 캐시 }) return res.json() } export default async function PostsPage() { const posts = await getPosts() return ( <div> <h1>블로그 포스트</h1> <Suspense fallback={<div>목록 로딩 중...</div>}> {/* 정적 데이터는 서버에서 렌더링 */} <PostList initialPosts={posts} /> </Suspense> </div> ) } // app/posts/PostList.tsx — Client Component (인터랙션 필요) 'use client' import { useState, useOptimistic, useTransition } from 'react' interface PostListProps { initialPosts: Array<{ id: string; title: string; published: boolean }> } export function PostList({ initialPosts }: PostListProps) { const [posts, setPosts] = useState(initialPosts) const [optimisticPosts, updateOptimistic] = useOptimistic(posts) const [isPending, startTransition] = useTransition() async function togglePublish(id: string) { startTransition(async () => { updateOptimistic((prev) => prev.map((p) => (p.id === id ? { ...p, published: !p.published } : p)) ) await fetch(`/api/posts/${id}/publish`, { method: 'PATCH' }) // 서버 상태와 동기화 const updated = await fetch('/api/posts').then((r) => r.json()) setPosts(updated) }) } return ( <ul> {optimisticPosts.map((post) => ( <li key={post.id}> {post.title} <button onClick={() => togglePublish(post.id)} disabled={isPending}> {post.published ? '비공개로 전환' : '공개'} </button> </li> ))} </ul> ) }
React 18에서 19로 마이그레이션 가이드
React 19는 대부분의 기존 코드와 하위 호환되지만, 일부 deprecated API와 동작 변경이 있다. 주요 변경사항을 확인하고 단계적으로 업그레이드하자.
Bash — React 19 업그레이드# React 19 및 타입 패키지 업그레이드 npm install react@19 react-dom@19 npm install -D @types/react@19 @types/react-dom@19 # 타입 에러 확인 (업그레이드 후 필수) npx tsc --noEmit # 코드모드: React 18 deprecated API 자동 변환 npx codemod@latest react/19/migration-recipe
next@15로 업그레이드 시 React 19가 함께 설치된다. App Router 사용 시 Server Components와 Actions의 대부분 기능을 바로 활용할 수 있다. Pages Router는 Actions 일부 기능이 제한될 수 있다.참고 자료:
- React 19 공식 블로그: react.dev/blog/2024/12/05/react-19
- React 19 업그레이드 가이드: react.dev/blog/2024/04/25/react-19-upgrade-guide
- useActionState API 문서: react.dev/reference/react/useActionState
- useOptimistic API 문서: react.dev/reference/react/useOptimistic