
2025년 10월, React 팀이 React 19.2를 공식 릴리즈했어요. React 19가 작년 12월에 나왔고 19.1이 올해 6월에 나왔으니, 이번이 1년 만에 세 번째 릴리즈인 셈이에요.
이번 업데이트는 단순한 버그 픽스가 아니에요. 오랫동안 실험적(experimental) 단계에 머물던 기능들이 드디어 정식 API로 들어왔고, 개발자 경험과 퍼포먼스 측면에서 굵직한 변화가 있었어요. 직접 공식 문서를 확인한 내용 기반으로 핵심만 정리해볼게요.
변경된 내용 먼저 확인
처음에 React 19.2의 변경 사항이 단순히 기존 기능의 안정화일 거라고 생각했는데, 실제로 확인해보니 생각보다 더 많은 게 바뀌어 있었어요.
- Activity 컴포넌트 — 정식 API로 승격 (experimental에서 stable로)
- useEffectEvent — 정식 API로 승격 (실험적 단계 졸업)
- ViewTransition 컴포넌트 — canary 채널에서 사용 가능 (아직 stable 아님)
- cacheSignal — Server Components 전용 새 API 추가
- Performance Tracks — Chrome DevTools 연동 성능 추적 트랙 추가
- Partial Pre-rendering — SSR 관련 새 기능
- useId prefix 변경 —
:r:에서_r_로 (View Transitions CSS 호환 때문) - SSR Suspense batching 개선
특히 View Transitions는 아직 stable이 아니라 react@canary 채널에서만 사용할 수 있어요. 이 점은 처음 알려진 내용과 달라서, 실제 프로덕션에 쓰려면 Activity와 useEffectEvent부터 시작하는 게 맞아요.
Activity 컴포넌트
Activity는 쉽게 말하면 "눈에 안 보여도 상태는 살려두는" 컴포넌트예요.
기존에 탭 전환이나 사이드패널을 만들 때 이런 식으로 썼죠.
// 기존 방식 - 숨기면 언마운트됨
{isVisible && <Page />}
이 방식은 탭을 다시 열 때마다 컴포넌트가 새로 마운트돼요. 입력 중이던 값도 사라지고, 스크롤 위치도 초기화되고, 이미 불러온 데이터도 다시 패치해야 해요. Activity를 쓰면 이렇게 바뀌어요.
// Activity 방식 - 숨겨도 상태 유지
<Activity mode={isVisible ? 'visible' : 'hidden'}>
<Page />
</Activity>
hidden 모드가 되면 CSS의 display: none으로 숨겨지지만, React 상태는 그대로 유지돼요. 단, Effects는 언마운트돼서 불필요한 백그라운드 사이드이펙트는 방지해요. visible로 돌아오면 이전 상태가 그대로 복원되고, Effects도 다시 마운트돼요.
장점
- 탭 인터페이스에서 전환해도 입력값, 스크롤 위치 유지
- 사용자가 곧 이동할 것 같은 페이지 백그라운드 프리렌더링
- 뒤로가기 시 폼 입력 상태 복원
- 숨겨진 동안 Effects 정리로 불필요한 네트워크 요청 차단
실제로 자주 쓰이는 패턴은 이런 식이에요.
function TabbedPanel({ activeTab }) {
return (
<>
<Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}>
<HomeTab />
</Activity>
<Activity mode={activeTab === 'profile' ? 'visible' : 'hidden'}>
<ProfileTab />
</Activity>
</>
);
}
앞으로 더 많은 mode가 추가될 예정이라고 해요.
useEffectEvent
useEffectEvent는 Effect 안에서 "반응형이 아닌 로직"을 분리할 때 쓰는 훅이에요.
이게 왜 필요하냐면, useEffect의 의존성 배열 때문이에요. 예를 들어 채팅 연결 Effect에 테마 알림 로직이 섞여 있는 경우를 보면.
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme); // theme 변경마다 재연결!
});
connection.connect();
return () => connection.disconnect();
}, [roomId, theme]); // theme도 deps에 넣어야 해서...
}
theme이 바뀔 때마다 채팅방이 끊겼다 다시 연결돼요. 말이 안 되는 동작인데, 린트 규칙 때문에 어쩔 수 없이 deps에 넣게 되는 케이스예요. 많은 개발자들이 이걸 eslint-disable로 처리해왔는데, 이제 useEffectEvent로 제대로 해결할 수 있어요.
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme); // 항상 최신 theme 참조
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected(); // deps에 넣지 않아도 됨
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // roomId만 deps에 남음
}
useEffectEvent로 감싼 함수는 항상 최신 props/state를 보지만, Effect의 의존성으로 취급되지 않아요.
팁
useEffectEvent를 쓸 때 주의할 점이 있어요. deps 배열에 절대 넣지 말고, 같은 컴포넌트나 훅 안에서만 선언해야 해요. 그리고 eslint-plugin-react-hooks를 최신 버전으로 업그레이드해야 린터가 deps 삽입을 시도하지 않아요. DOM 이벤트 핸들러(onClick 등)에는 쓸 필요가 없고 Effect 안에서만 의미가 있어요.
View Transitions (react@canary)
View Transitions는 현재 react@canary에서 사용 가능한 실험적 기능이에요. 프로덕션에 바로 쓰기는 이르지만, 어떤 방향인지 알아두면 좋아요.
브라우저의 startViewTransition API를 React 방식으로 래핑한 거예요. 페이지 전환이나 UI 상태 변화에 부드러운 애니메이션을 선언적으로 추가할 수 있어요.
"무엇을" 애니메이션할지는 ViewTransition 컴포넌트로 감싸서 정의해요.
<ViewTransition>
<div>animate me</div>
</ViewTransition>
"언제" 애니메이션이 트리거되는지는 세 가지 방식이에요.
// 1. startTransition
startTransition(() => setState(...));
// 2. useDeferredValue
const deferred = useDeferredValue(value);
// 3. Suspense fallback에서 content로 전환
<Suspense fallback={<Fallback />}>
<Component />
</Suspense>
"어떻게" 애니메이션할지는 CSS View Transition pseudo-selectors로 정의해요.
::view-transition-old(*) {
animation: 300ms ease-out fade-out;
}
::view-transition-new(*) {
animation: 300ms ease-in fade-in;
}
특정 전환 타입에 따라 다른 애니메이션을 적용하고 싶다면 addTransitionType을 사용해요.
startTransition(() => {
addTransitionType('slide-forward');
navigate(newPage);
});
html:active-view-transition-type(slide-forward) {
&::view-transition-old(root) { animation: slide-out-left; }
&::view-transition-new(root) { animation: slide-in-right; }
}
참고로 useId의 prefix가 :r: 에서 _r_로 바뀐 것도 이 View Transitions 지원 때문이에요. CSS view-transition-name으로 쓰려면 XML 1.0 규칙을 따라야 해서요.
그 외 눈에 띄는 변경사항
SSR Suspense Batching 개선도 있었어요. 스트리밍 SSR에서 Suspense 경계가 드러나는 타이밍이 개선됐어요. 예전에는 콘텐츠가 준비되면 즉시 fallback이 교체됐는데, 이제는 짧은 시간 동안 batching해서 더 많은 콘텐츠를 한번에 보여줘요. UX가 더 자연스러워지고, View Transitions와도 잘 연동돼요.
Partial Pre-rendering은 SSG와 SSR의 중간 형태예요. 정적인 부분은 CDN에서 미리 제공하고, 동적인 부분만 나중에 채워요. Next.js를 쓴다면 특히 관심 가질 만한 변화예요.
Performance Tracks는 Chrome DevTools 퍼포먼스 프로파일에 React 전용 트랙이 추가된 거예요. Scheduler 트랙에서는 우선순위별 작업 처리 흐름을, Components 트랙에서는 컴포넌트 트리의 렌더/이펙트 타이밍을 시각적으로 볼 수 있어요. 성능 병목 찾을 때 훨씬 편해질 것 같아요.
업그레이드 방법
# stable 기능 (Activity, useEffectEvent 등)
npm install react@19.2 react-dom@19.2
# canary 기능 (ViewTransition 등 - 실험적)
npm install react@canary react-dom@canary
# eslint 플러그인도 업데이트 필수
npm install eslint-plugin-react-hooks@latest
useEffectEvent 쓸 거라면 eslint 플러그인 업데이트는 필수예요. 안 하면 린터가 자꾸 deps 배열에 넣으라고 경고해서 혼란스러울 수 있어요.
정리
React 19.2는 "오래 기다려온 것들이 드디어 나온" 릴리즈예요.
Activity는 SPA에서 상태 보존 문제를 근본적으로 해결해줘요. useEffectEvent는 Effect 코드를 훨씬 깔끔하게 만들어줘요. View Transitions는 아직 canary지만, 네이티브 앱처럼 자연스러운 페이지 전환을 구현할 수 있는 방향을 제시해줘요.
특히 Activity는 Next.js App Router와 조합하면 상당히 강력한 UX를 만들 수 있을 것 같아요. useEffectEvent도 Effect 관련 코드에서 eslint-disable 달아두신 분들이라면 바로 리팩토링해보시면 좋을 것 같아요.
'🧑💻Tech' 카테고리의 다른 글
| Next.js 2026년 3월 업데이트 완벽 정리 | 보안 패치부터 Turbopack 최적화까지 (0) | 2026.03.17 |
|---|---|
| React 소유권이 독립 재단으로 이전 | React Foundation 공식 출범 (2026) (0) | 2026.02.25 |
| Next.js 'use cache'로 캐싱이 이렇게 쉬워진다고? 🚀 (0) | 2026.02.20 |
| Next.js가 AI 코딩 에이전트와 함께 진화한다 (0) | 2026.02.19 |
| Next.js로 API 만들기: 실전 가이드 (0) | 2026.02.16 |