[React] 성능 잡는 Hook! useMemo, useCallback, 그리고 React.memo 정리 끝판왕
React.memo
: 컴포넌트 자체를 메모이제이션하여 props가 바뀌지 않으면 다시 렌더링하지 않도록 한다.
1) 사용하는 경우
- 부모 컴포넌트가 자주 렌더링되는데, 자식 컴포넌트의 props가 변하지 않는 경우
- 불필요한 리렌더링 방지 목적
2) 예시
: React.memo를 사용해 부모 컴포넌트가 리렌더링되더라도 자식 컴포넌트의 props가 변하지 않으면 자식 컴포넌트의 렌더링을 생략하여 성능을 최적화한 예제
// Child.tsx
import React from 'react';
const Child = ({ value }: { value: number }) => {
console.log('🔄 자식 컴포넌트 렌더링');
return <div>값: {value}</div>;
};
// memo 적용이며, 이렇게 하면 다른 파일에서 import할 때 자동으로 memo된 최적화 버전이 import됨
export default React.memo(Child);
// Parent.tsx
import React, { useState } from 'react';
import Child from './Child';
export default function Parent() {
const [count, setCount] = useState(0);
const [value, setValue] = useState(100);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>카운트 증가</button>
<Child value={value} />
</>
);
}
(1) 최적화 포인트
- count가 바뀔 때마다 부모가 리렌더링되지만,
- value는 변하지 않기 때문에 Child는 React.memo로 렌더링을 막을 수 있다.
3) 결론
: 부모는 변했지만 자식한테 전달된 값은 그대로니까, 굳이 다시 그릴 필요가 없다
→ 그래서 React가 렌더링을 "스킵"해주는 거예요.
useMemo
: 값(value)을 메모이제이션한다. 즉, 특정 연산의 결과를 캐싱해서 의존값이 바뀌지 않으면 연산을 다시 하지 않는다.
0) 사용 방법
const memoizedValue = useMemo(() => 계산식, [의존성배열]);
- 계산식: 복잡하거나 비용이 많이 드는 연산
- 의존성 배열: 값이 바뀌면 다시 계산 / 안 바뀌면 이전 결과 재사용
1) 사용하는 경우
- 렌더링 시 비용이 큰 연산(heavy computation)이 수행될 경우
- 연산 결과가 동일한데도 매번 계산하는 불필요한 연산을 방지하고 싶을 때
- 복잡한 계산이 많은 컴포넌트에서 불필요한 성능 낭비 방지
2) 예시
: useMemo를 사용해 input값이 바뀔 때만 무거운 계산을 실행하고, 그렇지 않으면 이전 계산 결과를 재사용하여 렌더링 성능을 최적화한 예제
// Calculator.js
import React, { useState, useMemo } from 'react';
function heavyCalculation(num) {
console.log('💥 계산 중...');
let result = 0;
for (let i = 0; i < 100000000; i++) {
result += i % num;
}
return result;
}
export default function Calculator() {
const [count, setCount] = useState(0);
const [input, setInput] = useState(5);
// input이 바뀔 때만 계산이 다시 일어남
const calculated = useMemo(() => heavyCalculation(input), [input]);
return (
<>
<input
type="number"
value={input}
onChange={(e) => setInput(Number(e.target.value))}
/>
<p>계산 결과: {calculated}</p>
<button onClick={() => setCount((c) => c + 1)}>카운트 증가</button>
</>
);
}
(1) 최적화 포인트
- count는 계산과는 무관한 상태이지만, 값이 바뀌면 Calculator 컴포넌트가 리렌더링됨
- useMemo를 쓰지 않으면 heavyCalculation(input)이 매번 다시 실행됨 → 비효율 발생
- useMemo를 쓰면 input이 바뀌지 않으면 이전 계산 결과를 재사용
3) 결론
: 계산 결과가 이전과 같다면 굳이 다시 계산할 필요가 없다
→ 그래서 useMemo가 계산 결과를 기억하고 있다가 그대로 써주는 것이다.
→ 불필요한 계산 로드를 줄이고, 렌더링 시 퍼포먼스를 지키는 전략이다.
useCallback
: 함수(function)를 메모이제이션한다. 매번 새로운 함수 객체가 생성되는 걸 막고, 같은 참조를 유지함.
0) 사용 방법
const memoizedCallback = useCallback(() => {
// 실행할 함수 내용
}, [의존성배열]);
1) 사용하는 경우
- 자식 컴포넌트에 함수를 props로 전달할 때
- 부모 컴포넌트가 리렌더링되면서 함수 객체가 매번 새로 만들어지는 현상을 막고 싶을 때
- React.memo와 함께 사용하면 props 비교 시 참조값이 유지되기 때문에 리렌더링 방지에 효과적
2) 예시
: useCallback을 사용하여 함수의 참조값을 고정함으로써, 부모 컴포넌트가 리렌더링되어도React.memo로 감싼 자식 컴포넌트의 불필요한 렌더링을 방지하는 예시
// Child.js
import React from 'react';
function Child({ onClick }) {
console.log('🧒 자식 렌더링');
return <button onClick={onClick}>자식 버튼</button>;
}
export default React.memo(Child); // memo 적용
// Parent.js
import React, { useState, useCallback } from 'react';
import Child from './Child';
export default function Parent() {
const [count, setCount] = useState(0);
// 매 렌더링마다 새로 만들어지는 함수 (비효율)
// const handleClick = () => console.log('클릭됨');
// useCallback 사용 → 참조 유지됨
const handleClick = useCallback(() => {
console.log('클릭됨');
}, []);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>카운트 증가</button>
<Child onClick={handleClick} />
</>
);
}
(1) 최적화 포인트
- Child 컴포넌트는 React.memo로 감싸져 있지만
- 부모가 리렌더링될 때마다 handleClick 함수가 새로운 객체로 생성되면, 자식도 리렌더링된다.
- useCallback을 사용하면 함수의 참조값이 유지되어 Child는 렌더링을 건너뛴다.
3) 결론
: 함수 로직은 똑같은데, 매번 새로운 객체로 만들어지면 자식 컴포넌트 입장에서는 “다른 함수”로 인식한다.
→ 그래서 useCallback을 쓰면 같은 참조를 유지해서
React.memo와 함께 진짜로 바뀐 props만 감지하도록 도와주는 것이다.