인턴

useMemo와 useCallback

choijming21 2025. 3. 25. 16:10

useMemo와 React.memo 그리고 useCallback에 대해 공부한 것을 정리해보겠습니다!

 

먼저, Memoization이란! 이미 계산해본 연산 결과를 기억해두었다가 동일한 연산을 해야할 때 다시 연산하지 않고 기억해두었던 데이터를 반환시키는 방법입니다. 마치 시험을 볼 때 이미 풀어본 문제는 다시 풀어보지 않아도 답을 알고 있는 것과 유사합니다.

 

 

 

✅ useMemo란?

 useMemo는 특정 값이나 참조가 변경될 때만 재계산을 수행하고 그렇지 않으면 이전에 계산된 결과를 재사용합니다.

  • 복잡한 계산이나 처리가 필요한 경우
  • 렌더링 성능 최적화가 필요한 경우

 기본적으로 React는 부모 component로 부터 받는 state, props가 변동될 경우 자식 component도 리렌더링됩니다. 하지만 여기서 useMemo로 감싸준다면 두 개의 props 중 하나의 값만 바뀌었다면 그 값만 함수가 실행되고 나머지는 실행되지 않아서 성능 최적화면으로 아주 좋습니다.

 

 

 

< 실습 예제 코드 >

// components/memo/MemoControl.tsx
import { counter, textName } from "@/store/counterAtom";
import * as S from "@/styles/utils/components.style";
import { useAtom } from "jotai";
import { useState } from "react";
import MemoDisplay from "./MemoDisplay";

const MemoControl = () => {
  const [count, setCount] = useAtom<number>(counter);
  const [name, setName] = useAtom<string>(textName);

  const [inputValue, setInputValue] = useState<string>("");

  const handleOneHundredPlus = () => {
    setCount((prev) => prev + 100);
  };

  const handleOneHundredMinus = () => {
    setCount((prev) => prev - 100);
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInputValue(e.target.value);
  };

  const handleSubmit = () => {
    if (inputValue.trim()) {
      setName(inputValue);
      setInputValue("");
    }
  };

  return (
    <S.MainContainer>
      <h1>{count}</h1>

      <S.ButtonContainer>
        <S.Button onClick={handleOneHundredMinus}>- 100</S.Button>
        <S.Button onClick={handleOneHundredPlus}>+ 100</S.Button>
      </S.ButtonContainer>

      <h1>{name ? name : "이름을 입력해주세요..."}</h1>

      <S.InputContainer>
        <S.TextInput
          placeholder="텍스트를 입력해주세요..."
          onChange={handleChange}
        />
        <S.Button onClick={handleSubmit}>Add</S.Button>
      </S.InputContainer>

      <MemoDisplay count={count} name={name} />
    </S.MainContainer>
  );
};

export default MemoControl;


// components/memo/MemoDisplay.tsx
import * as S from "@/styles/utils/components.style";
import { useMemo } from "react";

type MemoDisplayProps = {
  count: number;
  name: string;
};

const getNumber = (count: number) => {
  console.log("숫자가 변동되었습니다.");
  return count;
};

const getText = (name: string) => {
  console.log("글자가 변동되었습니다.");
  return name;
};

const MemoDisplay = ({ count, name }: MemoDisplayProps) => {
  const showNumber = useMemo(() => getNumber(count), [count]);
  const showTest = useMemo(() => getText(name), [name]);

  return (
    <S.MemoDisplayContainer>
      <h2>{showNumber}</h2>
      <h2>{showTest}</h2>
    </S.MemoDisplayContainer>
  );
};

export default MemoDisplay;

 

 

< useMemo 사용 상세 설명 >

const showNumber = useMemo(() => getNumber(count), [count]);
const showTest = useMemo(() => getText(name), [name]);

 

이 부분이 핵심 로직입니다:

  1. useMemo는 첫 번째 인자로 함수를, 두 번째 인자로 의존성 배열을 받습니다.
  2. 컴포넌트가 리렌더링 될 때, 의존성 배열 내의 값이 변경되지 않으면 함수를 다시 실행하지 않고 이전에 계산된 결과를 재사용합니다.

 

< 구체적인 동작 >

  • showNumber의 경우:
    • count 값이 변경될 때만 getNumber 함수가 실행됩니다.
    • count 값이 동일하다면 이전에 계산된 결과를 사용합니다.
    • 콘솔에 "숫자가 변동되었습니다."라는 메세지는 count가 변경될 때만 출력됩니다.
  • showTest의 경우:
    • name 값이 변경될 때만 getText 함수가 실행됩니다.
    • name 값이 동일하다면 이전에 계산된 결과를 사용합니다.
    • 콘솔에 "글자가 변동되었습니다."라는 메세지는 name이 변경될 때만 출력됩니다.

 

 

 

 

 

✅ useCallback 이란?

useCallback 이란 useMemo와 비슷한 hook 이지만, useMemo는 특정 결과값을 재사용할 때 사용하는 반면, useCallback은 특정 함수를 새로 만들지 않고 재사용하고 싶을 때 사용하는 함수입니다.

 

 

< useCallback을 사용하는 경우 >

  • 함수를 하위 컴포넌트에 props로 전달하는 경우
  • 자주 재생성되는 함수를 최적화 시키려고 하는 경우
  • useEffect 내에 사용되는 함수의 불필요한 재실행을 막기 위한 경우

 

< useCallback 사용법 >

  • 첫 번째 인자는 함수를, 두 번째 인자는 의존하는 값(deps)
const memoizedCallback = useCallback(function, deps);
const add = useCallback(() => x + y, [x, y]);

 

 

 

 

 

✅ React.memo

컴포넌트에 동일한 props가 들어온다면 React.memo는 컴포넌트 렌더링 과정을 스킵하고 마지막에 렌더링된 결과를 재사용합니다.

 

 

 

 

 

 

✅ 실습: useCallback과 React.memo 제대로 이해하기 

이 실습은 SmartHome 컴포넌트 예제를 통해 useCallback과 React.memo를 함께 사용하여 리렌더링을 최적화 하는 방법을 알아보겠습니다.

 

 

1. 문제 상황: 불필요한 리렌더링

  • SmartHome 컴포넌트에는 침실, 주방, 욕실의 조명을 제어하는 세 개의 Light 컴포넌트가 있습니다. 각 방의 조명 상태는 독립적이지만, 하나의 조명 상태가 변경될 때마다 모든 Light 컴포넌트가 리렌더링 되는 문제가 있습니다!
const SmartHome = () => {
  const [masterOn, setMasterOn] = useState(false);
  const [kitchenOn, setKitchenOn] = useState(false);
  const [bathOn, setBathOn] = useState(false);

  // 토글 함수들...

  return (
    <S.MainContainer>
      <S.ButtonContainer>
        <Light room="침실" on={masterOn} toggle={toggleMaster} />
        <Light room="주방" on={kitchenOn} toggle={toggleKitchen} />
        <Light room="욕실" on={bathOn} toggle={toggleBath} />
      </S.ButtonContainer>
    </S.MainContainer>
  );
};

 

 

2. 문제 해결: useCallback과 React.memo 활용하기

  • 최적화 전략 1: useCallback으로 함수 참조 안정화하기
    • React에서 컴포넌트가 리렌더링될 때마다 내부 함수들은 새로 생성됩니다. 이는 매번 새로운 함수 참조(메모리 주소)가 만들어진다는 의미입니다.
    • useCallback은 의존성 배열에 있는 값이 변경될 때만 새 함수를 생성합니다. 이 예제에서는 masterOn 상태가 변경될 때만 toggleMaster 함수가 새로 생성됩니다.
// 최적화 전: 매 렌더링마다 새 함수 생성
const toggleMaster = () => {
  setMasterOn(!masterOn);
};

// 최적화 후: 의존성이 변경될 때만 새 함수 생성
const toggleMaster = useCallback(() => {
  setMasterOn(!masterOn);
}, [masterOn]);

 

 

  • 최적화 전략2: React.memo로 컴포넌트 리렌더링 제어하기
    • useCallback 만으로는 충분하지 않습니다. 부모 컴포넌트가 리렌더링될 때마다 자식 컴포넌트도 기본적으로 리렌더링 되기 때문입니다. 이를 방지하기 위해 React.memo를 사용합니다.
    • React.memo는 컴포넌트의 props가 변경되지 않았다면 리렌더링을 건너뛰게 합니다. 이 덕분에 침실 조명 상태가 변경되더라도 주방과 욕실의 Light 컴포넌트는 리렌더링 되지 않습니다.
const Light = React.memo(({ room, on, toggle }: LightProps) => {
  console.log(`${room} 렌더링됨`);

  return (
    <button onClick={toggle}>
      {room}
      {on ? "💡" : "⬛"}
    </button>
  );
});

// 디버깅을 위한 displayName 추가
Light.displayName = "Light";

 

 

  • 두 기술이 함께 필요한 이유
    1. useCallback 없이 React.memo만 사용하는 경우:
      • 부모 컴포넌트가 리렌더링될 때마다 새로운 toggle 함수가 생성됩니다.
      • 함수 참조가 매번 변경됨으로 모든 Light 컴포넌트가 여전히 리렌더링됩니다.
    2. React.memo 없이 useCallback만 사용하는 경우:
      • 함수 참조는 안정적이지만, 부모 컴포넌트의 리렌더링으로 인해 모든 자식 컴포넌트도 리렌더링됩니다.
    3. useCallback과 React.memo를 함께 사용하는 경우:
      • 함수 참조가 안정적으로 유지됩니다.
      • 변경된 props를 받는 Light 컴포넌트만 리렌더링됩니다.

 

 

3. 결과

 이제 침실 조명을 켜면 console에 "침실 렌더링됨"만 출력되고, 주방과 욕실 컴포넌트는 리렌더링되지 않습니다. 이는 성공적으로 최적화가 작동했다는 증거입니다. 결론적으로 React에서 효율적인 리렌더링 최적화를 위해서는 useCallback과 React.memo를 함께 사용하는 것이 중요하다는 것을 깨달았습니다. 이 두 기술을 이해하고 적절히 활용하면 불필요한 리렌더링을 효과적으로 줄여 애플리케이션의 성능을 향상시킬 수 있습니다!!