리액트 18버전에서 concurrent rendering을 소개했다.
여러가지 컴포넌트가 동시에 랜더링 될때 우선순위를 매기게 할 수 있는 api이다.
구체적으로는 useTransition , useDeferredValue
를 통해 가능하다.
위 두 api를 이용하여 랜더링을 한다면 랜더링을 후순위로 뺄 수 있다.
이는 가장먼저 랜더링을 해야하는 컴포넌트가 랜더링되는데 오래걸리는 무거운 컴포넌트에 의해 랜더링이 늦춰지는 것을 방지 할 수 있다.
가장 대표적인 예시가, debounce를사용해서 랜더링되는데 시간이 오래걸리는 것을 한 번만 수행하는 것일것이다.
그러나 이제는 debounce를 사용하지 않고 리액트에서 제공하는 useTransition , useDeferredValue
를 통해 구현가능하다.
아래 코드를 살펴보자. input에 숫자를 입력하면 30000개의 div가 계산되어 출력된다고 하자.
import { ChangeEvent, useState } from "react";
const CalResult = ({ value }: { value: number | string }) => {
return Number(value) > 0
? Array.from(Array(30000).keys()).map((i) => (
<div key={i} className={"m-0 p-0 col-1"}>
{Number(value) * (i + 1)}
</div>
))
: null;
};
export default function Slow() {
const [num, setNum] = useState(0);
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
setNum(Number(e.target.value));
};
return (
<main className=" container my-5">
<input type="text" name="num" id="num" value={num} onChange={onChange} />
<span className="ms-5 mt-3 h3">100,000 multiples of number: {num}</span>
<div className="flex flex-wrap gap-[10px] mt-5">
<CalResult value={num} />
</div>
</main>
);
}
그럼 input에 숫자를 입력할때마다 아래 영상 처럼 될것이다.
여기서 랜더링 우선순위를 정리해보자.
가장먼저 랜더링되어야하는것은 input
태그일것이고,
가장 나중에 랜더링되어야하는 것은 CalResult
컴포넌트일것이다.
그럼 CalResult
에 우선 useDeferredValue를 적용하여 우선순위를 늦춰주자.(useTransition을 이용한 방법은 추후에 설명하겠다)
export default function App() {
const [isPending, startTransition] = useTransition();
const [num, setNum] = useState(0);
const [delayedNum, setDelayedNum] = useState<string | number>(0);
const value = useDeferredValue(num);
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
setNum(Number(e.target.value));
};
const HeavyComponent = useMemo(() => <CalResult value={value} />, [value]);
return (
<main className=" container my-5">
<input type="text" name="num" id="num" value={num} onChange={onChange} />
<span className="ms-5 mt-3 h3">100,000 multiples of number: {num}</span>
<div className="flex flex-wrap gap-[10px] mt-5">
{HeavyComponent}
</div>
</main>
);
}
사용방법은 useDeferredValue에 자주 바뀌는 변수값
을 인자로 pass시킨다.
그럼 리턴되는 값 value
는 가장 먼저 바뀌는 num
이후에 변경이 된다.
이 개념을 useMemo에 적용하여 HeavyComponent를 만들었다.
이렇게 되면, 일반적은 setState 즉 setNum에 의한 랜더링이 가장 먼저 일어나고 이후에 value의 값이 num에 따라 바뀔 것이다.
그럼 useTransition은 무엇이냐? useDeferredValue는 값을 넣었지만 useTransition는 함수를 pass한다.
그리고 isPending이라는 값을 사용할 수 있다. 즉 랜더링되기 전까지는 isPending이 true이다. 이 값으로 로딩바를 구현할 수 있다.
export default function App() {
const [isPending, startTransition] = useTransition();
const [num, setNum] = useState(0);
const [delayedNum, setDelayedNum] = useState<string | number>(0);
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
setNum(Number(e.target.value));
startTransition(() => {
setDelayedNum(e.target.value);
});
};
return (
<main className=" container my-5">
<input type="text" name="num" id="num" value={num} onChange={onChange} />
<span className="ms-5 mt-3 h3">100,000 multiples of number: {num}</span>
<div className="flex flex-wrap gap-[10px] mt-5">
{isPending ? "Loading..." : <CalResult value={delayedNum} />}
</div>
</main>
);
}
useTransition이 리턴하는 startTransition에 setState를 넣으면 된다.
기억해야할 점은 리액트는 setState가 여러번 같은 스코프에서 호출될지 batch를 통해 랜더링을 최소화한다는 것이다. startTransition안에서도 마찬가지이다.
또한 startTransition안에서 스코프를 하나 더 만들어 setState를 호출하면 리액트가 랜더링 우선순위를 감지하는 콜스택에 쌓이지 않아 의도하지 않은 효과를 낼 수 있다. 아래와 같은 경우이다.
startTransition(() => {
setTimeout(() => {
// By the time setTimeout's callback is called
// we're already in another call stack
// This will be marked as a high priority update
// instead
setCount((count) => count + 1);
}, 1000);
});
startTransition(() => {
asyncWork().then(() => {
// Different call stack
setCount((count) => count + 1);
});
});
그래서 아래와 같이 변경해야한다.
setTimeout(() => {
startTransition(() => {
setCount((count) => count + 1);
});
}, 1000);
asyncWork().then(() => {
startTransition(() => {
setCount((count) => count + 1);
});
});
끝!