1. useMemo와 useCallback이란?
useMemo와 useCallback은 성능 최적화를 위한 훅이다.
두 훅 모두 메모이제이션(Memoization)을 활용해 불필요한 계산이나 렌더링을 방지한다.
메모이제이션이란 이전에 계산한 값을 저장해두고 동일한 입력이 들어오면 재계산 없이 저장된 값을 반환하는 기법이다.
React 컴포넌트는 상태나 props가 변경될 때마다 리렌더링되는데, 이때 모든 함수와 값도 재생성된다.
useMemo와 useCallback은 특정 값이나 함수를 의존성 배열의 값이 변경될 때만 재생성하도록 최적화해준다.
1.1. useMemo vs useCallback
useMemo는 계산된 값을 메모이제이션한다.
useCallback은 함수 자체를 메모이제이션한다.
기본적인 목적은 같지만 메모이제이션하는 대상이 다르다.
useMemo는 복잡한 계산 결과를, useCallback은 함수 정의 자체를 캐싱한다.
1.2. 필요한 상황
React에서 컴포넌트가 리렌더링될 때마다 컴포넌트 내부의 모든 변수와 함수가 다시 생성된다.
대부분의 경우 이는 문제가 되지 않지만, 작업을 하다보면 성능 이슈가 발생하는 상황이 생긴다.
1) 복잡한 계산을 수행하는 함수
2) 큰 배열이나 객체를 처리하는 로직
3) 자식 컴포넌트에 props로 함수를 전달하는 경우
이런 상황에서 useMemo와 useCallback을 사용하면 불필요한 연산이나 렌더링을 줄일 수 있다.
2. useMemo
2.1. 기본 문법
const memoizedValue = useMemo(() => computeValue(a, b), [a, b]);
첫 번째 인자는 계산 로직을 담은 함수이고, 두 번째 인자는 의존성 배열이다.
의존성 배열의 값이 변경될 때만 첫 번째 인자의 함수가 실행되고, 그 결과가 메모이제이션된다.
의존성 배열이 비어있으면 컴포넌트가 마운트될 때 한 번만 계산된다.
2.2. 필요한 상황
1) 계산 비용이 큰 연산을 수행할 때
// 비용이 큰 계산
const sortedData = useMemo(() => {
return largeArray.sort((a, b) => a.value - b.value);
}, [largeArray]);
2) 참조 동등성이 중요한 경우 (특히 객체나 배열)
// 객체 참조가 유지되어야 하는 경우
const memoizedObject = useMemo(() => {
return { id: user.id, name: user.name };
}, [user.id, user.name]);
3) React.memo와 함께 사용할 때
// MemoizedComponent는 props가 변경될 때만 리렌더링됨
const MemoizedComponent = React.memo(({ data }) => {
// 컴포넌트 내용
});
function Parent() {
// data 객체의 참조가 유지되어 불필요한 리렌더링 방지
const data = useMemo(() => ({ value: 42 }), []);
return <MemoizedComponent data={data} />;
}
3. useCallback 사용법
3.1. 기본 문법
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b]);
첫 번째 인자는 메모이제이션할 함수이고, 두 번째 인자는 의존성 배열이다.
의존성 배열의 값이 변경될 때만 새로운 함수가 생성되고, 그렇지 않으면 이전에 메모이제이션된 함수를 재사용한다.
3.2. 필요한 상황
1) React.memo를 사용한 자식 컴포넌트에 함수를 props로 전달할 때
const MemoizedChild = React.memo(({ onClick }) => {
// 컴포넌트 내용
});
function Parent() {
// 함수 참조 유지로 MemoizedChild의 불필요한 리렌더링 방지
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);
return <MemoizedChild onClick={handleClick} />;
}
2) useEffect의 의존성 배열에 함수를 포함해야 할 때
// 함수가 의존성 배열에 포함될 때 무한 루프 방지
const fetchData = useCallback(() => {
// API 호출 로직
}, [userId]);
useEffect(() => {
fetchData();
}, [fetchData]); // fetchData가 변경될 때만 effect 실행
3) 이벤트 핸들러가 자주 리렌더링되는 컴포넌트 내에 있을 때
function SearchComponent() {
// 검색 로직이 복잡하거나 리렌더링이 자주 발생하는 경우
const handleSearch = useCallback((query) => {
// 검색 로직
}, [dependencies]);
// 컴포넌트 내용
}
4. React.memo
4.1. React.memo란?
React.memo는 함수형 컴포넌트를 메모이제이션하는 고차 컴포넌트(HOC)이다.
컴포넌트의 props가 변경되지 않으면 리렌더링을 방지하여 성능을 최적화한다.
4.2. 기본 문법
const MemoizedComponent = React.memo(MyComponent);
기본적으로 React.memo는 얕은 비교(shallow comparison)를 통해 모든 props를 비교한다.
커스텀 비교 함수를 제공하여 더 세밀한 제어도 가능하다:
const MemoizedComponent = React.memo(MyComponent, (prevProps, nextProps) => {
// true를 반환하면 리렌더링을 건너뛰고, false를 반환하면 리렌더링을 수행
return prevProps.id === nextProps.id;
});
4.3. 필요한 상황
1) 객체와 함수 props
객체나 함수를 props로 전달할 때 주의가 필요하다.
매 렌더링마다 새로운 객체나 함수 레퍼런스가 생성되면 React.memo의 효과가 사라진다.
// 좋지 않은 예: 매 렌더링마다 새 객체 생성
<MemoizedComponent data={{ id: 1, name: 'John' }} />
// 좋은 예: useMemo로 객체 메모이제이션
const data = useMemo(() => ({ id: 1, name: 'John' }), []);
<MemoizedComponent data={data} />
// 좋지 않은 예: 매 렌더링마다 새 함수 생성
<MemoizedComponent onClick={() => console.log('Clicked')} />
// 좋은 예: useCallback으로 함수 메모이제이션
const handleClick = useCallback(() => console.log('Clicked'), []);
<MemoizedComponent onClick={handleClick} />
2) 과도한 사용 방지
모든 컴포넌트를 React.memo로 감싸는 것은 불필요하다.
단순하거나 자주 리렌더링이 필요한 컴포넌트에 사용하면 오히려 성능이 저하될 수 있다.
3) 컴포넌트의 역할과 책임 고려
자주 업데이트되는 데이터를 사용하는 컴포넌트는 React.memo의 효과가 제한적이다.
컴포넌트를 더 작은 단위로 분리하여 변경이 필요한 부분만 리렌더링되도록 설계하는 것이 효과적일 수 있다.
4. 최적화 고려사항
1) 무분별한 사용
useMemo와 useCallback도 자체적인 오버헤드가 있다.
메모리 사용량이 증가하고, 의존성 비교에도 비용이 든다.
단순한 연산이나 함수에 무분별하게 사용하면 오히려 성능이 저하될 수 있다.
// 불필요한 메모이제이션 (단순 계산)
const double = useMemo(() => number * 2, [number]); // 오버헤드 > 이득
// 간단한 함수에 불필요한 useCallback
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]); // 오버헤드 > 이득
2) 의존성 배열 관리
의존성 배열을 잘못 설정하면 예상치 못한 버그가 발생할 수 있다.
누락된 의존성은 오래된 값(stale values)을 참조하는 문제를 일으킨다.
불필요한 의존성은 메모이제이션의 효과를 감소시킨다.
// 잘못된 의존성 배열 (count가 누락됨)
const calculateTotal = useCallback(() => {
return items.reduce((sum, item) => sum + item.price * count, 0);
}, [items]); // 버그: count가 변경되어도 함수가 업데이트되지 않음
// 올바른 의존성 배열
const calculateTotal = useCallback(() => {
return items.reduce((sum, item) => sum + item.price * count, 0);
}, [items, count]);
'학습 > React' 카테고리의 다른 글
Million.js (0) | 2025.04.23 |
---|---|
React key (0) | 2025.04.23 |
리액트 기초 - 강의 정리 3 : Prop Drilling, useEffect (0) | 2025.04.04 |
use 훅 (0) | 2025.04.02 |
useImperativeHandle (0) | 2025.03.23 |