useMemo와 useCallback 이해하기

기운찬곰·2023년 5월 5일
1

React Core Concepts

목록 보기
2/7
post-thumbnail

참고(원본 글) : https://www.joshwcomeau.com/react/usememo-and-usecallback/
참고 2 : https://www.seonest.net/posts/react/understanding-usememo-and-usecallback

실습 : https://ckstn0777.github.io/react-playground/

📚 joshwcomeau 블로그에 있는 'Understanding useMemo and useCallback' 라는 글이 좋아서 번역하고 실습해서 정리한 글입니다. 'Why React Re-Renders' 에 이어서 2편으로 이어지는 글인데, 이 글 또한 매우 좋으니 한번 읽어보셨으면 합니다.

Overview

여러분은 useMemo와 useCallback이 조금 헷갈리지 않으신가요? 언뜻 보면 이 둘은 되게 비슷합니다. 근데 당신은 혼자가 아닙니다. 나(저자)는 이 두 개의 hook에 대해 머리를 긁적이고 있는 리액트 개발자들과 매우 많은 이야기를 나누었습니다.

이번 글에서의 목표는 이 모든 혼란을 해소하는 것입니다. 우리는 이 둘이 무엇을 하는지, 왜 그것들이 유용한지, 그리고 그것들을 최대한 활용하는 방법에 대해 배울 것입니다.


The Basic idea

저번 글에서 봤다싶이 React의 주요 기능은 UI를 애플리케이션 상태와 동기화하는 것입니다. 그리고 이를 위해 사용하는 도구를 “리렌더(re-render)”라고 합니다.

각각의 리렌더는 현재 애플리케이션 상태를 기준으로 특정 시점에 애플리케이션의 UI가 어떻게 보여야 하는지에 대한 스냅샷입니다. 우리는 그것을 사진 더미처럼 생각할 수 있습니다. 각각의 사진은 모든 상태 변수에 특정한 값이 주어진 것처럼 보입니다. 즉, 각각의 “리렌더”는 현재 상태를 기반으로 DOM이 어떻게 보여야 하는지에 대한 그림을 생성합니다. 이것을 우리는 때때로 “가상 DOM(Virtaul DOM)” 이라고 부릅니다.

우리는 어떤 DOM 노드를 변경해야 하는지 리액트에게 직접 말하지 않습니다. 대신 React가 리렌더링을 통해 새 스냅샷을 생성하고 “find the differences” game을 통해 변경해야 할 사항을 파악할 수 있습니다.

리액트는 처음부터 매우 최적화되어 있으므로 일반적으로 리렌더링은 큰 문제가 되지 않습니다. 그러나 특정 상황에서 이러한 스냅샷을 생성하는데 시간이 오래 걸립니다. 이로 인해 사용자가 작업을 수행한 후 UI가 충분히 빠르게 업데이트되지 않는 등 성능 문제가 발생할 수 있습니다.

기본적으로, useMemouseCallback리렌더링을 최적화하는데 도움이 되도록 만들어진 도구입니다. 이것들은 두 가지 방법으로 최적화를 수행합니다.

  1. 지정된 렌더에서 수행해야 하는 작업의 양을 줄인다.
  2. 컴포넌트를 다시 렌더링해야 하는 횟수를 줄인다.

이러한 전략에 대해 하나씩 이야기 해보도록 하겠습니다.

진짜 문장 하나하나마다 useMemo와 useCallback이 필요한 이유를 말하기 위해 빌드업하고 있는데, 이는 얼마나 알기 쉽게 설명하기 위해 저자가 노력했는지 알 수 있을 거 같습니다. 리스펙... 👍


useMemo Use case 1: Heavy computations (무거운 연산)

사용자 입력 값인 selectedNum을 받아서 1과 selectedNum 사이에 소수를 찾는 도구를 만든다고 가정해봅시다.

import { useState } from "react";

export default function PlaygroundTwo() {
  const [selectedNum, setSelectedNum] = useState(100);

  const allPrimes = [];
  for (let counter = 2; counter < selectedNum; counter++) {
    if (isPrime(counter)) {
      allPrimes.push(counter);
    }
  }

  return (
    <>
      <form>
        <label htmlFor="num">Your number:</label>
        <input
          id="num"
          type="number"
          value={selectedNum}
          onChange={(event) => {
            // 과하게 계산되는 것을 막기위해 최대 100k 까지만 허용
            const num = Math.min(100_000, Number(event.target.value));
            setSelectedNum(num);
          }}
        />
      </form>
      <p>
        There are {allPrimes.length} prime(s) between 1 and {selectedNum}:{" "}
        <span className="prime-list">{allPrimes.join(", ")}</span>
      </p>
    </>
  );
}

function isPrime(num: number) {
  const max = Math.sqrt(num);

  if (num === 2) {
    return true;
  }

  for (let counter = 2; counter <= max; counter++) {
    if (num % counter === 0) {
      return false;
    }
  }

  return true;
}

  • 이 코드는 상당히 많은 계산이 필요합니다. 사용자가 큰 숫자로 selectedNum에 입력하면 당연히 연산량도 많아질 것입니다.
  • 사용자가 selectedNum 을 업데이트하면 이 계산은 수행되어야 합니다. 그러나 이 계산을 수행할 필요가 없을 때에도 동작하게 된다면 잠재적으로 성능 문제가 발생할 수 있습니다.

예를 들어, 다음과 같이 디지털 시계 기능을 추가했다고 가정해볼까요?

import { useEffect, useState } from "react";
import format from "date-fns/format";

export default function PlaygroundTwo() {
  const [selectedNum, setSelectedNum] = useState(100);
  const time = useTime();

  const allPrimes = [];
  for (let counter = 2; counter < selectedNum; counter++) {
    if (isPrime(counter)) {
      allPrimes.push(counter);
    }
  }

  return (
    <>
      <p className="clock">{format(time, "hh:mm:ss a")}</p>

      <form>
        <label htmlFor="num">Your number:</label>
        <input
          id="num"
          type="number"
          value={selectedNum}
          onChange={(event) => {
            // 과하게 계산되는 것을 막기위해 최대 100k 까지만 허용
            const num = Math.min(100_000, Number(event.target.value));
            setSelectedNum(num);
          }}
        />
      </form>
      <p>
        There are {allPrimes.length} prime(s) between 1 and {selectedNum}:{" "}
        <span className="prime-list">{allPrimes.join(", ")}</span>
      </p>
    </>
  );
}

function isPrime(num: number) {
  const max = Math.sqrt(num);

  if (num === 2) {
    return true;
  }

  for (let counter = 2; counter <= max; counter++) {
    if (num % counter === 0) {
      return false;
    }
  }

  return true;
}

function useTime() {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    const intervalId = setInterval(() => {
      setTime(new Date());
    }, 1000);

    return () => clearInterval(intervalId);
  }, []);

  return time;
}

  • 이제 1초마다 time 상태는 현재 시간을 반영하기 위해 업데이트가 됩니다. 그리고 그 값은 디지털 시계를 렌더링하기 위해 사용됩니다.
  • 여기서 문제가 있습니다. 2개의 상태(selectedNum, time) 중 하나라도 값이 바뀌면, 이 비싼 소수 계산 전체를 다시 실행합니다. 저는 selectedNum만 바뀌었을때만 소수 계산하기를 원합니다.

만약 입력 값을 100,000까지 늘리면 한 번 렌더링하는데 25.4ms가 걸리는 되는 것을 알 수 있습니다.

이제 우리는 time 값이 바뀌어도 소수 계산 전체를 다시 실행할 필요가 없게 수행해볼 것입니다. 즉, 이미 우리가 상태에 대해 소수 리스트를 저장하고 있는 경우 매번 다시 계산하지 않고 이전 값을 재활용하게 할 것입니다. 이것이 useMemo가 할 수 있는 일입니다. (메모리제이션!!)

const allPrimes = React.useMemo(() => {
  const result = [];
  for (let counter = 2; counter < selectedNum; counter++) {
    if (isPrime(counter)) {
      result.push(counter);
    }
  }
  return result;
}, [selectedNum]); // selectedNum 가 변경될때만 리렌더링되어 값을 재계산. 그게 아니라면 데이터 재사용(캐시)

"useMemo is essentially like a lil’ cache, and the dependencies are the cache invalidation strategy."

useMemo는 본질적으로 작은 캐시와 같으며 종속성은 캐시 무효화 전략과 같습니다. 종속성 리스트를 확인해서, 이전 렌더링 이후 변경된 사항이 있는지 확인합니다. 만약 변경 사항이 있다면 새로운 값을 계산하기 위해 함수를 재실행합니다. 그렇지 않다면 이전에 계산된 값을 재사용합니다.

이것은 일반적으로 memoization으로 알려져 있고, 이 hook을 "useMemo"라고 부르는 이유입니다. 이것의 성능을 실제 확인해보면 앞에서는 25.4ms가 나오던게 지금은 2~3ms 밖에 안나옵니다. 엄청난 차이네요.


An alternative approach

useMemo hook은 불필요한 계산을 피하는 데 도움이 될 수 있지만, 이것이 정말 최고의 해결책일까요? 종종, 우리는 애플리케이션의 내용을 재구성(restructuring)함으로써 useMemo의 사용을 피할 수 있습니다.

대표적으로 컴포넌트를 분리시켜서 각각 자체 상태를 관리하도록 하면 됩니다. Clock에는 time을 관리하고, PrimeCalculator는 selectedNum을 관리하는 식입니다.

import React from 'react';

import Clock from './Clock';
import PrimeCalculator from './PrimeCalculator';

function App() {
  return (
    <>
      <Clock />
      <PrimeCalculator />
    </>
  );
}

function Clock() {
  const time = useTime();

  return <p className="clock">{format(time, "hh:mm:ss a")}</p>;
}

function PrimeCalculator() {
  const [selectedNum, setSelectedNum] = useState(100);

  const allPrimes = useMemo(() => {
    const result = [];
    for (let counter = 2; counter < selectedNum; counter++) {
      if (isPrime(counter)) {
        result.push(counter);
      }
    }
    return result;
  }, [selectedNum]);

  return (
    <>
      <form>
        <label htmlFor="num">Your number:</label>
        <input
          id="num"
          type="number"
          value={selectedNum}
          onChange={(event) => {
            // 과하게 계산되는 것을 막기위해 최대 100k 까지만 허용
            const num = Math.min(100_000, Number(event.target.value));
            setSelectedNum(num);
          }}
        />
      </form>
      <p>
        There are {allPrimes.length} prime(s) between 1 and {selectedNum}:{" "}
        <span className="prime-list">{allPrimes.join(", ")}</span>
      </p>
    </>
  );
}


export default App;

이러면 한 컴포넌트에서 리렌더링하더라도 다른 컴포넌트에 영향을 주지 않습니다. 더 구분이 깔끔해진거 같고, 성능 상으로도 더 좋아졌습니다.

우리는 상태를 올리는 것에 대해 많이 듣지만, 때때로 더 나은 접근법은 상태를 내리는 것이다! 각 컴포넌트는 하나의 책임을 져야 하며, 위의 예에서 App은 전혀 관련이 없는 두 가지 일을 하고 있었습니다. (SOLID의 원칙 중 첫번째 SRP을 의미하네요)

하지만 이것이 항상 선택할 수 있는 것은 아닙니다. 크고, 실제 앱에서, 많은 상태들이 존재하고 꽤 높게 상태를 끌어올려야 하지만 아래로 내릴 수 없는 상태가 많이 있습니다.

이 상황에 대해 또 다른 속임수가 있습니다. 예를 들어 보겠습니다. time 변수를 PrimeCalculator 위에 올려놓아야 한다고 가정해봅시다. 시간에 따른 배경색 변경을 위해 useTime을 상위에 사용해야 하는 상황입니다.

import { useEffect, useMemo, useState } from "react";
import format from "date-fns/format";
import { getHours } from "date-fns";

export default function PlaygroundTwo() {
  const time = useTime();

  // 시간에 따른 배경색 변경
  const backgroundColor = getBackgroundColorFromTime(time);

  return (
    <div style={{ backgroundColor }}>
      <Clock time={time} />
      <PrimeCalculator />
    </div>
  );
}

// ...(이하 생략)...

const getBackgroundColorFromTime = (time: Date) => {
  const hours = getHours(time);

  if (hours < 12) {
    // A light yellow for mornings
    return "hsl(50deg 100% 90%)";
  } else if (hours < 18) {
    // Dull blue in the afternoon
    return "hsl(220deg 60% 92%)";
  } else {
    // Deeper blue at night
    return "hsl(220deg 100% 80%)";
  }
};

function Clock({ time }: { time: Date }) {
  return <p className="clock">{format(time, "hh:mm:ss a")}</p>;
}

// ✅ React.memo를 사용해 순수 컴포넌트로 변경합니다.
const PrimeCalculator = React.memo(_PrimeCalculator);

function _PrimeCalculator() {
  const [selectedNum, setSelectedNum] = useState(100);

  // 사실상 useMemo는 더 이상 없어도 된다. 
  /*
  const allPrimes = useMemo(() => {
    const result = [];
    for (let counter = 2; counter < selectedNum; counter++) {
      if (isPrime(counter)) {
        result.push(counter);
      }
    }
    return result;
  }, [selectedNum]);
  */
  const allPrimes = [];
  for (let counter = 2; counter < selectedNum; counter++) {
    if (isPrime(counter)) {
      allPrimes.push(counter);
    }
  }

  return (
    <>
      <form>
        <label htmlFor="num">Your number:</label>
        <input
          id="num"
          type="number"
          value={selectedNum}
          onChange={(event) => {
            // 과하게 계산되는 것을 막기위해 최대 100k 까지만 허용
            const num = Math.min(100_000, Number(event.target.value));
            setSelectedNum(num);
          }}
        />
      </form>
      <p>
        There are {allPrimes.length} prime(s) between 1 and {selectedNum}:{" "}
        <span className="prime-list">{allPrimes.join(", ")}</span>
      </p>
    </>
  );
}

이 경우에는 바로, 순수 컴포넌트를 만들어서 React.memo로 감싸는 것입니다. 이러면 관련없는 업데이트로부터 보호가 됩니다.

여기서 흥미로운 관점에 전환을 발견할 수 있습니다. 이전에는 소수를 계산하고 결과를 memo하고 있었습니다. 하지만 이 경우에는 전체 컴포넌트를 대신 메모해 두었습니다.

어느 것이 더 나은 접근이라고 말하려는 것이 아닙니다. 각각의 도구는 그에 알맞게 사용해야 합니다. 그러나 이처럼 특정 케이스인 경우에는 저(저자)는 React.memo 접근을 더 선호합니다.


useMemo Use case 2: Preserved references

아래 예시에서 Boxes 컴포넌트를 만들었습니다. 장식 용도로 나열된 색깔을 가진 box 모음을 보여줍니다. 또한 관련되어 있지 않은 상태인 name 을 만들었습니다.

import React from "react";
import { useState } from "react";
import "./style.css";

export default function PlaygroundThree() {
  const [name, setName] = useState("");
  const [boxWidth, setBoxWidth] = React.useState(1);

  const boxes = [
    { flex: boxWidth, background: "hsl(345deg 100% 50%)" },
    { flex: 3, background: "hsl(260deg 100% 40%)" },
    { flex: 1, background: "hsl(50deg 100% 60%)" },
  ];

  const id = React.useId();

  return (
    <>
      <Boxes boxes={boxes} />

      <section>
        <label htmlFor={`${id}-name`}>Name:</label>
        <input
          id={`${id}-name`}
          type="text"
          value={name}
          onChange={(event) => {
            setName(event.target.value);
          }}
        />
        <label htmlFor={`${id}-box-width`}>First box width:</label>
        <input
          id={`${id}-box-width`}
          type="range"
          min={1}
          max={5}
          step={0.01}
          value={boxWidth}
          onChange={(event) => {
            setBoxWidth(Number(event.target.value));
          }}
        />
      </section>
    </>
  );
}

const Boxes = React.memo(({ boxes }: { boxes: any[] }) => (
  <div className="boxes-wrapper">
    {boxes.map((boxStyles, index) => (
      <div key={index} className="box" style={boxStyles} />
    ))}
  </div>
));
.boxes-wrapper {
  width: 600px;
  display: flex;
  gap: 16px;
}

.box {
  height: 100px;
  border-radius: 4px;
  background: hsl(350deg 100% 50%);
}

section {
  padding: 32px;
  display: flex;
  flex-direction: column;
  align-items: center;
}

input {
  margin-bottom: 24px;
}

Boxes를 둘러싼 React.memo 덕분에 Boxes는 순수한 컴포넌트입니다. 이는 props가 바뀔 때는 다시 렌더링해야 한다는 의미입니다. 근데 여기서 특이하게도 사용자가 name을 변경할 때마다 Box도 다시 렌더링됩니다.

왜 React.memo 를 사용했는데도 렌더링을 막지 못했을까요?

분명 props로 전달받은 boxes라는 상태는 동일한 데이터를 제공하는 것처럼 보입니다. boxWidth 라는 상태 변수도 바뀌지 않았으니까요. 하지만 분명 boxes는 리액트가 리렌더링 될 때마다 새로운 array를 다시 생성하고 있습니다. 값 측면에서는 동일하지만 참조(reference) 측면에서는 그렇지 않습니다.

간단한 자바스크립트 예시를 볼까요? firstResult랑 secondResult가 동일하다고 생각하시나요?

function getNumbers() {
  return [1, 2, 3];
}

const firstResult = getNumbers();
const secondResult = getNumbers();

console.log(firstResult === secondResult);

당연히 다릅니다. 내용물은 같지만 완전히 다른 배열이기 때문입니다. getNumbers 함수를 호출할 때마다 컴퓨터 메모리에 저장되는 고유한 배열인 새로운 배열을 만듭니다.

string, numbers, boolean 같은 단순한 데이터 유형은 값으로 비교할 수 있습니다. 그러나 array나 object에 대해서는 참조(reference)로 비교됩니다. 이 차이에 대한 자세한 내용은 Dave Ceddia의 멋진 블로그 게시글을 참조해주세요.

다시 React로 넘어와서, 결국 리액트 컴포넌트는 리렌더링될 때마다 매번 호출되므로 boxes는 완전히 새로운 배열을 계속해서 만들게 됩니다. React.memo도 소용이 없었던 것이지요.

// 해당 컴포넌트가 렌더링될 때마다 이 함수를 매번 호출하게 된다...
function App() {
  // 그리고 결국 완전히 새로운 배열을 만들게 됩니다.
  const boxes = [
    { flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
    { flex: 3, background: 'hsl(260deg 100% 40%)' },
    { flex: 1, background: 'hsl(50deg 100% 60%)' },
  ];
  // 새로운 배열이 매번 prop로 전달되므로 React.memo는 소용이 없습니다
  return (
    <Boxes boxes={boxes} />
  );
}

이 문제를 해결하기 위해서는 우리는 useMemo를 사용할 수 있습니다.

const boxes = React.useMemo(() => {
  return [
    { flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
    { flex: 3, background: 'hsl(260deg 100% 40%)' },
    { flex: 1, background: 'hsl(50deg 100% 60%)' },
  ];
}, [boxWidth]);

앞서 본 Use case 1과 달리 무거운 연산을 막기 위해 사용한 것이 아닙니다. 여기서의 목표는 특정 배열에 대한 참조를 보존하기 위해 사용했습니다

useMemo 사용 이전에는 각 스냅샷의 일부로 완전히 새로운 배열을 생성하고 있었습니다. 그러나 useMemo를 사용한 이후에는 이전에 만든 boxes 배열을 다시 사용합니다.

여러 렌더링 간에 동일한 참조를 유지함으로써 UI에 영향을 미치지 않는 렌더링은 무시하고 순수한 컴포넌트가 원하는 방식으로 작동할 수 있습니다.


The useCallback hook

이제 useMemo는 마스터한거 같은데… useCallback은 뭘까요?

사실 이 둘은 기능적으로 완전히 동일하지만, useCallback은 배열/객체 대신 function에 사용됩니다. 배열과 객체와 동일하게 function도 reference로 비교됩니다. 참조형 데이터 타입이기 때문이죠.

const functionOne = function() {
  return 5;
};
const functionTwo = function() {
  return 5;
};

console.log(functionOne === functionTwo); // false

즉, 컴포넌트 내에서 함수를 정의하면 각 렌더링에서 함수가 다시 생성되어 매번 동일하지만 새로운 함수가 생성됩니다.

다음 예시를 보겠습니다.

import React from "react";

export default function PlaygroundFour() {
  const [count, setCount] = React.useState(0);

  function handleMegaBoost() {
    setCount((currentValue) => currentValue + 1234);
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Click me!</button>
      <MegaBoost handleClick={handleMegaBoost} />
    </div>
  );
}

const MegaBoost = React.memo(_MegaBoost);

function _MegaBoost({ handleClick }: { handleClick: () => void }) {
  console.log("Render MegaBoost");

  return (
    <button className="mega-boost-button" onClick={handleClick}>
      MEGA BOOST!
    </button>
  );
}

전형적인 Counter 애플리케이션인데, 특별히 Mega Boost 라는 것을 만들었습니다. 이 녀석을 클릭하면 카운트를 크게 증가시킬 수 있습니다. MegaBoost 컴포넌트는 React.memo 덕분에 순수한 컴포넌트입니다. 원래대로라면 count에 따라 리렌더링 되지 않을 것입니다.. 하지만 이상하게도 리렌더링이 발생합니다.

이는 boxes 배열에서 봤듯이, 동일한 문제입니다. 바로 모든 렌더링에서 완전히 새로운 function (handleMegaBoost) 을 생성하고 있다는 것이죠. 그렇다면 useMemo를 사용해서 문제를 해결해볼까요?

const handleMegaBoost = React.useMemo(() => {
  return function() {
    setCount((currentValue) => currentValue + 1234);
  }
}, []);

물론 이렇게 해도 가능하지만 일반적으로 useCallback을 쓰는 것이 더 깔끔합니다.

const handleMegaBoost = React.useCallback(() => {
  setCount((currentValue) => currentValue + 1234);
}, []);

useCallback은 useMemo와 동일한 목적으로 사용되지만 function을 위해 특별히 만들어졌습니다. 우리는 직접 함수를 전달하고, 그 함수를 기억(메모)하여 렌더링간 스레드링(연결) 합니다.

결국, 이 둘은 같은 녀석입니다. 방식만 다를뿐 효과는 동일합니다.

// This:
React.useCallback(function helloWorld(){}, []);
// ...Is functionally equivalent to this:
React.useMemo(() => function helloWorld(){}, []);

useCallback은 문법적 설탕(syntactic sugar)라고 할 수 있겠네요. 개발자가 좀 더 편하라고 존재하는 것입니다.


When to use these hooks

이제 useMemo와 useCallback이 왜 사용되는지 알게되었습니다. 문제는 우리가 얼마나 그것을 자주 사용해야 하는가 입니다.

개인적인 생각으로는 모든 object, array, function을 이 hook에 감싸는 것은 시간 낭비일 것입니다. 대부분의 경우 그냥 무시할 수 있습니다. 리액트는 매우 최적화 되어있으며, 리렌더링은 우리가 생각하는 것처럼 느리거나 비싸지 않습니다.

이러한 hook를 사용하는 가장 좋은 방법은 문제에 대응하는 것입니다. 애플리케이션이 다소 느려지는 것을 발견하면 React Profiler 등을 사용해 느린 렌더링을 찾아낼 수 있습니다. 경우에 따라 애플리케이션을 재구성(restruct)하여 성능을 향상시킬 수 있습니다. 혹은 useMemo나 useCallback을 사용해서 속도를 높일 수 있습니다.

그렇긴 하지만, 제가 이 hook을 선제적으로 적용하는 몇 가지 시나리오가 있습니다.

Inside generic custom hooks

내가 가장 좋아하는 작은 커스텀 후크 중 하나는 useToggle 인데, 이는 useState와 거의 동일하게 작동하지만 true와 false 사이의 상태 변수만 전환할 수있는 친근한 helper 역할을 합니다.

function App() {
  const [isDarkMode, toggleDarkMode] = useToggle(false);
  return (
    <button onClick={toggleDarkMode}>
      Toggle color theme
    </button>
  );
}
function useToggle(initialValue) {
  const [value, setValue] = React.useState(initialValue);

  const toggle = React.useCallback(() => {
    setValue(v => !v);
  }, []);

  return [value, toggle];
}

토글 기능은 useCallback으로 메모 됩니다.

이런 재사용 가능한 커스텀 hook를 만들 때, 저는 그것들을 가능한 효율적으로 만들고 싶습니다. 왜냐면 그것들이 미래에 어디에 사용될지 모르기 때문입니다. 조금 과한 최적화일 수 있지만 만약 이 hook을 30~40번 사용하면 애플리케이션 성능을 향상시킬 수 있습니다.

Inside context providers

컨텍스트가 포함된 애플리케이션 간에 데이터를 공유할 때 큰 객체를 값 속성으로 전달하는 것이 일반적입니다. 일반적으로 이 객체를 메모하는 것이 좋습니다.

const AuthContext = React.createContext({});

function AuthProvider({ user, status, forgotPwLink, children }){
  const memoizedValue = React.useMemo(() => {
    return {
      user,
      status,
      forgotPwLink,
    };
  }, [user, status, forgotPwLink]);

  return (
    <AuthContext.Provider value={memoizedValue}>
      {children}
    </AuthContext.Provider>
  );
}

이것이 왜 좋을까요? 이 컨텍스트를 사용하는 순수한 컴포넌트가 수십개 있을 수 있습니다. useMemo를 사용하지 않으면 AuthProvider의 부모가 다시 렌더링할 경우 이러한 모든 컴포넌트가 강제로 다시 렌더링될 것입니다.


마치면서

useMemo와 useCallback은 리액트 개발자라면 물론 알고 있었지만, 이렇게 쉽고 재미있고 알기 쉽게 풀어낸 글을 보니 이 글의 저자가 대단하다는 생각이 듭니다. 내가 알고 있는 것과 남들에게 가르쳐주는 건 정말 다른거 같습니다. 머리 속으로는 알고 있어도 다른 사람에게 쉽게 설명하는 건 전혀 다른 문제니까요.

아무튼 저번 시간에 'Why React Re-Renders' 글에 이어서 리액트에 대해 좀 더 정리해보는 시간을 가질 수 있어서 좋았습니다.

profile
배움을 좋아합니다. 새로운 것을 좋아합니다.

0개의 댓글