React는 매 업데이트마다 개발자의 생산성과 사용자 경험을 개선하기 위해 새로운 기능을 도입해왔습니다. 특히 React 18과 19에서는 비동기 작업 관리와 UI 반응성을 향상시키는 여러 Hook이 추가되었습니다. 이번 글에서는 useId
, useTransition
, 그리고 useOptimistic
과 같은 최신 Hook을 살펴보고, 이를 통해 어떻게 효율적인 비동기 처리가 가능한지 탐구해 보겠습니다.
useId
useId
는 React 18에서 고유한 ID 생성을 위해 도입된 Hook입니다. 서버와 클라이언트에서 동일한 ID를 생성하여 렌더링의 일관성을 보장하며, 클라이언트-서버 간 불일치를 방지할 수 있습니다.
아래는 useId
를 사용하여 고유 ID를 생성하고, 폼의 label
과 input
을 연결하는 예제입니다.
import React, { useId } from 'react';
function Form() {
const id = useId();
return (
<div>
<label htmlFor={`${id}-username`}>Username</label>
<input id={`${id}-username`} type="text" />
<label htmlFor={`${id}-password`}>Password</label>
<input id={`${id}-password`} type="password" />
</div>
);
}
export default Form;
유의사항
컴포넌트가 여러 번 렌더링되더라도 ID의 일관성이 유지됩니다.
리스트의 key를 생성하기 위해 useId를 사용하지 말아야 합니다.
useTransition
useTransition
은 사용자 인터페이스의 반응성을 유지하면서 비동기 상태 관리를 간편하게 처리할 수 있도록 제공되는 Hook입니다.
import React, { useState, useTransition } from 'react';
function UpdateName() {
const [name, setName] = useState('');
const [isPending, startTransition] = useTransition();
const handleSubmit = () => {
startTransition(() => {
// 비동기 작업 수행
setName('Updated Name');
});
};
return (
<div>
<p>{isPending ? 'Updating...' : name}</p>
<button onClick={handleSubmit}>Update Name</button>
</div>
);
}
export default UpdateName;
특징
- 데이터 로딩이나 상태 업데이트 시 UI가 멈추지 않도록 보장합니다.
- 이 진행되는 동안 사용자는 현재 상태를 확인할 수 있습니다.
예제 코드를 봤을 때, "useState로 처리하는 것과 어떤 것이 다르지? 🤔"라는 의문이 들 수 있습니다.
useState와의 비교
useState
로 상태 관리하기
import React, { useState } from 'react';
function UpdateNameWithState() {
const [name, setName] = useState('');
const [isPending, setIsPending] = useState(false);
const handleSubmit = () => {
setIsPending(true);
setTimeout(() => {
setName('Updated Name');
setIsPending(false);
}, 1000);
};
return (
<div>
<p>{isPending ? 'Updating...' : name}</p>
<button onClick={handleSubmit} disabled={isPending}>
Update Name
</button>
</div>
);
}
export default UpdateNameWithState;
useTransition
으로 상태 관리하기
import React, { useState, useTransition } from 'react';
function UpdateNameWithTransition() {
const [name, setName] = useState('');
const [isPending, startTransition] = useTransition();
const handleSubmit = () => {
startTransition(() => {
setTimeout(() => {
setName('Updated Name');
}, 1000);
});
};
return (
<div>
<p>{isPending ? 'Updating...' : name}</p>
<button onClick={handleSubmit} disabled={isPending}>
Update Name
</button>
</div>
);
}
export default UpdateNameWithTransition;
주요 차이점
특징 | useState | useTransition |
---|---|---|
로딩 상태 관리 | isPending을 직접 관리해야 함 | 자동으로 isPending 관리 |
우선순위 처리 | 모든 업데이트가 동등한 우선순위 | 낮은 우선순위 작업을 비동기로 처리 |
UI 반응성 | 복잡한 작업 시 UI가 멈출 수 있음 | 높은 우선순위 작업을 차단하지 않음 |
렌더링 빈도 | 상태 변경마다 즉시 렌더링 발생 | 낮은 우선순위 작업을 배치 처리 |
참고 영상
예를 들어, 대규모 리스트 필터링을 처리하는 검색 창을 생각해봅시다. 사용자가 검색어를 입력할 때마다 데이터를 필터링하고 렌더링해야 합니다.
useState
를 사용하면 렌더링 과정에서 UI가 잠시 멈추거나 입력 반응이 느려질 수 있습니다.- 반면
useTransition
은 필터링 작업을 낮은 우선순위로 처리하므로, 입력 창은 부드럽게 반응합니다.
테스트 코드
import React, { useEffect, useState, useTransition } from 'react';
function PerformanceComparison() {
const \[inputValue, setInputValue\] = useState('');
const \[inputValue2, setInputValue2\] = useState('');
const \[listWithState, setListWithState\] = useState(\[\]);
const \[isPendingWithState, setIsPendingWithState\] = useState(false);
const \[listWithTransition, setListWithTransition\] = useState(\[\]);
const \[isPendingWithTransition, startTransition\] = useTransition();
const LIST\_SIZE = 4000;
// Independent animation (not re-rendered every time)
useEffect(() => {
const animate = () => {
const dots = document.getElementsByClassName('dot');
for (let i = 0; i < dots.length; i++) {
const dot = dots\[i\];
const currentTop = parseInt(dot.style.top || '0', 10);
dot.style.top = ((currentTop + 1) % 50) + 'px';
}
requestAnimationFrame(animate);
};
animate();
}, \[\]);
// Generate a large list with useState
const handleChangeWithState = e => {
const value = e.target.value;
setInputValue(value);
setIsPendingWithState(true);
setTimeout(() => {
const newList = \[\];
for (let i = 0; i < LIST\_SIZE; i++) {
newList.push(\`${value} - Item ${i + 1}\`);
}
setListWithState(newList);
setIsPendingWithState(false);
}, 0); // Simulate async work
};
// Generate a large list with useTransition
const handleChangeWithTransition = e => {
const value = e.target.value;
setInputValue2(value);
startTransition(() => {
const newList = \[\];
for (let i = 0; i < LIST\_SIZE; i++) {
newList.push(\`${value} - Item ${i + 1}\`);
}
setListWithTransition(newList);
});
};
return (
<div style={{ padding: '20px' }}>
<h1>useState vs useTransition Performance Comparison</h1>
<div>
<h2>Using useState (Blocking)</h2>
<input
type="text"
value={inputValue}
onChange={handleChangeWithState}
placeholder="Type something..."
/>
<p>
{isPendingWithState
? 'Updating...'
: \`Rendering ${listWithState.length} items...\`}
</p>
<div
style={{
position: 'relative',
height: '50px',
overflow: 'hidden',
margin: '10px 0',
}}
>
{Array.from({ length: 50 }).map((\_, index) => (
<div
key={index}
className="dot"
style={{
width: '5px',
height: '5px',
backgroundColor: 'blue',
position: 'absolute',
top: \`${Math.random() \* 50}px\`,
left: \`${index \* 10}px\`,
borderRadius: '50%',
}}
></div>
))}
</div>
<ul
style={{
height: '200px',
overflowY: 'scroll',
border: '1px solid #ddd',
}}
>
{listWithState.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
<div style={{ marginTop: '20px' }}>
<h2>Using useTransition (Non-blocking)</h2>
<input
type="text"
value={inputValue2}
onChange={handleChangeWithTransition}
placeholder="Type something..."
/>
<p>
{isPendingWithTransition
? 'Updating...'
: \`Rendering ${listWithTransition.length} items...\`}
</p>
<div
style={{
position: 'relative',
height: '50px',
overflow: 'hidden',
margin: '10px 0',
}}
>
{Array.from({ length: 50 }).map((\_, index) => (
<div
key={index}
className="dot"
style={{
width: '5px',
height: '5px',
backgroundColor: 'blue',
position: 'absolute',
top: \`${Math.random() \* 50}px\`,
left: \`${index \* 10}px\`,
borderRadius: '50%',
}}
></div>
))}
</div>
<ul
style={{
height: '200px',
overflowY: 'scroll',
border: '1px solid #ddd',
}}
>
{listWithTransition.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
</div>
);
}
export default PerformanceComparison;
useOptimistic
useOptimistic
을 사용하면 비동기 작업 중 낙관적 UI 업데이트를 간편하게 구현할 수 있습니다.
import { useOptimistic, useRef, useState } from 'react';
async function deliverMessage(message) {
await new Promise(res => setTimeout(res, 1000));
return message;
}
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} />;
}
특징
- 서버 작업 완료 전, 즉각적인 피드백을 제공합니다.
- 사용자 경험을 개선하고, 비동기 작업 중에도 끊김 없는 UI를 유지합니다.
참고 영상
낙관적 상태 관리를 통해 서버 응답 시간을 기다리지 않고도 UI를 업데이트할 수 있어, 렌더링 지연 없이 빠르게 상태를 반영할 수 있었습니다.
결론
React의 최신 버전에서 제공하는 useTransition, useOptimistic와 같은 Hook은 비동기 작업 중에도 UI의 반응성을 유지하고, 사용자 경험을 한층 개선할 수 있는 강력한 도구들입니다.
물론, 이러한 Hook들은 특정 작업에서는 매우 유용하지만, 데이터 캐싱, 동기화, 오류 복구와 같은 서버 상태 관리를 포괄하는 react-query와 같은 라이브러리를 완전히 대체하기는 어려울 것같습니다.
'Frontend > Development' 카테고리의 다른 글
javascript 이미지 색상 추출하기 (0) | 2024.08.11 |
---|---|
내부 요소 스크롤 위치 도달 시 부모 요소 스크롤 방지하기 (0) | 2024.05.28 |
useLoaderData와 useRouteLoaderData의 사용법과 차이점 (2) | 2023.12.31 |
FontFace를 활용하여 웹사이트의 글꼴을 동적으로 로딩하기 (0) | 2023.12.17 |
왜 컴포넌트 안에서 new QueryClient 사용을 지양해야 할까? (0) | 2023.11.24 |