
웹 서비스의 성능을 개선하는 모든 행위로, 아주 단순한 것부터 아주 어려운 방법까지 매우 다양합니다.
일반적으로 서버의 응답속도 개선, 이미지 / 폰트 / 코드 파일 등의 정적 파일 로딩 개선, 불필요한 네트워크 요청 줄임 등의 최적화가 있을 수 있습니다.
React 내부에서는 컴포넌트 내부의 불필요한 연산, 함수 재생성, 리렌더링 방지 등이 최적화의 주요 대상입니다.
이번 글에서는 React 내부에서 가능한 대표적인 최적화 기법들을 하나씩 살펴보겠습니다.
useMemo는 메모이제이션(Memoization) 기법을 기반으로, 값의 재계산을 방지하여 성능을 최적화하는 훅입니다.
useMemo(() => {
return value;
}, [item]);
const App = () => {
const { totalCount, doneCount, yetCount } = useMemo(() => {
const totalCount = todos.length;
const doneCount = todos.filter((todo) => todo.isDone).length;
const yetCount = totalCount - doneCount;
return { totalCount, doneCount, yetCount };
}, [todos]);
return (
<div>
<div>total: {totalCount}</div>
<div>done: {doneCount}</div>
<div>yet: {yetCount}</div>
</div>
);
};
기존의 filter는 검색을 하거나 다른 버튼이 눌릴 때도 다시 계산되었겠지만, useMemo로 감쌈에 따라todos가 변경될 때만 연산을 다시 수행하므로, 매 렌더링마다 filter 연산이 반복되는 것을 방지합니다.
모든 연산에 useMemo를 사용하는 것은 좋지 않습니다. 다음과 같은 기준으로 구분합니다.
✅ 사용 권장:
filter, sort, map하는 경우❌ 지양:
useMemo(() => ({name, age}), [name, age])) 등 비교 비용이 더 클 때💡
useRef와의 차이점:useRef는 변경되지 않는 값을 저장하기 위한 용도이고,useMemo는 특정 값의 계산 결과를 캐싱하는 용도입니다.
React.memo는 컴포넌트를 인수로 받아, props가 변경되지 않으면 재렌더링을 방지하는 고차 컴포넌트(HOC)입니다.
const MemoizedComponent = memo(Component);
// 또는
export default memo(Component);
예를 들어, 입력창을 수정할 때 프로필, 시계 등 관련 없는 컴포넌트가 함께 리렌더링된다면 이는 불필요한 낭비입니다. React.memo는 이런 문제를 해결합니다.
import { memo } from 'react';
const Profile = ({ user }) => {
console.log('Profile 렌더링');
return <div>{user.name}</div>;
};
export default memo(Profile);
부모 컴포넌트가 리렌더링되더라도, user prop이 바뀌지 않으면 Profile은 다시 렌더링되지 않습니다.
React.memo는 얕은 비교(shallow compare)를 수행합니다. 객체 내부 속성까지 비교하려면 두 번째 인자로 커스터마이즈 함수를 전달할 수 있습니다.
export default memo(Component, (prev, next) => {
return (
prev.id === next.id &&
prev.content === next.content &&
prev.isDone === next.isDone
);
});
React.memo는 props가 자주 바뀌는 경우, 비교 자체가 오히려 오버헤드가 될 수 있습니다.
💡 피해야 할 경우:
또한 부모로부터 onClick={handleClick}과 같은 함수를 props로 받을 때, 부모 리렌더링 시 함수 참조가 새로 생성되어 memo 비교가 무의미해집니다. 이를 해결하기 위해 useCallback을 함께 사용합니다.
useCallback은 함수 자체를 메모이제이션하는 훅으로, 매 렌더링마다 새 함수가 생성되는 문제를 방지합니다.
const onUpdate = useCallback((targetId) => {
dispatch({ type: 'UPDATE', targetId });
}, []);
React.memo로 감싼 자식 컴포넌트에 함수를 props로 전달할 때 useCallback을 사용하면, props가 변경되지 않았음을 보장할 수 있습니다.
function Counter() {
const [count, setCount] = useState(0);
const handleAlert = useCallback(() => {
alert('현재 카운트: ' + count);
}, []); // ❌ count가 빠져서 항상 0만 출력
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
<button onClick={handleAlert}>카운트 확인</button>
</div>
);
}
이처럼 의존성 배열을 잘못 관리하면, 함수가 과거의 상태를 영원히 기억하는 버그가 발생합니다. 항상 훅 내부에서 사용하는 외부 변수는 의존성 배열에 정직하게 포함시켜야 합니다.
eslint-plugin-react-hooks를 사용하면 이러한 실수를 자동으로 탐지할 수 있습니다.
| 상황 | 적합한 훅 / 기능 | 설명 |
|---|---|---|
| 값 계산이 무거움 | useMemo | 복잡한 연산의 결과값을 캐싱 |
| 함수가 자주 재생성됨 | useCallback | 동일한 함수를 메모이제이션 |
| 불필요한 리렌더링 발생 | React.memo | props가 바뀌지 않으면 재렌더링 방지 |
기능 구현 완료 후 최적화 고려
React DevTools Profiler로 실제 병목 구간 확인
다음 순서로 적용:
React.memouseCallbackuseMemo성능 최적화는 감(感)이 아니라 측정을 기반으로 해야 합니다. Profiler를 통해 실제로 렌더링이 지연되는 컴포넌트를 찾아 필요한 곳에만 정확히 적용하세요.
📚 참고 자료:
한 입 크기로 잘라먹는 리액트 (Inflearn)
React 공식 문서 - Performance Optimization