React Hooks - useTransition / useDeferredValue

박정호·2023년 1월 29일
0

React Hook

목록 보기
12/12
post-thumbnail

🚀 Start

useTransition 과 useDeferredValue는 React 18에서 도입된 새로운 훅이다.

React 18에서는 동시성이라는 새로운 핵심 개념을 도입했다. React 18 이전에는 React의 렌더링은 동기식이었다. 즉, React가 렌더링을 시작하면 구성 요소 렌더링을 완료할 때까지 아무 것도 멈출 수 없었다.

하지만, 동시 렌더링을 사용한다면 React가 렌더링을 일시 중지하고 나중에 계속하거나 렌더링을 완전히 중단시킬 수 있게 된다.

따라서, 이는 사용자에게 더욱 유동적인 사용자 경험을 제공할 수 있게 된다.

그리고 이 두개의 훅이 그 중심에 있다. 둘 모두 상태 업데이트의 우선 순위를 낮추는데 도움을 주어 UI를 차단하지 않은 상태로 상태를 업데이트할 수 있게 된다.

아직 뭔말인지 확실히 와닿지 않으니 예시를 들어서 이해해보자.


Example

0 ~ 10000개의 상품이 있다. 그리고 상품을 필터링할 수 있는 검색창이 있다. 물론, 서버 측에서 필터링을 수행하여 클라이언트 측에 응답을 주는 방법도 있겠지만, 해당 상황은 아래의 코드와 같이 클라이언트 측에서 필터링하는 상황이다.

import { useState, useTransition } from "react";

const numbers = [...new Array(10000).keys()];

export default function App() {
    const [query, setQuery] = useState("");

    const handleChange = (e) => {
        setQuery(e.target.value);
    };

    return (
        <div>
            <input type="number" onChange={handleChange} value={query} />
            <div>
                {
                    numbers.map((i, index) => (
                        query
                            ? i.toString().startsWith(query)
                            && <p key={index}>{i}</p>
                            : <p key={index}>{i}</p>
                    ))
                }
            </div>
        </div>
    );
}

만약 검색창에 텍스트를 입력하면 어떻게 될까?

배열에는 10,000개의 요소가 존재하므로 필터링은 약간 시간이 소요될 수 있는 프로세스일 것이다. 즉, 프로그램 속도를 저하시키는 무거운 작업이다.

이때 만약 동시성을 적용한다면?

필터링 기능을 지연시켜놓고 입력된 텍스트를 일단 검색창에 즉시 나타나도록 출력시키는 것이다.
즉, 필터링과 관련 렌더링은 낮은 순위로, 텍스트 상태는 우선 순위로 부여해주는 것이다.


💡 참고) Debouncing & Throttling

자바스크립트에서는 위와 같은 예시의 상황처럼 짧은 시간에 유저 입력 등의 이벤트가 많이 일어나는 경우 등에 대해 최적화를 진행할 수 있다.

Debouncing
: 연이어 호출되는 함수들 중 마지막 함수(또는 제일 처음)만 호출하도록 하는 것

Throttling
: 마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 하는 것

👉 Debouncing & Throttling



🪝 useTransition

useTransition()은 React에게 특정 상태 업데이트의 우선순위가 낮음을 알려 다른 렌더링이 더 빠르게 수행되어 응답성이 뛰어난 UI를 제공한다.

일반적으로 입력, 클릭 및 누르기는 좋은 사용자 경험을 보장하기 위해 사용자에게 즉각적인 응답을 제공되야하는 상황들이 있을 것 같다.


useTransition은 두개의 요소가 포함된 배열을 반환

  • isPending

    • 우선순위가 낮은 상태 업데이트가 아직 보류 중인지 여부를 알려주는 boolean값
  • startTransition

    • 상태 업데이트를 감싸서 React에 낮은 우선순위임을 알리는 함수
const [isPending, startTransition] = useTransition();

위의 예시에 동시성 주입

사용자가 검색창에 입력하는 동안 지연이 발생하지 않는다.

import { useState, useMemo, useTransition } from "react";

const numbers = [...new Array(10000).keys()];

export default function App() {
    const [query, setQuery] = useState("");
    const [text, setText] = useState("");
    const [isPending, startTransition] = useTransition();

    const handleChange = (e) => {
        setText(e.target.value); // 우선순위 더 높음
        startTransition(() => { // 우선순위 낮음
            setQuery(e.target.value);
        });
    };

    const list = useMemo(() => ( // 입력한 텍스트에 대한 필터링
        numbers.map((i, index) => (
            query
                ? i.toString().startsWith(query)
                && <p key={index}>{i}</p>
                : <p key={index}>{i}</p>
        ))
    ), [query]);

    return (
        <div>
            <input type="number" onChange={handleChange} value={text} />
            {isPending ?  "Loading..." : list}// 작업이 지연되고 있음을 알림 
        </div>
    );
}



🪝 useDeferredValue

useDeferredValueuseTransition처럼 상태 업데이트 코드를 래핑하는 것이 아닌 상태 업데이트로 인해 생성되거나 변경된 을 래핑한다.

useDeferredValueuseTransition을 사용할 수 없거나 상태를 업데이트하는 코드에 접근할 수 없을 경우에 사용하게 된다.


코드에 접근할 수 없는 상황?

만약 화면에 출력될 query 상태값을 변경시키는 setState 메서드를 트리거하고 싶다면 useTransition을 통해 setQuery라는 상태 업데이트 코드를 래핑할 것이다.

하지만, 아래의 경우 컴포넌트가 분리되어 있어 setState 메서드를 제어할 수 없는 상황이다. 따라서, useDeferredValue를 사용하여 전달된 prop 값이 전환 업데이트를 트리거 한다고 React에게 알릴 수 있다.

쉽게 말해, prop으로 전달된 query값은 연기할 값입니다라고 알리는 것이다.

import { useState, useMemo, useDeferredValue } from "react";

const numbers = [...new Array(100000).keys()];

export default function App() {
    const [query, setQuery] = useState("");

    const handleChange = (e) => {
        setQuery(e.target.value);
    };

    return (
        <div>
            <input type="number" onChange={handleChange} value={query} />
            <List query={query} />
        </div>
    );
}

function List(props) {
    const { query } = props;
    const defQuery = useDeferredValue(query);

    const list = useMemo(() => (
        numbers.map((i, index) => (
            defQuery
                ? i.toString().startsWith(defQuery)
                && <p key={index}>{i}</p>
                : <p key={index}>{i}</p>
        ))
    ), [defQuery]);
    
    return (
        <div>
            {list}
        </div>
    );
}


🧐 Finally...

✚ 상태 제어가 가능할때는useTransition를 사용하고 props에서 값에만 접근이 가능한 경우엔 useDeferredValue를 사용하면 좋을 것 같다.

단, 항상 디바운싱을 사용하지 않는 것 처럼, 이 훅들도 항상 사용할 필요는 없다.

모든 상태 업데이트를 useTransition으로 래핑하거나 모든 값을 useDeferredValue로 무조건적으로 래핑할 필요는 없다. 다른 방법으로 최적화할 수 없는 복잡한 사용자 인터페이스나 구성 요소가 있는 경우 이러한 후크를 사용하면 될 것이다.

✚ 두개의 훅은 기존에 렌더링 차단을 방지하려는 목적으로 사용되었던 debounce / throttle을 대체할 수 있는 기능이다.
기존의 방법은 딜레이가 고정적이라서 사용자의 의도와는 상관없이 반드시 설정된 시간만큼을 기다려야만 했었죠. 가령 debounce에 2000의 타임아웃을 부여한다면, 사용자는 무조건 입력 완료 후 2초가 지나야 결과를 받아볼 수 있었다.

✚ 두개의 훅은 사용하면 입력이 완료되는 즉시 업데이트 작업을 수행하도록 변경할 수가 있다. 위의 예시 코드의 경우라면, value가 변경되더라도 usedeferredValue에는 변경된 값이 업데이트되는 게 아니라 이전 값을 리턴해준다. 그리고 사용자의 입력이 완료되었다고 판단되면 그제야 새로운 값을 리턴한다.

그렇지만 이 기능을 사용할 때는 주의사항이 있다. 이 hook의 관심사는 어디까지나 해당 값의 동일 여부이므로, 해당 값으로 인한 component의 업데이트를 제한하기 위해서는 useMemo와 함께 사용할 것을 권장하고 있다.

✚ 두개의 훅은 concurrent mode를 반영한다.

  • concurrent mode의 동작 원리
    • 특정 state가 변경되었을 때 현 UI를 유지하고 해당 변경에 따른 UI 업데이트를 동시에 준비. 준비 중인 UI의 렌더링 단계가 특정 조건에 부합하면 실제 DOM에 반영한다.

사용자 경험 개선 2편 - react concurrent mode



🔗 Reference
👉 React 공식문서
👉 React: useTransition() vs useDeferredValue()
👉 React 18에서 UseTransition() 대 UseDeferredValue()
👉 useTransition and useDeferredValue in React 18
👉 React v18: useTransition hook — Why???
👉 React 18 hooks를 알아보자

profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글