Frontend/React.js

[React 성능 개선] 2. useMemo()

P.Venti 2023. 3. 1. 23:16

 

 

[React 성능 개선] 1. 디바운싱

해당 포스트는 리액트 문법에 대한 설명은 대부분 생략되어 있습니다. 기본적으로 리액트는 다른 프런트앤드 언어와 다른 방식으로 랜더링을 진행합니다. 상태 변경이 발생하면 기존의 UI를 모

pventi.tistory.com

 

 위 글에서도 설명했듯 리액트는 상태 정보에 변경이 발생하면 전체 컴포넌트를 리랜더링 합니다. 그렇기에 리액트 자체에서 해당 문제를 해결할 함수 몇 가지를 제공하고 있습니다. 이번 포스트에서 알아볼 함수는 useMemo()입니다. 

 

메모이제이션

 

 코딩 테스트를 준비해 본 사람이라면 메모이제이션이라는 단어를 들어보셨을 겁니다. 들어보지 못한 분들을 위해 간단하게 설명드리자면 메모리에 이전에 계산한 값을 저장해 반복적인 계산 작업을 최소화하는 것을 말합니다. 실생활로 예를 들자면 곱셈연산을 하실 때 12 x 12의 정답이 머릿속에 있는 분들은 따로 계산할 필요 없이 바로 정답을 말하실 수 있습니다. 하지만 한 번도 계산하거나 외운 적이 없는 경우에는 직접 계산해서 답을 구하는데 시간이 소요됩니다. 이렇게 메모리(뇌)에 기억해 놓고 빠르게 답을 도출해 내는 것을 메모이제이션이라 합니다. 

 

useMemo()

 

 아래 간단한 리액트 예제를 준비했습니다. 단순하게 숫자를 입력하면 그 숫자에 해당하는 피보나치 수를 구하는 프로그램입니다. 

 

import { useState, useEffect, useMemo } from 'react';
import './App.css';


// 피보나치 수 연산
function solve(num) {
  if(num < 2) return num;
  return solve(num - 1) + solve(num - 2);
}
function ResultViewer({ num }) {
  const [ result, setResult ] = useState(0)
  // 버튼 클릭시 연산
  const onClick = () => {
    const currentTime = Date.now()
    setResult(solve(Number(num)));
    console.log(Math.floor(Date.now() - currentTime) / 1000 + " 초")
  }
  
  return(
    <>
      <div id="result-viewer">
        <span>
          정답 : {result}
        </span>
        <button onClick={onClick}>구하기</button>      
      </div>
    </>
  )
}

function App() {
  // 게임판 사이즈
  const [ num, setNum ] = useState(0)

  const onChange = (e) => {
    const { value } = e.target
    
    setNum(value)
  }



  return (
    <div className='container'>
      <div id='fibo-form'>
        <div className='title'>
          피보나치 수열 구하기
        </div>
        <div className='form'>
          <label htmlFor='fibo_num'>몇번째 ?</label>
          <input type="text" onChange={onChange} value={num} name='num'/>
         
        </div>
      </div>

     
      <ResultViewer num={num}/>
    </div>
  );
}

export default App;

 

 해당 숫자가 커질수록 피보나치 값을 구하는 시간은 오래 걸립니다. 

 

\

 계산 시간을 측정해 보면 40을 입력했을 때 약 1초대의 연산 시간이 걸리는 것을 콘솔로 확인할 수 있습니다. 위 사진은 동일한 값을 3번을 입력했을 경우 전부 약 1초대 시간이 나온 사진입니다. 즉 동일한 값을 입력하더라도 매 입력마다 값을 다시 계산했다는 뜻입니다. 만약 연산값이 더 크다면 상당히 오랜 시간이 걸릴 것으로 예상됩니다.(실제로 45를 입력했을 때 약 11초 정도 소요됐습니다.)

 

 아래 코드는  useMemo() 함수를 통해 연산을 진행한 코드입니다.(실제로는 아래 코드처럼 사용하지 않습니다.) 추가로 카운팅 하는 기능을 추가함으로써 리랜더링 되었을 때 연산이 재진행되는지 확인해 보겠습니다.

function ResultViewer({ num }) {
  const [ result, setResult ] = useState(0)
  const [ count, setCount ] = useState(0)
  const onClick = () => {
    console.log("구하기 버튼 클릭")
    setResult(calculatorResult);
  }

   // 랜더링될때 같이 연산됩니다.
  const calculatorResult = useMemo(() => {
    console.log(num + "의 피보나치 수열")
    const currentTime = Date.now()
    const ret = solve(Number(num))
    console.log(Math.floor(Date.now() - currentTime) / 1000 + " 초")
    return ret;
  }, [num])
  

  const onCountBtnClick = (e) => {
    const { name } = e.target;
    console.log("카운트 업/다운 버튼 클릭");
    if(name === "up") { 
      setCount(count + 1)
    }else if(name === "down") {
      setCount(count - 1)
    }
  }
  console.log("컴포넌트 랜더링!")
  return(
    <>
      <div id="result-viewer">
        <span>
          정답 : {result}
        </span>
        <button onClick={onClick}>구하기</button>      
      </div>

      <div>
        <button onClick={onCountBtnClick} name="up">Up</button>
        <button onClick={onCountBtnClick} name="down">Down</button>
      </div>
      <div>
        카운터 : {count}
      </div>
    </>
  )
}

 

 

 위 사진을 보시면 처음 40을 입력했을 때 연산을 한번 진행합니다. 그 후 리랜더링이나 구하기 버튼을 재차 클릭했을 경우 동일 값에 대한 연산은 진행하지 않는 것을 확인하실 수 있습니다.  

 

useMemo() 함수는 다음과 같습니다.

const sampleMemo = useMemo(() => {
  진행할 코드
},[모니터링 할 변수])

 모니터링할 변수를 입력해 주면 해당 값이 변할 경우에만 함수가 실행됩니다. 

 

무조건은 아닙니다.

 

 성능면에서는 확실히 차이가 납니다. 하지만 useMemo()를 남발해서는 안됩니다. 메모리에 값을 저장한다는 것은 비용이 발생한다는 뜻입니다. useMemo()를 남발하는 경우 비용이 많이 발생하게 됩니다. 그렇다고 useMemo()를 아예 사용하지 않으면 특정 연산에 성능이 저하되는 경우가 발생합니다. 그러므로 복잡하지 않은 로직에는 굳이 해당 함수를 사용해 비용을 낭비할 필요는 없습니다.