dwook.record
Published on

렌더링 최적화: memo, useMemo, useCallback

Authors
  • avatar
    Name
    dwook

초기 렌더링 단계

  • [1단계] Render 단계
    • JSXReact Element로 전환.
    • React Element는 자바스크립트 객체.
    • 리액트가 컴퍼넌트 트리의 루트에서 시작하여, 가장자리 컴퍼넌트로 내려간다.
    • 각 컴퍼넌트를 가로지르면서 리액트는 createElement() 메서드를 실행하여 JSXReact Element로 변환하고 Render 결과물을 저장한다.
    • 모든 컴퍼터넌트 트리에서 JSXReact Element로 바뀌면, Commit 단계로 넘어간다.
  • [2단계] Commit 단계
    • React Element는 react-dom 패키지를 이용해 DOM으로 바뀐다.
    • DOM은 HTML 엘리먼트에 대한 정보를 지닌 자바스크립트 객체

리렌더링 단계

  • [1단계] Render 단계
    • 리액트가 컴퍼넌트 트리의 루트에서 시작. 가장자리 컴퍼넌트로 내려가면서 업데이트가 되어야할 컴퍼넌트로 플래그 된 모든 컴퍼넌트를 찾는다.
    • 컴퍼넌트는 스스로 플래그를 한다. (useState 의 setter 함수, useReducer의 dispatch 함수)
    • 플래그된 각각의 컴퍼넌트들에 대해 리액트는 createElement() 메서드를 실행하여 JSXReact Element로 변환하고 Render 결과물을 저장한다.
    • 전환이 이뤄지면, 리액트는 마지막 Render와 새로운 Render를 비교한다.
    • 모든 변화를 리스트화해서 DOM에 전달하고 Commit 단계로 넘어간다.
  • [2단계] Commit 단계
    • 변화가 DOM에 적용된다.

강조하고 싶은 부분은 rendering은 DOM을 updating 하는 것과 다르다. 이 차이점이 매우 중요하다. 컴퍼넌트는 DOM에 보이는 변화없이 렌더링될 수 있다. (a component may be rendered without any visible changes to the DOM)

  • 렌더링을 하는 동안, 컴퍼넌트가 이전 Render와 동일한 리액트 엘리먼트로 변환. 엘리먼트는 폐기된고 변화없이 DOM에 반영된다.
  • 퍼포먼스 이슈는 느린 DOM 업데이트 때문에 발생한다. 리액트는 DOM 업데이트를 효율적으로 모든 변경사항을 배치작업으로 한번에 업데이트. 이것이 DOM을 빠르게 연속적으로 업데이트하면서 생기는 퍼포먼스 이슈를 줄인다.

"The commit phase is usually very fast, but rendering can be slow"

렌더링이 되는 4가지 상황

  • state가 변경될 때

  • props가 변경될 때

  • 부모 컴포넌트가 변경될 때

  • forceUpdate()가 호출될 때

  • 최적화

    • 현재상태와 이전상태를 비교하여, 상태가 바뀌었을 때만 연산/렌더링을 다시 한다.
    • 상태비교 연산이 수행작업 보다 속도가 빠르다.
memo:        컴포넌트 (dom)
useMemo:     상태    (state)
useCallback: 함수    (function, method)
대상연산의 정의
memo컴포넌트 (dom)가상돔 변경 및 렌더링
useMemo상태, 값 (state)처리한 결과값
useCallback함수 (function, method)함수 생성

memo

  • 컴퍼넌트를 memo로 감싸면, 감싼 컴퍼넌트에 보내지는 props를 비교하여 해당 컴퍼넌트를 다시 렌더링할지 판단
  • 만약 props를 직접 연산하고 싶다면, 이전상태와 현재상태를 인자로 받고 boolean 타입을 반환하는 함수를 전달.
    • true는 이전상태와 현재상태가 동일하다는 것. 다시 렌더링되지 않는다.
const Counter = memo(props => {
  return (
    <div>
      <span> {props.value} </span>
      <button onClick={() => {
                console.log(props.value);
                props.increment(props.value + 2);
            }}>
                cnt 증가
            </button>
    </div>
  );
}, (prevState, currentState) => {
  console.log(prevState)
  console.log(currentState)
  return true
})
import React, { memo, useEffect } from 'react';

function NormalCompoent({name}) {
  useEffect(() => {
    console.log("rendered")
  });
  return <div>{name}</div>
}

export default memo(NormalComoponent);
const [name, setName] = useState('Nicholas');
useEffect(() => {
  setTimeout(setName, 5000, "Dali")
})

useCallback

  • 인라인을 사용하면 렌더링할 때마다 새로운 함수/값/객체가 생성.
  • 함수형 컴퍼넌트는 함수임을 인지해야한다. 렌더링이 다시 된다는 것은 해당 컴퍼넌트가 다시 호출됨을 의미한다.
  • useCallback은 두 번째 인자로 전달한 dependecies 변경에 의해 다시 함수를 만들어 주기 때문에, 해당 함수에서 사용하는 값이 가장 최신 상태를 유지해야 한다면 반드시 두 번째 인자로 해당 상태를 dependecies로 전달해야 한다.
const Counter = memo(props => {
  const onClickHandler = useCallback(() => {
    props.increment(props.value + 2);
  }, [props.value]);

  return (
    <div>
      <span> {props.value} </span>
      <button onClick={onClickHandler} >cnt 증가</button>
    </div>
  );
})
// 자바스크립트 클로저. 처음 렌더링될 때 값이 들어가고 끝.
const addOne = useCallback(() => {
  setNumbers([...numbers, numbers.length + 1]);
}, [])

// 1차 해결: numbers를 deps에 추가
const addOne = useCallback(() => {
  setNumbers([...numbers, numbers.length + 1]);
}, [numbers])

// 2차 해결: 함수방식으로 변경
const addOne = useCallback(() => {
  setNumbers(currentNumbers => [
    ...currentNumbers,
    currentNumbers.length + 1
  ]);
}, [])

useCallback 주의점! 클로저로 변수값을 가두고 있는지 확인할 것.

useMemo

  • 함수의 연산 결과값을 메모이제이션