[React] 리액트 19 beta 변경점 알아보기
리액트 19 Beta 버전이 4.25일 이틀전에 나왔다! 리액트 팀에서 19에 어떤 변경사항을 적용할지 알아보자.
본 글은 리액트 팀블로그를 참고했다.
https://react.dev/blog/2024/04/25/react-19#improvements-in-react-19
먼저 크게 보면 아래 항목들을 중점적으로 개발한 것 같다.
1. 비동기 작업의 편의성 ( 동시성에 초점을 둔게 느껴진다 )
2. ref 개선
3. 성능 개선
1. useTransition에서 비동기 함수 지원
useTransition은 startTransition으로 감싼 상태변화의 우선순위를 낮춰주는 훅이다.
startTransition으로 감싼 변화들은 isPending이 false로 바뀐 후 동작된다.
즉 원래 아래와 같던 코드를
// Before Actions
function UpdateName({}) {
const [name, setName] = useState("");
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);
const handleSubmit = async () => {
setIsPending(true);
const error = await updateName(name);
setIsPending(false);
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>
);
}
아래처럼 굳이 useState를 이용하여 직접 구한하지 않아도 쉽게 만들 수 있다.
// Using pending state from Actions
function UpdateName({}) {
const [name, setName] = useState("");
const [error, setError] = useState(null);
const [isPending, startTransition] = useTransition();
const handleSubmit = async () => {
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>
);
}
The async transition will immediately set the isPending state to true, make the async request(s), and switch
isPending to false after any transitions. This allows you to keep the current UI responsive and interactive while the data is changing.
요약하면 비동기로 transition을 설정해도 비동기 작업이 다 끝난 후에 isPending을 false로 바꿔준다는 의미이다.
Action
해당 문서에서 Action을 아래와 같이 정의하고 있다.
By convention, functions that use async transitions are called “Actions”.
관례적으로 비동기 transiton을 사용하는 함수를 "Action"이라고 합니다.
즉 비동기적으로 동작하고, 요청이 진행되는 동안의 상태를 알 수 있고, 에러를 관리하는 작업을 의미한다.
( react-query로 주로 해결하던 것들을 말하는 것 같다 )
이 Action을 잘 지원해주기 위한 훅들을 새로 추가했다고 한다! form 액션을 다루는 아래 예제를 봐보자.
뭔가 못보던 훅이 생긴 것을 알 수 있다.
// 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 (
<form action={submitAction}>
<input type="text" name="name" />
<button type="submit" disabled={isPending}>Update</button>
{error && <p>{error}</p>}
</form>
);
}
useActionState
"Action" 을 하는 함수를 인자로 받고 래핑된 Action을 반환한다.
Action이 호출되면 useActionState는 action의 가장 최근 결과 data와 pending 상태를 반환해준다.
const [error, submitAction, isPending] = useActionState(async (previousState, newName) => {
const error = await updateName(newName);
if (error) {
// action의 결과값을 리턴해야함
// 에러가 발생한다면 에러를 반환하도록
return error;
}
// handle success
});
form action
form, input, button action안에서 이 새로운 훅들은 동작할 수 있다!
<form action={actionFunction}>
useFormStatus
form의 현재 상태를 쉽게 관리할 수 있게 하는 훅이다.
const { pending, data, method, action } = useFormStatus();
아래처럼 action으로 넘겨주는 함수 내에서 pending과 같은 상태를 보다 쉽게 관리할 수 있다.
import { useFormStatus } from "react-dom";
import { submitForm } from "./actions.js";
function Submit() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Submitting..." : "Submit"}
</button>
);
}
function Form({ action }) {
return (
<form action={action}>
<Submit />
</form>
);
}
export default function App() {
return <Form action={submitForm} />;
}
위처럼 Submit이 진행되는 동안의 상태를 통해 다른 UI를 제공할 수 있다.
useOptimistic
비동기 작업이 진행중인 동안 다른 것들을 보유줄 수 있게 하는 훅이다.
const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);
좀 더 자세한 예는 아래와 같다.
import { useOptimistic, useState, useRef } from "react";
import { deliverMessage } from "./actions.js";
function Thread({ messages, sendMessage }) {
const formRef = useRef();
async function formAction(formData) {
addOptimisticMessage(formData.get("message"));
formRef.current.reset();
await sendMessage(formData);
}
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [
...state,
{
text: newMessage,
sending: true
}
]
);
return (
<>
{optimisticMessages.map((message, index) => (
<div key={index}>
{message.text}
{!!message.sending && <small> (Sending...)</small>}
</div>
))}
<form action={formAction} ref={formRef}>
<input type="text" name="message" placeholder="Hello!" />
<button type="submit">Send</button>
</form>
</>
);
}
export default function App() {
const [messages, setMessages] = useState([
{ text: "Hello there!", sending: false, key: 1 }
]);
async function sendMessage(formData) {
const sentMessage = await deliverMessage(formData.get("message"));
setMessages((messages) => [...messages, { text: sentMessage }]);
}
return <Thread messages={messages} sendMessage={sendMessage} />;
}
즉 위와 같이 보내는동안 추가적으로 보여줄 화면을 보다 쉽게 그려줄 수 있다.
use
새로운 API인 use가 추가되었다.
promise를 use를 통해서 읽을 수 있으며 resolve되기 전까지 Suspend를 수행한다.
import {use} from 'react';
function Comments({commentsPromise}) {
// `use` will suspend until the promise resolves.
const comments = use(commentsPromise);
return comments.map(comment => <p key={comment.id}>{comment}</p>);
}
function Page({commentsPromise}) {
// When `use` suspends in Comments,
// this Suspense boundary will be shown.
return (
<Suspense fallback={<div>Loading...</div>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
)
}
단, 아직 렌더링 중 생성되는 Promise에 대해서는 지원하지 않으며 추가할 계획이 있다고 한다.
아직은 시험단계인 듯 하다.
ref를 props로 사용
원래 현재 ref를 props로 넘기려면 forwardRef 훅을 사용해야 했다. 하지만 이제는 ref를 props로 넘길 수 있을것으로 보이며 forwardRef훅은 사라질 예정이라 한다.
function MyInput({placeholder, ref}) {
return <input placeholder={placeholder} ref={ref} />
}
//...
<MyInput ref={ref} />
ref에서 cleanup 함수 지원
구성 요소가 mount 해제되면 cleanup함수를 시행한다고 한다.
<input
ref={(ref) => {
// ref created
// NEW: return a cleanup function to reset
// the ref when element is removed from DOM.
return () => {
// ref cleanup
};
}}
/>
스타일 시트 지원
이제 리액트 내에서 link태그로 css파일을 넣거나 style태그를 직접적으로 넣는 것이 가능해 질것으로 보인다.
특징으로는 이렇게 넣은 스타일들의 대부분은 자체적으로 처리가 되어서 head부분에서 처리되게 동작된다고 한다.
function ComponentOne() {
return (
<Suspense fallback="loading...">
<link rel="stylesheet" href="foo" precedence="default" />
<link rel="stylesheet" href="bar" precedence="high" />
<article class="foo-class bar-class">
{...}
</article>
</Suspense>
)
}
function ComponentTwo() {
return (
<div>
<p>{...}</p>
<link rel="stylesheet" href="baz" precedence="default" /> <-- will be inserted between foo & bar
</div>
)
}
비동기 스크립트 지원
async, defer등 비동기 스크립트도 지원될 예정이다.
function MyComponent() {
return (
<div>
<script async={true} src="..." />
Hello World
</div>
)
}
function App() {
<html>
<body>
<MyComponent>
...
<MyComponent> // won't lead to duplicate script in the DOM
</body>
</html>
}
리소스 사전 로드 지원
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom'
function MyComponent() {
preinit('https://.../path/to/some/script.js', {as: 'script' }) // loads and executes this script eagerly
preload('https://.../path/to/font.woff', { as: 'font' }) // preloads this font
preload('https://.../path/to/stylesheet.css', { as: 'style' }) // preloads this stylesheet
prefetchDNS('https://...') // when you may not actually request anything from this host
preconnect('https://...') // when you will request something but aren't sure what
}
브라우저에게 가능한 빨리 다운받으면 좋은 리소스들을 미리 알려줘서 성능개선을 할 수 있다.
그외에도 여러가지 기능들이 자잘하게 추가되거나 개선되었다.
해당 기능들을 잘 활용하면 react-query를 안쓰고도 간단한 비동기 작업에 대처하는 기능은 쉽게 만들 수 있을 것 같다라는 생각이 들었다.
스타일, ref, 리소스 사전로드 등의 기능도 흥미로운 것 같다!
확실히 동시성에 집중을 많이 한다는게 느껴졌다.