만족

[React] useMemo를 이용한 최적화 본문

[React] useMemo를 이용한 최적화

FrontEnd/React Satisfaction 2021. 8. 28. 20:24

크롬의 개발자 도구에는 Performance(성능) 탭이 존재한다.

 

이를 통해 웹 성능을 측정하고, 어떤 것이 시간을 오래 잡아먹는지 손쉽게 알아볼 수 있다.

 

캡쳐 시작은 왼쪽 위의 녹화 버튼으로, 중지는 붉게 변한 녹화 버튼을 통해 할 수 있다.

 

문제

내 프로젝트에서 렉이 심하게 걸리는 구간을 캡쳐해보니 

특정 구간에서 프레임 드랍이 심하게 일어나는 것을 확인할 수 있었다.

 

이는 컴포넌트 렌더링 중 오래 걸리는 함수가 렌더링을 블러킹하여 해당 구간에서 웹이 멈추는 현상인 것임을 알 수 있다.

(Frame이 급격히 줄어드는 구간=> Frames가 붉게 표시되는 구간)

 

컴포넌트를 진단해본 결과 어떤 함수가 오래 걸리는 함수인지를 알았다.

 

그런데 그 함수는 다시 계산될 필요가 없는데도 컴포넌트 리렌더링마다 무의미한 연산을 반복하는 함수였다.

 

예제와 함께 알아보자.

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

//foo는 매우 오래 걸리는 함수의 예이다.
const foo = param => {
  console.log("foo is called");
  console.time("foo");
  //약 0.2~0.8초 소요
  for (let i = 0; i < param * 1000000000; i++) {}
  console.timeEnd("foo");
  return param;
};
// component
const Test = () => {
  const [param, setParam] = useState(1);
  const [cnt, setCnt] = useState(0);

  const fooValue = foo(param);

  const updateParam = () => {
    if (param === 1) {
      setParam(2);
    } else {
      setParam(1);
    }
  };
  return (
    <div>
      <p>param is {param}</p>
      <button onClick={updateParam}>switch param</button>
      <br />
      <button onClick={() => setCnt(cnt + 1)}>increase cnt ({cnt})</button>
    </div>
  );
};
export default Test;

이 컴포넌트는 컴포넌트가 렌더링될 때 마다 foo(param)값을 새로 계산한다.

 

foo()는 값을 반환할때까지 약 1초가 걸리는 매우 무거운 작업을 수행하는 함수의 예이다.

 

"increase cnt" 버튼을 눌러보면 cnt값이 변해서 컴포넌트가 리렌더링되고,

param값이 동일해서 foo(param)도 동일하므로 굳이 다시 계산할 필요가 없는데도 foo(param)값을 다시 계산한다.

 

실제로 퍼포먼스 도구로 측정해본 결과 foo(param)을 계산하는데 매우 오랜시간이 걸리고 그 동안 프레임이 매우 낮아진다.

 

어떻게 해결할 수 있을까?

메모이제이션이라는 개념이 있다.

 

예를 들어, 수학을 매우 못하는 사람이 있는데

그 사람은 f(x)= x+1이라는 함수가 있을 때 f(2)의 값이 f(2)= 2+1= 3이라는걸 계산할때 까지 10초가 걸린다고 해보자.

 

이 사람은 f(2)의 값을 여러번 계산하게 해도 똑같이 10초씩 걸릴 것이다.

 

따라서 이 사람 자체를 바꿀 수 없다면, 이미 계산한 값에 대해서는 외워두게 하고 다시 계산하지 않도록 만드는 것이 메모이제이션이다.

 

f(2)= 3이라는걸 한 번 계산했다면, f(2)= 3이라는걸 외우고 또다시 f(2)를 물어봤을땐 맹목적으로 3이라고 답하게 하는 것이다.

 

React에서도 이런 기능을 해주는 함수가 있다.

 

React.useMemo

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

//foo는 매우 오래 걸리는 함수의 예이다.
const foo = param => {
  console.log("foo is called");
  console.time("foo");
  //약 0.2~0.8초 소요
  for (let i = 0; i < param * 1000000000; i++) {}
  console.timeEnd("foo");
  return param;
};
// component
const Test = () => {
  const [param, setParam] = useState(1);
  const [cnt, setCnt] = useState(0);

  //param값이 동일하면 새로 계산하지 않고 메모이제이션(기억된) 값을 사용한다
  //param값이 바뀔 때만 새로 계산한다
  const fooValue = useMemo(() => foo(param), [param]);

  const updateParam = () => {
    if (param === 1) {
      setParam(2);
    } else {
      setParam(1);
    }
  };
  return (
    <div>
      <p>param is {param}</p>
      <button onClick={updateParam}>switch param</button>
      <br />
      <button onClick={() => setCnt(cnt + 1)}>increase cnt ({cnt})</button>
    </div>
  );
};
export default Test;

 

여기에서는 useMemo를 사용했다.

 

useMemo의 첫 번째 매개변수로는 계산된 값을 반환하는 함수를,

두 번째 매개변수로는 메모이제이션할 기준이 될 값들을 전달한다.

 

useMemo(()=> foo(param), [param]); 은

param값이 바뀌었을 때만 ()=> foo(param)을 실행해 반환하고,

바뀌지 않았을 때는 이전에 계산했던 값을 그대로 사용한다는 의미를 갖는다.

 

다시 성능을 측정해 본 결과 useMemo에 의해 메모이제이션 되기 전에는 foo(param)을 어쨌든 계산해야 하므로, 이 때 프레임 드랍이 발생하고 그 이후부터는 메모이제이션된 값이 사용되기 때문에 프레임드랍이 발생하지 않는다.

 

물론 이 경우에도 "switch param"를 누를 경우 param이 달라져 다시 계산해야 하므로, foo(param)이 다시 호출된다.

(foo(1)과 foo(2)의 값은 다르기 때문이다)

useMemo는 동일한 결과를 반환하는 연산을 막아준다는 것을 명심하자.

 

실제 프로젝트에 적용

useMemo 사용 전
useMemo 사용 후

useMemo를 사용한 결과 첫 로딩 때 프레임드랍이 발생하지만, 그 이후부터는 발생하지 않는다.

(실제로 Frames가 붉은 부분이 초기에만 발생하고, 이후에는 발생하지 않는다)

 

당연하게도, 불필요한 연산을 제거했기 때문에 그만큼 컴퓨팅 자원에 여유가 생겨 정상적으로 프레임이 뽑히는 것이다.

 

 



Comments