[React] useMemo, useCallback, 메모이제이션

김건휘·2024년 4월 30일
0

React

목록 보기
2/19
post-thumbnail

🍀들어가며

이번 시간에는 컴포넌트 최적화를 위해 사용되는 대표적인 Hook에 대한 소개가 있겠습니다. 이 중에서 useMemouseCallback을 중점으로 다루어 보는 시간을 가지도록 하겠습니다.

🍀Memoization(메모이제이션)

useMemo에서 MomoMemoization을 의미하며, 동일한 값을 return하는 함수를 반복적으로 호출해야 된다면 맨 처음 값을 계산 할 때 해당 값을 메모리에 저장해서 필요할 때 마다(또 다시 계산하지 않고)메모리에서 꺼내서 재사용 하는 기법.

=> 즉, 이전에 계산한 값을 메모리에 저장하여 중복적인 계산을 제거하여 전체적인 실행속도를 빠르게 해주는 기법

🍀Memoization(메모이제이션) 예시


위의 사진과 같이 동일한 값을 return하는 함수(calculate함수)를 반복적으로 호출해야 된다면, 맨처음 값을 계산할 때 해당값을 메모리에 저장해서 필요할 때 마다 메모리에서 꺼내서 재사용한다.

🍀useMemo를 사용하는 이유

앞선 예시와 같이 간단한 일을 하는 함수가 아닌, 무거운 일을 하는 함수라고 한다면 컴포넌트가 렌더링이 될 때마다 반복적으로 호출되게 되면 매우 비효율적이고 성능에도 악영양을 끼칠것이다.
이를 useMemo를 활용하여 간단하게 해결이 가능하다.

🍀useMemo 예시 코드 및 영상

🌱 어려운 계산기 코드

import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

const hardCalculate = (number) => {
  console.log('어려운 계산!');
  for (let i = 0; i < 999999999; i++) {} // 생각하는 시간
  return number + 10000;
}

function App() {
  const [hardNumber, setHardNumber] = useState(1);

  const hardSum = hardCalculate(hardNumber);

  return (
  <div>
    <h3>어려운 계산기</h3>
    <input
      type="number"
      value={hardNumber}
      onChange={(e) => setHardNumber(parseInt(e.target.value))}
    />
    <span> + 10000 = {hardSum}</span>
  </div>
  );
}

export default App


=>delay가 있는 것을 확인 할 수 있다.

🌱 어려운 계산기 코드에 쉬운 계산기 추가 코드

import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

const hardCalculate = (number) => {
  console.log('어려운 계산!');
  for (let i = 0; i < 999999999; i++) {} // 생각하는 시간
  return number + 10000;
}

const easyCalculate = (number) => {
  console.log("쉬운 계산!")
  return number + 1;
}

function App() {
  const [hardNumber, setHardNumber] = useState(1);
  const [easyNumber, setEasyNumber] = useState(1);

  const hardSum = hardCalculate(hardNumber);
  const easySum = easyCalculate(easyNumber);

  return (
  <div>
    <h3>어려운 계산기</h3>
    <input
      type="number"
      value={hardNumber}
      onChange={(e) => setHardNumber(parseInt(e.target.value))}
    />
    <span> + 10000 = {hardSum}</span>

    <h3>쉬운 계산기</h3>
    <input
      type="number"
      value={easyNumber}
      onChange={(e) => setEasyNumber(parseInt(e.target.value))}
    />
    <span> + 1 = {easySum}</span>
  </div>
  );
}

export default App


=>return number + 1;의 쉬운 계산만 하였을 뿐인데 첫번째와 동일한 delay가 발생

❔도대체 왜❔ 나는 쉬운 계산(number+1)만 했을 뿐인데, 딜레이가 발생하는걸까?

App컴포넌트가 함수형 컴포넌트 이기 때문이다. 쉬운 계산기의 number를 증가시켜주면 easyNumber의 state가 변경된다.
state가 변경되었다는 말은 App컴포넌트가 다시 랜더링 된다는 것을 의미한다. 그래서, hardSum과 easySum 변수가 모두 초기화가 된다.
즉, hardNumber를 바꾸던 easyNumber를 바꾼던 상관없이 hardCalculate안에 있는 의미없는 for루프가 돌아가게되어서 delay가 발생한다. => 너무 비효율적이다.

🌱3. useMemo를 활용한 코드

import { useMemo, useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

const hardCalculate = (number) => {
  console.log('어려운 계산!');
  for (let i = 0; i < 999999999; i++) {} // 생각하는 시간
  return number + 10000;
}

const easyCalculate = (number) => {
  console.log("쉬운 계산!")
  return number + 1;
}

function App() {
  const [hardNumber, setHardNumber] = useState(1);
  const [easyNumber, setEasyNumber] = useState(1);

  //const hardSum = hardCalculate(hardNumber);
  const hardSum = useMemo(() => {              //useMemo 부분
    return hardCalculate(hardNumber);
  }, [hardNumber]);
  const easySum = easyCalculate(easyNumber);

  return (
  <div>
    <h3>어려운 계산기</h3>
    <input
      type="number"
      value={hardNumber}
      onChange={(e) => setHardNumber(parseInt(e.target.value))}
    />
    <span> + 10000 = {hardSum}</span>

    <h3>쉬운 계산기</h3>
    <input
      type="number"
      value={easyNumber}
      onChange={(e) => setEasyNumber(parseInt(e.target.value))}
    />
    <span> + 1 = {easySum}</span>
  </div>
  );
}

export default App


=> (useMemo를 사용하여)쉬운 계산을 할 때, delay이 없이 계산이 이루어지는 것을 확인할 수 있다.

🌱예시에 사용된 useMemo 구조 살펴보기

const hardSum = useMemo(() => {
    return hardCalculate(hardNumber);
  }, [hardNumber]);

콜백 함수: () => { return hardCalculate(hardNumber); }는 useMemo에 전달된 콜백 함수로, 실제로 메모이제이션하고자 하는 계산을 수행한다. 위의 예에서는 hardCalculate 함수를 호출하고 hardNumber를 인자로 전달하여 그 결과를 반환한다.
의존성 배열: [hardNumber]는 의존성 배열로, useMemo가 결과를 새로 계산해야 하는 조건을 정의합니다. 여기서는 hardNumber 값이 변경될 때마다 useMemo가 콜백 함수를 다시 실행하여 결과를 새로 계산하도록 설정되어 있다. 만약 hardNumber가 변경되지 않는다면, 이전에 계산한 hardSum 값을 재사용한다.

비슷한 역할을 하는 Hook이 또 있다고 하는데, 바로 useCallback이다.

🍀useCallback이란?

함수를 메모이제이션(memoization)하는 데 사용된다.
즉, 함수의 참조를 재사용할 필요가 있을 때 사용한다.

🍀useCallback 예시 코드 및 영상

🌱useCallback 사용 X

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


function App() {
  const [Number, setNumber] = useState(0);
  const [toggle, setToggle] = useState(true);

  const someFunction = () => {
    console.log(`someFunc: number: ${number}`);
    return;
  };

  useEffect(() => {
    console.log('someFuction이 변경되었습니다.');
  }, [someFunction]);


  return (
  <div>
    <input
      type="number"
      value={Number}
      onChange={(e) => setNumber(parseInt(e.target.value))}
    />
    <button onClick={() => setToggle(!toggle)}>{toggle.toString()}</button>
    <br />
    <button onClick={someFunction}>Call someFunc</button>
  </div>
  );
}

export default App

=> 콘솔창을 확인해보니, 토글 버튼을 눌러 토글 스테이트가 변경될 때마다 useEffect가 실행되어 'someFuction이 변경되었습니다.'라는 로그가 콘솔에 출력되고 있다.
=> useEffect가 불필요하게 여러번 실행되고 있음.

🌱useCallback 사용 O 코드

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


function App() {
  const [number, setNumber] = useState(0);
  const [toggle, setToggle] = useState(true);

  const someFunction = useCallback(() => {
    console.log(`someFunc: number: ${number}`);
    return;
  }, [number]); //의존성 배열 number로 지정

  useEffect(() => {
    console.log('someFuction이 변경되었습니다.');
  }, [someFunction]);


  return (
  <div>
    <input
      type="number"
      value={number}
      onChange={(e) => setNumber(parseInt(e.target.value))}
    />
    <button onClick={() => setToggle(!toggle)}>{toggle.toString()}</button>
    <br />
    <button onClick={someFunction}>Call someFunc</button>
  </div>
  );
}

export default App

=> 이전 코드와 달리 토글 버튼을 클릭시 useEffect가 실행되지 않아 'someFuction이 변경되었습니다.'라는 로그가 콘솔창에 뜨지 않는 것을 확인 할 수 있다.
=> 대신 의존성 배열로 지정해준 number의 값이 변경 될 때만 콘솔창에 'someFuction이 변경되었습니다.'가 뜨는 것을 확인 할 수 있다.

🌱예시에 사용된 useCallback 구조 살펴보기

const someFunction = useCallback(() => {
    console.log(`someFunc: number: ${number}`);
    return;
  }, [number]);

useCallback 훅: useCallback은 첫 번째 인자로 전달된 함수를 메모리에 저장합니다. 저장된 함수는 의존성 배열([number])에 있는 값들의 변화에만 반응하여 업데이트됩니다.
함수 정의: () => { console.log(someFunc: number: ${number}); return; } 이 부분은 실제 저장되는 콜백 함수입니다. 이 함수는 콘솔에 number 변수의 현재 값을 출력합니다.
의존성 배열: [number] 이 배열은 useCallback에 의해 추적되는 의존성을 명시합니다. 여기서 number는 함수가 의존하는 변수입니다. 이 변수의 값이 변경될 때마다 useCallback은 새로운 함수를 메모리에 저장하여 이전 함수를 대체합니다.

요약

useMemo

  • useMemo는 값의 계산을 메모이제이션하는 데 사용됩니다. 복잡한 계산 결과, 객체 리터럴, 배열 등 함수 호출 결과를 캐싱하여 재계산의 필요성을 줄이기 위해 사용합니다.
  • useMemo는 계산된 값 자체를 반환합니다.

useCallback

  • useCallback은 함수를 메모이제이션(memoization)하는 데 사용됩니다. 즉, 함수의 참조를 재사용할 필요가 있을 때 사용합니다. 이것은 특히 함수를 자식 컴포넌트에 props로 전달할 때 유용하며, 자식 컴포넌트가 불필요하게 재렌더링되는 것을 방지할 수 있습니다.
  • useCallback은 함수 자체를 반환합니다.
profile
공유할 때 행복을 느끼는 프론트엔드 개발자

5개의 댓글

comment-user-thumbnail
2024년 5월 1일

메모이제이션의 의미부터 시작해서 useMemo와 useCallback의 구조부터 어떤식으로 사용되는지까지 꼼꼼하게 정리해주셨네요. 내용의 흐름이 매끄럽고 알차서 이해에 많은 도움이 되었어요. 특히나 훅들의 필요성을 예시 코드를 기반한 영상을 통해 확인할 수 있어서 더 좋았어요!
추가로 useMemo와 useCallback을 쓰는 것이 항상 도움이 될까? 라는 의문이 생겨서 메모이제이션 기법을 지양해야 하는 상황에 대해서도 알아봤습니다!
1. 최적화하려는 계산의 비용이 크지 않은 경우 - 앞서 건휘님이 언급하신 복잡한 계산은 useMemo를 사용하기 적합한 상황이겠네요!
2. 메모이제이션이 필요한지 확실하지 않은 경우
3. 메모이제이션 하고 있는 값이 구성 요소로 전달되지 않고 있는 경우
4. 의존성 배열이 너무 자주 변경되는 경우
해당 경우들에는 useMemo나 useCallback 훅을 사용하는 것이 오히려 성능상의 이슈를 해결하는 것보다 과도한 최적화를 야기하여 메모리 상의 문제를 발생시킬 수 있다고 합니다! 참고하셔서 메모이제이션 훅의 무분별한 사용에 대해 한 번 더 생각해보시는 것도 좋을 것 같다는 생각이에요. 더 생각해볼 수 있었던 유익한 아티클 감사합니다☺️

답글 달기
comment-user-thumbnail
2024년 5월 1일

적절한 설명 이미지와 예제 영상까지 포함되어 있는... 너무 친절한 아티클인 것 같아요 감사합니다! ✨ 예제도 useMemo와 useCallback 사용 전과 후를 둘 다 보여주셔서 이해하기 너무 좋았습니다!! 위의 설명만 읽고는 둘의 차이점이 어떤 것인지 바로 와닿지 않았는데 마지막에 요약도 해 주셔서 두 개의 차이점도 확실히 알아갑니다...!! 자세한 아티클 감사해요!

답글 달기
comment-user-thumbnail
2024년 5월 1일

메모이제이션에 대해 한번에 이해할 수 있는 그림과, useCallback, useMemo를 사용해야하는 이유를 영상으로 첨부해주셔서 정말 유익했던 아티클이었습니다😊 보통 영상까지 찍어서 올리려면 번거로움이 많이 클텐데.. 덕분에 직접 테스트를 안해보고도 테스트를 하며 공부한 느낌이 나는 것 같아 정말 좋았던 아티클이었습니다! 그리고 초기 렌더링시 함수의 내용이 같은데도, 계속 선언되는 모습을 useEffect를 사용하여 보여주셔서, useCallback의 필요성을 더욱 체감할 수 있었습니다. 아티클 작성하시느라 고생 많으셨습니다 ! 👍

답글 달기
comment-user-thumbnail
2024년 5월 1일

멋있는 사진들이 여러개에다가 코드 예시도 길게 정리해주셔서 이해하기 넘 좋았어요!
내용을 다시한번 기억할 수 있게 요약까지 해주셔서 다음에 코딩할 때 생각나서 또 찾으러 올 것 같네요 :) 정리도 깔끔하게 해주셔서 아티클 정독하기 편했답니닷!
담에 코딩할 때 또 놀러오겠습니다~~!!

답글 달기
comment-user-thumbnail
2024년 5월 1일

useMemo와 useCallback 훅 함수들의 동작원리를 이해하기 너무 좋은 아티클이네요!! 중간중간 적절한 예시 코드들과 코드들에 대한 설명, 구현된 영상까지 첨부되어있어서 함수의 동작을 이해하기 좋았습니다! 특히 useMemo의 예시부분을 읽으면서 useEffect와 비슷한 기능을 가지고 있지만 값을 반환해준다는 차이가 있다는 부분 알아갑니다! 덕분에 이제 언제 어떻게 해당 함수들을 사용해야할지 감이 잡힌거 같아요~ 좋은 아티클 써주셔서 감사합니다 잘 읽고갑니다!!

답글 달기