19.0 이후의 안정화 단계
React 19.0이 큰 패러다임 전환이었다면, 19.1과 19.2는 그 위에 작지만 의미 있는 기능을 쌓아 올린 단계입니다. Server Components와 Actions가 안정화되는 동안, React 팀은 사용자 경험을 개선하는 새로운 API들을 추가했습니다.
이 글에서는 19.1과 19.2의 핵심 추가 사항들을 정리합니다.
Activity API, 숨겨도 살아있는 컴포넌트
문제
복잡한 앱에서는 자주 마주치는 상황이 있습니다. 사용자가 탭을 전환하거나, 모달을 닫거나, 라우트를 이동했다가 돌아왔을 때, 이전 화면의 상태를 그대로 복원하고 싶을 때입니다.
기존에는 두 가지 선택지밖에 없었습니다.
- 컴포넌트를 unmount하기 , 메모리는 절약되지만 모든 상태가 사라집니다. 스크롤 위치, 입력 중이던 텍스트, 로드된 데이터가 다 날아갑니다
- CSS로 숨기기 ,
display: none이나visibility: hidden으로 가립니다. 상태는 유지되지만 컴포넌트가 계속 렌더링되고 effect가 계속 실행됩니다
둘 다 만족스럽지 않습니다. 첫째는 UX가 나쁘고, 둘째는 성능 낭비입니다.
Activity의 해결
React 19.2가 도입한 Activity API 는 이 문제를 해결합니다. 컴포넌트를 "비활성 상태"로 만들어 상태는 유지하면서 effect는 일시 중지 할 수 있습니다.
import { Activity } from 'react';
function App({ currentTab }) {
return (
<>
<Activity mode={currentTab === 'home' ? 'visible' : 'hidden'}>
<HomeTab />
</Activity>
<Activity mode={currentTab === 'profile' ? 'visible' : 'hidden'}>
<ProfileTab />
</Activity>
<Activity mode={currentTab === 'settings' ? 'visible' : 'hidden'}>
<SettingsTab />
</Activity>
</>
);
}<Activity> 컴포넌트는 두 가지 모드를 가집니다.
visible, 일반 컴포넌트처럼 렌더링되고 동작합니다hidden, DOM에서 숨겨지고, 모든 effect가 cleanup됩니다. 하지만useState의 값과 컴포넌트 인스턴스는 메모리에 유지됩니다
다시 visible이 되면 컴포넌트가 다시 mount되는 것이 아니라, 이전 상태 그대로 복원됩니다. effect는 다시 실행됩니다.
Activity가 적합한 경우
- 탭 인터페이스 , 다른 탭으로 갔다 돌아와도 입력값이 유지됨
- 마법사/스텝 , 이전 단계로 돌아가도 데이터가 그대로
- 모달/사이드 패널 , 닫았다 다시 열어도 폼 입력이 살아있음
- 라우터 , 이전 페이지로 돌아갈 때 스크롤 위치 복원
주의사항
Activity는 React 19.2에서 안정 API로 도입되었습니다. 다만 hidden 상태의 컴포넌트는 메모리에 남아있으므로, 너무 많이 두면 메모리 압박이 생길 수 있습니다. 향후 추가 모드 (예: 우선순위에 따라 자동 정리되는 모드) 가 도입될 가능성이 있지만, 19.2 시점에서는 visible/hidden 두 가지뿐이며 hidden 컴포넌트가 자동 정리된다고 가정하면 안 됩니다.
View Transitions, 부드러운 화면 전환
브라우저의 View Transitions API
브라우저에는 View Transitions API 라는 표준이 있습니다. 두 화면 상태 사이의 전환에 부드러운 애니메이션을 자동으로 적용해주는 API입니다.
// 브라우저 네이티브 API
document.startViewTransition(() => {
updateDOM();
});이 API를 호출하면 브라우저가 변경 전후의 화면을 캡처하여 자동으로 크로스페이드 같은 전환 효과를 만들어줍니다.
문제는 이 API가 React의 렌더링과 잘 통합되지 않았다는 것입니다. React는 비동기적으로 DOM을 업데이트하므로, startViewTransition 안에서 단순히 setState를 호출해도 의도한 대로 동작하지 않을 수 있습니다.
React에서의 통합
React는 View Transitions API를 React의 렌더링 사이클과 통합하기 위한 <ViewTransition> 컴포넌트를 도입하고 있습니다. 19.2 시점에서는 아직 안정 채널이 아니라 Canary/Experimental 채널에서만 사용할 수 있습니다.
import { unstable_ViewTransition as ViewTransition } from 'react';
function Gallery({ images, selectedImage, onSelect }) {
return (
<>
{images.map(img => (
<ViewTransition key={img.id}>
<img
src={img.url}
onClick={() => onSelect(img)}
className={img.id === selectedImage?.id ? 'selected' : ''}
/>
</ViewTransition>
))}
</>
);
}<ViewTransition>으로 감싼 요소들은 상태가 변경될 때 자동으로 부드러운 전환 애니메이션이 적용됩니다. 같은 key를 가진 요소가 위치나 크기가 바뀌면, 브라우저가 그 변화를 부드럽게 애니메이션합니다.
트랜지션과 결합
<ViewTransition>은 useTransition과 자연스럽게 결합됩니다. 트랜지션 안에서 일어난 상태 변경은 View Transition으로 처리됩니다.
function App() {
const [page, setPage] = useState('home');
const [isPending, startTransition] = useTransition();
function navigate(newPage) {
startTransition(() => {
setPage(newPage);
});
}
return (
<ViewTransition>
{page === 'home' && <HomePage />}
{page === 'about' && <AboutPage />}
</ViewTransition>
);
}페이지가 바뀔 때 useTransition 안에서 일어나므로, React가 이 변화를 View Transition으로 표시합니다. 브라우저는 자동으로 이전 화면과 새 화면 사이의 전환 애니메이션을 만듭니다.
적합한 경우
- 라우트 전환 , 페이지가 바뀔 때 부드러운 전환
- 리스트 정렬/필터링 , 항목들의 위치가 바뀔 때 자연스러운 이동
- 상세 보기 진입 , 카드가 확대되어 상세 페이지가 되는 효과
- 탭 전환 , 활성 탭의 인디케이터가 부드럽게 이동
브라우저 지원
View Transitions API는 비교적 최신 브라우저에서만 동작합니다. 지원하지 않는 브라우저에서는 애니메이션 없이 즉시 전환됩니다, 기능이 없는 것이지 깨지는 것이 아닙니다. 이를 progressive enhancement 패턴이라고 합니다.
Performance Tracks, DevTools 통합
React 19.2는 Chrome DevTools의 Performance 패널과 통합되었습니다. React의 내부 작업 (렌더링, 커밋, 이펙트 실행) 이 별도의 트랙으로 표시됩니다.
이전에는 React의 렌더링 작업이 requestAnimationFrame이나 일반 JavaScript 작업과 구분되지 않아, "이 느린 부분이 React 때문인가, 다른 코드 때문인가"를 판단하기 어려웠습니다.
19.2부터는 DevTools에 다음과 같은 트랙이 표시됩니다.
- Scheduler , React가 렌더링을 어떻게 스케줄링하는지
- Components , 각 컴포넌트의 렌더링 시간
- Effects ,
useEffect,useLayoutEffect실행 시간
병목을 찾는 것이 훨씬 쉬워집니다. "어느 컴포넌트가 가장 오래 렌더링되는가", "어떤 effect가 메인 스레드를 막는가" 같은 질문에 정확하게 답할 수 있습니다.
사용 방법
별도의 설정이 필요하지 않습니다. React 19.2 이상으로 빌드하고 Chrome DevTools의 Performance 패널을 열면 자동으로 트랙이 표시됩니다. 개발 모드에서만 활성화되며, 프로덕션 빌드에는 영향이 없습니다.
useEffectEvent, effect 의존성 문제의 해결
React 19.2에서 안정 API로 정식 출시된 또 하나의 핵심 Hook이 useEffectEvent 입니다. 오래된 effect 의존성 문제를 해결합니다.
useEffectEvent는 effect 안에서 호출되지만 effect의 재실행을 트리거하지 않는 함수를 만드는 Hook입니다.
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('연결됨', theme);
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => {
onConnected(); // theme의 최신 값을 읽음
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // theme이 의존성에 없어도 OK
return <Chat />;
}이전에는 theme을 effect 안에서 사용하면 의존성 배열에 넣어야 했고, 그러면 theme이 바뀔 때마다 connection이 재생성되는 문제가 있었습니다. useEffectEvent는 "최신 값을 읽되 의존성으로 추적되지 않는" 함수를 만들어줍니다.
사용 규칙
useEffectEvent에는 몇 가지 중요한 규칙이 있습니다.
- 의존성 배열에 넣지 않습니다.
useEffectEvent로 만든 함수는 effect의 의존성에서 제외합니다. ESLint 규칙 (eslint-plugin-react-hooks최신 버전) 이 이를 강제합니다 - 같은 컴포넌트나 Hook 안에서만 호출 가능합니다. 다른 컴포넌트로 props로 전달하면 안 됩니다
- effect 안에서만 호출합니다. 일반 렌더링 도중에 호출하면 의도치 않은 동작을 할 수 있습니다
이 규칙들은 useEffectEvent가 "최신 값을 읽는다"는 특수한 능력을 안전하게 유지하기 위한 제약입니다.
Owner Stacks, 더 정확한 디버깅 (19.1)
React 19.1 (2025.03) 의 헤드라인 기능은 Owner Stacks 입니다. 컴포넌트가 어디서 렌더링되었는지를 추적하는 새로운 디버깅 API입니다.
기존의 React 컴포넌트 스택은 "부모 컴포넌트"를 보여줬습니다. 하지만 부모는 단순히 트리 구조상의 위치를 의미할 뿐, 누가 그 컴포넌트를 렌더링하기로 결정했는지는 다른 문제입니다. 같은 컴포넌트가 라이브러리 내부에서 렌더링될 수도 있고, 사용자 코드에서 렌더링될 수도 있습니다. 디버깅에서는 후자가 더 유용한 정보입니다.
import { captureOwnerStack } from 'react';
function MyComponent() {
const stack = captureOwnerStack();
console.log(stack); // 이 컴포넌트를 렌더링한 호출 스택
return <div>...</div>;
}captureOwnerStack은 개발 모드에서만 동작하며, "Owner", 즉 이 컴포넌트를 렌더링하기로 결정한 컴포넌트의 스택을 반환합니다. 에러 메시지나 경고에 이 정보가 포함되어, 문제의 원인을 찾기가 훨씬 쉬워집니다.
Partial Pre-rendering과 Server APIs (19.2)
React 19.2는 Server 렌더링 영역에서도 큰 진전을 이뤘습니다. 핵심은 Partial Pre-rendering 입니다.
기존의 SSR은 두 가지 모드 중 하나였습니다, 완전한 정적 HTML (SSG) 이거나, 매 요청마다 동적으로 렌더링 (SSR) 이거나. 그 사이의 영역, 즉 "정적 부분은 미리 렌더링하고 동적 부분만 요청 시 채우는" 패턴은 까다로웠습니다.
19.2의 새 API들이 이를 해결합니다.
// 정적 부분을 미리 렌더링 (빌드 타임)
const { prelude, postponed } = await prerender(<App />, {
signal: abortController.signal
});
// 요청 시 동적 부분을 이어서 렌더링
const stream = await resume(<App />, postponed);prerender(19.0 도입), AbortController로 제어 가능한 사전 렌더링resume,resumeAndPrerender(19.2 추가), Partial Pre-rendering의 두 번째 단계. 미리 렌더링한 결과를 받아서 동적 부분을 이어서 렌더링cacheSignal(19.2 추가), Server Components의 캐시가 무효화될 시점을 알려주는 AbortSignal. 백그라운드 작업을 적절히 정리할 수 있게 해줍니다- SSR Web Streams for Node (19.2 추가), 이전에는 Node 환경에서 Pipeable Stream만 사용 가능했는데, 19.2부터는 Web Streams API도 Node에서 직접 사용할 수 있습니다
이 변화들은 Next.js의 Partial Prerendering 같은 프레임워크 기능을 React 자체가 지원하게 만든 것입니다.
그 외 19.1, 19.2의 작은 개선사항
위의 큰 기능 외에도 여러 작은 개선이 있습니다.
19.1 (2025.03)
- 에러 보고 메커니즘 개선
- Suspense 경계 처리 개선
19.2 (2025.10)
useId접두사 변경 , 생성되는 ID의 접두사가 19.0의:r:→ 19.1의«r»를 거쳐 19.2에서_r_로 정착했습니다. View Transition name 등 valid XML name이 필요한 곳에서 쓸 수 있게 하려는 의도입니다. CSS에서 옛날 접두사를 하드코딩한 코드가 있다면 깨질 수 있으니 확인이 필요합니다eslint-plugin-react-hooksv6 , Flat Config를 지원하고,useEffectEvent의존성 규칙이 추가되었습니다- 에러 메시지와 개발 도구 경고 개선
시리즈 정리
7편에 걸쳐 React 18부터 19.2까지의 변화를 살펴봤습니다. 큰 그림을 다시 정리하면 이렇습니다.
React 18은 렌더링 모델을 바꿨습니다. 동기적이고 끝까지 진행되던 렌더링이, Concurrent 환경에서 멈출 수 있고 우선순위를 가질 수 있게 되었습니다. useTransition, useDeferredValue, useSyncExternalStore 같은 Hook은 이 모델 위에서 의미를 가집니다.
React 19는 데이터 처리 모델을 바꿨습니다. Server Components가 데이터 패칭의 위치를 클라이언트에서 서버로 옮겼고, Actions가 폼과 mutation을 React 자체의 기능으로 통합했습니다. use() Hook은 이 새로운 모델의 핵심 연결 고리입니다.
19.1과 19.2는 사용자 경험을 다듬었습니다. Activity로 컴포넌트의 생명주기를 더 세밀하게 제어할 수 있게 되었고, View Transitions로 부드러운 화면 전환을 표현할 수 있게 되었으며 (canary), DevTools 통합으로 성능 분석이 정확해졌습니다. 19.1의 Owner Stacks는 디버깅 경험을 한 단계 끌어올렸고, 19.2의 Partial Pre-rendering은 SSR과 SSG의 경계를 흐렸습니다.
같은 시기에 React Compiler 1.0이 안정화되었습니다. 이 시리즈의 범위 밖이지만 함께 알아둘 만한 변화입니다. React Compiler (2025.10) 는 컴포넌트 코드를 자동으로 최적화하여 useMemo, useCallback, memo 같은 수동 메모이제이션을 줄여줍니다. React 19와 함께 사용하도록 설계되었으며, 별도의 빌드 플러그인으로 도입할 수 있습니다.
핵심 베스트 프랙티스 요약
지금까지의 모든 베스트 프랙티스를 한 줄로 정리하면 이렇습니다.
- 렌더링 우선순위는 명시적으로 표현하라 ,
useTransition과useDeferredValue를 활용 - 외부 상태에는
useSyncExternalStore, tearing 방지 - 폼과 mutation은 Actions로 ,
useActionState,useFormStatus,useOptimistic - 데이터 패칭은 Server Component에서 , 클라이언트 번들 최소화
use()로 Server와 Client를 잇기 , Promise 전달 패턴forwardRef없이 ref 받기 , 새 코드는 일반 prop으로- Suspense 경계는 작게, 자주 , 점진적 렌더링의 이점 극대화
- effect 의존성 문제는
useEffectEvent로 , 최신 값 읽기와 의존성 추적의 분리
React는 여전히 "UI를 위한 라이브러리"라는 정체성을 유지하지만, 그 안에서 다룰 수 있는 영역이 18~19를 거치며 크게 확장되었습니다. 이 시리즈가 그 변화의 큰 그림을 잡는 데 도움이 되었기를 바랍니다.