React v19가 나오면서 새로운 훅들을 내용 정리.
전체적으로 Concurrent 렌더링을 최대한 활용하고, 코드의 가독성을 높이는데 주력한 느낌..
1. useTransition
useTransition은 React가 중요하지 않은 UI 업데이트를 뒤로 미루고,
더 중요한 작업(예: 입력 반응 등)을 먼저 처리하도록 도와주는 훅.
왜 사용해야 할까?
사용자 입력(setInput)은 빠르게 반응해야 하고, 리스트 필터링은 좀 늦게 떠도 괜찮음.
•
startTransition() 안의 setFiltered()는 느려도 되는 작업 → 비동기 처리
즉, React 내부에서 우선순위를 낮춘 비동기적 스케줄링
•
input의 UI 업데이트는 즉시 실행하지만 filtered 동작은 뒤로 미룸. isPending으로 상태까지 관리 가능
input에 검색어를 입력하고, 대량 데이터를 필터링해서 보여주는 UI의 상황
import { useState, useTransition } from 'react';
function SearchComponent({ data }) {
const [input, setInput] = useState('');
const [filtered, setFiltered] = useState(data);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setInput(value);
startTransition(() => {
const filteredData = data.filter((item) =>
item.toLowerCase().includes(value.toLowerCase())
);
setFiltered(filteredData);
});
};
return (
<>
<input value={input} onChange={handleChange} />
{isPending && <p>로딩 중...</p>}
<ul>
{filtered.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</>
);
}
JavaScript
복사
2. useDeferredValue
변화는 바로 일어나되, 실제로 UI 반영은 나중에 일어나게 하고 싶을 때 사용.
왜 사용해야 할까?
사용자는 input 타이핑할 때마다 즉시 화면에 반영되는 걸 원함.
하지만 필터된 리스트가 너무 커서 렌더링이 느림 → 그래서 렌더링 기준이 되는 값만 지연
•
useTranstion과 좀 헷길리는 부분이 있는데, defer하는 대상이 다르다.
useTransition → setState가 defer(즉, 무거운 상태 업데이트가 있을때)
useDeferredValue → 값 자체의 렌더링 반영을 defer(렌더링만 무거울 때)
•
즉, 입력값은 즉시 반영할게. 대신 이 입력값을 기반으로 렌더링은 나중에.
Debounce처럼 동작하는 것처럼 보이지만, useTransition처럼 취소가 되진 않고, 최신 값만 반영한다.
따라서, 디바운스가 필요한 경우에는 디바운스를 사용하는게 좋아보임.
const deferredSearchTerm = useDeferredValue(searchTerm);
useEffect(() => {
fetchData(deferredSearchTerm); // 너무 자주 안 불러지게 함
}, [deferredSearchTerm]);
JavaScript
복사
3. useActionState
useActionState는 form 제출 + 상태 업데이트 + 로딩 처리를 한 번에 처리하는 훅
서버 액션(form action)과 폼 기반 요청 처리에 최적화 되어있다.
기존에는 form 데이터를 처리하려면:
1.
useState로 상태를 만들고
2.
onSubmit 핸들러 만들어서
3.
fetch하고
4.
isLoading 상태 따로 관리하고
5.
응답 처리도 따로…
하지만 useActionState에서는 한번에 가능하다.
const [state, formAction, isPending] = useActionState(actionFn, initialState);
JavaScript
복사
state | 현재 액션의 상태 (예: 에러, 결과 등) |
formAction | <form action={formAction}> 처럼 바로 쓸 수 있는 함수 |
isPending | 해당 액션이 실행 중인지 여부 (로딩 UI 용도) |
로그인 관련
// 로그인 액션
async function loginAction(prevState, formData) {
const email = formData.get("email");
const password = formData.get("password");
const res = await fetch("/api/login", {
method: "POST",
body: formData,
});
if (!res.ok) return { error: "로그인 실패" };
return { success: true };
}
// 로그인 폼
export default function LoginForm() {
const [state, formAction, isPending] = useActionState(loginAction, null);
return (
<form action={formAction}>
<input name="email" />
<input name="password" type="password" />
<button disabled={isPending}>로그인</button>
{state?.error && <p style={{ color: "red" }}>{state.error}</p>}
{state?.success && <p style={{ color: "green" }}>로그인 성공!</p>}
</form>
);
}
JavaScript
복사
Tanstack-Query와 굉장히 유사하다. 만약 Tanstack-Query를 쓰고있다면 굳이 쓸 필요가 없어보인다..?
4. useOptimistic
버튼을 눌렀을 때 딜레이 없이 UI가 바로 바뀌어야 사용자 경험을 높일 수 있다.
하지만 서버 응답이 느릴 수도 있으니까 “일단 바뀌었다고 가정하고 보여주는” UI가 필요
기존에는 임시 상태를 만들어서 해결했었다.
const [optimisticState, addOptimistic] = useOptimistic(actualState, updaterFn);
JavaScript
복사
actualState | 서버의 진짜 응답 데이터 (또는 초기 상태) |
updaterFn(prev, newInput) | 낙관적으로 반영할 방법 |
optimisticState | 실제 보여줄 UI용 데이터 (진짜 + 낙관적 예측 포함) |
addOptimistic(input) | 낙관적 업데이트 트리거 함수 |
댓글 추가 UI
const [comments, setComments] = useState([]);
const [optimisticComments, addOptimisticComment] = useOptimistic(comments, (prev, newComment) => {
return [...prev, { id: 'temp', text: newComment, pending: true }];
});
async function handleSubmit(formData) {
const commentText = formData.get('comment');
addOptimisticComment(commentText); // 먼저 UI에 추가
const res = await fetch('/api/comment', {
method: 'POST',
body: formData,
});
const saved = await res.json();
setComments(prev => [...prev, saved]); // 진짜 데이터 반영
}
// 댓글 폼
<form action={handleSubmit}>
<input name="comment" />
</form>
<ul>
{optimisticComments.map((c) => (
<li key={c.id}>{c.text}{c.pending && ' (전송 중...)'}</li>
))}
</ul>
JavaScript
복사
낙관적 UI를 구현한 경험은 없지만, 구현한다고 생각을 해보니 useOptimitic이 굉장히 유용하게 쓰일거 같다.
5. useFormStatus
useFormStatus는 form이 현재 제출 중인지, 성공했는지, 실패했는지 form 내부의 컴포넌트 어디에서든 알 수 있게 해주는 훅.
•
useActionState와 좀 헷갈리지만, useActionState가 좀 더 범용성이 있다.
import { useFormStatus } from "react-dom"; // ✅ React 19
function SubmitButton() {
const { pending, data, action, method } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "제출 중..." : "제출"}
</button>
);
}
JavaScript
복사