치춘짱베리굿나이스

useMemo, useCallback 본문

ClientSide/React

useMemo, useCallback

치춘 2023. 7. 24. 15:46

useMemo, useCallback

리액트를 쓰면서 이름만 몇번 들어본 훅이지만 내가 잘 사용할 엄두가 안나서 (…) 사용해 본 적도 정리도 못하고 있었다 (핑계임)

캐싱 비슷한 기능으로 불필요한 연산 수를 줄이는 것이라고만 알고 있었는데 한번 정리하면서 익혀보려고 한다

사실 필수적인 훅이 아니라 성능 최적화를 위한 훅이라 크게 필요성을 못 느낀 것도 있는데 그렇게 배움을 미루는 건 너무나도 게으르다

useMemo

기본 예시

import { useState } from "react";

function multiply10(n: number) {
  console.log("곱하기 10 계산 중...");
  return n * 10;
}

export const MemoTestPage = () => {
  const [count, setCount] = useState<number>(1);
  const [count2, setCount2] = useState<number>(1);
  const result = multiply10(count2);

  const handleClickButton = () => {
    console.log("왼쪽 버튼 클릭");
    setCount((prev) => prev + 1);
  };

  const handleClickButton2 = () => {
    console.log("오른쪽 버튼 클릭");
    setCount2((prev) => prev + 1);
  };

  return (
    <div>
      <div style={{ display: "flex", flexDirection: "row", gap: 50 }}>
        <h2>왼쪽: {count}</h2>
        <h2>오른쪽: {result}</h2>
      </div>
      <button type="button" onClick={handleClickButton}>
        왼쪽 숫자 증가
      </button>
      <button type="button" onClick={handleClickButton2}>
        오른쪽 숫자 증가
      </button>
    </div>
  );
};

이런 예시가 있다고 해 보자

리렌더링을 잔뜩 발생시키기 위해서 버튼을 2개로 나눠두었다

  • multiply10count2에만 영향을 받고, count에는 영향을 받지 않는다
  • 따라서 count가 증가할 때는 multiply10이 실행되지 않았으면 좋겠다

최초 1회 (마운트 시에 발생하는 렌더링) multiply10이 실행되고, 오른쪽에는 1 대신 10이 곱해진 10이 출력되는 것을 볼 수 있다

이제 왼쪽 버튼을 클릭해서 진짜로 multiply10이 영향을 받지 않는지 살펴보자

 

세상은 그렇게 호락호락하지 않은 것 같다

왼쪽 버튼을 클릭했을 때 count2가 전혀 바뀌지 않았음에도 불구하고 multiply10이 반복적으로 실행되는 것을 볼 수 있다

이건 우리가 원하는 동작이 아니다! 우리가 원하는 것은 multiply10count2가 변할 때만 실행되도록 하고 싶은 것이다

만약 multiply10 이 동작을 수행하는 데 10초 이상 걸리는 엄청 느린 함수라면, 사용자 경험이 얼마나 안 좋아질까?

useMemo를 사용했을 때

const [count, setCount] = useState<number>(1);
const [count2, setCount2] = useState<number>(1);
const result = useMemo(() => multiply10(count2), [count2]);

multiply10 의 결과값을 메모이징 해보자

multiply10의 결과값에 영향을 주는 값은 count2 뿐이므로, 의존성 배열에 count2를 추가한다

메모이제이션, 메모이징이 말이 어렵지 그냥 중복 연산을 피하기 위해 어딘가 메모장 같은 곳에 저장해서 나중에 가져다 쓸 수 있게 한다는 뜻이다

 

우리가 원하는 그림이 나왔다

왼쪽 숫자가 증가할 때는 multiply10이 실행되지 않고, 오른쪽 숫자가 증가할 때만 multiply10이 영향을 받아 실행되는 것을 볼 수 있다

useMemo란

성능 최적화에 사용되는 훅으로, 콜백 함수의 연산 결과값을 어딘가에 저장 (메모이징) 해 두고 동일한 의존성 값들에 대해서 저장해둔 값을 가져다 재활용할 수 있게 도와준다

  • 위의 예시에서 우리는 count, count2 두 개의 값을 버튼을 통해 지속적으로 변화시켰는데, 이때 상태값이 변화하므로 리렌더링이 발생하면서 multiply10 함수가 재실행된다
  • useMemo를 사용하면 multiply10의 결과값을 임시저장하고, count2 값이 바뀌지 않는 이상 메모이징 된 (어딘가에 임시저장한) 값을 반환하게 된다

값을 임시저장하는 데에 메모리를 조금 더 사용하긴 하지만, 함수의 재연산량이 줄어드므로 결과적으론 훨씬 효율적인 컴포넌트가 된다

사용 방법

const value = useMemo(callback, [deps]);

첫 번째 인자로 연산값을 가져다 쓰려는 콜백 함수를 넣고, 두 번째 인자로 의존성 배열을 넣는다

useEffect와 상당히 비슷한 것을 볼 수 있다

의존성 배열 안에 있는 값이 변경될 때만 콜백이 다시 호출되며 반환하는 값이 업데이트된다

의존성 배열이 비어있을 경우, 컴포넌트 최초 마운트 시에만 콜백이 호출되며 그때의 반환값을 계속 재활용한다

useEffect처럼 두 번째 인자에 아무 값도 넣지 않고 마운트 시마다 재실행되게끔 하는 것은 불가능하다

useCallback

기본 예시

import { useEffect, useState } from "react";

export const CallbackTestPage = () => {
  const [count, setCount] = useState<number>(1);
  const [count2, setCount2] = useState<number>(1);

  const handleClickButton = () => {
    setCount((prev) => prev + 1);
  };

  const handleClickButton2 = () => {
    setCount2((prev) => prev + 1);
  };

  useEffect(() => {
    console.log("handleClickButton 재생성");
  }, [handleClickButton]);

  useEffect(() => {
    console.log("handleClickButton2 재생성");
  }, [handleClickButton2]);

  return (
    <div>
      <div style={{ display: "flex", flexDirection: "row", gap: 50 }}>
        <h2>왼쪽: {count}</h2>
        <h2>오른쪽: {count2}</h2>
      </div>
      <button type="button" onClick={handleClickButton}>
        왼쪽 숫자 증가
      </button>
      <button type="button" onClick={handleClickButton2}>
        오른쪽 숫자 증가
      </button>
    </div>
  );
};

아까의 예시를 간단하게 수정하였다

우리는 이 컴포넌트가 렌더링될 때마다 함수가 재정의되는 것 (또 다시 생성되는 것) 을 원치 않는다

재정의 되는지 여부를 살펴보기 위해 함수 재정의 시마다 useEffect의 콜백이 호출되도록 하였다

 

와 ~ 놀랍지않게도 매 버튼 클릭으로 상태값이 변경되고 리렌더링이 일어날 때마다 함수가 재정의되는 것을 볼 수 있다

리렌더링이 발생하면, 함수 컴포넌트 자체가 다시 호출되기 때문에 그 안에 선언된 모든 함수들 또한 재정의된다

useCallback을 사용했을 때

const handleClickButton = useCallback(() => {
    setCount((prev) => prev + 1);
}, []);

const handleClickButton2 = useCallback(() => {
    setCount2((prev) => prev + 1);
}, []);

useEffect(() => {
    console.log("handleClickButton 재생성");
}, [setCount]);

useEffect(() => {
    console.log("handleClickButton2 재생성");
}, [handleClickButton2]);

이번엔 핸들러 함수들을 useCallback으로 감싸주자

handleClickButton이 의존성을 가질 만한 (변경을 감지할 만한) 변수가 없으므로 의존성 배열은 비워 둔다

 

아무리 버튼을 눌러도 함수 재생성이 되지 않으면서, 함수의 동작은 제대로 되는 것을 볼 수 있다

useCallback이란

마찬가지로 성능 최적화에 사용되며, 콜백 함수 그 자체를 어딘가에 저장 (메모이징) 해 두고 동일한 값 (의존성 배열의 값들) 에 대해서 저장해둔 함수를 재활용할 수 있게 도와준다

  • 위의 예시에서 우리는 count, count2 두 개의 값을 지속적으로 변화시켜 리렌더링을 발생시켰고, 리렌더링 때문에 handleClickButton, handleClickButton2 이 재정의되었다
  • useCallback을 사용하면 handleClickButton, handleClickButton2 함수 자체를 어딘가 임시저장하고, 마운트 시에만 함수를 재정의하여 반복적으로 재활용할 수 있다

사용 방법

const func = useCallback(callback, [deps]);

첫 번째 인자로 메모이제이션 하려는 콜백 함수를 넣고, 두 번째 인자로 의존성 배열을 넣는다

마찬가지로 useMemo, useEffect와 상당히 비슷한 것을 볼 수 있다

의존성 배열 안에 있는 값이 변경되면 메모이제이션 된 콜백 함수가 업데이트된다

의존성 배열이 비어있을 경우, 컴포넌트 최초 마운트 시에만 콜백이 호출되며 그때의 반환값 함수를 계속 재활용한다

만약 함수 내부에서 연산에 사용하는 상태값이나 prop이 있다면 반드시 의존성 배열에 등록해 주어야 연산 결과가 제대로 나오므로 주의하자

부작용

  • 너무 과한 메모이제이션은 오히려 메모리를 과하게 사용하고, 성능 개선에는 별 도움이 안 될 가능성이 있다
    • useMemo, useCallback이 좋아 보인다고 아무데나 다 갖다 붙이기보단 정말로 메모이제이션이 쓸모가 있는 곳에만 붙여주는 것이 좋다
    • 예를 들면, 엄청나게 빈번히 변화하는 상태값에 영향을 받는 값이나 함수가 있다고 할 때, 이 값 또는 함수를 메모이제이션 하는 것은 (어차피 의존성 배열에 의해 값이 계속 변화할 것이므로) 사실상 의미가 없다
  • 또한 useMemo, useCallback은 의존성을 통해 코드를 더욱 복잡하게 만드므로 의존성 관리가 원활할 때 사용하자
    • 의존성 값을 잘못 지정하면 함수 값이 엉망이 되는 대참사가 발생하고, 의존성 값을 과하게 지정하면 위와 같이 메모이제이션의 의미가 없어진다

참고 자료

https://www.daleseo.com/react-hooks-use-memo/

https://react.vlpt.us/basic/17-useMemo.html

'ClientSide > React' 카테고리의 다른 글

react-app-env.d.ts  (0) 2023.08.30
마운트와 렌더링  (0) 2023.07.24
React의 useContext  (0) 2023.05.06
Suspense와 Error Boundary를 이용한 로딩과 예외처리  (0) 2022.12.10
React를 클론코딩 #1 가상 돔  (0) 2022.10.05
Comments