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]);
이 부분이 핵심 로직입니다:
- useMemo는 첫 번째 인자로 함수를, 두 번째 인자로 의존성 배열을 받습니다.
- 컴포넌트가 리렌더링 될 때, 의존성 배열 내의 값이 변경되지 않으면 함수를 다시 실행하지 않고 이전에 계산된 결과를 재사용합니다.
< 구체적인 동작 >
- 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";
- 두 기술이 함께 필요한 이유
- useCallback 없이 React.memo만 사용하는 경우:
- 부모 컴포넌트가 리렌더링될 때마다 새로운 toggle 함수가 생성됩니다.
- 함수 참조가 매번 변경됨으로 모든 Light 컴포넌트가 여전히 리렌더링됩니다.
- React.memo 없이 useCallback만 사용하는 경우:
- 함수 참조는 안정적이지만, 부모 컴포넌트의 리렌더링으로 인해 모든 자식 컴포넌트도 리렌더링됩니다.
- useCallback과 React.memo를 함께 사용하는 경우:
- 함수 참조가 안정적으로 유지됩니다.
- 변경된 props를 받는 Light 컴포넌트만 리렌더링됩니다.
- useCallback 없이 React.memo만 사용하는 경우:
3. 결과
이제 침실 조명을 켜면 console에 "침실 렌더링됨"만 출력되고, 주방과 욕실 컴포넌트는 리렌더링되지 않습니다. 이는 성공적으로 최적화가 작동했다는 증거입니다. 결론적으로 React에서 효율적인 리렌더링 최적화를 위해서는 useCallback과 React.memo를 함께 사용하는 것이 중요하다는 것을 깨달았습니다. 이 두 기술을 이해하고 적절히 활용하면 불필요한 리렌더링을 효과적으로 줄여 애플리케이션의 성능을 향상시킬 수 있습니다!!
'인턴' 카테고리의 다른 글
Next.js 프로젝트의 효율적인 폴더 구조 개선하기 📂 (0) | 2025.03.26 |
---|---|
React와 styled-components로 만드는 커스텀 토글 버튼 (0) | 2025.03.25 |
Jotai (0) | 2025.03.25 |
[ Troubleshooting🛠️ ] GNB 자동 스크롤과 페이지 이동 기능 통합 (0) | 2025.03.25 |
[ Troubleshooting🛠️ ] Map 객체와 배열 간 타입 불일치 (0) | 2025.03.25 |