React 18에서 19로 안전하게 업그레이드하는 단계별 가이드입니다. 제거된 API, 타입 변경, 동작 변화, 자동 마이그레이션 도구를 다룹니다.
9장에서 React 19의 성능 최적화 전략을 다루었습니다. 이번 장에서는 기존 React 18 프로젝트를 React 19로 업그레이드하는 구체적인 과정을 안내합니다. 제거된 API, 동작 변화, TypeScript 타입 변경, 자동 마이그레이션 도구 활용법을 단계별로 살펴봅니다.
React 19로의 마이그레이션은 세 단계로 진행합니다.
React 18.3은 React 19에서 제거될 API에 대한 deprecation 경고를 추가한 버전입니다. 먼저 18.3으로 업그레이드하여 문제를 미리 파악합니다.
pnpm add react@18.3 react-dom@18.3콘솔에 나타나는 경고를 모두 수정합니다.
React 팀에서 제공하는 codemod를 실행합니다.
# React 코드 마이그레이션
npx codemod@latest react/19/migration-recipe
# TypeScript 타입 마이그레이션
npx types-react-codemod@latest preset-19 ./srcmigration-recipe에 포함된 codemod 목록:
| Codemod | 변환 내용 |
|---|---|
replace-reactdom-render | ReactDOM.render → createRoot |
replace-string-ref | 문자열 ref → useRef |
replace-act-import | react-dom/test-utils → react |
replace-use-form-state | useFormState → useActionState |
prop-types-typescript | PropTypes → TypeScript 타입 |
pnpm add react@^19.0.0 react-dom@^19.0.0
pnpm add -D @types/react@^19.0.0 @types/react-dom@^19.0.0// React 18 (제거됨)
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
ReactDOM.hydrate(<App />, document.getElementById('root'));
// React 19
import { createRoot, hydrateRoot } from 'react-dom/client';
// 클라이언트 렌더링
const root = createRoot(document.getElementById('root')!);
root.render(<App />);
// 하이드레이션 (SSR)
hydrateRoot(document.getElementById('root')!, <App />);// React 18 (제거됨)
ReactDOM.unmountComponentAtNode(container);
// React 19
const root = createRoot(container);
root.render(<App />);
// 나중에 언마운트
root.unmount();// React 18 (제거됨)
class MyComponent extends React.Component {
componentDidMount() {
const node = ReactDOM.findDOMNode(this);
}
}
// React 19: useRef 사용
function MyComponent() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const node = ref.current;
}, []);
return <div ref={ref}>...</div>;
}// React 18 (제거됨)
import { act } from 'react-dom/test-utils';
// React 19
import { act } from 'react';// React 18 (제거됨)
import PropTypes from 'prop-types';
MyComponent.propTypes = {
name: PropTypes.string.isRequired,
count: PropTypes.number,
};
// React 19: TypeScript 타입
interface MyComponentProps {
name: string;
count?: number;
}
function MyComponent({ name, count = 0 }: MyComponentProps) {
return <div>{name}: {count}</div>;
}// React 18 (함수 컴포넌트에서 제거됨)
function Button({ size, variant }) { /* ... */ }
Button.defaultProps = {
size: 'medium',
variant: 'primary',
};
// React 19: ES6 기본값
function Button({
size = 'medium',
variant = 'primary',
}: ButtonProps) {
// ...
}클래스 컴포넌트의 defaultProps는 React 19에서도 유지됩니다. 함수 컴포넌트에서만 제거되었습니다.
// React 18 (제거됨)
class MyComponent extends React.Component {
render() {
return <input ref="myInput" />;
}
handleClick() {
this.refs.myInput.focus();
}
}
// React 19: useRef 또는 콜백 ref
function MyComponent() {
const inputRef = useRef<HTMLInputElement>(null);
function handleClick() {
inputRef.current?.focus();
}
return <input ref={inputRef} />;
}// React 18 (제거됨)
class Provider extends React.Component {
getChildContext() {
return { theme: 'dark' };
}
static childContextTypes = {
theme: PropTypes.string,
};
}
class Consumer extends React.Component {
static contextTypes = {
theme: PropTypes.string,
};
render() {
return <div>{this.context.theme}</div>;
}
}
// React 19: createContext + useContext
const ThemeContext = createContext('light');
function Provider({ children }: { children: React.ReactNode }) {
return (
<ThemeContext value="dark">
{children}
</ThemeContext>
);
}
function Consumer() {
const theme = use(ThemeContext);
return <div>{theme}</div>;
}// React 18: 인자 없이 호출 가능
const ref = useRef<HTMLDivElement>(); // ref.current: HTMLDivElement | undefined
// React 19: 인자 필수
const ref = useRef<HTMLDivElement>(null); // ref.current: HTMLDivElement | null
// undefined를 명시적으로 전달하면 MutableRefObject
const ref = useRef<number>(undefined); // ref.current: number | undefinedReact 19에서 모든 ref는 mutable입니다. ref.current에 값을 할당할 수 있습니다.
// React 18: useRef<HTMLDivElement>(null)의 ref.current는 readonly
const ref = useRef<HTMLDivElement>(null);
ref.current = someElement; // TypeScript 에러
// React 19: 모든 ref.current가 writable
const ref = useRef<HTMLDivElement>(null);
ref.current = someElement; // 허용// React 18
type Props = React.ReactElement['props']; // any
// React 19
type Props = React.ReactElement['props']; // unknown
// 명시적 타입 지정이 필요
type Props = React.ReactElement<{ title: string }>['props'];// React 18: 암묵적 반환 허용
<div ref={(node) => (myRef = node)} />
// React 19: 블록 바디 필요 (클린업 함수와의 혼동 방지)
<div ref={(node) => { myRef = node; }} />// React 18: 전역 JSX 네임스페이스
declare global {
namespace JSX {
interface IntrinsicElements {
'my-element': React.HTMLAttributes<HTMLElement>;
}
}
}
// React 19: React 모듈 범위
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'my-element': React.HTMLAttributes<HTMLElement>;
}
}
}// React 18: 에러를 다시 throw
// console.error + window.onerror에 동일 에러가 중복 보고됨
// React 19: 에러를 다시 throw하지 않음
// Error Boundary에서 잡힌 에러 → console.error
// 잡히지 않은 에러 → window.reportError
// 커스텀 에러 핸들링
const root = createRoot(container, {
onCaughtError(error, errorInfo) {
// Error Boundary에서 잡힌 에러 로깅
},
onUncaughtError(error, errorInfo) {
// 잡히지 않은 에러 로깅
},
});// React 18: 이중 렌더링 시 매번 새로 계산
// React 19: 이중 렌더링 시 첫 번째 결과를 재사용 (메모이제이션)
// 이로 인해 useMemo, useCallback의 두 번째 호출에서
// 첫 번째 호출의 결과가 재사용됩니다.// React 18: fallback 표시에 약간의 지연이 있음
// React 19: fallback이 즉시 표시됨
// 이 변경으로 인해 기존에 "눈에 보이지 않던" 짧은 로딩이
// 이제 눈에 보일 수 있습니다. skeleton UI를 더 신경 써야 합니다.// React 19: src, href에 javascript: URL 사용 시 에러
<a href="javascript:void(0)">클릭</a> // 에러
<script src="javascript:alert(1)" /> // 에러
// 대체 방법
<button onClick={handleClick}>클릭</button>React 19와의 호환성을 확인해야 하는 주요 라이브러리들입니다.
{
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "^15.0.0",
"@tanstack/react-query": "^5.0.0",
"react-hook-form": "^7.50.0",
"zustand": "^5.0.0",
"framer-motion": "^11.0.0"
}
}라이브러리 업그레이드 전에 반드시 해당 라이브러리의 React 19 호환성 문서를 확인하세요. 일부 라이브러리는 메이저 버전 업데이트가 필요할 수 있습니다.
React 19에서 UMD 빌드가 제거되었습니다. CDN에서 스크립트 태그로 React를 로드하는 경우 ESM으로 전환해야 합니다.
<!-- React 18: UMD -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<!-- React 19: ESM -->
<script type="module">
import React from 'https://esm.sh/react@19';
import ReactDOM from 'https://esm.sh/react-dom@19/client';
</script>migration-recipe + preset-19)ReactDOM.render → createRoot 전환forwardRef → ref props 전환 (선택)PropTypes → TypeScript 타입 전환defaultProps → ES6 기본값 전환useRef/콜백 ref 전환createContext 전환useFormState → useActionState 전환act import 경로 변경useActionState, useOptimistic) 적용모든 것을 한 번에 바꿀 필요는 없습니다. React 19의 새 기능은 점진적으로 도입할 수 있습니다.
React 19 설치 (Breaking Changes만 수정)
↓
forwardRef 제거 (새 코드부터)
↓
Server Components 도입 (새 페이지부터)
↓
Server Actions 도입 (새 폼부터)
↓
React Compiler 활성화 (annotation 모드)
↓
기존 useMemo/useCallback 정리
각 단계는 독립적이며, 팀의 상황에 맞게 순서와 속도를 조절할 수 있습니다.
ReactDOM.render, findDOMNode, PropTypes, defaultProps(함수 컴포넌트), 문자열 ref, Legacy Context 등이 제거되었습니다.useRef는 인자가 필수이며, 모든 ref가 mutable이고, ref 콜백의 암묵적 반환이 금지되었습니다.javascript: URL이 차단됩니다.다음 장에서는 이 시리즈의 마지막으로, React 19의 핵심 기능을 모두 활용한 풀스택 앱을 처음부터 구축합니다.
이 글이 도움이 되셨나요?
React 19의 핵심 기능을 모두 활용한 풀스택 북마크 앱을 구축합니다. Server Components, Server Actions, 새로운 훅, Suspense 패턴을 실전에 적용합니다.
React 19 애플리케이션의 성능을 극대화하는 전략을 다룹니다. 번들 최적화, 렌더링 성능, Core Web Vitals 개선, 측정 도구 활용법을 배웁���다.
React 19의 DX 개선사항을 다룹니다. ref를 일반 props로 전달하는 방법, 컴포넌트 내 메타데이터 태그, 리소스 프리로딩 API를 살펴봅니다.