React 19 주요 변경사항
먼저, 메모이제이션을 자동으로 해주는 새로운 리액트 컴파일러는 리액트 19에 포함되지 않습니다. 리액트 팀은 리액트 19가 곧 출시됨을 발표하는 하나의 블로그 게시물에서 컴파일러를 함께 발표해서 생긴 오해입니다.
앞서 리액트 19에서의 변경사항을 간단히 요약하자면, 서버 컴포넌트를 위한 변경사항과 편의성을 위한 hook이 추가되었습니다.
⚠️ 해당 글에서는 주요 변경사항을 담았으며, 모든 변경사항을 담지는 않았음을 알려드립니다.
새롭게 추가된 hook
useActionState
// Using <form> Actions and useActionState
function ChangeName({ name, setName }) {
const [error, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const error = await updateName(formData.get('name'));
if (error) {
return error;
}
redirect('/path');
return null;
},
null
);
return (
<form action={submitAction}>
<input type="text" name="name" />
<button type="submit" disabled={isPending}>
Update
</button>
{error && <p>{error}</p>}
</form>
);
}
관례에 따라 비동기 트랜지션을 사용하는 함수를 Action이라 칭하며, useActionState를 사용하면 서버 컴포넌트에서도 Hydration 과정 없이도 즉각적으로 form을 사용할 수 있습니다. RSC를 사용하지 않는다면 일반적인 state와 동일하게 동작합니다.
useFormStatus
import { useFormStatus } from 'react-dom';
function DesignButton() {
const { pending } = useFormStatus();
return <button type="submit" disabled={pending} />;
}
useFormStatus는 하위 컴포넌트에서도 상위 컴포넌트의 form을 읽을 수 있게 해줍니다.
useOptimistic
const [optimisticState, toggleOptimisticIsLike] = useOptimistic<State, Value>(
state,
(currentState: State, optimisticValue: Value): State => {
return {
isLike: optimisticValue,
count: optimisticValue ? currentState.count + 1 : currentState.count - 1,
};
}
);
const handleClick = () => {
startTransition(async () => {
const nextIsLike = !optimisticState.isLike;
// 낙관적 업데이트
toggleOptimisticIsLike(nextIsLike);
try {
const response = nextIsLike ? await addLike() : await removeLike();
// 서버 응답 결과로 UI를 업데이트
setState(response);
setError('');
} catch (error) {
if (error instanceof Error) {
setError(error.message);
}
}
});
};
// 출처: <https://seungwoo.dev/posts/react-use-optimistic>
낙관적 업데이트를 쉽게 해줍니다.
리액트 18에서 추가된 useTransition에서도 비동기 함수를 지원할 수 있도록 변경되면서, 동일하게 낙관적 업데이트를 쉽게 다룰 수 있게 되었습니다.
// useTransition
function UpdateName({}) {
const [name, setName] = useState('');
const [error, setError] = useState(null);
const [isPending, startTransition] = useTransition();
const handleSubmit = () => {
startTransition(async () => {
const error = await updateName(name);
if (error) {
setError(error);
return;
}
redirect('/path');
});
};
return (
<div>
<input value={name} onChange={(event) => setName(event.target.value)} />
<button onClick={handleSubmit} disabled={isPending}>
Update
</button>
{error && <p>{error}</p>}
</div>
);
}
use
import { use } from 'react';
function Comments({ commentsPromise }) {
// `use` 는 promise가 완료될 때 까지 suspend 상태입니다.
const comments = use(commentsPromise);
return comments.map((comment) => <p key={comment.id}>{comment}</p>);
}
function Page({ commentsPromise }) {
return (
<Suspense fallback={<div>Loading...</div>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
);
}
use는 가장 상위의 컨텍스트 제공자를 찾아 읽을 수 있습니다. 최상위에서 호출할 필요가 없으며 조건문에서도 사용이 가능합니다.
import { use } from 'react';
import ThemeContext from './ThemeContext';
function Heading({ children }) {
if (children == null) {
// <- 조건문
return null;
}
// useContext를 사용할 때에는 할 수 없던 조건문 내 context 사용
const theme = use(ThemeContext);
return <h1 style={{ color: theme.color }}>{children}</h1>;
}
SSG를 위한 정적 API
import { prerender } from 'react-dom/static';
async function handler(request) {
const { prelude } = await prerender(<App />, {
bootstrapScripts: ['/main.js'],
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}
정적 HTML 생성을 위해 데이터를 로드하는 과정을 기다린 후 결과를 반환하며, 기존의 renderToString보다 향상된 기능을 제공합니다. 특히, Node.js 스트림이나 Web Streams와 같은 스트리밍 환경에서 동작하도록 설계되었습니다.
서버컴포넌트에서 사용할 수 있는 API
cache
const getUser = cache(async (userId: string) => {
return db.getUser(userId);
});
cache()
API는 데이터 페칭이나 연산 결과를 렌더링할 때마다 캐싱할 수 있게 할 수 있도록 합니다.
meta
정적 컴포넌트 혹은 정적 렌더링 환경에서 meta 태그를 사용 가능합니다.
function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<title>{post.title}</title>
<meta name="author" content="Josh" />
<link rel="author" href="<https://twitter.com/joshcstory/>" />
<meta name="keywords" content={post.keywords} />
<p>Eee equals em-see-squared...</p>
</article>
);
}
이전 버전에서 업그레이드 시 주의할점
코드 자동 변환 (Codemods)
업그레이드를 돕기 위해, codemod.com 팀과 협력하여 리액트 19의 새로운 API와 패턴으로 코드를 자동으로 업데이트할 수 있는 codemod(AST를 분석해서 코드구조를 바꿈)를 배포했습니다. codemod
명령어를 사용하여 코드 마이그레이션을 할 수 있습니다.
더이상 사용할 수 없는 API들
- propTypes, defaultProps (2017년 4월 v15.5.0 지원중단)
- 더이상 지원하지 않으며 타입스크립트를 사용해야함
contextTypes
와getChildContext
를 사용하는 레거시 Context (2018년 10월 v16.6.0 지원중단)- 클래스 컴포넌트의 문자열 refs (2018년 3월 v16.3.0 지원중단)
React.createFactory
(2020년 2월 v16.13.0 지원중단)react-test-renderer/shallow
대신react-shallow-renderer
사용- Shallow rendering은 내부동작에 의존하기 때문에 @testing-library/react를 권장합니다.
ReactDOM.render
대신ReactDom.createRoot
사용ReactDOM.hydrate
대신ReactDOM.hydrateRoot
사용unmountComponentAtNode(document.getElementById("root"))
대신root.unmount()
사용
더이상 사용할 수 없는 기능
- element.ref 이 아닌 element.props.ref 사용 (리액트 19는
ref
를 prop으로 제공 ) react-test-renderer
지원 중단- script 태그로 모듈을 불러올 때 사용하는 UMD 빌드 제공 중단 (ESM 대체 사용)
그 외 변경사항
Suspense 개선사항
이전 버전에서는 컴포넌트가 suspense 상태가 되면, 일시 중단된 형제 컴포넌트(같은 레벨의 컴포넌트)들이 먼저 렌더링된 후에 fallback UI가 적용되었습니다. 리액트 19에서는 컴포넌트가 suspense 상태가 되면, 먼저 fallback UI가 적용되고 그 후에 일시 중단된 형제 컴포넌트들이 렌더링됩니다.
useRef 변경사항
ref를 property로 전달 가능
ref를 기본적으로 forwardRef
는 삭제될 예정입니다. forwardRef
는 더 이상 권장되지 않지만 리액트 19에서는 호환성을 위해 계속 동작합니다.
타입스크립트 useRef
사용시 인자 필요
useRef();
-> 타입스크립트 에러발생, useRef(undefined);
-> 통과
const ref = useRef<number>(null);
// 'current'는 읽기 전용 속성이므로 할당할 수 없음
ref.current = 1; // undefiend를 넣을 수 있기 때문에 더이상 이런 문제가 발생하지 않음
- 타입스크립트의 전역
JSX
네임스페이스를React.JSX
로 대체- 다른 JSX 라이브러리와의 충돌 방지
참고 https://react.dev/blog/2024/12/05/react-19 https://react.dev/reference/react/cache https://www.intelligencelabs.tech/062b17fe-7b82-4bae-ba7f-0c17baca1bab#a520a537-7c18-49fe-8730-a200274514f8https://velog.io/@eunbinn/react-19-upgrade-guide https://velog.io/@eunbinn/react-compiler-soon#리액트-19는-리액트-컴파일러가-아닙니다 https://siosio3103.medium.com/번역-리액트-19-forwardref-지원-중단-앞으로-ref를-전달하기-위한-표준-가이드-13c02855efd8