React 19 새 기능 — use, Actions, 그리고 달라진 것들
React 18에서 19로 넘어오면서 무엇이 달라졌을까요? 단순한 버전 업데이트가 아니라 React가 바라보는 방향 자체가 바뀐 건 아닐까요?
개념 정의
React 19는 2024년 12월에 정식 릴리스된 메이저 버전으로, use() 훅, Actions, React Compiler, ref as prop 등 개발 경험을 근본적으로 개선하는 변경사항들을 포함합니다.
왜 필요한가
React 18의 Concurrent 기능들이 저수준 도구에 가까웠다면, React 19는 이를 바탕으로 개발자가 직접 체감할 수 있는 고수준 기능 을 제공합니다. 특히 데이터 페칭, 폼 처리, 메모이제이션 같은 일상적 작업이 크게 간소화되었습니다.
use() 훅
기본 개념
use()는 Promise나 Context를 읽는 새로운 훅입니다. 가장 큰 특징은 다른 훅들과 달리 조건문이나 반복문 안에서 호출할 수 있다 는 것입니다.
Promise 읽기
import { use, Suspense } from 'react';
function UserProfile({ userPromise }) {
// Promise가 resolve될 때까지 가장 가까운 Suspense의 fallback을 보여줌
const user = use(userPromise);
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
function App() {
const userPromise = fetchUser(1); // Promise를 생성
return (
<Suspense fallback={<div>로딩 중...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
조건부 Context 읽기
function StatusMessage({ isSpecial }) {
// 조건문 안에서 use 호출 가능 — 기존 useContext로는 불가능했음
if (isSpecial) {
const theme = use(ThemeContext);
return <p style={{ color: theme.primary }}>특별 메시지</p>;
}
return <p>일반 메시지</p>;
}
Actions — 폼과 비동기 처리의 혁신
useActionState
비동기 액션의 상태(pending, 결과, 에러)를 선언적으로 관리합니다.
import { useActionState } from 'react';
function UpdateProfile() {
const [state, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const name = formData.get('name');
try {
await updateUser({ name });
return { success: true, message: '프로필이 업데이트되었습니다' };
} catch (error) {
return { success: false, message: error.message };
}
},
null // 초기 상태
);
return (
<form action={submitAction}>
<input name="name" placeholder="이름" />
<button type="submit" disabled={isPending}>
{isPending ? '저장 중...' : '저장'}
</button>
{state?.message && (
<p className={state.success ? 'success' : 'error'}>
{state.message}
</p>
)}
</form>
);
}
useFormStatus
폼 내부의 자식 컴포넌트에서 부모 폼의 제출 상태를 읽을 수 있습니다.
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '처리 중...' : '제출'}
</button>
);
}
function ContactForm() {
async function handleSubmit(formData) {
await sendMessage(formData);
}
return (
<form action={handleSubmit}>
<textarea name="message" />
<SubmitButton /> {/* 폼 상태를 자동으로 인식 */}
</form>
);
}
useOptimistic
서버 응답을 기다리지 않고 UI를 먼저 업데이트 하는 낙관적 업데이트를 쉽게 구현합니다.
import { useOptimistic } from 'react';
function TodoList({ todos, addTodoAction }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(currentTodos, newTodoText) => [
...currentTodos,
{ id: 'temp', text: newTodoText, sending: true },
]
);
async function handleSubmit(formData) {
const text = formData.get('todo');
addOptimisticTodo(text); // 즉시 UI 업데이트
await addTodoAction(text); // 서버에 실제 저장
}
return (
<div>
<form action={handleSubmit}>
<input name="todo" />
<button type="submit">추가</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.sending ? 0.5 : 1 }}>
{todo.text}
</li>
))}
</ul>
</div>
);
}
ref as prop — forwardRef 불필요
React 19에서는 함수형 컴포넌트가 ref를 일반 prop으로 받을 수 있습니다.
// React 18 이전 — forwardRef 필수
const Input = forwardRef(function Input(props, ref) {
return <input ref={ref} {...props} />;
});
// React 19 — 일반 prop으로 받기
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// 둘 다 같은 방식으로 사용
function Form() {
const inputRef = useRef(null);
return <Input ref={inputRef} placeholder="입력" />;
}
forwardRef는 여전히 동작하지만, 향후 deprecated될 예정입니다.
React Compiler
React Compiler(이전 이름: React Forget)는 컴파일 타임에 자동으로 메모이제이션을 적용 합니다.
// React Compiler가 자동으로 최적화하므로
// 개발자가 직접 useMemo/useCallback을 작성할 필요가 줄어듦
// 이전: 수동 메모이제이션
function ProductList({ products, onSelect }) {
const sorted = useMemo(
() => products.sort((a, b) => a.price - b.price),
[products]
);
const handleSelect = useCallback(
(id) => onSelect(id),
[onSelect]
);
return sorted.map(p => (
<ProductItem key={p.id} product={p} onSelect={handleSelect} />
));
}
// React Compiler 사용 시: 그냥 작성하면 됨
function ProductList({ products, onSelect }) {
const sorted = products.sort((a, b) => a.price - b.price);
return sorted.map(p => (
<ProductItem key={p.id} product={p} onSelect={() => onSelect(p.id)} />
));
}
React Compiler는 아직 선택적 도입(opt-in) 단계이며, 기존 코드와 점진적으로 호환됩니다.
Document Metadata
컴포넌트 내에서 <title>, <meta>, <link> 등을 렌더링하면 React가 자동으로 <head>로 호이스팅합니다.
function BlogPost({ post }) {
return (
<article>
<title>{post.title}</title>
<meta name="description" content={post.summary} />
<meta property="og:title" content={post.title} />
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
react-helmet이나 next/head 같은 서드파티 라이브러리 없이도 메타데이터를 관리할 수 있습니다.
기타 주요 변경사항
리소스 프리로딩
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';
function App() {
preinit('https://cdn.example.com/main.js', { as: 'script' });
preload('https://cdn.example.com/font.woff2', { as: 'font' });
preconnect('https://api.example.com');
return <div>앱 내용</div>;
}
에러 처리 개선
// 하이드레이션 에러 시 더 상세한 정보 제공
// 이전: "Text content does not match"
// React 19: 구체적인 불일치 내용과 위치를 표시
cleanup 함수를 반환하는 ref callback
function Component() {
return (
<div ref={(node) => {
// 마운트 시
node.style.opacity = '1';
// 언마운트 시 cleanup — React 19 새 기능
return () => {
node.style.opacity = '0';
};
}}>
내용
</div>
);
}
주의할 점
use()로 컴포넌트 내부에서 Promise를 생성하면 안 됨
use()에 전달하는 Promise는 컴포넌트 바깥 에서 생성해야 합니다. 렌더링 중에 use(fetch(...))처럼 새 Promise를 만들면, 매 렌더링마다 새 Promise가 생성되어 무한 Suspense 루프에 빠집니다.
// ❌ 렌더링 중 Promise 생성 — 무한 루프
function Bad({ id }) {
const data = use(fetchUser(id)); // 매 렌더마다 새 Promise
}
// ✅ 부모에서 Promise를 생성하여 전달
function Parent({ id }) {
const promise = useMemo(() => fetchUser(id), [id]);
return <Child userPromise={promise} />;
}
마이그레이션 시 breaking changes
defaultProps는 함수형 컴포넌트에서 deprecated — 구조 분해 기본값 사용propTypes는 런타임 체크에서 제거 — TypeScript 사용 권장forwardRef는 당장 제거되지 않지만 점진적 마이그레이션 권장- StrictMode에서 이중 렌더링 시
console.log음소거가 해제됨
React Compiler의 전제 조건
React Compiler는 Rules of React(순수한 렌더링, 훅 규칙 등)를 따르는 코드에서만 올바르게 동작합니다. 기존에 이 규칙을 위반하는 코드가 있으면 Compiler 도입 시 예기치 않은 동작이 발생할 수 있습니다.
정리
| 항목 | 설명 |
|---|---|
| use() | Promise와 Context를 조건부로 읽는 새 훅 — 훅 규칙의 예외 |
| useActionState | 비동기 액션의 pending/결과/에러를 선언적으로 관리 |
| useFormStatus | 자식에서 부모 폼의 제출 상태를 읽기 |
| useOptimistic | 서버 응답 전 UI를 먼저 업데이트하는 낙관적 업데이트 |
| ref as prop | forwardRef 없이 ref를 일반 prop으로 전달 |
| React Compiler | 컴파일 타임 자동 메모이제이션 (opt-in) |
| 메타데이터 호이스팅 | <title>, <meta> 등을 컴포넌트에서 렌더링하면 자동으로 <head>로 이동 |
React 19는 복잡했던 패턴들을 내장 기능으로 흡수하는 방향입니다. 특히 Actions는 폼 처리의 보일러플레이트를 크게 줄여줍니다.