React의 메모이제이션으로 성능 최적화
복잡한 계산이 필요한 경우, 컴포넌트가 자주 리렌더링 되는 경우 등 성능 최적화가 필요한 경우 도움이 됨,
React.memo
컴포넌트의 props가 변경되지 않았을 때 리렌더링 방지해 성능 최적화
const Child = React.memo(({ value }) => {
console.log("Child rendered");
return <div>{value}</div>;
});
function Parent() {
const [count, setCount] = React.useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<Child value={count} />
</div>
);
}
Child는 React.memo로 감싸져있어 value가 변경되지 않으면 리렌더되지 않음.
setCount가 호출되면 count값이 변해 Child의 value가 변하니까 리렌더 됨.
컴포넌트의 props가 변경되지 않으면 컴포넌트를 리렌더링하지 않음.
부모 컴포넌트가 자주 업데이트 되는 상황에서 유용함.
useMemo
저장된 값을 반환해서 재계산을 방지해 성능 최적화.
function App() {
const [count, setCount] = React.useState(0);
const expensiveCalculation = React.useMemo(() => {
console.log("Calculating...");
return count * 2;
}, [count]);
return (
<div>
<p>Result: {expensiveCalculation}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
count값이 변경될 때만 expensiveCalculation이 재계산됨. 비용이 비싼 계산에 적용하면 좋음. (CPU를 많이 쓰거나..)
useCallback
함수를 메모이제이션해 불필요한 함수 재생성을 방지해 성능 최적화.
function Parent() {
const [count, setCount] = React.useState(0);
const increment = React.useCallback(() => {
setCount((prev) => prev + 1);
}, []);
return <Child onIncrement={increment} />;
}
const Child = React.memo(({ onIncrement }) => {
console.log("Child rendered");
return <button onClick={onIncrement}>Increment</button>;
});
- increment 함수는 useCallback으로 메모이제이션 됨.
- Parent가 리렌러링되어도 동일한 참조 유지함.
- Child는 props가 변경되지 않아 리렌더링 되지 않음.
코드 스플리팅
큰 어플리케이션을 여러 개의 작은 조각(chunk)로 나누어, 필요한 부분만 로드해 초기 로드 시간 줄임
- SPA에서는 각 페이지가 별도의 기능과 UI를 가져, 라우트별로 필요한 코드만 분리해 로드할 수 있음.
- React.lazy, Suspense 사용해 라우트별 컴포넌트 동적으로 불러올 때 유용함.
import React, { Suspense } from "react";
const Home = React.lazy(() => import("./Home"));
const About = React.lazy(() => import("./About"));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
);
}
- 라우트별 코드 분할 예시
- Home, About는 초기 로드 시 포함되지 않고, 해당 라우트에 접근할 때 관련 코드를 동적으로 로드(React.lazy)함.
- Suspense는 로딩 상태를 처리하기 위한 fallback UI(동적으로 컴포넌트를 로드할 때, 로드 전까지 표시되는 UI)를 제공.
React DevTools의 Profiler
불필요한 리렌더링이 발생하는 컴포넌트를 식별하고 최적화
컴포넌트 트리를 <Profiler>로 감싸줌
<Profiler id="App" onRender={onRender}>
<App />
</Profiler>
id: 성능을 측정하는 UI 컴포넌트를 식별하기 위한 문자열
onRender: 프로파일링된 트리 내의 컴포넌트가 업데이트 될 때마다 React가 호출하는 함수
function onRender(id, phase, actualDuration, baseDuration, startTime, commitTime) {
// 렌더링 시간 집계 혹은 로그...
}
onRender의 각 요소의 의미 | |
id | 프로파일러를 다중으로 사용하고 있는 트리 내에서 어떤 부분이 커밋 되었는지 식별하도록 도움 |
phase | "mount", "update", "nested-update". 트리가 최초로 마운트 되었는지, props, state, hook의 변경으로 리렌더링 되었는지? |
actualDuration | 현재 업데이트에 대해 와 자식들을 렌더링하는데 소요된 시간. 하위 트리가 메모이제이션을 얼마나 잘하는지 지표가 됨. 이상적인 상황이라면, 최초 마운트 이후에는 메모이제이션에 의해 랜더링 시간이 많이 감소해야 함. |
baseDuration | 최적화 없이 하위 트리에 걸리는 시간을 추정하는 소요된 시간(ms 단위) 트리에 있는 각 컴포넌트의 가장 최근 렌더링 시간을 합산해 계산 최악의 렌더링 비용(최초 마운트할 때 처럼 모든 요소를 다 불러오는 시간이 걸린다고 가정) 산정 이 값과 actualDuration을 비교해서 메모이제이션이 잘 되고 있는지 확인한다 |
startTime | React가 현재 업데이트 렌더링을 시작한 시점에 대한 숫자 타임 스탬프 |
commitTime | React가 현재 업데이트를 커밋한 시점에 대한 숫자 타임스탬프 커밋된 모든 프로파일러 간에 공유되므로 원하는 경우 그룹화할 수 있음 |
Profiler 컴포넌트를 여러 개 사용해서 어플리케이션을 부분적으로 측정할 수 있다.
Profiler 컴포넌트를 중첩해서 사용할 수 있다.
Profiler는 가벼운 컴포넌트이지만 사용 시 계산 때문에 CPU 일부와 메모리 오버헤드를 추가하기 때문에 필요 시에만 사용
이 오버히드 때문에 프로덕션 빌드에서는 기본적으로 비활성화되어 있음.
React.memo | props 변경될 때만 리렌더링 |
useMemo | 저장된 값을 반환해 재계산을 방지함 |
useCallback | 저장된 함수를 반환해 불필요한 함수 재생성을 방지 |
React.lazy, Suspense | 사용자가 해당 라우트에 접근할 때 동적으로 로드하고, 로딩 중에는 fallback UI를 보여줌 |
React <Profiler> | 컴포넌트 렌더링-리렌더링 시간을 측정해 메모이제이션 성능을 비교할 수 있음 |
useCallback과 useMemo 같은 메모이제이션은 많이 적용하면 무조건 좋을까?
메모이제이션도 결국 메모리에 저장하면서 메모리 비용이 발생함.
또, 메모이제이션 함수는 의존성 배열을 확인해 값을 업데이트하는 데, 이 각 항목을 비교하는 작업도 비용이 듦
// 이런 간단한 함수의 경우 메모이제이션이 오히려 불필요합니다.
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
// 반면, 복잡한 연산의 경우 메모이제이션이 효율적입니다.
const expensiveValue = useMemo(() => {
return complexCalculation(items);
}, [items]);
출처
[1] 매일메일. 241223. 리액트에서 성능 최적화를 위해 적용할 수 있는 방법들을 설명해 주세요. 18번. https://maeil-mail.kr
[2] gpt에게 React 메모이제이션에 대해 묻다.
[3] 리액트에서 컴포넌트가 불필요하게 리렌더링되는 상황을 방지하기 위한 방법을 설명해주세요. 79번.
[4] React v19 docs. Profiler. https://ko.react.dev/reference/react/Profiler
이 블로그 관련 글
웹 어플리케이션의 성능을 최적화하는 방법? https://developer-dreamer.tistory.com/119
로딩 속도 개선을 위한 이미지 크기 최적화 https://developer-dreamer.tistory.com/126
'개발자 강화 > 프론트엔드' 카테고리의 다른 글
🌟[공부, 매일메일] Suspense란? (3) | 2025.02.10 |
---|---|
[매일메일] React의 Concurrent Mode(동시성 모드) (FE.250207) (0) | 2025.02.08 |
[매일메일] Error Boundary란? (FE.250205) (3) | 2025.02.06 |
🌟[매일메일] 자바스크립트의 Promise란? (FE.250129/250203) (1) | 2025.02.03 |
[매일메일] 클로저란? (FE.241212) (1) | 2025.02.01 |