React 19에서 상태의 개념과 동작원리가 변하는가?
리액트 애플리케이션에서는 데이터를 변경하고 이에 대한 응답을 기반으로 상태를 업데이트하는 것이 일반적인 사례
- e.g 사용자가 이름을 변경하기 위해 양식(form)을 제출하면, API 요청을 보낸 다음 응답을 처리
이전 : 대기 상태(pending state), 에러, 낙관적 업데이트, 순차적 요청을 직접 처리하는 방식
// 액션(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>
);
}
리액트 19에서는 트랜지션에서 비동기 함수를 사용하여 대기 상태, 에러, 양식, 낙관적 업데이트를 자동으로 처리하는 기능이 추가됨
아래와 같이 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>
);
}
비동기 트랜지션은 isPending
값을 즉시 true로 설정하며 비동기 요청을 보내고,
트랜지션이 수행되면 isPending
을 false로 전환
- 이러한 방식은 데이터가 변경되는 동안에도 현재 UI의 반응성 및 상호작용을 유지 가능.
컨벤션에 따라 비동기 트랜지션을 사용하는 함수는 "액션(Actions)"라고 부름
액션은 데이터 제출을 자동으로 관리함
- 대기 상태: 액션은 요청이 시작될 때 함께 시작하며, 최종 상태 업데이트가 커밋되면 자동으로 재설정되는 대기 상태를 제공
- 낙관적 업데이트: 액션은 새로운 훅인 useOptimistic을 지원, 사용자들이 요청을 제출했을 때 즉각적인 응답을 제공 가능
- 에러 처리: 액션은 에러 처리 기능을 제공하므로 요청이 실패했을 때 에러 바운더리를 표시 가능 또한 낙관적 업데이트로 수정된 값을 자동으로 원래의 값으로 복구 가능
- 양식(form):
<form>
요소에action
과formAction
프로퍼티에 함수를 전달 가능,action
프로퍼티에 함수를 전달하면 기본적으로 액션을 사용하고 제출 후에 자동으로 양식을 재설정
-
액션을 기반으로 구축된 리액트 19는 낙관적 업데이트의 처리를 위한
useOptimistic
, 액션의 일반적인 사용을 다루기 위한 새로운 훅React.useActionstate
을 도입 -
react-dom
에서는 양식을 자동으로 관리하는form
액션, 양식에서 액션을 지원하는useFormStatus
추가하고 있습니다. -
리액트 19에서는 위의 예시를 아래와 같이 단순하게 작성 가능
// <form> 액션과 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>
);
}
액션의 일반적인 경우를 더 쉽게 처리하기 위해 useActionState
훅을 추가
const [error, submitAction, isPending] = useActionState(
async (previousState, newName) => {
const error = await updateName(newName);
if (error) {
// 액션의 어떤 결과든 반환할 수 있습니다.
// 이 예제에서는 에러만 반환합니다.
return error;
}
// 요청이 성공한 경우
return null;
},
null
);
useActionState
는 함수("액션")를 받아 호출할 래핑된 액션을 반환.- 이는 각 액션이 조합되므로 가능, 래핑된 액션이 호출되면
useActionState
는 액션의 마지막 결과를data
로, 액션의 대기 상태를pending
으로 반환합니다.
- 이는 각 액션이 조합되므로 가능, 래핑된 액션이 호출되면
디자인 시스템에서 컴포넌트가 속한 <form>
에 대한 정보를 접근해야 할 때가 많은데,
컴포넌트에 프로퍼티를 드릴링하지 않고 접근하려면 컨텍스트를 사용할 수도 있지만
이를 좀 더 쉽게 다루도록 useFormStatus
훅을 새롭게 추가
import { useFormStatus } from "react-dom";
function DesignButton() {
const { pending } = useFormStatus();
return <button type="submit" disabled={pending} />;
}
useFormStatus
훅은 컨텍스트 프로바이더처럼 부모의 <form>
의 상태를 읽습니다.
데이터 변경을 수행할 때 비동기 요청이 진행되는 동안 최종 상태를 낙관적으로 표시하는 것도 흔한 UI 패턴.
리액트 19에서 이를 쉽게 처리하는 useOptimistic
훅이 추가됨
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>
);
}
useOptimistic
훅은updateName
요청이 진행되면optimisticName
값을 바로 보여줌- 업데이트가 종료되거나 실패하면 리액트는 자동으로
currentName
값으로 다시 전환됨
- 업데이트가 종료되거나 실패하면 리액트는 자동으로
리액트 19에서는 렌더링에서 리소스를 읽는 새로운 API인 use
를 도입.
import { use } from "react";
function Comments({ commentsPromise }) {
// `use`는 프로미스가 해결될 때까지 일시 중단됩니다.
const comments = use(commentsPromise);
return comments.map((comment) => <p key={comment.id}>{comment}</p>);
}
function Page({ commentsPromise }) {
// Comments에서 `use`가 일시 중단되면,
// 이 컴포넌트의 서스펜스 바운더리가 보여집니다.
return (
<Suspense fallback={<div>Loading...</div>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
);
}