[React] useMemo와 useCallback

박기영·2022년 10월 2일
0

React

목록 보기
15/32

useMemo, useCallback은 보통 변수, 상수, 함수 등에 대한 memoization을 위하여 사용된다.
그런데, 사용하다보면 두 hooks가 뭔 차이가 있는건지 이해가 안될 때가 있다.
둘이 어떤 차이가 있을까?

useMemouseCallback은 모두 최적화를 위해 사용되는 hooks이다.

useMemo

useMemo는 "값"을 반환한다.

useMemo에 대한 간단한 이해를 돕기 위해 필자가 작성해놓은 것이다.
슉 보고 오시면 이해하는데 도움이 되실 것 같아서 적어놨습니다 ㅎㅎ...

function App() {
  const [trigger, setTrigger] = useState("");

  const dontWantReRender = () => {
    console.log("랜더링 싫어!");
    return "Nope!";
  };

  const triggerHandler = (e) => {
    setTrigger(e.target.value);
    console.log("랜더링!");
  };

  return (
    <div>
      <p>{dontWantReRender()}</p>
      <input value={trigger} onChange={triggerHandler} />
    </div>
  );
}

위와 같은 컴포넌트가 있다.

input 태그에 trigger라는 state가 사용된다.
값을 입력하는 순간 onChange가 발동해서 triggerHandler 함수를 실행한다.
그러면 이 컴포넌트는 재랜더링이 된다.
재랜더링이 되면? 컴포넌트 내부에 있는 값들이 다시 호출된다.

즉, 재랜더링 할 필요가 없는 dontWantReRender이 다시 생성된다.

한번 저 상황이 어떻게 돌아가는지 살펴보자.

참고 동영상

우려했던대로, state의 변화로 인해 재랜더링되는 컴포넌트 내의 모든 것들이 재생성되었다.

useMemo는 이런 현상을 막을 수 있다.
코드를 수정해봤다.

function App() {
  const [trigger, setTrigger] = useState("");

  const dontWantReRender = useMemo(() => {
    console.log("랜더링 싫어!");
    return "Nope!";
  }, []);

  const triggerHandler = (e) => {
    setTrigger(e.target.value);
    console.log("랜더링!");
  };

  return (
    <div>
      <p>{dontWantReRender}</p>
      <input value={trigger} onChange={triggerHandler} />
    </div>
  );
}

무엇이 변했는가?
재랜더링이 굳이 필요없는, 초기 상태만 보여줘도 되는 함수(혹은 값)를 useMemo로 감쌌다.

결과를 살펴보자.

참고 동영상

완벽하다. 불필요한 재랜더링을 막아냈다.

그런데....코드에서 하나가 더 바뀌었다.
컴포넌트의 return 부분을 잘 보면 p 태그로 보여줬던 값이 변했다.
어떻게?

// 변경 전
<p>{dontWantReRender()}</p>

// 변경 후
<p>{dontWantReRender}</p>

useMemo를 사용하기 전에는 함수형으로 작성했던 것과 달리
사용한 후에는 변수(혹은 상수)처럼 작성했다.

왜 굳이 이렇게 바뀐걸까?
맨 위에서 지나가듯 말한 말이 있다.

useMemo는 값을 반환한다.

이게 도대체 무슨 말일까?

useMemo는 값을 반환한다.

useMemo를 사용해놓고 그 값을 사용할 때, 변수 형태로 바꿔주지 않으면 아래와 같은 에러가 발생한다.

아, 참고로 이는 필자가 처음에 함수 타입으로 작성했기 때문으로,
함수 타입으로 작성하시지않고 그냥 숫자형, 문자형 등으로 쓰신 분은 이런 일이 없을 겁니다.
설명을 위해 이런 오류를 발생시킨 것입니다!

참고 이미지

주목할 것은 dontWantReRender의 타입이 string으로 변경된 것과
dontWantReRender is not a function이라는 문구이다.(둘이 같은 말이긴한데)

참고로 useMemo를 쓰지 않았을 때는 아래와 같이 타입이 뜬다.

참고 이미지

즉, useMemo를 쓰기 전에는 함수 타입(본인이 처음 작성한 그 타입),
쓰고 난 후는 return에 적어놓은 것의 타입을 따라간다는 것이다.

이를 통해, useMemo가 "값"을 반환한다는 것을 이해할 수 있었다.
그래서

// 변경 전
<p>{dontWantReRender()}</p>

// 변경 후
<p>{dontWantReRender}</p>

이런 변화가 있었던 것이다.

더 쉽게 말하면 useMemo 내부에 적은 함수를 "실행"해서 보여준다는 것이다.

useCallback

useCallback도 마찬가지로 최적화와 관련된 hooks이다.
useCallback은 "함수"를 반환한다.

바로 예시를 살펴보자.

function App() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  const wantMemoization = () => {
    console.log(b);
  };

  wantMomoization();

  return (
    <div>
      <button onClick={() => setA((curr) => curr + 1)}>a++</button>
      <button onClick={() => setB((curr) => curr + 1)}>b++</button>

      <p>a: {a}</p>
      <p>b: {b}</p>
    </div>
  );
}

wantMemoization 함수는 b값을 콘솔에 보여주기 때문에
a++를 클릭할 때는 0만 출력되다가, b++를 누르는 순간부터 증가하는 값을 볼 수 있다.

참고 동영상

wantMemoizationuseCallback을 사용해보자.
dependency Arraya를 넣어줬다.
즉, a 값이 변해야지 useCallback 내부의 함수가 재선언된다는 것이다.

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

export default function App() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  const wantMemoization = useCallback(() => {
    console.log("b", b);
  }, [a]);

  wantMemoization();

  return (
    <div>
      <button onClick={() => setA((curr) => curr + 1)}>a++</button>
      <button onClick={() => setB((curr) => curr + 1)}>b++</button>

      <p>a: {a}</p>
      <p>b: {b}</p>
    </div>
  );
}

어떻게 동작할까?

참고 동영상

신기하게 동작한다.
b++ 버튼을 클릭했음에도 불구하고 콘솔에는 b의 초기값인 0만 찍힌다.
분명 b의 state는 3으로 증가했는데 말이다.
그 후, a++ 버튼을 클릭했더니 b의 state인 3이 그제서야 콘솔에 나왔다.

어떻게 된 일일까?

코드를 다시 살펴보자.

const wantMemoization = useCallback(() => {
  console.log("b", b);
}, [a]);

wantMemoizationuseCallback을 통해 a라는 state를 지켜보고있다.
즉, a라는 state에 변화가 생겨야지만 wantMemoization 함수가 재선언된다는 것이다.

우리는 b++ 버튼을 클릭했었다. 3까지 증가시켰다.
그러나 이는 b라는 state를 변화시킨 것이지,a라는 state에는 영향을 주지 않는다.
그래서 useCallbacka가 변하지 않았다고 판단,
wantMemoization을 재선언하지 않고 있는 것이다.

그러면 이게 무슨 효과가 있느냐?
wantMemoization 함수가 처음 선언되었을 때의 함수를 간직하고 있게 만든다.
즉, b라는 state가 0일 때의 함수를 계속 가지고 있게 된다.

그래서 b++ 버튼을 눌렀음에도 불구하고 wantMemoization 함수는 b가 0일 때의 바로 그 함수를 가지고 실행하는 것이다.

그 후, 우리는 a++ 버튼을 눌렀다.
a라는 state에 변화가 생겼다!
useCallbacka라는 state의 변화를 감지하고,
wantMemoization 함수를 재선언한다.
이 때, b의 값은 3이다.
그래서 이 때부터는 useCallbackb가 3일 때의 함수를 가지고 실행하기 시작한다.

그 후, 또 다시 b++ 버튼을 클릭했다.
그러나 a라는 state에는 변화가 없으므로 실행되는 것은 b가 3일 때의 wantMemoization인 것이다.

일부러 과정을 말로 설명해봤다.
이제 useCallback이 어떤 방식으로 작동하는건지 이해했다!

useCallback은 함수를 반환한다.

useCallback을 사용하기 전 함수의 타입은 다음과 같았다.

참고 이미지

뭐...함수로 선언했으니까 당연하게도 함수가 나왔다.

이번에는 useCallback을 사용했을 때를 살펴보자.

참고 이미지

놀랍게도(?) 함수 타입을 유지한다.
useMemo에서 함수 타입에서 다른 타입으로 변했던 것을 생각하면 차이가 명확하다.

즉, useCallback은 "함수"를 반환한다.

이들은 마냥 좋기만한 hooks인가?

useMemouseCallback은 대표적인 최적화 hooks이다.
그러나 최적화라고 생각하고 이걸 남용하면 안된다.
왜? 최적화되면 좋은건데?

우선, Memoization이라는 것의 개념을 이해할 필요가 있다.

Memoization은 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다. 동적 계획법의 핵심이 되는 기술이다. 메모이제이션이라고도 한다. - 위키백과 -

위키백과라 죄송합니다 ㅎㅎ;;(요즘은 위키도 질이 되게 좋아졌어요 ㅠ)

뜻을 보면 "메모리에 저장"이라는 말이 보인다.
그렇다. 아무런 대가도 받지않고 최적화를 해주지않는다.

우리는 두 hooks를 살펴보면서 특정 값을 저정하고 있다가 사용한다는 것을 알았다.
이 때, 저장되는 장소는 메모리인 것이다.
그래서 남용을 하게되면 오히려 메모리를 사전에 많이 먹고 시작해버리는 역설적인 현상이 발생한다.

따라서, 사용하기 전에 재랜더링에 영향을 많이 받게 되는지 등을 고려하고 사용해야겠다.

참고 자료

이화랑님 블로그
녘_님 블로그
Basemenks님 블로그

profile
나를 믿는 사람들을, 실망시키지 않도록

0개의 댓글