그럼에도 불구하고

👨‍💻

[React with TS] useTransition과 useDeferredValue 본문

React/React basics

[React with TS] useTransition과 useDeferredValue

zenghyun 2023. 7. 17. 16:17

 

useTransition과 useDeferredValue에 대해 알아보겠습니다.

 

 

🧑🏻‍💻 전환 기능

리액트 애플리케이션으로 개발하는 화면에서의 UI 업데이트는 두 가지 유형이 있습니다. 그중 하나는 사용자에게 즉각적인 피드백을 전달해야 하는 업데이트입니다. 예를 들면 화면의 입력 필드에 사용자가 타이핑하는 것을 예시로 들 수 있습니다. 사용자가 타이핑할 때 UI 업데이트가 느리면 실행 도중 화면이 깜빡거리거나 입력이 느려지는 등의 문제가 있을 수 있습니다. 

 

이러한 경우를 긴급한 업데이트(urgent update)라고 부릅니다.

 

또 다른 업데이트 유형은 사용자가 입력한 필드의 값을 이용해 백엔드의 데이터를 조회해서 화면을 갱신하는 작업입니다.

 

이 유형은 긴급한 업데이트보다는 긴급하지 않은 업데이트(non-urgent update) 또는 전환(transition)이라고 부릅니다.

 

애플리케이션 실행 도중에 긴급하지 않은 업데이트로 인해서 사용자에게 즉각적으로 피드백해야 하는 작업이 느려지게 되면 사이트의 성능 이슈가 발생할 수 있겠죠?? 리액트 18에서는 이런 문제를 해결할 수 있는 전환(transition) 기능이 추가되었습니다. 

 

전환 기능은 "애플리케이션에서 긴급한 업데이트를 우선 처리할 수 있도록 긴급하지 않아도 되는 UI 업데이트 부분을 전환 업데이트로 지정하는 기능"입니다.

 

 

아래의 예시를 볼까요?

 

📌 App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import React, { useState, useEffect, ChangeEvent } from "react";
import logo from "./assets/react.svg";
 
// 대량으로 생성할 아이템의 타입 정의
type ItemType = {
  id: number;
  keyword: string;
};
 
const App = () => {
  const [keyword, setKeyword] = useState<string>("");
  const [results, setResults] = useState<Array<ItemType>>([]);
 
  // useTransition 훅을 이용해 startTransition 함수를 받아냄
  const [isPending, startTransition] = useTransition();
 
  const handleChange = (e: ChangeEvent<HTMLInputElement>=> {
    setKeyword(e.target.value);
  };
 
  // 컴포넌트가 마운트될 때 results 상태에 빈 배열을 생성
  // keyword 상태가 바뀔 때 keyword에 입력이 되었다면 5,000개의 아이템 생성
  // 대량의 데이터를 업데이트하는 것은 긴급하지 않음
  useEffect(() => {
    if (keyword.trim() === "") {
      setResults([]);
    } else {
      const items = Array.from(Array(5000), (item, index) => {
        return { id: index, keyword: keyword };
      });
        setResults(items);
     }
  }, [keyword]);
 
  // results 상태를 이용해 div 대량 생성
  const divRows = results.map((item) => (
    <div key={item.id}>
      id: {item.id}
      <br />
      keyword: {item.keyword}
      <br />
      <img src={logo} style={{ width: 100, height: 100 }} />
      <hr />
    </div>
  ));
 
  // 사용자가 입력 필드에 타이핑하면 handleChange 함수를 실행하여 상태 변경
  // onChange 이벤트에 의해 바뀐 값을 렌더링 하는 것은 긴급한 업데이트가 요구됨
 
  return (
    <div style={{ margin: 10 }}>
      <div className="SearchInput">
        Keyword: <input type="text" value={keyword} onChange={handleChange} />
      </div>
      <hr />
      <div>
      {divRows}
      </div>
    </div>
  );
};
 
export default App;
 
cs

 

useEffect 훅을 이용해서 컴포넌트가 처음 마운트될 때와 keyword 상태가 바뀔 때마다 5,000개의 아이템을 생성합니다. 이것은 긴급하지 않은 업데이트, 즉 전환에 해당하는 UI 업데이트로도 충분합니다. 

 

반면 입력 필드에서 onChange 이벤트의 핸들러로 실행하여 keyword 상태를 바꾸면 입력 필드 UI가 업데이트되어야 합니다. 이때는 사용자와의 즉각적인 상호작용이 요구되므로 긴급하게 업데이트되어야 합니다. 

 

하지만 예제를 실행해보면 입력 필드에 입력하는 순간, 대량의 데이터를 렌더링 하는 작업이 실행되는데, 이로 인해 긴급해야 하는 입력 필드의 업데이트가 영향을 받는 문제가 발생합니다. 

 

개발 서버를 구동하고 브라우저의 입력 필드에 빠르게 글자를 여러 개 입력해 보세요. 첫 글자가 입력되고 나서 화면에 5,000개의 아이템이 모두 나타날 때까지 나머지 입력한 글자가 보이지 않고 잠시 먹통이 됩니다. 

 

 

 

이렇게 작동하는 이유는 첫 글자가 입력되고 keyword 상태가 바뀌면서 다시 렌더링 되는데 이때 useEffect 훅에 의해 5,000개의 아이템이 생성되는 상태 변화가 일어나고, 이로 인해 발생하는 대량의 UI 업데이트가 입력 필드의 갱신과 사용자 입력 작업에 영향을 주기 때문입니다.

 

전환 기능을 적용하면 이 문제를 해결할 수 있습니다.

 

📌 useTransition (startTransition)

 전환 기능을 적용하기 위해서는 startTransition 함수를 사용하면 됩니다. 함수를 직접 참조해 사용할 수도 있고, useTransition() 훅을 이용해 리턴 받은 startTransition 함수를 사용해도 됩니다. 이 예제에서는 후자를 사용하겠습니다. 

 

📌 App.tsx

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import React, { useState, useEffect, ChangeEvent, useTransition } from "react";
import logo from "./assets/react.svg";
 
// 대량으로 생성할 아이템의 타입 정의
type ItemType = {
  id: number;
  keyword: string;
};
 
const App = () => {
  const [keyword, setKeyword] = useState<string>("");
  const [results, setResults] = useState<Array<ItemType>>([]);
 
  // useTransition 훅을 이용해 startTransition 함수를 받아냄
  const [isPending, startTransition] = useTransition();
 
  const handleChange = (e: ChangeEvent<HTMLInputElement>=> {
    setKeyword(e.target.value);
  };
 
  // 컴포넌트가 마운트될 때 results 상태에 빈 배열을 생성
  // keyword 상태가 바뀔 때 keyword에 입력이 되었다면 5,000개의 아이템 생성
  // 대량의 데이터를 업데이트하는 것은 긴급하지 않음
  useEffect(() => {
    if (keyword.trim() === "") {
      setResults([]);
    } else {
      const items = Array.from(Array(5000), (item, index) => {
        return { id: index, keyword: keyword };
      });
      // 전환 작업으로 업데이트하도록 지정
      startTransition(() => {
        setResults(items);
      });
    }
  }, [keyword]);
 
  // results 상태를 이용해 div 대량 생성
  const divRows = results.map((item) => (
    <div key={item.id}>
      id: {item.id}
      <br />
      keyword: {item.keyword}
      <br />
      <img src={logo} style={{ width: 100, height: 100 }} />
      <hr />
    </div>
  ));
 
  // 사용자가 입력 필드에 타이핑하면 handleChange 함수를 실행하여 상태 변경
  // onChange 이벤트에 의해 바뀐 값을 렌더링 하는 것은 긴급한 업데이트가 요구됨
 
  return (
    <div style={{ margin: 10 }}>
      <div className="SearchInput">
        Keyword: <input type="text" value={keyword} onChange={handleChange} />
      </div>
      <hr />
      <div>
        {
          // isPending인 동안 true이면 fallback UI를 렌더링
          isPending ? <h2>로딩 중입니다.</h2> : divRows
        }
      </div>
    </div>
  );
};
 
export default App;
 
cs

 

import React, { useState, useEffect, ChangeEvent, useTransition } from "react"; 

... 

const [isPending, startTransition] = useTransition();

...

 startTransition(() => {
        setResults(items);
      });
      
...

  {
       // isPending인 동안 true이면 fallback UI를 렌더링
       isPending ? <h2>로딩 중입니다.</h2> : divRows
   }

 

다음과 같이 변경 후 브라우저의 입력 필드에 글자를 빠르게 입력해보세요. 이제는 부드럽게 입력되면서 입력 필드의 UI가 업데이트될 것입니다. 동시에 전환으로 업데이트가 진행되는 동안 다음과 같이 지정한  fallback UI가 나타납니다. 

 

 

 

 

이처럼 전환 기능으로 대량이 데이터를 UI로 업데이트하는 작업을 긴급하지 않은 업데이트로 지정해서 사용자의 입력에 대응하는 즉각적인 업데이트가 우선 처리되도록 조치할 수 있습니다.

 

 

🧑🏻‍💻 지연된 값 

지연된 값(deferred value)은 전환과 마찬가지로 지정한 값에 대해 긴급하지 않은 업데이트를 하도록 합니다. 전환과 다른 점은 전환은 직접 상태를 제어할 수 있어서 상태를 설정하는  setState()를 startTransition()으로 감싸주도록 처리하지만 지연된 값은 컴포넌트 자신이 제어할 수 있는 상태가 아니라는 점입니다. 

 

예를 들어 대량의 데이터를 속성으로 전달받는 상황을 생각해보세요. 부모 컴포넌트에서 대량의 상태가 생성되어 속성으로 전달되는 경우에 자식 컴포넌트는 상태를 직접 제어하지 못합니다. 이런 경우에 지연된 값을 사용합니다.

 

아래의 예시를 살펴보겠습니다.

 

App2는 App 컴포넌트와 유사합니다만 대량의 상태 데이터를 ItemList 컴포넌트의 속성으로 전달합니다.

 

📌 App2.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { useState, useEffect, ChangeEvent } from "react";
import ItemList from "./ItemList";
 
export type ItemType = {
  id: number;
  keyword: string;
};
 
const App = () => {
  const [keyword, setKeyword] = useState<string>("");
  const [results, setResults] = useState<Array<ItemType>>([]);
 
  const handleChange = (e: ChangeEvent<HTMLInputElement>=> {
    setKeyword(e.target.value);
  };
 
  useEffect(() => {
    if (keyword.trim() === "") {
      setResults([]);
    } else {
      const items = Array.from(Array(5000), (item, index) => {
        return { id: index, keyword: keyword };
      });
      setResults(items);
    }
  }, [keyword]);
 
  return (
    <div style={{ margin: 10 }}>
      <div className="SearchInput">
        Keyword: <input type="text" value={keyword} onChange={handleChange} />
      </div>
      <hr />
      <ItemList results={results} />
    </div>
  );
};
 
export default App;
 
cs

 

📌 ItemList.tsx (변경 전)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { ItemType } from "./App2";
import logo from "./assets/react.svg";
 
type Props = { results: Array<ItemType> };
 
const ItemList = (props: Props) => {
  const divRows = props.results.map((item) => (
    <div key={item.id}>
      id: {item.id}
      <br />
      keyword: {item.keyword}
      <br />
      <img src={logo} style={{ width: 100, height: 100 }} />
      <hr />
    </div>
  ));
 
  return <div>{divRows}</div>;
};
 
export default ItemList;
 
cs

 

이대로 실행하면 전환 예제에서 처음 겪었던 현상을 똑같이 경험할 수 있습니다. 즉, 입력 필드에 빠르게 글자를 입력하면 첫 글자가 나타나고 잠시 먹통이 될 것입니다. 이제 지연된 값을 적용하겠습니다. 지연된 값을 적용하기 위해 useDeferredValue() 훅을 이용하여 ItemList 컴포넌트를 변경해 보겠습니다. 

 

📌 ItemList.tsx (변경 후)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { ItemType } from "./App2";
import React, { useDeferredValue } from "react";
import logo from "./assets/react.svg";
 
type Props = { results: Array<ItemType> };
 
const ItemList = (props: Props) => {
  const deferredResults = useDeferredValue(props.results);
 
  const divRows = deferredResults.map((item) => (
    <div key={item.id}>
      id: {item.id}
      <br />
      keyword: {item.keyword}
      <br />
      <img src={logo} style={{ width: 100, height: 100 }} />
      <hr />
    </div>
  ));
  
 
  return <div>{divRows}</div>;
};
 
export default ItemList;
 
cs

 

import React, { useDeferredValue } from "react";

...

 const deferredResults = useDeferredValue(props.results);

 const divRows = deferredResults.map((item) => (
 
 ...

 

다음과 같이 변경 후 입력 필드에 글자를 입력해보면 연속으로 입력된 긴급한 업데이트가 우선으로 처리됨을 확인할 수 있습니다.

Comments