[React] Hooks (5) - memoization

invisibleVoice·2025년 2월 3일

리액트

목록 보기
12/14
post-thumbnail

memoization

리액트에서 리렌더링은 꽤나 빈번하게 일어난다. 그러나 리렌더링이 너무 자주 일어나면 비용이 발생하므로, 이를 최대한 줄이는 것이 도움이 된다. 이렇게 불필요한 렌더링이 발생하지 않도록 최적화하는 방법이 필요한데, 대표적인 방법이 3가지가 있다.

  • React.memo : 컴포넌트를 캐싱
  • useCallback : 함수를 캐싱
  • useMemo : 값을 캐싱

React.memo

React.memo는 훅은 아니지만, 메모이제이션에서 빼놓을 수 없다. 부모 컴포넌트가 리렌더링 되면 자식 컴포넌트들을 모두 리렌더링된다. 자식 컴포넌트 입장에서 자기는 아무런 변화가 없는데 리렌더링 되는것은 비효율적이다. React.memo를 사용하면 이런 컴포넌트의 불필요한 리렌더링을 막을 수 있다.

// App.js
import React, { useState } from "react";
import Box1 from "./components/Box1";
import Box2 from "./components/Box2";

function App() {
  console.log("App 컴포넌트가 렌더링되었습니다");
  const [count, setCount] = useState(0);

  const onPlusButtonClickHandler = () => {
    setCount(count + 1);
  };

  const onMinusButtonClickHandler = () => {
    setCount(count - 1);
  };

  return (
    <>
      <p>현재 카운트 : {count}</p>
      <button onClick={onPlusButtonClickHandler}>+</button>
      <button onClick={onMinusButtonClickHandler}>-</button>
      <div>
        <Box1 />
        <Box2 />
      </div>
    </>
  );
}

export default App;
// Box1.jsx
import React from "react";

function Box1() {
  console.log("Box1이 렌더링되었습니다");
  return <div>Box1</div>;
}

export default React.memo(Box1);	// 🤪React.memo로 감싸주기!
// Box2.jsx
import React from "react";

function Box2() {
  console.log("Box2이 렌더링되었습니다");
  return <div>Box2</div>;
}

export default Box2;


// 결과
// App 컴포넌트가 렌더링되었습니다
// Box2이 렌더링되었습니다

위 코드처럼 export에서 React.memo로 감싸주면 변화가 없던 Box1은 렌더링이 안 되는 것을 확인할 수 있다.

useCallback

useCallback은 인자로 들어오는 함수 자체를 기억한다. Box1이 count를 초기화하는 버튼을 가진다고 해보자.

// App.jsx
...

  // count를 초기화하는 함수
  const initCount = () => {
    setCount(0);
  };

  return (
    <>
      <p>현재 카운트 : {count}</p>
      <button onClick={onPlusButtonClickHandler}>+</button>
      <button onClick={onMinusButtonClickHandler}>-</button>
      <div>
        <Box1 initCount={initCount} />
        <Box2 />
      </div>
    </>
  );
}

...
// Box1.jsx
...

function Box1({ initCount }) {
  console.log("Box1이 렌더링되었습니다");

  const onInitButtonClickHandler = () => {
    initCount();
  };

  return (
    <div>
      <button onClick={onInitButtonClickHandler}>초기화</button>
    </div>
  );
}

export default React.memo(Box1);

// 결과
// App 컴포넌트가 렌더링되었습니다
// Box1이 렌더링되었습니다

Q. 어라? +, -, 초기화 어떤 버튼을 눌러도 마찬가지로 Box1이 리렌더링되네요? React.memo를 사용했는데도 왜 이런 일이 생기죠?

A. 생각해보면 당연하다. 상태가 변하면서 App.jsx가 리렌더링 되는데 이 과정에서 initCount에 함수가 새로 할당된다. 따라서 참조하는 주소값이 달라지므로 자식 컴포넌트는 props가 변경됐다고 인식한다. 그래서 자식 컴포넌트도 리렌더링되는 것이다.

이런 리렌더링을 막기 위해서는 initCount함수를(정확히 말하면 참조값을) 메모리 공간에 저장해놓고(=메모이제이션) 특정 조건이 아니고서야 변경되지 않도록 만들어야 한다.

// App.jsx
// 변경 전
const initCount = () => {
  setCount(0);
};

// 변경 후
const initCount = useCallback(() => {
  setCount(0);
}, []); // 배열은 의존성 배열

의존성 배열이 빈 배열인 useCallback을 사용하면 부모 컴포넌트가 리렌더링 되더라도 initCount는 여전히 같은 참조값을 갖는다. Box1은 initCount가 바뀌지 않았으므로 리렌더링이 되지 않는다.

여기서도 의존성 배열을 잘 의식해야한다. 빈 배열인 경우에는 첫 마운트 시에만 useCallback이 실행되므로 이후 아무리 상태를 바꿔도 실행되지 않고 첫 주소값을 기억한다. 의존성 배열에 count를 넣어주게 된다면 count가 바뀔때마다 새로운 참조값을 initCount에 할당하게 되고 자식 컴포넌트도 리렌더링 될 것이다.

useMemo

useMemo는 값을 캐싱할 때 사용한다. 값은 그냥 새로 생성해도 안 되나... 싶겠지만 모든 값들이 가볍진 않다. 어떤 엄청 무거운 함수의 반환값일 경우 리렌더링마다 무거운 연산을 반복해야하니 성능에 큰 영향을 줄 수 있다. 이런 경우에 첫 마운트에서만 값을 계산하고 메모리에 저장해놓으면 이후에는 계산 없이 값만 꺼내올 수 있다.

구조는 useCallback과 똑같다.

const value = useMemo(()=> {
	return 반환할_함수값()
}, [의존성 배열]);

주의할 점

리렌더링을 줄일 수 있으니 메모이제이션을 남발하다가 큰 코 다친다. 메모이제이션은 추가적인 메모리를 사용하기 때문에 남발하면 오히려 성능이 저하될 수 있다. 연산 비용이 적은 경우 오히려 그냥 새로 게산하는 것이 더 효율적일 수도 있다.

언제 사용하는 것이 좋을까?

메모이제이션 기법을 사용할 때는 실제 성능에 영향을 미치는지 판단한 후 적용하는 것이 중요하다.

  • React.memo
    리렌더링 비용이 크고, props가 자주 변하지 않는 컴포넌트에 사용
    ❌ 단순한 UI 컴포넌트에 사용하면 불필요한 비교 비용이 더 클 수 있음

  • useCallback
    자식 컴포넌트에 props로 넘기는 함수가 있고, 해당 함수의 참조값이 자주 바뀌는 경우 사용
    ❌ 부모 컴포넌트의 리렌더링이 자식에 영향을 주지 않는다면 굳이 사용하지 않아도 됨

  • useMemo
    계산 비용이 큰 연산(예: 필터링, 정렬, 복잡한 계산 등)을 최적화할 때 사용
    ❌ 단순한 연산이라면 그냥 매번 계산하는 것이 더 나을 수도 있음

profile
내배캠 React 9기 수료 후 Wecommit 풀스택 개발자로 근무중

0개의 댓글