본문 바로가기

Front-End: Web/React.js

[코딩애플] 성능개선 3: useTransition, useDeferredValue

반응형

automatic batching 기능

state 변경 함수가 쭉 작성되어 있다 하자.

state1변경()
state2변경()
state3변경()

state가 변경될 때마다 재렌더링이 일어난다고 했으니 재렌더링이 3번 일어날까?

아니다. 맨 마지막인 state3변경()에서만 재렌더링이 1회 일어난다. 이걸 batching이라 한다. 뭉쳐서 실행되기 때문이다.

예외도 있다. 리액트 17버전인 경우, ajax, setTimeout과 같은 비동기 함수 내부에서 위 코드를 작성했다고 하자. 그럼 batching이 일어나지 않고 각각 재렌더링이 일어나서 총 3번의 재렌더링이 일어난다.

하지만 리액트 18버전부터는 state가 변경되는 코드가 어디에 있던지 batching이 잘 일어나게 되었다. 그래서 비동기 함수 내부에서도 batching이 일어나서 마지막에만 실행되어 총 1번의 재렌더링이 일어난다.

useTransition: 느린 컴포넌트 성능향상가능

동작이 느린 컴포넌트를 혁신적으로 빠르게 동작시킬 수 있다. 카드 빚 돌려막기식으로 말이다.

🕹️App.js

import { useState } from 'react';

function App(){
    let [name, setName] = useState('');
    
    return(
    	<div className="App">
            <input onChange={(e) => setName(e.target.value)} />
        </div>)
}

유저가 뭘 입력하면 name에 입력한 값이 저장된다.

여기서 의도적으로 성능저하를 일으켜보자.

import { useState } from 'react';

function App(){
    let [name, setName] = useState('');
    
    return(
    	<div className="App">
            <input onChange={(e) => setName(e.target.value)} />
            {
                new Array(10000).fill(0).map(() => <div>{name}</div>)
            }
        </div>)
}

그럼 input 아래에 div가 10000개 생긴다. 타이핑을 할 때마다 name state가 변경되고, name이 변경되면 10000개의 div 박스도 렌더링되어야 한다. 그래서 화면이 버벅이는 현상이 발생한다.

근데 유저는 조작 후에 0.2초 넘게 반응이 없으면 유저 다 떠난다.. 그래서 이 문제를 해결해보자.

솔루션1. html 10000개 지우기

원인을 없애면 젤 좋다. 근데 어쩔 수 없이 써야하는 상황이라면?

솔루션2. startTransition 쓰기

import { useState, useTransition } from 'react';

function App(){
    let [name, setName] = useState('');
    let [isPending, startTransition] = useTransition();
    
    return(
    	<div className="App">
            <input onChange={(e) => 
                startTransition(() => {
                    setName(e.target.value)}
            })/>
            {
                new Array(10000).fill(0).map(() => <div>{name}</div>)
            }
        </div>)
}
  1. import useTransition
  2. startTransition으로 문제의 state변경 감싸기

지연의 원인은 setName(e.target.value)다. 그 함수를 startTransition으로 감싸자.

그럼 성능이 아까보다 좀 나아진 걸 볼 수 있다.

앞으로 state 변경함수가 성능 저하를 일으킨다, 그러면 startTransition 함수로 감싸자.

startTransition 동작원리

먼저 왜 성능저하를 일으켰냐? 브라우저는 single-threaded기 때문에 동시 작업을 못하고 한 번에 하나의 작업만 수행할 수 있다. 그래서 타이핑을 했을 때 브라우저가 할 일은 다음과 같다.

  1. <input>에 name을 보여주기
  2. `<div> 를 10000개 만들기

그런데 startTransition 함수로 감싸주게 되면,

startTransition 안에 있는 코드들을 모두 늦게 처리해준다. 코드 시작 시점을 뒤로 늦추고, 다른 중요한 작업을 먼저 한다는 거다.

그러면

  1. 먼저 name을 <input>에 보여주기
  2. **한가할 때** <div>를 10000개 만들기

로 실행하게 된다. 우선순위가 변경되어서 성능이 향상되는 것처럼 보이는 거다.

더 명시적으로 작성하자면 이렇게 되겠다.

const [isPending, 늦게처리] = useTransition()

isPending: startTransition이 처리 중일 때 true로 변함

처리 중일 때 isPending은 true다. 이걸 이용해서 로딩중을 띄울 수도 있다.

import { useState, useTransition } from 'react';

function App(){
    let [name, setName] = useState('');
    let [isPending, startTransition] = useTransition();
    
    return(
    	<div className="App">
            <input onChange={(e) => 
                startTransition(() => {
                    setName(e.target.value)}
            })/>
            {
                isPending ? '로딩중' : new Array(10000).fill(0).map(() => <div>{name}</div>) // ⬅️
            }
        </div>)
}

useDeferredValue써도 느린 컴포넌트 성능향상기능

위 코드에서 이어서 보자. useDeferredValue를 써도 느린 컴포넌트의 성능을 향상시킬 수 있다.

얘도 기능이 같다. 대신 얘는 함수를 감싸지 않고, state나 props를 넣으면 다시 변수롤 퉤-뱉어준다. 뱉어져서 나온 이 변수는 변동사항이 생기면 늦게 처리해준다.

import { useState, useTransition } from 'react';

function App(){
    let [name, setName] = useState(''); 
    let state = useDeferredValue(name);
    
    return(
    	<div className="App">
            <input onChange={(e) => setName(e.target.value)} )/>
            {
                new Array(10000).fill(0).map(() => <div>{state}</div>)
            }
        </div>)
}

name이라는 state가 변할 때마다, name을 사용하는 것들이 나중에 처리가 되므로 중요한 작업들을 먼저 처리할 수 있다. 그래서 더 빠른 느낌의 웹사이트를 만들 수 있다.

반응형