Ray Book
React 18에서 19까지

ref와 Context의 변화, forwardRef의 종말

React 19가 정리한 ref, Context, metadata, Asset Preloading, 매일 쓰는 코드의 보일러플레이트를 줄여주는 작지만 큰 변화들.

reactreact19refcontextforwardRefmetadata

작지만 매일 쓰는 변화

React 19의 큰 변화는 Concurrent, Server Components, Actions입니다. 하지만 매일 코드를 작성할 때 가장 자주 마주치는 변화는 의외로 사소한 것들입니다, forwardRef가 사라지고, Context Provider가 단순해지고, metadata를 직접 쓸 수 있게 된 것 같은 변화들입니다.

이 글에서는 그런 작지만 매일 영향을 주는 변화들을 정리합니다.

ref가 일반 prop이 되다

이전, forwardRef의 시대

React 18까지 함수 컴포넌트는 ref를 직접 받을 수 없었습니다. ref는 특별한 prop이라서, 함수 컴포넌트에 그냥 ref를 넘기면 React가 경고했습니다. 받으려면 forwardRef로 감싸야 했습니다.

// React 18, forwardRef 필수
import { forwardRef } from 'react';

const Input = forwardRef(function Input({ placeholder }, ref) {
  return <input ref={ref} placeholder={placeholder} />;
});

// 사용
function Form() {
  const inputRef = useRef(null);
  return <Input ref={inputRef} placeholder="이름" />;
}

이 패턴은 동작은 했지만 불편했습니다.

  • forwardRef로 감싸면 함수 컴포넌트가 객체 형태로 바뀝니다
  • 두 번째 인자로 ref를 받는 시그니처가 어색합니다
  • TypeScript에서 제네릭을 추가하면 더 복잡해집니다
  • 디스플레이 이름 (displayName) 을 따로 설정해야 할 때가 많습니다
  • 컴포넌트를 만들 때마다 "이 컴포넌트는 ref를 받아야 하나?"를 미리 결정해야 합니다

React 19, ref는 그냥 prop

React 19부터는 ref를 다른 prop과 똑같이 받을 수 있습니다. forwardRef가 필요 없습니다.

// React 19, forwardRef 불필요
function Input({ ref, placeholder }) {
  return <input ref={ref} placeholder={placeholder} />;
}

// 사용은 동일
function Form() {
  const inputRef = useRef(null);
  return <Input ref={inputRef} placeholder="이름" />;
}

함수 컴포넌트의 props에 ref를 그냥 추가하면 끝입니다. 호출하는 쪽도 동일하게 사용합니다.

forwardRef는 어떻게 되는가

forwardRef는 React 19에서 공식적으로 deprecated 되었습니다. 여전히 동작하므로 기존 코드는 깨지지 않지만, 향후 메이저 버전에서 제거될 예정입니다. 새로 작성하는 코드는 forwardRef 없이 작성하고, 기존 코드는 여유가 있을 때 마이그레이션하는 것을 권장합니다.

TypeScript에서

TypeScript에서도 더 깔끔해집니다.

// React 18
const Input = forwardRef<HTMLInputElement, { placeholder: string }>(
  ({ placeholder }, ref) => {
    return <input ref={ref} placeholder={placeholder} />;
  }
);

// React 19
function Input({ ref, placeholder }: {
  ref?: React.Ref<HTMLInputElement>;
  placeholder: string;
}) {
  return <input ref={ref} placeholder={placeholder} />;
}

제네릭이 사라지고 일반 props 타입 정의로 충분해집니다.

Context Provider의 단순화

이전, <Context.Provider>

Context를 사용하려면 항상 .Provider를 붙여야 했습니다.

const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Page />
    </ThemeContext.Provider>
  );
}

createContext가 반환하는 객체에는 ProviderConsumer라는 속성이 있었고, JSX에서 사용할 때 그 속성에 접근해야 했습니다. 작은 불편이지만 매번 .Provider를 쓰는 것이 어색했습니다.

React 19, <Context> 자체가 Provider

React 19부터는 Context 객체 자체를 Provider로 사용할 수 있습니다.

const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext value="dark">
      <Page />
    </ThemeContext>
  );
}

.Provider가 사라졌습니다. 코드가 더 직관적이 되었습니다. <ThemeContext.Provider>는 React 19에서 공식적으로 deprecated 되었으며, 여전히 동작하지만 향후 버전에서 제거될 예정입니다. 새 코드는 단순한 형태를 사용하는 것이 권장됩니다.

Context의 use()와 결합

Context를 읽는 쪽도 5편에서 다룬 use()를 사용할 수 있습니다.

// React 18 방식
const theme = useContext(ThemeContext);

// React 19 방식
const theme = use(ThemeContext);

useContext도 여전히 동작합니다. 하지만 use()를 사용하면 조건문 안에서도 Context를 읽을 수 있다는 장점이 있습니다.

Ref Cleanup 함수

React 19는 ref callback에 cleanup 함수를 반환할 수 있게 했습니다. 이전에는 ref callback에서 정리 작업을 수행할 명확한 방법이 없었습니다.

function VideoPlayer() {
  return (
    <video
      ref={(node) => {
        if (!node) return;
        const observer = new ResizeObserver(() => {
          // 크기 변경 처리
        });
        observer.observe(node);

        // cleanup 함수 반환, node가 unmount될 때 호출
        return () => {
          observer.disconnect();
        };
      }}
    />
  );
}

이전 React에서는 ref callback이 두 번 호출되었습니다, element가 mount될 때 node를, unmount될 때 null을 인자로. 19부터는 cleanup 함수를 반환하면 React가 unmount 시점에 자동으로 호출합니다. 더 명확하고 useEffect의 cleanup 패턴과 일관됩니다.

useDeferredValue의 initialValue 옵션

React 19에서 useDeferredValue에 두 번째 인자가 추가되었습니다. 컴포넌트의 첫 렌더링 시 사용할 초기값을 지정할 수 있습니다.

function Search({ query }) {
  // 초기 렌더링에서는 빈 문자열을 사용
  const deferredQuery = useDeferredValue(query, '');
  // ...
}

이 옵션은 SSR과 결합될 때 특히 유용합니다. 서버에서는 가벼운 초기값으로 빠르게 렌더링하고, 클라이언트가 hydrate된 후 실제 값으로 전환합니다.

<script async> 자동 중복 제거

React 19는 <script async> 를 자동으로 중복 제거합니다. 같은 스크립트가 여러 컴포넌트에서 렌더링되어도, React가 중복을 감지하여 한 번만 로드합니다.

function Analytics() {
  return <script async src="https://analytics.example.com/tag.js" />;
}

// 여러 곳에서 렌더링되어도 한 번만 로드됨
function App() {
  return (
    <>
      <Analytics />
      <Analytics />
    </>
  );
}

이전에는 외부 라이브러리나 수동 관리가 필요했던 패턴입니다.

Custom Elements 완전 지원

React는 오랫동안 Custom Elements (Web Components) 와의 호환에서 한계가 있었습니다. 특히 prop을 attribute로 보낼지 property로 설정할지의 구분이 모호했고, custom event 처리가 까다로웠습니다.

React 19는 이 문제를 정식으로 해결했습니다. Custom Element의 prop을 React가 자동으로 적절히 처리합니다.

  • 원시 값 (string, number, boolean), HTML attribute로 설정
  • 객체나 함수, JavaScript property로 설정
  • on* prefix가 붙은 함수, custom event 리스너로 등록

이로써 Lit, Stencil 같은 Web Components 라이브러리와 React를 자연스럽게 함께 사용할 수 있게 되었습니다.

Document Metadata, <title>, <meta>를 직접 쓰기

이전에는 페이지 제목이나 메타 태그를 동적으로 설정하려면 react-helmet 같은 외부 라이브러리가 필요했습니다. <head>에 직접 접근할 방법이 없었기 때문입니다.

React 19부터는 컴포넌트 안에서 <title>, <meta>, <link> 같은 태그를 직접 사용할 수 있습니다. React가 이들을 자동으로 <head>로 옮겨줍니다.

function ProductPage({ product }) {
  return (
    <article>
      <title>{product.name} | 우리 가게</title>
      <meta name="description" content={product.description} />
      <link rel="canonical" href={`/products/${product.id}`} />

      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </article>
  );
}

이 코드를 렌더링하면 React는 <title>, <meta>, <link> 태그를 자동으로 <head> 안으로 이동시킵니다. 외부 라이브러리 없이 자연스럽게 동작합니다.

동작 방식과 주의점

중복 처리. 같은 페이지에 여러 <title>이 있으면 React는 마지막 것을 사용합니다. <meta><link>는 모두 보존됩니다.

Server Components에서도 동작. Server Component에서 metadata 태그를 사용하면 SSR HTML에 직접 포함됩니다. SEO와 초기 로딩에 유리합니다.

라이브러리와의 호환. Next.js나 Remix 같은 프레임워크는 자체 metadata 시스템을 가지고 있습니다. React 19의 기능과 충돌하지 않으며, 어느 쪽을 써도 됩니다.

Stylesheet의 우선순위 관리

비슷한 맥락으로, React 19는 <link rel="stylesheet"> 의 우선순위 관리도 자동화했습니다.

function Component() {
  return (
    <>
      <link rel="stylesheet" href="/styles/base.css" precedence="default" />
      <link rel="stylesheet" href="/styles/component.css" precedence="high" />
      <div>...</div>
    </>
  );
}

precedence 속성으로 스타일시트의 우선순위를 지정합니다. React는 같은 precedence 안에서는 등장 순서대로, 다른 precedence는 우선순위에 따라 정렬하여 <head>에 삽입합니다.

이 기능은 Suspense와 결합되어 동작합니다. 컴포넌트가 렌더링되기 전에 스타일시트가 로드되도록 보장합니다.

Asset Preloading, 리소스 미리 가져오기

React 19는 리소스를 미리 로드할 수 있는 새로운 API를 추가했습니다.

import { preload, preinit, prefetchDNS, preconnect } from 'react-dom';

function App() {
  // 폰트 미리 다운로드
  preload('/fonts/inter.woff2', {
    as: 'font',
    type: 'font/woff2',
    crossOrigin: 'anonymous'
  });

  // 외부 도메인에 미리 연결
  preconnect('https://api.example.com');

  // DNS 미리 조회
  prefetchDNS('https://cdn.example.com');

  // 스크립트 다운로드 + 실행
  preinit('/scripts/analytics.js', { as: 'script' });

  return <Page />;
}

각 함수는 다른 종류의 최적화에 해당합니다.

  • preload , 리소스를 미리 다운로드 (실행은 안 함)
  • preinit , 다운로드하고 즉시 초기화/실행
  • preconnect , 도메인에 미리 TCP 연결
  • prefetchDNS , DNS 조회만 미리

이전에는 <link rel="preload">를 직접 작성하거나 빌드 도구의 도움을 받아야 했습니다. React 19에서는 컴포넌트 안에서 명시적으로 호출할 수 있고, React가 중복을 자동으로 제거합니다.

errorBoundary와 에러 처리 개선

React 19는 에러 처리도 개선했습니다. 이전에는 catch되지 않은 에러가 콘솔에 두 번 출력되는 문제가 있었습니다 (한 번은 React가 throw하면서, 한 번은 ErrorBoundary가 catch하면서). React 19에서는 이 중복이 사라졌습니다.

또한 createRoot에 새로운 옵션이 추가되었습니다.

const root = createRoot(container, {
  onUncaughtError: (error, errorInfo) => {
    // ErrorBoundary로 잡히지 않은 에러
    logToService(error, errorInfo);
  },
  onCaughtError: (error, errorInfo) => {
    // ErrorBoundary로 잡힌 에러
    logToService(error, errorInfo);
  },
  onRecoverableError: (error, errorInfo) => {
    // 자동 복구된 에러 (예: hydration mismatch)
    logToService(error, errorInfo);
  }
});

세 가지 종류의 에러 콜백을 별도로 제공받을 수 있습니다. 에러 모니터링 도구와 통합할 때 더 정확한 로깅이 가능해졌습니다.

베스트 프랙티스 정리

1. 새 컴포넌트는 forwardRef 없이 작성한다. ref를 일반 prop처럼 받습니다. 기존 코드는 codemod로 점진적으로 마이그레이션합니다.

2. Context Provider는 .Provider 없이 사용한다. <MyContext value={...}> 형태가 더 깔끔합니다. 기존 코드는 동작하므로 급하게 바꿀 필요는 없습니다.

3. Context를 읽을 때는 useContext 또는 use() 둘 다 사용 가능하다. 조건부 읽기가 필요하면 use(), 그렇지 않으면 어느 쪽이든 됩니다.

4. 페이지 metadata는 컴포넌트 안에서 직접 작성한다. react-helmet 같은 외부 라이브러리가 더 이상 필요하지 않습니다. 다만 Next.js나 Remix를 쓴다면 프레임워크의 metadata 시스템을 우선합니다.

5. 리소스 preloading은 명시적으로 표현한다. 빌드 도구에 의존하기보다, 컴포넌트 안에서 preloadpreconnect를 호출하는 것이 의도가 명확합니다.

6. 에러 모니터링은 createRoot 옵션을 활용한다. Sentry 같은 도구를 통합할 때 onUncaughtError, onCaughtError, onRecoverableError를 모두 활용하면 에러 추적이 정확해집니다.

다음 단계

마지막 글에서는 React 19.1과 19.2가 추가한 최신 기능들을 다룹니다. Activity API , 컴포넌트를 unmount 없이 숨기는 기능, View Transitions , 부드러운 화면 전환을 React에 통합한 기능, 그리고 19.2에서 도입된 Performance Tracks 까지, 가장 최신 React가 보여주는 방향을 살펴봅니다.