React 19의 DX 개선사항을 다룹니다. ref를 일반 props로 전달하는 방법, 컴포넌트 내 메타데이터 태그, 리소스 프리로딩 API를 살펴봅니다.
7장에서 React Compiler를 다루었습니다. 이번 장에서는 React 19의 나머지 주요 변경사항들을 살펴봅니다. forwardRef 없이 ref를 전달하는 새로운 방식, 컴포넌트에서 직접 <title>이나 <meta> 태그를 렌더링하는 메타데이터 지원, 그리고 리소스 로딩을 세밀하게 제어하는 새로운 API들을 다룹니다.
React 18까지는 부모 컴포넌트에서 자식의 DOM 요소에 접근하려면 forwardRef로 감싸야 했습니다.
import { forwardRef } from 'react';
const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
function TextInput({ label, ...props }, ref) {
return (
<label>
{label}
<input ref={ref} {...props} />
</label>
);
}
);
// 사용
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return <TextInput ref={inputRef} label="이름" />;
}React 19에서는 ref가 일반 props의 일부가 되었습니다.
function TextInput({
label,
ref,
...props
}: {
label: string;
ref?: React.Ref<HTMLInputElement>;
}) {
return (
<label>
{label}
<input ref={ref} {...props} />
</label>
);
}
// 사용 방법은 동일
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return <TextInput ref={inputRef} label="이름" />;
}forwardRef 래퍼가 사라지면서 코드가 간결해지고, 타입 정의도 단순해졌습니다.
forwardRef는 즉시 제거되지 않습니다. 기존 코드는 계속 동작하지만, 향후 deprecated 예정입니다. 새 코드에서는 ref를 일반 props로 사용하는 것이 권장됩니다.
자동 마이그레이션 도구를 사용할 수 있습니다.
npx codemod@latest react/19/replace-forward-refref props 패턴은 복합 컴포넌트에서 여러 내부 요소의 ref를 노출할 때 특히 유용합니다.
interface DialogRefs {
overlay: HTMLDivElement | null;
content: HTMLDivElement | null;
closeButton: HTMLButtonElement | null;
}
function Dialog({
children,
refs,
onClose,
}: {
children: React.ReactNode;
refs?: React.Ref<DialogRefs>;
onClose: () => void;
}) {
const overlayRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const closeRef = useRef<HTMLButtonElement>(null);
useImperativeHandle(refs, () => ({
overlay: overlayRef.current,
content: contentRef.current,
closeButton: closeRef.current,
}));
return (
<div ref={overlayRef} className="overlay" onClick={onClose}>
<div ref={contentRef} className="content" onClick={e => e.stopPropagation()}>
{children}
<button ref={closeRef} onClick={onClose}>닫기</button>
</div>
</div>
);
}React 19에서 ref 콜백이 클린업 함수를 반환할 수 있게 되었습니다. useEffect의 클린업과 유사한 패턴입니다.
function MeasuredBox() {
const [height, setHeight] = useState(0);
return (
<div
ref={(node) => {
if (node) {
// 마운트: node가 존재
setHeight(node.getBoundingClientRect().height);
}
// 언마운트: null이 전달됨 (암묵적)
}}
>
높이: {height}px
</div>
);
}function MeasuredBox() {
const [height, setHeight] = useState(0);
return (
<div
ref={(node) => {
// 마운트: 설정
const observer = new ResizeObserver(([entry]) => {
setHeight(entry.contentRect.height);
});
observer.observe(node);
// 언마운트: 클린업 함수 반환
return () => {
observer.disconnect();
};
}}
>
높이: {height}px
</div>
);
}클린업 함수 패턴은 리소스 정리가 필요한 경우에 특히 유용합니다.
function Tooltip({ targetRef }: { targetRef: React.RefObject<HTMLElement> }) {
return (
<div
ref={(node) => {
// Popper.js 인스턴스 생성
const popper = createPopper(targetRef.current!, node, {
placement: 'top',
});
return () => {
// 언마운트 시 정리
popper.destroy();
};
}}
>
툴팁 내용
</div>
);
}TypeScript에서 ref 콜백이 클린업 함수가 아닌 값을 반환하면 타입 에러가 발생합니다. 기존에 암묵적 반환(implicit return)을 사용하던 코드를 수정해야 합니다.
// React 18: 암묵적 반환 (DOM 요소를 반환)
<div ref={node => (instanceRef = node)} />
// React 19: 블록 바디로 변경 (반환값 없음)
<div ref={node => { instanceRef = node; }} />React 18까지는 <title>, <meta>, <link> 등의 <head> 태그를 관리하려면 서드파티 라이브러리(react-helmet, next/head 등)나 프레임워크의 generateMetadata를 사용해야 했습니다.
React 19에서는 컴포넌트 트리 어디에서든 <title>, <meta>, <link> 태그를 렌더링할 수 있으며, React가 자동으로 <head>로 호이스팅합니다.
function ArticlePage({ article }: { article: Article }) {
return (
<article>
{/* 이 태그들은 자동으로 <head>에 삽입됨 */}
<title>{article.title} - Kreath Archive</title>
<meta name="description" content={article.excerpt} />
<meta name="author" content={article.author.name} />
<meta property="og:title" content={article.title} />
<meta property="og:description" content={article.excerpt} />
<link rel="canonical" href={`https://archive.kreathlab.com/tech/${article.slug}`} />
{/* 실제 콘텐츠 */}
<h1>{article.title}</h1>
<p>{article.content}</p>
</article>
);
}<link rel="stylesheet">에 precedence 속성을 추가하면 스타일시트의 로딩 순서를 제어할 수 있습니다.
function ThemeSwitcher({ theme }: { theme: 'light' | 'dark' }) {
return (
<>
{/* 기본 스타일: 항상 먼저 로드 */}
<link rel="stylesheet" href="/styles/base.css" precedence="default" />
{/* 테마 스타일: 기본 스타일 다음에 로드 */}
<link
rel="stylesheet"
href={`/styles/${theme}.css`}
precedence="high"
/>
</>
);
}
function FeatureSection() {
return (
<section>
{/* 이 컴포넌트 전용 스타일 */}
<link rel="stylesheet" href="/styles/feature.css" precedence="default" />
<div className="feature-grid">...</div>
</section>
);
}precedence 속성이 있는 스타일시트는 다음과 같이 동작합니다.
<head>에 포함되어 전송됩니다.href의 스타일시트는 자동으로 중복 제거됩니다.function AnalyticsProvider({ children }: { children: React.ReactNode }) {
return (
<div>
{/* 자동으로 <head>에 삽입, 중복 제거됨 */}
<script async src="https://analytics.example.com/script.js" />
{children}
</div>
);
}
function Widget() {
return (
<div>
{/* 같은 스크립트를 여러 컴포넌트에서 선언해도 한 번만 로드 */}
<script async src="https://analytics.example.com/script.js" />
<div className="widget">...</div>
</div>
);
}Next.js App Router를 사용하는 경우, generateMetadata 함수가 더 강력한 메타데이터 관리를 제공합니다. React 19의 네이티브 메타데이터 지원은 프레임워크 없이 사용하거나, 동적으로 메타데이터를 변경해야 하는 경우에 유용합니다.
React 19는 react-dom에서 리소스 로딩을 제어하는 새로운 API들을 제공합니다.
import {
prefetchDNS,
preconnect,
preload,
preinit,
} from 'react-dom';| API | 역할 | 생성되는 HTML |
|---|---|---|
prefetchDNS(url) | DNS 조회만 수행 | <link rel="dns-prefetch" href="..."> |
preconnect(url) | TCP + TLS 핸드셰이크 | <link rel="preconnect" href="..."> |
preload(url, opts) | 리소스 다운로드 (실행 안 함) | <link rel="preload" as="..." href="..."> |
preinit(url, opts) | 다운로드 + 즉시 실행 | <script> 또는 <link rel="stylesheet"> |
import { preload, preconnect, prefetchDNS } from 'react-dom';
function ArticleCard({ article }: { article: Article }) {
function handleMouseEnter() {
// 마우스 호버 시 다음 페이지의 리소스를 미리 로드
preload(`/api/articles/${article.id}`, { as: 'fetch' });
preload(`/styles/article.css`, { as: 'style' });
}
return (
<a
href={`/articles/${article.slug}`}
onMouseEnter={handleMouseEnter}
>
<h2>{article.title}</h2>
<p>{article.excerpt}</p>
</a>
);
}function App() {
// 앱 초기화 시 외부 서비스 연결 준비
prefetchDNS('https://fonts.googleapis.com');
preconnect('https://cdn.example.com');
preload('https://cdn.example.com/fonts/pretendard.woff2', {
as: 'font',
crossOrigin: 'anonymous',
});
// 분석 스크립트 즉시 로드 및 실행
preinit('https://analytics.example.com/v2/script.js', {
as: 'script',
});
return <Layout />;
}function ImageGallery({ images }: { images: ImageData[] }) {
// 첫 번째 이미지는 즉시 로드
preload(images[0].src, { as: 'image' });
return (
<div>
{images.map((img, i) => (
<img
key={img.id}
src={img.src}
alt={img.alt}
loading={i === 0 ? 'eager' : 'lazy'}
onMouseEnter={() => {
// 다음 이미지 미리 로드
if (images[i + 1]) {
preload(images[i + 1].src, { as: 'image' });
}
}}
/>
))}
</div>
);
}React 19에서는 Context.Provider 대신 Context를 직접 Provider로 사용할 수 있습니다.
import { createContext } from 'react';
const ThemeContext = createContext<Theme>({ mode: 'light', primary: '#000' });
// React 18
function App() {
return (
<ThemeContext.Provider value={{ mode: 'dark', primary: '#fff' }}>
<Layout />
</ThemeContext.Provider>
);
}
// React 19
function App() {
return (
<ThemeContext value={{ mode: 'dark', primary: '#fff' }}>
<Layout />
</ThemeContext>
);
}Context.Provider는 향후 deprecated될 예정입니다.
React 19는 서버-클라이언트 불일치(Hydration Mismatch) 에러 메시지를 대폭 개선했습니다.
React 18에서는 모호한 경고만 표시되었지만, React 19에서는 diff 형태로 정확히 어떤 부분이 다른지 보여줍니다.
Uncaught Error: Hydration failed because the server rendered HTML
didn't match the client.
<div>
<h1>제목</h1>
- <p>서버에서 렌더링된 내용</p>
+ <p>클라이언트에서 렌더링된 내용</p>
</div>
또한, 서드파티 브라우저 확장 프로그램이 DOM을 수정하여 발생하는 하이드레이션 에러를 더 잘 처리합니다. 불필요한 전체 재렌더링이 줄어들었습니다.
React 19는 Web Components(Custom Elements)를 완전히 지원합니다.
// React 18: 속성이 문자열로만 전달됨
<my-component data={JSON.stringify(complexData)} />
// React 19: 모든 타입의 속성 전달 가능
<my-component
count={42} // number
items={['a', 'b']} // array
config={{ key: 'value' }} // object
onCustomEvent={handleEvent} // function
/>React 19는 속성의 타입에 따라 DOM property와 HTML attribute 중 적절한 방식을 자동으로 선택합니다.
React 19에서 에러 처리 방식이 변경되었습니다.
// React 18: 에러를 다시 throw하여 window.onerror에 잡힘
// -> 같은 에러가 console.error와 window.onerror에 중복 보고
// React 19: 에러를 다시 throw하지 않음
// -> Error Boundary에서 잡힌 에러: console.error로만 보고
// -> 잡히지 않은 에러: window.reportError로 보고커스텀 에러 보고가 필요하면 createRoot의 새 옵션을 사용합니다.
const root = createRoot(document.getElementById('root')!, {
onCaughtError: (error, errorInfo) => {
// Error Boundary에서 잡힌 에러
logToService('caught', error, errorInfo.componentStack);
},
onUncaughtError: (error, errorInfo) => {
// 잡히지 않은 에러
logToService('uncaught', error, errorInfo.componentStack);
},
onRecoverableError: (error, errorInfo) => {
// 복구 가능한 에러 (하이드레이션 불일치 등)
logToService('recoverable', error, errorInfo.componentStack);
},
});forwardRef 없이 ref를 일반 props로 전달할 수 있으며, ref 콜백에서 클린업 함수를 반환할 수 있습니다.<title>, <meta>, <link> 태그를 컴포넌트 내에서 렌더링하면 React가 자동으로 <head>로 호이스팅합니다.precedence 속성으로 스타일시트 로딩 순서를 제어하고 FOUC를 방지합니다.prefetchDNS, preconnect, preload, preinit API로 리소스 로딩을 세밀하게 최적화합니다.<Context value={...}>), 하이드레이션 에러 메시지가 diff 형태로 개선되었습니다.다음 장에서는 이 모든 기능을 종합하여 React 19 애플리케이션의 성능 최적화 전략을 다룹니다.
이 글이 도움이 되셨나요?
React 19 애플리케이션의 성능을 극대화하는 전략을 다룹니다. 번들 최적화, 렌더링 성능, Core Web Vitals 개선, 측정 도구 활용법을 배웁���다.
React Compiler의 동작 원리, HIR 기반 분석, 자동 메모이제이션, 설치와 설정, ESLint 통합, 실전 적용 전략을 다룹니다.
React 18에서 19로 안전하게 업그레이드하는 단계별 가이드입니다. 제거된 API, 타입 변경, 동작 변화, 자동 마이그레이션 도구를 다룹니다.