React 19.3, 뭐가 달라졌나요?

작년에 experimental로 공개됐던 View TransitionsFragment Refs가 드디어 React 19.3에서 stable로 승격됐어요. 여기에 더해 서버 렌더링을 컴포넌트 단위로 건너뛸 수 있는 browser() API, Server Component에서 Context를 직접 렌더링하는 기능까지 들어왔습니다.

이번 릴리즈는 "새로운 문법"보다는 기존 앱의 UX를 한 단계 끌어올리는 도구들이 중심이에요. 애니메이션, 로딩 상태, 하이드레이션 같은 부분에서 그동안 어설프게 처리했던 것들을 React가 공식 API로 흡수했다고 보시면 됩니다.

이 글에서는 실무에서 바로 쓸 수 있는 4가지 핵심 변경점만 짚어드릴게요.

근거자료: React 19.3 공식 릴리즈 노트

Developer reviewing React 19.3 View Transitions API code on dual monitors IT Technology Image

1. View Transitions: 이제 <ViewTransition>으로 감싸기만 하면 됩니다

브라우저의 View Transition API를 React 컴포넌트로 감싼 형태예요. enter / exit / update / share 네 가지 케이스를 React가 트리 변화를 보고 자동으로 판단해줍니다.

import { ViewTransition, useState, startTransition } from 'react';
import { Video } from './Video';

export default function Component() {
  const [showItem, setShowItem] = useState(false);

  return (
    <>
      <button
        onClick={() => {
          // Transition으로 표시해야 애니메이션이 트리거됩니다
          startTransition(() => {
            setShowItem((prev) => !prev);
          });
        }}
      >
        {showItem ? '➖' : '➕'}
      </button>

      {showItem && (
        <ViewTransition>
          <Video />
        </ViewTransition>
      )}
    </>
  );
}

주의할 점: startTransition, useDeferredValue, use()로 감싼 업데이트가 아니면 애니메이션이 동작하지 않아요. 일반 setState는 urgent update로 취급돼서 즉시 반영됩니다.

addTransitionType으로 애니메이션 분기하기

같은 state 변화라도 "다음 슬라이드"와 "이전 슬라이드"는 다른 애니메이션이 필요하죠. 이럴 때 addTransitionType을 씁니다.

function nextSlide() {
  startTransition(() => {
    addTransitionType('next');   // CSS에서 :active-view-transition-type(next)로 스코프 지정 가능
    setCurrentSlide((c) => c + 1);
  });
}

function previousSlide() {
  startTransition(() => {
    addTransitionType('previous');
    setCurrentSlide((c) => c - 1);
  });
}

Suspense fallback 애니메이션 (실무에서 제일 유용함)

Suspense 경계를 <ViewTransition>으로 감싸면 fallback → 실제 컨텐츠로 넘어가는 순간을 애니메이션할 수 있어요. 다만 원칙을 지켜야 UX가 좋아집니다.

  • fallback은 애니메이션 없이 즉시 등장
  • fallback → 최종 컨텐츠 전환은 애니메이션
  • suspend되지 않는 자식은 애니메이션 없이 즉시 등장

이 원칙을 코드로 옮기면 update만 애니메이션을 켜는 형태가 됩니다.

<ViewTransition update="auto" enter="none" exit="none">
  <Suspense fallback={<VideoPlaceholder />}>
    <LazyVideo />
  </Suspense>
</ViewTransition>

이미 캐시된 UI에까지 애니메이션이 붙으면 오히려 앱이 굼떠 보이니, enter/exit는 꺼두는 걸 권장합니다.

React ViewTransition component wrapping a video card with enter exit animation Development Concept Image

2. Fragment Refs: wrapper div 없이 자식 DOM 제어

<div>로 감싸지 않고도 자식 노드들에 이벤트, 포커스, IntersectionObserver를 붙일 수 있게 됐어요. 라이브러리 컴포넌트가 ref를 forward하지 않아도 동작합니다.

import { useRef, useEffect } from 'react';

function Component({ posts }) {
  const fragmentRef = useRef(null);

  useEffect(() => {
    const instance = fragmentRef.current;
    instance.focus(); // 첫 번째 자식에 포커스
  }, []);

  return (
    <Fragment ref={fragmentRef}>
      {posts.map((post) => (
        <article key={post.id}>{post.title}</article>
      ))}
    </Fragment>
  );
}

FragmentInstance가 제공하는 메서드는 다음과 같아요.

  • addEventListener / removeEventListener / dispatchEvent — 1차 자식 이벤트 관리
  • focus / focusLast / blur — depth-first 포커스 이동
  • observeUsing / unobserveUsing — IntersectionObserver, ResizeObserver 연결
  • getClientRects / getRootNode / compareDocumentPosition / scrollIntoView — 측정·스크롤

3. use(browser()): 서버 렌더링 건너뛰기

localStorage, Intl.DateTimeFormat().resolvedOptions().timeZone 같이 브라우저에서만 의미 있는 값을 쓰는 컴포넌트는 그동안 useEffect + mounted 상태 같은 우회 패턴을 썼죠. 19.3부터는 공식 API가 있습니다.

import { use } from 'react';
import { browser } from 'react-dom';

function TimeZone() {
  use(browser()); // 서버에서는 suspend, 클라이언트에서는 그대로 진행
  const timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
  return <span>{timeZone}</span>;
}

조건부로 서버 렌더링을 opt-out 할 수도 있어요. 예를 들어 initialData가 있으면 서버에서도 렌더링하고, 없으면 브라우저에서만 렌더링하는 패턴입니다.

function useBrowserQuery(query, options) {
  if (options.initialData === undefined) {
    use(browser()); // 초기 데이터가 없으면 브라우저에서만 실행
  }
  return useQuery(query, options);
}

이 패턴은 SSR 프레임워크에서 loader 값을 넘겨줄 때 특히 유용합니다. 초기 데이터가 있으면 HTML에 포함되어 SEO에 유리하고, 없으면 클라이언트에서 fetch하도록 자연스럽게 분기되니까요.

4. Server Component에서 Context 직접 렌더링

그동안은 'use client' 모듈에서 Provider 래퍼를 하나 더 만들어야 했어요. 이제는 Context 객체를 그대로 import 해서 <UserContext value={...}> 형태로 바로 렌더링할 수 있습니다.

// user-context.js
'use client';
import { createContext } from 'react';
export const UserContext = createContext(null);

// server-component.js
import { UserContext } from './user-context';

export async function Layout({ children }) {
  const currentUser = await getCurrentUser();
  return (
    <UserContext value={currentUser}>
      {children}
    </UserContext>
  );
}

이 기술의 한계와 주의사항

  • View Transitions는 DOM 전용입니다. React Native 지원은 아직 진행 중이에요. RN에서 같은 패턴을 기대하면 안 됩니다.
  • 브라우저 지원 편차가 있습니다. View Transition API 자체가 모든 브라우저에서 완벽하진 않아서, 폴백 CSS를 준비해두는 게 안전해요.
  • Suspense 애니메이션 남용 금지. 캐시된 UI에까지 애니메이션이 붙으면 체감 성능이 오히려 떨어집니다. 위에서 언급한 3원칙을 꼭 지키세요.
  • Trusted Types는 CSP 정책과 함께 써야 의미가 있습니다. require-trusted-types-for 'script' 없이는 그냥 통과됩니다.

한국 개발 생태계에서의 적용 맥락

국내 SI·금융권 프로젝트는 아직 React 18에 머무는 경우가 많아서, 19.3 기능을 바로 도입하기 어려운 환경도 있어요. 다만 자체 서비스나 신규 프로젝트에서는 browser()와 View Transitions 정도는 지금부터 도입 검토해볼 만합니다. 특히 어드민 대시보드처럼 Suspense 경계가 많은 화면에서 체감 차이가 큽니다.

또한 국내에서 많이 쓰이는 Next.js App Router 환경에서는 Server Component에서 Context를 직접 렌더링하는 변경이 체감이 큰데, 기존에 Provider 래퍼를 만들던 보일러플레이트를 상당 부분 제거할 수 있습니다.

다음 단계 학습 방향

  1. 공식 문서의 View Transitions 가이드를 정독하세요.
  2. addTransitionType과 CSS의 :active-view-transition-type() 조합을 직접 실습해보세요.
  3. Fragment Refs로 기존 라이브러리 컴포넌트를 감싸는 패턴을 만들어보면 활용도가 확 와닿습니다.

함께 보면 좋은 글

Frontend engineer testing Fragment Refs and browser() API in React 19.3 on laptop Dev Environment Setup

정리하면

React 19.3은 "화려한 신문법"보다는 실무 UX를 다듬는 도구 모음에 가까워요. View Transitions로 애니메이션을 React 트리 변화에 자연스럽게 얹고, Fragment Refs로 DOM 구조를 건드리지 않고 동작을 주입하고, browser()로 SSR 우회 패턴을 정리하는 것 — 이 세 가지만 제대로 익혀도 이번 릴리즈의 절반은 뽑아먹은 겁니다.

특히 Suspense + View Transitions 조합은 어드민, 대시보드, 커머스 상세 페이지처럼 "로딩 → 컨텐츠" 전환이 잦은 화면에서 바로 효과를 봅니다. 다만 애니메이션은 적게, 정확하게가 정답이에요. 😅

본 콘텐츠는 신뢰할 수 있는 출처를 바탕으로 AI 도구를 활용하여 초안이 작성되었으며, 편집자의 검토를 거쳐 발행되었습니다. 전문가의 조언을 대체하지 않습니다.