2024년 4월 25일 공개된 React 19 베타 뭐가 새로 생기고 달라졌을까
pending
상태를 표현하기 위해 사람들은 보통 isPending, SetIsPending 이라는 이름으로 state를 설정합니다.
React 19 베타에서는 useTransition
이라는 훅을 사용해 state
, error
, form
을 다루는 transition에 있는 비동기 함수를 지원합니다.
// 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>
);
}
// 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>
);
}
아니면 일반적인 action을 처리할 때 useActionState
, 새로운 훅을 사용할 수 있습니다. form action과 그리고 관련된 상태까지 관리합니다.
// 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) {
// You can return any result of the action.
// Here, we return only the error.
return error;
}
// handle success
redirect("/path");
}
);
return (
<form action={submitAction}>
<input type="text" name="name" />
<button type="submit" disabled={isPending}>Update</button>
{error && <p>{error}</p>}
</form>
);
}
반드시 부모 컴포넌트에 form 태그가 있어야 하며, 마지막 form 제출에 대한 상태 정보를 가져옵니다.
import { useFormStatus } from "react-dom";
function Submit() {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Submitting..." : "Submit"}
</button>
);
}
비동기 요청이 진행되는 동안 최종적인 상태를 즉시 표기합니다.
updateName
요청이 진행중인 동안 즉시 렌더링됩니다. 업데이트가 끝났을 때 react는 자동으로 현재 상태를 교체합니다.
function ChangeName({currentName, onUpdateName}) {
const [optimisticName, setOptimisticName] = useOptimistic(currentName);
const submitAction = async formData => {
const newName = formData.get("name");
setOptimisticName(newName);
const updatedName = await updateName(newName);
onUpdateName(updatedName);
};
return (
<form action={submitAction}>
<p>Your name is: {optimisticName}</p>
<p>
<label>Change Name:</label>
<input
type="text"
name="name"
disabled={currentName !== optimisticName}
/>
</p>
</form>
);
}
promise를 use
를 통해 읽을 수 있습니다.
promise resolves가 될 때까지 렌더링을 보류합니다.
use는 렌더링 때 생성된 Promise를 지원하지 않습니다.
문제를 해결하려면 promise 캐싱을 지원하는 suspend 기반 라이브러리나 프레임워크에서 promise를 전달해야 합니다.
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>
)
}
또한 context 값을 읽을 때도 use를 사용할 수 있습니다.
import {use} from 'react';
import ThemeContext from './ThemeContext'
function Heading({children}) {
if (children == null) {
return null;
}
// This would not work with useContext
// because of the early return.
const theme = use(ThemeContext);
return (
<h1 style={{color: theme.color}}>
{children}
</h1>
);
}
use는 오직 렌더링 시에, 때에 따라 호출됩니다.
서버 컴포넌트를 사용해 SSR 서버와 별도의 환경에서 번들로 묶기 전에 컴포넌트를 미리 렌더링할 수 있습니다.
서버 컴포넌트는 CI 서버에서 빌드 시 단 한 번만 실행되거나 웹 서버를 이용해 각 요청에 대해 실행할 수 있습니다.
자세한 건 아래 참고
https://react.dev/reference/rsc/server-components
클라이언트 컴포넌트에서 서버에서 실행되는 비동기 함수를 호출할 수 있습니다.
"use server" 지시문으로 프레임워크는 자동으로 Server Action에 대한 참조를 생성하고 클라이언트 컴포넌트로 속성이 전달됩니다.
해당 함수가 클라이언트에서 호출되면 React는 서버에 함수 실행 요청을 보내고 결과를 반환합니다.
하지만 서버 컴포넌트에 대한 지시문은 없습니다.
서버 컴포넌트는 "use server"로 표시되지만 지시문은 필요 없습니다. "use server"는 오직 Server Action에서만 사용됩니다.
React 19 부터 함수 컴포넌트에서 ref를 속성처럼 사용할 수 있습니다.
더이상 forwardRef가 필요하지 않습니다.
ref prop으로 컴포넌트를 자동으로 업데이트 할 수 있습니다.
추후 forwardRef가 사용할 일이 없고 제거됩니다.
function MyInput({placeholder, ref}) {
return <input placeholder={placeholder} ref={ref} />
}
//...
<MyInput ref={ref} />
react-dom hydartion 오류 표기를 개선했습니다.
불일치에 대한 정보없이 DEV에 여러 오류를 기록하지 않습니다.
Warning: Text content did not match. Server: “Server” Client: “Client” at span at App
Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.
Warning: Text content did not match. Server: “Server” Client: “Client” at span at App
Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.
Uncaught Error: Text content does not match server-rendered HTML. at checkForUnmatchedText …
to
Uncaught Error: Hydration failed because the server rendered HTML didn’t match the client. As a result this tree will be regenerated on the client. This can happen if an SSR-ed Client Component used:
- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it’s called.
- Date formatting in a user’s locale which doesn’t match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
https://react.dev/link/hydration-mismatch
<App>
<span>
+ Client
- Server
at throwOnHydrationMismatch
…
<Context>
의 provider 화<Context.Provider>
대신 <Context>
를 제공자처럼 렌더링할 수 있습니다.
<Context.Provider>
는 추후 더 이상 사용되지 않습니다.
const ThemeContext = createContext('');
function App({children}) {
return (
<ThemeContext value="dark">
{children}
</ThemeContext>
);
}
컴포넌트가 내려갈 때 ref 콜백에 대한 cleanup 함수를 반환합니다.
이전엔 컴포넌트가 내려갈 때 React에서 null과 ref 함수를 호출했지만 cleanup 함수를 반환한다면 React는 이 과정을 넘깁니다.
추후엔 컴포넌트가 내려갈 때 null과 refs 호출은 사용되지 않습니다.
<input
ref={(ref) => {
// ref created
// NEW: return a cleanup function to reset
// the ref when element is removed from DOM.
return () => {
// ref cleanup
};
}}
/>
타입스크립트에서 사용할 때 ref 콜백에서 아무거나 반환할 때 거부될 수 있으므로 다음과 같이 사용하는 것을 추천합니다.
원래 코드는 HTMLDivElement 인스턴스를 반환하지만 타입스크립트는 정리 함수인지 정리 함수를 반환하고 싶은 건지 알 수 없기 때문입니다.
- <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />
html에서 <title>
이나 <meta>
같은 메타 데이터 태그는 <head>
섹션에 배치하도록 예약되어 있습니다.
React에서 <head>
를 렌더링하는 위치에서 멀리 떨어져 있거나 React가 전혀 렌더링하지 않을 수 있으므로 과거에는 수동으로 집어 넣거나 react-helmet과 같은 라이브러리를 사용했으며 서버에서 렌더링할 때 신중하게 처리했던 반면에,
지금은 그냥 return에 넣어주기만 하면 자동으로 <head>
에 넣어줍니다.
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>
);
}
외부 링크 및 인라인 스타일 시트는 우선 순위 규칙으로 인해 dom에서 위치 선정을 잘 해야됩니다.
원하는 스타일 시트 기능을 구축하기 어렵기 때문에 종속될 수 있는 컴포넌트에서 멀리 떨어진 곳에서 모든 스타일을 로드하거나 이를 캡슐화 하는 스타일 라이브러리를 사용하게 되는 경우가 있습니다.
React에 스타일시트의 우선순위를 알려주면 DOM에서 스타일시트의 삽입 순서를 관리하고 해당 스타일 규칙에 의존하는 콘텐츠를 공개하기 전에 스타일시트(외부인 경우)가 로드되었는지 확인합니다.
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>
)
}
html에서는 일반 스크립트나 지연된 스크립트가 문서 순서대로 로드됩니다.
React 19에서는 스크립트 인스턴스를 재배치하거나 중복 제거를 관리할 필요가 없어졌습니다.
그저 스크립트에 실제로 의존하는 컴포넌트 내부, 컴포넌트 내부 어디서나 렌더링할 수 있도록 하며 비동기 스크립트를 지원합니다.
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>
}
브라우저 자원을 로드하고 미리 로드하기 위한 여러 가지 새로운 API가 포함되어 있어 비효율적인 자원 로드로 인해 방해받지 않는 훌륭한 경험을 최대한 쉽게 구축할 수 있습니다.
스타일시트 로드에서 글꼴과 같은 추가 리소스 검색을 이동하여 초기 페이지 로드를 최적화할 수 있습니다.
자원 목록을 미리 가져온 다음 클릭하거나 마우스를 가져갈 때 해당 리소스를 미리 로드하여 클라이언트 업데이트를 더 빠르게 수행할 수 있다.
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
}
<!-- the above would result in the following DOM/HTML -->
<html>
<head>
<!-- links/scripts are prioritized by their utility to early loading, not call order -->
<link rel="prefetch-dns" href="https://...">
<link rel="preconnect" href="https://...">
<link rel="preload" as="font" href="https://.../path/to/font.woff">
<link rel="preload" as="style" href="https://.../path/to/stylesheet.css">
<script async="" src="https://.../path/to/some/script.js"></script>
</head>
<body>
...
</body>
</html>
<head>
및 <body>
의 예상치 못한 태그를 건너뛰어 불일치 오류를 방지합니다.
React가 관련 없는 수화 불일치로 인해 전체 문서를 다시 렌더링해야 하는 경우 타사 스크립트 및 브라우저 확장 프로그램에 의해 삽입된 스타일시트를 그대로 둡니다.
중복을 제거하고 포착된 오류와 포착되지 않은 오류를 처리하기 위한 옵션을 제공하기 위해 React 19의 오류 처리를 개선했습니다.
Uncaught Error: hit
at Throws
at renderWithHooks …
Uncaught Error: hit <-- Duplicate
at Throws
at renderWithHooks …
The above error occurred in the Throws component:
at Throws
at ErrorBoundary
at App
React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.
to
Error: hit
at Throws
at renderWithHooks …
The above error occurred in the Throws component:
at Throws
at ErrorBoundary
at App
React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.
at ErrorBoundary
at App
또한 onRecoverableError를 보완하기 위해 두 가지 새로운 루트 옵션을 추가했습니다.
이전 버전에서는 React에서 인식할 수 없는 prop을 속성이 아닌 속성으로 처리했기 때문에 React에서 사용자 정의 요소를 사용하는 것이 어려웠습니다.
React 19에서는 다음 전략을 사용하여 클라이언트 및 SSR 중에 작동하는 속성에 대한 지원을 추가했습니다.
forwardRef
가 필요하지 않아졌습니다.<title>
, <link>
, <meta>
)를 컴포넌트 내에서 네이티브하게 렌더링할 수 있도록 지원됩니다. 이를 통해 클라이언트 전용 앱, 스트리밍 SSR 및 서버 컴포넌트와 함께 사용할 수 있게 되었습니다.<link rel="stylesheet" href="...">
) 및 인라인 스타일시트(<style>...</style>
)를 DOM 내에서 적절한 위치에 삽입할 수 있는 스타일시트 지원이 추가되었습니다. 이를 통해 컴포넌트 내부에서 스타일시트의 우선순위를 관리하고 로딩 순서를 보장할 수 있습니다.<script async src="...">
)를 어디서든지 컴포넌트 트리 내부에 렌더링할 수 있는 지원이 개선되었습니다. 이를 통해 스크립트를 필요한 컴포넌트 안에 자유롭게 배치할 수 있고 중복으로 렌더링되는 것을 방지할 수 있습니다.기존 React의 성능과 편의성을 높이고 기존 html 태그에 대한 지원을 늘린 것이 돋보인다.