React 19의 use()는 promise나 context 같은 resource를 component 안에서 읽게 해. promise가 pending이면 가장 가까운 Suspense boundary로 제어를 넘겨. 일반 hook과 달리 조건문이나 루프 안에서도 호출할 수 있지만, component나 hook 안에서만 호출한다는 경계는 그대로야.
Actions — 그냥 동작하는 form submission
Action은 form에 연결하는 async 함수야. React가 submission의 pending 흐름을 추적하고, useActionState는 마지막 결과와 pending flag를 함께 줘. 그래서 화면마다 setLoading(true)와 finally 의식을 복제할 필요가 줄어.
The compiler — 자동 메모이제이션
React Compiler는 빌드할 때 컴포넌트와 hook의 안전한 부분을 자동으로 memoize해. 베타도 이미 지났어. 2025-10-07에 1.0 이 안정판으로 나왔거든. 예전처럼 모든 값을 useMemo와 useCallback으로 감싸는 습관을 줄일 수 있어. 다만 그건 켜야 생기는 변화야. cwkPippa 의 Vite 빌드는 지금 compiler 를 켜지 않아서, 코드에 보이는 memoization — 메시지 목록의 자체 비교 함수 같은 것 — 은 여전히 손으로 쓴 거고 여전히 제 몫을 하고 있어. 그래도 compiler가 잘못된 상태 모델을 고쳐 주는 건 아니야. 먼저 component를 순수하게 써야 해.
수동으로 over-optimize 하지 마: compiler가 처리할 자리에 useMemo부터 얹으면 노이즈만 늘어. 먼저 profile하고, 실제로 뜨거운 경로만 optimize해.
새 API 셋의 공통점은 상태 전이를 React가 볼 수 있게 만든다는 거야. promise를 render 바깥 변수로 몰래 굴리거나, form pending을 서로 다른 boolean 셋으로 흩뜨리면 UI와 실제 작업이 쉽게 어긋나. Suspense boundary와 Action state를 화면 구조에 맞게 놓으면 loading, success, error가 하나의 흐름이 돼. 최신 문법을 썼다는 사실보다 이 경계가 더 중요해.
사실 새 API를 채택할 기준은 "modern한가"가 아니야. 기존 상태 조립보다 실패 경로를 더 또렷하게 드러내는가가 기준이지. Suspense fallback이 어디서 보일지, Action error가 어느 화면에 남을지 말할 수 없다면 문법만 새것이고 설계는 그대로 흐릿해.
Code
use() with a promise — inline async data·tsx
function ConversationDetail({ conversationPromise }: { conversationPromise: Promise<Conversation> }) {
// Suspends until the promise resolves; <Suspense> above renders fallback.
const conversation = use(conversationPromise);
return <ChatView conversation={conversation} />;
}
Actions — useActionState for a save form·tsx
type State = { title: string; error: string | null };
function TitleEditor({ conversationId }: { conversationId: string }) {
async function saveTitle(prevState: State, formData: FormData): Promise<State> {
const title = formData.get('title') as string;
// 이 앱이 실제로 쓰는 route: PATCH /api/conversations/{id}/title
const res = await fetch(`/api/conversations/${conversationId}/title`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
});
// HTTP 에러는 저장 실패야. 성공으로 돌려주면 안 돼
if (!res.ok) return { title: prevState.title, error: `Save failed (${res.status})` };
return { title, error: null };
}
const [state, formAction, isPending] = useActionState(saveTitle, { title: '', error: null });
return (
<form action={formAction}>
<input name="title" defaultValue={state.title} disabled={isPending} />
<button type="submit" disabled={isPending}>{isPending ? 'Saving…' : 'Save'}</button>
{state.error && <p role="alert">{state.error}</p>}
</form>
);
}
Progress
Progress is local-only — sign in to sync across devices.