2024. 6. 22. 09:45ㆍ리액트 (React)/리액트 기술 (React Tech)
useDeferredValue
state 또는 props 변경을 미룹니다.
다만 리렌더링을 방지하려면, memo와 함께 쓰는 것이 좋습니다.
역할은 useTransition 훅과 비슷합니다.
다만 useTransition은 state에 대한 변경 동작에 적용하는 것이고
useDeferredValue는 변경된 값에 적용하기 때문에, 순수 state를 쓰든 deferred value를 쓰든 자유입니다.
하지만 리렌더링을 방지하기 위해서는 memo와 함께 사용하는 것이 좋습니다.
useDeferredValue만 쓰면 부모 컴포넌트와 리렌더링을 함께 하기 때문입니다.
Lag in Search
간단하게 과일 이름을 검색하는 예제를 만들어보겠습니다.
(과일 이름 출처는 wikipedia - List of culinary fruits 입니다.)
import { useState } from "react";
import "./FruitBox.scss";
import FruitList from "./FruitList";
export default function FruitBox() {
const [inputtedName, setInputtedName] = useState<string>("");
return (
<div className="tech-use-deferred-value">
<div className="container">
<div className="box">
<p className="title">Fruits</p>
<input
className="name"
value={inputtedName}
onChange={(e) => {
setInputtedName(e.target.value);
}}
/>
<FruitList query={inputtedName} />
</div>
</div>
</div>
);
}
import { useState } from "react";
import { useMount } from "../hooks/use-mount";
import FruitLine from "./FruitLine";
import "./FruitList.scss";
import { Fruit } from "./model";
const FruitList = ({ query }: { query: string }) => {
const [fruits, setFruits] = useState<Fruit[]>([]);
useMount(() => {
import("./dataset/fruits.json").then((fruits) => {
const sortedFruits = fruits.default.sort((a, b) => a.name.localeCompare(b.name));
setFruits(sortedFruits);
});
});
let filteredFruits: Fruit[] = [];
if (query !== "") {
filteredFruits = fruits.filter((fruit) =>
fruit.name.toLowerCase().includes(query.toLowerCase())
);
}
return (
<ul className="list">
{filteredFruits.map((fruit, index) => (
<FruitLine key={index} name={fruit.name} url={fruit.url} />
))}
</ul>
);
};
export default FruitList;
export default function FruitLine({ name, url }: { name: string; url: string | null }) {
const startTime = performance.now();
while (performance.now() - startTime < 2) {}
return url ? (
<p>
<a href={url} rel="noreferrer" target="_blank">
{name}
</a>
</p>
) : (
<p>{name}</p>
);
}
일부러 과일 이름 한 줄을 만들 때 2 밀리초의 지연 시간을 만들었습니다.
따라서 과일 이름을 검색해보면, 아래와 같이 심각하게 답답한 그림이 연출됩니다.
이제 useDefferedValue 훅을 적용해보면,
import { useDeferredValue, useState } from "react";
...
export default function FruitBox() {
const [inputtedName, setInputtedName] = useState<string>("");
const deferredInputtedName = useDeferredValue<string>(inputtedName);
return (
<div className="tech-use-deferred-value">
<div className="container">
<div className="box">
...
<FruitList query={deferredInputtedName} />
</div>
</div>
</div>
);
}
아까와 달리 입력이 비교적 자연스럽게 됩니다.
하지만 여전히 글자를 지울 때 뭔가 시원찮게 느립니다.
또 한꺼번에 글자를 입력하는 것이 아닌,
'ap'나 'ch' 처럼 두 글자를 먼저 입력하고 나머지 글자를 입력했을 때도 체감이 되죠.
분명 useDeferredValue를 사용하면 값 변경을 미루기 때문에
FruitList에 props으로 제공하는 query도 동일할꺼라, UI 업데이트도 미룰꺼라 생각했는데
어떻게 이런 상황이 발생할까요?
Rerendering with Parent
음... 필자가 생각했을 때, 이는 향후 고쳐야될 점으로 보이긴 합니다.
FruitBox, FruitList 각각에 console.log를 찍어보겠습니다.
...
export default function FruitBox() {
const [inputtedName, setInputtedName] = useState<string>("");
const deferredInputtedName = useDeferredValue<string>(inputtedName);
console.log(`deferred input : ${deferredInputtedName}`);
...
}
...
const FruitList = ({ query }: { query: string }) => {
console.log("FruitList is Rendering");
...
};
export default FruitList;
이제 'apple'을 검색해보면,
FruitBox.tsx:10 deferred input :
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input : a
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input : ap
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input : app
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input : appl
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input : apple
FruitList.tsx:16 FruitList is Rendering
입력값이 변경될 때마다 매번 렌더링됩니다.
(근데 빈 문자열이 왜 함께 등장하는 지는 잘 모르겠네요...)
한 글자씩 지워보면 어떨까요?
FruitBox.tsx:10 deferred input : apple
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input : apple
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input : apple
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input : apple
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input : apple
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input : apple
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitList.tsx:16 FruitList is Rendering
더 명확하게 보려면 아예 state를 안 넣으면 됩니다.
export default function FruitBox() {
const [inputtedName, setInputtedName] = useState<string>("");
const deferredInputtedName = useDeferredValue<string>("");
...
}
FruitBox.tsx:10 deferred input :
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitList.tsx:16 FruitList is Rendering
inputtedName state와 deferredInputtedName이 연관이 없는데도 FruitList는 여전히 리렌더링됩니다.
즉, 부모 컴포넌트가 리렌더링 될 때, deferred value를 사용하는 자식 컴포넌트도 함께 리렌더링됩니다.
memo
메모이제이션(memoization)을 통해, props가 동일하면 리렌더링을 하지 않는 API입니다.
일전에 부모 컴포넌트가 리렌더링을 했으니 뭐가 됬든 자식 컴포넌트도 리렌더링을 한 걸 봤었는데,
memo를 적용하면 부모 컴포넌트가 리렌더링을 하던지 말던지 props의 차이만 보고 리렌더링을 합니다.
import { memo, useState } from "react";
...
const FruitList = memo(({ query }: { query: string }) => {
...
});
다시 'apple'를 입력하고 콘솔을 보면 빈 값에 대해서는 렌더링을 하지 않는 걸 확인할 수 있습니다.
FruitBox.tsx:10 deferred input :
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitBox.tsx:10 deferred input : a
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitBox.tsx:10 deferred input : ap
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitBox.tsx:10 deferred input : app
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitBox.tsx:10 deferred input : appl
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input :
FruitBox.tsx:10 deferred input : apple
FruitList.tsx:16 FruitList is Rendering
한 글자씩 지워도 'apple'에 대해서는 렌더링을 하지 않죠.
FruitBox.tsx:10 deferred input : apple
FruitBox.tsx:10 deferred input : appl
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input : apple
FruitBox.tsx:10 deferred input : app
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input : apple
FruitBox.tsx:10 deferred input : ap
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input : apple
FruitBox.tsx:10 deferred input : a
FruitList.tsx:16 FruitList is Rendering
FruitBox.tsx:10 deferred input : apple
FruitBox.tsx:10 deferred input :
FruitList.tsx:16 FruitList is Rendering
이제 실제로 입력해보면 너무 자연스럽게 느껴집니다.
References
useDeferredValue – React
The library for web and native user interfaces
react.dev
memo – React
The library for web and native user interfaces
react.dev
'리액트 (React) > 리액트 기술 (React Tech)' 카테고리의 다른 글
Hook - useTransition (0) | 2024.06.18 |
---|