2025. 2. 26. 03:40ㆍ개발
스터디 책을 읽다가 평소 관심이 있던 최적화와 관련된 부분이 있어서 정리해보기로 했다.
메모이제이션이란?
계산 결과를 메모리에 저장해 두고 동일한 계산이 요청될 때 다시 계산하지 않고 저장된 값을 반환하는 최적화 기법이다.
리액트에서는 useMemo, useCallback 훅과 고차 컴포넌트인 memo를 활용해 메모이제이션을 하곤한다.
추가: React.memo, useMemo, useCallback
React.memo
import React from 'react';
const ChildComponent = React.memo((props) => {
console.log('ChildComponent rendered');
return <div>Child Value: {props.value}</div>;
});
const ParentComponent = () => {
const [count, setCount] = React.useState(0);
const [value, setValue] = React.useState('Hello');
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<button onClick={() => setValue('World')}>Change Value</button>
<ChildComponent value={value} />
</div>
);
};
export default ParentComponent;
- 자식 컴포넌트를 React.memo로 감싸서 메모이제이션을 적용한다.
- 자식 컴포넌트의 props가 변경되지 않으면 컴포넌트를 다시 렌더링하지 않는다.
- 첫 렌더링시 ChildComponent가 렌더링된다.
- Increment Count 이 눌리면 ChildComponent는 재렌더링 되지 않는다.
- Change Value이 눌리면 ChildComponent는 재렌더링 된다.
React.memo와 areEqual 함수
기본적으로 React.memo 는 prop를 얕은 비교로 비교한다고 한다. 그렇기 때문에 복잡한 객체나 배열을 props로 전달할 때는, 비교 로직을 커스텀 해야만 한다.
const ChildComponent = React.memo((props) => {
console.log('ChildComponent rendered');
return <div>Child Value: {props.value}</div>;
}, (prevProps, nextProps) => {
// 커스텀 비교 로직
return prevProps.value === nextProps.value;
});
useMemo
기본 구조
const memoizedValue = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]);
- 첫번째 인자로 메모이제이션할 계산 함수를 받는다
- 두번째 인자로, 의존성 배열을 받는다, 이 배열의 값이 변경될 때만 첫 번째 인자로 전달된 함수가 다시 실행된다.
ex
import React, { useState } from 'react';
const ExpensiveCalculationComponent = () => {
const [count, setCount] = useState(0);
const [inputValue, setInputValue] = useState('');
// 비싼 계산
const computeExpensiveValue = (num) => {
console.log('Expensive calculation running...');
let total = 0;
for (let i = 0; i < 1000000000; i++) {
total += num;
}
return total;
};
const result = computeExpensiveValue(count);
return (
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<h1>Result: {result}</h1>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
</div>
);
};
export default ExpensiveCalculationComponent;
극단적으로 비싼 계산을 포함하고 있는 함수(computeExpensiveValue)가 있다고 할때
사용자가 Input에 text를 입력할 때마다 ExpensiveCalculationComponent는 리렌더링이 된다.
이때 인풋과는 전혀 관계 없는 computeExpensiveValue도 매번 실행되기 때문에 성능 저하가 발생할 수 있다.
UseMemo로 성능 최적화
import React, { useState, useMemo } from 'react';
const ExpensiveCalculationComponent = () => {
const [count, setCount] = useState(0);
const [inputValue, setInputValue] = useState('');
// 비싼 계산
const computeExpensiveValue = (num) => {
console.log('Expensive calculation running...');
let total = 0;
for (let i = 0; i < 1000000000; i++) {
total += num;
}
return total;
};
// useMemo를 사용하여 count가 변경될 때만 계산
const memoizedResult = useMemo(() => computeExpensiveValue(count), [count]);
return (
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<h1>Result: {memoizedResult}</h1>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
</div>
);
};
export default ExpensiveCalculationComponent;
- 처음 렌더링될 때 computeExpensiveValue가 실행되고, 결과가 기록된다
- 사용자가 텍스트 입력할때는 count 값이 변하지 않기 때문에 계산이 다시 실행되지 않고 이전 결과를 반환한다.
- Increment Count을 눌러 count 값이 변할때만 계산이 다시 실행된다.
useCallback
const memoizedCallback = useCallback(() => {
// 수행할 작업
}, [의존성 배열]);
- 첫 번째 인자로 메모이제이션할 함수를 받는다.
- 두 번째 인자로 의존성 배열을 전달한다.
ex
import React, { useState } from 'react';
const ChildComponent = ({ onClick }) => {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Click Me</button>;
};
const ParentComponent = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('Button clicked');
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<ChildComponent onClick={handleClick} />
</div>
);
};
export default ParentComponent;
- ParentComponent는 상태를 관리하고, 자식 컴포넌트에게 onClick 콜백 함수를 props로 전달한다
- 매번 부모 컴포넌트가 리렌더링될 때, handleClick 함수도 새로 생성된다.
- 자식 컴포넌트는 props로 받은 함수가 변경된 걸로 인식하고 리렌더링한다.
import React, { useState, useCallback } from 'react';
const ChildComponent = ({ onClick }) => {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Click Me</button>;
};
const ParentComponent = () => {
const [count, setCount] = useState(0);
// useCallback으로 함수 메모이제이션
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<ChildComponent onClick={handleClick} />
</div>
);
};
export default ParentComponent;
useCallback을 사용하여 함수가 재생성 되는것을 막았다.
그럼 무조건 하면 되는 거 아니야? 뭐가 문젠데..?
여기서 쟁점이 발생한다.
렌더링이 일어날 것 같은 모든 영역에 모조리 추가할까? 아니면 의존성 배열이 생략된 useEffect를 모든 컴포넌트에 추가해서 실제로 렌더링이 돌아가는지 확인해 보아야 할까? 무거운 연산의 기준이 무엇일까? 와 같은 쟁점이다.
이 책에서는 “메모이제이션은 꼭 필요한 곳에서만 써야한다.” 라는 주장과 “모조리 메모이제이션을 해야한다.” 라는 주장에 대해서 각자의 이유를 들며 비교해보고 있다.
섣부른 최적화는 독이다. 꼭 필요한 곳에만 메모이제이션을 추가하자!
function sum(a, b){
return a + b
}
극단적이긴 하지만 위 같은 연산들은 너무 간단하다.
이러한 연산들은 메모이제이션해서 메모리 어딘가에서 꺼내오는 것보다 매번 이 작업을 수행해 결과를 반환하는 것이 빠를 수 있다.
다른 많은 연산들에도 비용이 들듯이, 메모이제이션 과정에도 꺼내오기, 저장하기, 값을 비교해 랜더링이 필요한지 확인하는 작업과 같은 비용이 든다.
따라서 메모이제이션은 어느 정도의 트레이드 오프가 항상 존재한다.
그리고 또한 리액트 공식문서에는
useMemo는 성능 최적화를 위해 사용할 수 있지만 의미상으로 그것이 보장된다고 생각하지마세요
라는 내용이 나와있다고 한다.
리액트는 가능한 오랫동안 캐시 결과를 저장하려고 하겠지만, 어느 순간 캐시가 무효화되는 경우도 있을 것이다.
그러므로 메모이제이션을 활용한 최적화는 신중을 가해야한다.
요약 → 메모이제이션이 간단한 연산보다 더 많은 비용을 지불해야하는 경우도 있다!
렌더링 과정의 비용은 비싸다. 모조리 메모이제이션해 버리자!
실무에 임하는 모든 개발자들은 최적화에 쏟을 시간이 넘쳐나지 않다는 사실이다.
전 의견은 메모이제이션 비용이 하지 않았을 때의 비용보다 비싸질 때를 고려하여 주장한 의견이다.
잘못된 메모로 지불해야 하는 비용은 바로 props에 대한 얕은 비교가 발생하면서 지불해야 하는 비용이다.
하지만 메모를 하지 않았을 때는
- 렌더링을 함으로써 발생하는 비용
- 컴포넌트 내부의 복잡한 로직의 재실행
- 그리고 위 두 가지 모두가 모든 자식 컴포넌트에서 반복해서 일어남
- 리액트가 구트리와 신규 트리를 비교
할때 비용을 지불해야한다.
또한 useMemo와 useCallback을 사용하면 값이 변경되지 않는 한 같은 결과물을 가질 수 있고, 그 덕분에 참조의 투명성을 유지할 수 있게 된다.
메모이제이션을 하지 않았을 때보다, 메모이제이션했을 때 더 많은 이점을 누릴 수 있다. 이러한 이점들이 실수로 빠뜨렸을 때 치러야 할 위험비용이 더 크기때문에 전문가가 아니라면 가능한 모든 곳에 메모이제이션을 활용하는 것이 좋다!
그래서 어떡하라고?
이 책의 저자는 리액트를 공부하고 있거나 깊은 이해를 하고 싶다면, 어느 지점에서 성능상 이점을 누릴 수 있는지 분석하고 메모이제이션을 활용하는 것을 권장하고
현업에서 리액트를 활용하고 있거나 혹은 성능에 대해 깊게 연구해 볼 시간적 여유가 없는 상황이라면 의심스러운 곳에 먼저 다 적용해 볼 것을 권장한다.
하지만 나는 아직 리액트를 배우고 있고, 실제 현업에서 리액트를 쓰기까지 시간적 여유가 많이 남았기 때문에 전자의 의견을 따를 거 같다.

'개발' 카테고리의 다른 글
| [React] 리액트 개발 도구를 활용해 좋은 웹 어플리케이션 만들기 (0) | 2025.02.26 |
|---|---|
| [React] SSR은 뭘까..? (0) | 2025.02.26 |
| [React] useThrottle과 useDebounce에 대해서 알아보자 (0) | 2025.02.26 |
| 깃허브 페이지 커스텀 도메인 연결하기 (0) | 2025.02.01 |
| 정규표현식 (1) | 2024.03.26 |