오늘은 react 개발 시 메모이제이션을 이용해서 성능을 최적화하는 것에 대한 내용을 좀 다뤄보고자 합니다.
0. 그전에, 메모이제이션이 뭐임?
메모이제이션은 컴퓨터 프로그래밍에서 사용되는 최적화 기법 중 하나입니다. 핵심 아이디어는 간단한데요.
"한 번 계산했던 결과를 메모리(캐시)에 저장해 두었다가, 다음에 동일한 입력이 들어오면 다시 계산하지 않고 저장된 결과를 즉시 반환하는 것" 입니다.
이는 특히 계산 비용이 비싼 함수나 불필요하게 자주 실행되는 로직에서 빛을 발합니다. 동일한 입력에 대해 항상 동일한 출력을 보장하는 순수 함수(Pure Function)에 특히 효과적이죠.
React에서는 컴포넌트의 렌더링 과정에서 발생하는 불필요한 재연산을 줄이는 데 이 메모이제이션 기법을 적극적으로 활용합니다이제 이 메모이제이션 원리가 React의 세 가지 핵심 도구(useMemo, React.memo, useCallback)에서 어떻게 다르게 적용되는지 살펴보겠습니다.
1. useMemo: "값"을 메모이징 할 때
useMemo는 계산 비용이 높은 값(Value)을 메모이징하는 훅입니다. 특정 연산의 결과값을 기억해두었다가, 해당 연산에 사용된 의존성(dependencies)이 변경되지 않았다면 이전에 기억해둔 값을 재사용합니다. 즉, 불필요한 재계산을 방지하여 렌더링 성능을 향상시킵니다.
어떤 것을 메모이징 하는가?
- 함수의 반환 값 (복잡한 계산 결과, 큰 객체/배열 생성 등)
언제 사용하는가?
- 복잡한 계산 결과를 캐싱하여 재계산을 피하고 싶을 때
- 렌더링 성능에 영향을 미칠 수 있는 큰 객체나 배열을 생성할 때
- 자식 컴포넌트에 prop으로 전달되는 객체나 배열 참조의 불필요한 변경을 막아 자식 컴포넌트의 불필요한 리렌더링을 방지할 때 (React.memo와 함께 사용될 때 강력)
import React, { useMemo, useState } from 'react';
function ProductList({ products, filter }) {
const [query, setQuery] = useState('');
// filter가 변경되거나, products가 변경되지 않는 한 이 계산은 다시 실행되지 않음
const filteredProducts = useMemo(() => {
console.log('필터링 중...'); // filter 또는 products가 변경될 때만 호출
return products.filter(product =>
product.name.includes(filter) && product.name.includes(query)
);
}, [products, filter, query]); // 의존성 배열
return (
<div>
<input type="text" value={query} onChange={e => setQuery(e.target.value)} placeholder="상품 검색" />
<h2>상품 목록</h2>
{filteredProducts.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}
위 예시에서 filteredProducts는 products, filter, query 중 하나라도 변경될 때만 다시 계산됩니다. 그렇지 않으면 이전 계산 결과를 재사용합니다.
2. useCallback: "함수"를 메모이징 할 때
useCallback은 함수 자체를 메모이징하는 훅입니다. React 컴포넌트가 리렌더링될 때마다 함수는 새로 생성되는데, useCallback은 이 함수가 의존성에 변화가 없을 때 재사용될 수 있도록 함수 정의 자체를 기억합니다. 이는 특히 자식 컴포넌트에 콜백 함수를 props로 전달할 때 유용합니다.
어떤 것을 메모이징 하는가?
- 함수 정의 자체
언제 사용하는가?
- 자식 컴포넌트(특히 React.memo로 최적화된 자식 컴포넌트)에 콜백 함수를 props로 전달할 때
- useEffect나 useMemo의 의존성 배열에 함수가 포함될 때, 불필요한 이펙트/메모이제이션 재실행을 방지할 때
import React, { useState, useCallback } from 'react';
// React.memo로 최적화된 자식 컴포넌트
const Button = React.memo(({ onClick, label }) => {
console.log(`${label} 버튼 렌더링`);
return <button onClick={onClick}>{label}</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// count가 변경될 때만 이 함수가 다시 생성됨
const increment = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []); // 의존성 배열이 비어있으므로 컴포넌트 마운트 시 한 번만 생성
// text가 변경될 때만 이 함수가 다시 생성됨
const handleTextChange = useCallback((e) => {
setText(e.target.value);
}, []); // 빈 의존성 배열: 이 함수는 한 번만 생성되므로 setText가 항상 같은 함수를 참조한다는 가정 하에 유효
return (
<div>
<p>카운트: {count}</p>
<Button onClick={increment} label="증가" />
<input type="text" value={text} onChange={handleTextChange} />
<p>텍스트: {text}</p>
</div>
);
}
increment 함수는 count가 변경될 때만 재정의되므로, Button 컴포넌트가 React.memo로 최적화되어 있다면 count 외의 다른 상태(text)가 변경되어도 불필요하게 리렌더링되지 않습니다.
3. React.memo: "컴포넌트"를 메모이징 할 때
React.memo는 고차 컴포넌트(Higher-Order Component, HOC)로, 컴포넌트 자체를 메모이징합니다. 래핑된 컴포넌트는 전달받은 props가 변경되지 않았다면 불필요한 리렌더링을 건너뜁니다. 이는 PureComponent의 함수형 컴포넌트 버전이라고 생각할 수 있습니다.
어떤 것을 메모이징 하는가?
- 컴포넌트의 렌더링 결과 (props가 같으면 이전 결과 재사용)
언제 사용하는가?
- 자주 리렌더링되지만 props가 잘 변하지 않는 컴포넌트를 최적화할 때
- 부모 컴포넌트의 리렌더링 때문에 불필요하게 다시 렌더링되는 자식 컴포넌트가 있을 때
import React from 'react';
// MyPureComponent는 props.value가 변경될 때만 리렌더링됩니다.
const MyPureComponent = React.memo(({ value }) => {
console.log('MyPureComponent 렌더링:', value);
return <p>현재 값: {value}</p>;
});
function App() {
const [count, setCount] = React.useState(0);
const [otherState, setOtherState] = React.useState('');
return (
<div>
<h1>부모 컴포넌트</h1>
<button onClick={() => setCount(count + 1)}>Count 증가 ({count})</button>
<input
type="text"
value={otherState}
onChange={e => setOtherState(e.target.value)}
placeholder="다른 상태 변경"
/>
{/* count가 변경될 때만 MyPureComponent가 리렌더링됨 */}
<MyPureComponent value={count} />
{/* otherState가 변경되어도 MyPureComponent는 리렌더링되지 않음 */}
</div>
);
}
이 경우에서 MyPureComponent는 props.value가 변경될 때만 console.log가 찍히며 리렌더링됩니다. otherState가 변경되어 App 컴포넌트가 리렌더링되더라도 MyPureComponent는 리렌더링을 건너뜁니다.
4. 세 가지 도구의 상호작용 및 최적화 전략
- React.memo는 props를 비교: React.memo는 props가 변경되었는지 얕은 비교(shallow comparison)를 통해 확인합니다. 얕은 비교란 객체나 배열 같은 참조 타입의 데이터는 값 자체의 내용이 아니라 메모리 주소(참조)만 비교한다는 의미인데요. 문제는 React 컴포넌트가 리렌더링될 때마다, 컴포넌트 내부에서 새로 생성되는 객체나 함수는 매번 새로운 메모리 주소를 가진다는 점입니다. 아무리 내용이 똑같아도, 기술적으로는 다른 객체/함수가 되는 거죠.
- useMemo와 useCallback은 참조 동일성 유지: 이 문제를 해결하기 위해 useMemo로 객체/배열을 메모이징하고, useCallback으로 함수를 모두 메모이징하여 자식 컴포넌트에 전달되는 props의 참조 동일성(reference equality)을 유지시켜줍니다. 이렇게 하면 React.memo가 props가 변하지 않았다고 올바르게 판단하여 불필요한 자식 컴포넌트의 리렌더링을 방지할 수 있습니다.
결론적으로, 이 세 가지 메모이제이션 도구는 개별적으로도 강력하지만, 서로 유기적으로 함께 사용될 때 React 애플리케이션의 렌더링 성능을 극대화할 수 있는거죠.
이상입니다.
읽어주셔서 감사합니다.
'웹개발 > ReactJS' 카테고리의 다른 글
[React] React Compiler 당장 씁시다 (0) | 2025.06.20 |
---|---|
[React] useEffect hook에 대해 araboza (0) | 2025.06.20 |
[React] useRef hook에 대해 araboza (1) | 2025.06.19 |
[React] useReducer hook에 대해 araboza (1) | 2025.06.13 |
[React] React 함수형 컴포넌트의 생명주기에 대해 araboza. (1) | 2025.06.11 |