React에서 렌더링(Rendering)이란 컴포넌트 함수를 호출하여 React Element를 반환하는 과정입니다. 이때 Virtual DOM을 생성하고, 실제 DOM과 비교하여 변경된 부분만 반영합니다. 하지만 이과정에서 불필요한 렌더링이 발생하면 성능 저하가 일어날 수 있기 때문에, 이를 최적화하는 기법이 필요합니다.
React의 렌더링 과정은 크게 두 단계로 나뉩니다.
Render Phase
컴포넌트를 호출하여 새로운 Virtual DOM을 생성하고, 기존 Virtual DOM과 비교하여 변경된 부분을 찾습니다.
Commit Phase
변경된 부분만 실제 DOM에 반영합니다.
재조정이 필요 없는 경우, 이 과정은 생략됩니다(Skip됨).
React는 또한 componentDidMount와 componentDidUpdate와 같은 생명주기 메서드를 호출함
React는 Commit Phase 이후 브라우저의 렌더링 과정에서 리플로우와 리페인트 과정을 거칩니다.
React에서는 Virtual DOM을 활용하여 불필요한 리플로우 및 리페인트를 최소화하는 최적화 기법을 제공합니다.
크게 보면 메모이제이션과 코드 스플리팅 두 가지 기법이 있습니다.
React에서 메모이제이션을 활용하면 불필요한 렌더링을 방지하고 성능을 최적화할 수 있습니다.
대표적인 메모이제이션 기법으로는 React.memo, useMemo, useCallback이 있습니다.
React.memo
React.memo는 컴포넌트의 props가 변경되지 않으면, 렌더링을 건너뛰도록 합니다.
import React from "react";
const ChildComponent = React.memo(({ value }) => {
console.log("ChildComponent 렌더링");
return <div>{value}</div>;
});
export default ChildComponent;
React.memo는 얕은 비교를 통해 props가 변경되지 않았을 경우, Render Phase를 건너뜁니다.
단, 참조 타입은 새로운 객체가 생성될 경우 값이 달라졌다고 판단하므로 주의가 필요합니다.
useMemo
useMemo는 값 자체를 메모이제이션하여 렌더링 시 불필요한 계산을 방지합니다.
import { useMemo } from "react";
const ParentComponent = ({ item }) => {
const memoizedItem = useMemo(() => item, [item]);
return <ChildComponent item={memoizedItem} />;
};
useMemo는 비용이 큰 연산 결과를 저장하고, 동일한 의존성 배열 값을 유지하면 다시 계산하지 않습니다.
객체를 props로 넘길 때도 useMemo를 사용하면 부모 컴포넌트가 리렌더링되어도 객체의 참조 값이 유지되어 불필요한 렌더링을 방지할 수 있습니다.
useCallback
useCallback은 함수를 메모이제이션하여, 같은 함수가 매번 새롭게 생성되지 않도록 합니다.
import { useCallback } from "react";
const ParentComponent = ({ onClick }) => {
const memoizedCallback = useCallback(() => {
console.log("Button clicked");
}, []);
return <ChildComponent onClick={memoizedCallback} />;
};
useCallback은 함수의 참조 값이 유지되므로, props로 넘길 때 불필요한 렌더링을 방지할 수 있습니다.
React.memo와 함께 사용하면 최적화 효과를 극대화할 수 있습니다.
그렇지 않습니다.
메모이제이션 자체도 비용이 발생해서 무조건적으로 적용하는 것은 성능을 오히려 저하시킬 수 있습니다. 따라서 다음과 같은 경우 먼저 고려한 후 적용하는 것이 좋습니다.
React.lazy
React.lazy를 사용하면 동적 import를 통해 컴포넌트를 나누어 렌더링할 수 있습니다.
이를 통해 애플리케이션의 초기 로딩 시간을 단축, 필요한 시점에서만 컴포넌트를 로드하여 성능 최적화가 가능합니다.
React.Suspense
아직 렌더링이 준비되지 않은 컴포넌트가 있을 경우, fallback을 통해 로딩 화면을 보여줄 수 있습니다.
주로 Route 컴포넌트에서 사용되며, 해당 경로로 이동할 때 비로소 필요한 컴포넌트를 로드하는 방식입니다.
사용 방법
import { Suspense, lazy } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
const Home = lazy(() => import("./routes/Home"));
const About = lazy(() => import("./routes/About"));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</Router>
);
주의할 점
- 페이지 이동 시마다 설정된 로딩 컴포넌트가 표시될 수 있으므로, 사용자 경험을 고려하여 적용해야 합니다.
- 코드 스플리팅은 네트워크 요청을 동반하기 때문에, 너무 작은 단위로 나누면 오히려 성능이 저하될 수도 있습니다.
React의 렌더링을 최적화하는 방법에는 메모이제이션과 코드 스플리팅이 있으며, 이를 적절히 활용하면 성능을 향상시킬 수 있습니다.
메모이제이션 : React.memo, useMemo, useCallback을 활용한 렌더링 최소화
코드 스플리팅 : React.lazy, Suspense를 이용한 코드 스플리팅으로 초기 로딩 최적화
주의 사항 : 불필요한 최적화는 오히려 성능을 저하시킬 수 있으므로, 실제 성능 문제가 발생하는 부분에서만 적용
성능 최적화는 무조건 적용하는 것이 아니라, 필요할 때 적절하게 활용하는 것이 중요합니다.
[10분 테코톡] 앨버의 리액트 렌더링 최적화
React - React Deverloper Tool을 이용한 성능 최적화