
✨ 성능 최적화를 해야하는 이유
React 앱을 만들다보면 성능이 중요한 문제가 될 때가 있다.
리액트에서는 컴포넌트의 상태가 변경될 때마다 렌더링 과정을 거치게 되는데, 이 과정 중에 변화가 없는 컴포넌트까지 불필요하게 렌더링되는 경우가 존재한다. 그리고 복잡한 계산이나 데이터 처리는 애플리케이션의 반응 속도를 늦출 수 있어 사용자 경험을 저해할 수 있다. 따라서 이를 위해서 useMemo, useCallback, React.memo라는 기능을 제공하고 있다. 세 가지가 어떻게 다른지 어떤 방식으로 쓰이는지 알아보자!
📝 useMemo
useMemo는 리액트에서 컴포넌트의 성능을 최적화하는데 사용되는 훅이다. 여기서 memo는 memoization을 뜻하는데 "메모리에 넣기"라는 의미이며 컴퓨터가 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다.
쉽게 말하면 동일한 입력이 들어왔을때 값을 재사용함으로써 불필요한 재계산을 방지하고 성능을 개선한다.
const memo = useMemo(calculateValue, dependencies);
- calculateValue : 캐싱하려는 값을 계산하는 함수가 들어간다. React는 초기 렌더링 중에서 함수를 호출한 결과를 반환하는데 다음 렌더링에서 dependencies가 변경되지 않았다면 동일한 값을 다시 반환한다. 변경이 되었다면 calculateValue를 호출하고 결과를 반환하며, 나중에 재사용할 수 있도록 저장한다.
- dependencies : calculateValue 코드 내에서 참조된 모든 반응형 값들의 목록이다. 만약 빈 배열을 넘겨준다면 맨 처음 컴포넌트가 마운트 되었을 때만 값을 계산하고 이후에는 항상 memoization된 값을 꺼내 와서 사용한다.
export default function TodoList({ todos, tab, theme }) {
// 재렌더링 사이에 계산을 캐싱하도록 지시
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab] // 의존성이 변경될 때만 다시 계산
);
return (
<div className={theme}>
{/* List 컴포넌트에 동일한 props가 전달되면 재렌더링을 막을 수 있다 */}
<List items={visibleTodos} />
</div>
);
}
useMemo를 사용하여서 의존성 배열안에 있는 todos랑 tab이 바뀔때만 다시 계산하고, 값이 변하지 않았다면 visibleTodos는 캐시된 값을 그대로 반환한다.
또한 List 컴포넌트는 item props가 바뀌면 리렌더링되는데, useMemo 덕분에 visibleTodos가 불필요하게 새로 생성되지 않으므로 List도 불필요한 리렌더링을 피할 수 있다.
📚 useCallback
useCallback은 함수를 메모이제이션하여 컴포넌트의 불필요한 리렌더링을 방지하는 역할을 한다. 이 훅은 함수를 props로 하위 컴포넌트에 전달할 때 유용하게 사용된다. useCallback은 함수를 메모리에 저장하고 의존성 배열에 명시된 변수들의 값이 변경될 때만 함수를 새로 생성한다. 이를 통해서 컴포넌트가 재렌더링되더라도 동일한 함수 참조를 유지할 수 있어서 성능을 개선할 수 있다.
const cachedFn = useCallback(fn, dependencies)
- fn : 메모이제이션하려는 함수가 들어가고 의존성 배열의 값이 바뀌지 않는 한 동일한 함수 참조를 유지한다.
- dependencies : calculateValue 코드 내에서 참조된 모든 반응형 값들의 목록이다. 만약 빈 배열을 넘겨준다면 컴포넌트가 처음 마운트 될 때 한번만 함수를 생성하고, 이후에는 항상 동일한 함수 참조를 반환한다. 즉, 의존성 배열에 있는 값이 변경될 때만 새로운 함수가 생성되고 그렇지 않다면 이전에 메모이제이션된 함수를 재사용하게 된다.
function Example({ onClick }) {
const handleClick = useCallback(() => {
console.log('button clicked');
onClick();
}, [onClick]);
return (
<button onClick={handleClick}>Click me</button>
);
}
useCallback으로 handleClick을 감싸 의존성 배열 안에 담긴 onClick이 변하지 않는 한 동일한 함수 참조를 재사용하고 있다.
따라서 불필요한 리렌더링을 방지하여 성능 최적화를 하고 있다.
📒 React.memo
React.memo는 컴포넌트의 불필요한 렌더링을 방지해 성능을 향상시키는 고차 컴포넌트이다. React에서 컴포넌트가 리렌더링될 때, 해당 컴포넌트의 props나 state가 변경되면 기본적으로 컴포넌트를 다시 렌더링한다. 그러나 때때로 컴포넌트의 props가 변경되더라도 컴포넌트 내부에서 처리해야 할 로직에 영향을 주지않는 경우가 있는데 이럴때 React.memo를 사용해서 불필요한 렌더링을 방지할 수 있다.
import React, { useState } from 'react';
// React.memo를 사용하여 리렌더링 최적화
const ChildComponent = React.memo(({ name }) => {
console.log('ChildComponent rendered');
return <div>Hello, {name}!</div>;
});
const ParentComponent = () => {
const [name, setName] = useState('Alice');
const [count, setCount] = useState(0);
return (
<div>
<ChildComponent name={name} />
<button onClick={() => setCount(count + 1)}>
Increase Count
</button>
</div>
);
};
export default ParentComponent;
React.memo로 감싸져 있는 ChildComponent는 name이 바뀌지 않으면 리렌더링을 생략한다. ParentComponent에서 count가 변경되었더라도 name값은 그대로라면 ChildComponent는 다시 리렌더링되지 않는다. 이로인해 불필요한 재렌더링을 방지하고 애플리케이션의 성능을 향상시킬 수 있다.
⚠️ React.memo를 사용할 때 주의할 점
React.memo는 props를 얕은 비교로만 판별한다. 즉 원시값 (number, string, boolean)은 값이 같으면 동일하다고 보지만 객체/배열/함수 같은 참조형 값인 경우에는 내용이 같아도 참조가 달라지면 "props가 변경되었구나" 라고 판단한다. 그래서 부모가 렌더링될 때마다 새로 생성되는 객체/배열/함수를 props로 넘기면 React.memo로 감싸도 자식이 매번 리렌더링이 된다.
// 잘못된 사용 예시
function ParentComponent() {
return (
<MemoizedComponent
user={{ name: 'John' }} // 매번 새로운 메모리 주소가 할당됨
/>
);
}
// 올바른 사용 예시
function ParentComponent() {
// useMemo를 사용하여 동일한 메모리 주소 유지
const user = useMemo(() => ({ name: 'John' }), []);
return (
<MemoizedComponent
user={user} // 항상 같은 메모리 주소를 참조
/>
);
}
따라서 객체나 배열 타입의 props인 경우에는 useMemo나 useCallback으로 메모이제이션을 해야 똑같은 참조값을 유지할 수 있다.
⁉️ 언제 사용하는 것이 좋을까?
useMemo
- 계산 비용이 높은 함수의 결과를 재사용할 때
- 렌더링 과정에서 동일한 결과를 여러번 재사용해야 하는 경우
- 참조 동일성을 유지해야 하는 복잡한 객체를 다룰 때
useCallback
- 이벤트 핸들러 함수를 자식 컴포넌트에 props로 전달할 때
- 렌더링 최적화가 필요한 대규모 리스트나 테이블에서 항목의 이벤트 처리
- 빈번하게 업데이트되는 상태나 props에 의존하는 함수에서
React.memo
- 자주 업데이트되지 않는 props를 가진 컴포넌트
- 부모 컴포넌트의 상태 변경에 따라 불필요하게 렌더링되는 경우
- 렌더링 최적화가 필요한 대규모 리스트나 테이블
🙋🏻♀️ 항상 메모이제이션 하는 것이 좋을까?
useCallback과 useMemo는 성능 최적화의 목적으로 사용되긴 하지만, 무분별하게 사용할 경우 오히려 성능 저하를 초래할 수 있다.
굉장히 비싼 연산이나 새로운 함수가 반복적으로 생성된다면 위의 훅을 사용했을 때 확실하게 성능을 끌어올릴 수 있겠지만 메모이제이션 자체에도 비용이 있기 때문에, 렌더링 비용이 큰 경우에만 쓰는 것이 좋다.
✅ 참고문헌
- 리액트 성능 최적화: useMemo, React.memo, useCallback 활용하기
'React' 카테고리의 다른 글
| [React] Debounce로 실시간 검색 기능 최적화하기 (+throttle) (2) | 2025.07.23 |
|---|---|
| [React] Lazyloading으로 Code Splitting하기 (1) | 2025.07.11 |
| [React] useCallback이란? (0) | 2024.01.22 |
| [React] useMemo란? (2) | 2024.01.18 |
| [React] useReducer란? (0) | 2024.01.17 |