성능 최적화를 위해 적용할 수 있는 방법
성능 최적화를 위해 적용할 수 있는 방법
리액트 애플리케이션이 커질수록 성능 저하가 발생하기 쉽다. 이런 경우, 불필요한 리렌더링을 줄이고 렌더링 비용을 최적화하기 위한 다양한 방법을 적용할 수 있다. 이번 글에서는 대표적인 성능 최적화 기법인 메모이제이션(memoization), 함수/값 최적화 훅, 코드 스플리팅(code splitting) 에 대해 알아본다.
1. memo로 컴포넌트 메모이제이션하기
리액트의 memo 함수는 컴포넌트를 메모이제이션하여 불필요한 리렌더링을 방지하는 역할을 한다. 즉, props가 이전과 동일하다면 해당 컴포넌트는 다시 렌더링되지 않는다.
1
2
3
4
5
6
7
8
import React, { memo } from "react";
const UserProfile = memo(({ name }: { name: string }) => {
console.log("렌더링 발생!");
return <div>{name}</div>;
});
export default UserProfile;
위 코드에서 memo를 적용하지 않으면 부모 컴포넌트가 리렌더링될 때마다 UserProfile도 함께 리렌더링된다. 하지만 memo를 사용하면 props가 변경되지 않는 한 렌더링을 건너뛰게 되어 성능을 크게 개선할 수 있다.
✅ 장점
- 렌더링 비용이 큰 컴포넌트에서 효과적임
- props 변화가 잦지 않은 UI에 적합함
2. useCallback과 useMemo로 함수·값 메모이제이션하기
컴포넌트가 리렌더링될 때마다 내부의 함수와 값도 새로 생성된다. 이로 인해 자식 컴포넌트로 전달된 props가 변경된 것으로 인식되어 불필요한 리렌더링이 발생할 수 있다. 이를 방지하기 위해 useCallback과 useMemo를 사용할 수 있다.
🧩 useCallback
useCallback은 함수를 메모이제이션하는 훅이다. 의존성 배열이 변경되지 않는 한, 동일한 함수 인스턴스를 재사용한다.
1
2
3
4
5
6
7
8
9
10
11
import React, { useCallback, useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
return <button onClick={handleClick}>클릭: {count}</button>;
}
➡️ 함수가 매 렌더링마다 새로 생성되지 않아, 자식 컴포넌트로 전달될 때 불필요한 리렌더링을 방지할 수 있음.
🧩 useMemo
useMemo는 연산 비용이 큰 값을 메모이제이션하는 훅이다. 의존성이 변경되지 않으면 이전 계산 결과를 재사용한다.
1
2
3
4
5
6
7
8
9
10
import React, { useMemo } from "react";
function ExpensiveCalculation({ num }: { num: number }) {
const result = useMemo(() => {
console.log("복잡한 연산 수행 중...");
return num * 1000;
}, [num]);
return <p>결과: {result}</p>;
}
➡️ 복잡한 계산이나 필터링, 정렬 연산 등이 매번 실행되는 것을 방지함으로써 렌더링 성능을 크게 향상시킬 수 있음.
3. 코드 스플리팅(Code Splitting)으로 초기 로딩 속도 개선하기
리액트 애플리케이션이 커질수록, 모든 코드를 한 번에 로드하면 초기 로딩 속도가 느려질 수 있다. 이때 코드 스플리팅(Code Splitting) 을 사용하면 앱을 여러 개의 작은 코드 조각으로ㄴ 분리하여, 필요한 부분만 로드할 수 있다.
리액트에서는 React.lazy와 Suspense를 이용해 쉽게 구현할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Suspense, lazy } from "react";
const DetailPage = lazy(() => import("./DetailPage"));
function App() {
return (
<Suspense fallback={<div>로딩 중...</div>}>
<DetailPage />
</Suspense>
);
}
export default App;
✅ 장점
- 초기 로드 시 필요한 코드만 불러오기 때문에 초기 렌더링 속도 향상
- 나중에 필요한 컴포넌트는 동적 import로 효율적으로 로드 가능
✅ 코드 스플리팅이 필요한 상황
초기 로딩 시간이 길어지는 경우
- 애플리케이션이 커질수록 모든 코드를 한 번에 불러오는 것은 비효율적임
- 핵심 기능만 먼저 로드하고, 나머지 기능은 필요한 시점에 동적으로 로드하면 초기 속도를 개선할 수 있음
라우트별 코드 분할이 필요한 경우
- SPA(단일 페이지 애플리케이션)에서는 각 페이지가 서로 다른 기능과 UI를 가짐
- 라우트별로 코드를 분리하면 페이지 전환 시 필요한 코드만 로드 가능함
React.lazy+Suspense조합으로 손쉽게 구현 가능함
💡 마무리
리액트의 성능 최적화는 결국 불필요한 렌더링을 얼마나 줄이느냐의 문제이다.
memo: 컴포넌트 자체를 메모이제이션useCallback: 함수를 메모이제이션useMemo: 계산 결과를 메모이제이션코드 스플리팅: 필요한 코드만 효율적으로 로드
이러한 기법들을 상황에 맞게 적용하면, 사용자에게 더 빠르고 부드러운 리액트 앱 경험을 제공할 수 있다.
