0. 서론
React는 컴포넌트 기반의 UI 라이브러리로, 빠르고 효율적인 사용자 경험을 제공하는 것을 목표로 한다. 하지만 복잡한 애플리케이션에서는 성능이 저하되는 문제가 발생할 수 있다. React의 성능에 가장 큰 영향을 미치는 요소 중 하나는 reconciliation 과정이다. 특히, virtual DOM을 새로 생성하고 diffing 알고리즘을 통해 이전 DOM과 비교하는 과정은 가장 비용이 많이 드는 작업이다. 따라서 컴포넌트 재렌더링을 줄이는 것이 성능 최적화의 핵심이다. 이 글에서는 React에서 컴포넌트의 재렌더링과 이를 최적화하기 위한 전략에 대해 구체적으로 알아본다.
1. React에서 컴포넌트 재렌더링
React에서 재렌더링은 render phase에서 컴포넌트의 UI를 다시 계산하는 과정을 의미한다. 이 과정은 주로 setState에 의해 트리거된다. React는 상태가 변경될 때, 새로운 상태를 fiber로 전달하여 해당 컴포넌트를 재렌더링한다.
한편, 부모 컴포넌트의 재렌더링은 자식 컴포넌트에도 영향을 미친다. 많은 개발자들이 자식 컴포넌트가 전달받은 props가 변하지 않으면 재렌더링되지 않는다고 생각하지만, 이는 React 19에서 React Compiler를 사용하는 경우에만 해당된다. 이전 버전에서는 부모 컴포넌트가 재렌더링될 때 자식 컴포넌트의 props나 상태가 변경되지 않았더라도 자식 컴포넌트가 재렌더링된다. 이는 React의 fiber 구조에서 새로운 fiber를 생성하거나 기존의 fiber를 재사용할지 결정하는 과정에서 발생하는 문제 때문이다. 새로운 fiber가 생성되면 해당 서브트리가 완전히 재구축된다.
따라서 컴포넌트 재렌더링을 줄이기 위해서는 다음 두 가지 방법이 필요하다.
1. 상태 변화를 최소화한다.
2. 경로를 최적화한다.
2. 상태 변화 최소화
2.1 상태와 상수의 구분
React에서 상태는 setState로 인해 재렌더링을 트리거한다. 따라서 상태로 선언할지 여부를 신중히 판단해야 한다. 상태가 아닌 값을 상태로 선언하면 불필요한 재렌더링이 발생한다. 상태를 최소화하려면, render phase에서 평가될 수 있는 값은 상수로 선언해야 한다. 예를 들어, 변경되지 않는 데이터는 상태로 선언하지 않는 것이 바람직하다.
2.2 상태 위치 최적화
상태를 어느 컴포넌트에 위치시킬지 결정하는 것도 중요하다. 상태가 너무 상위에 위치하면 불필요한 하위 컴포넌트들까지 재렌더링될 가능성이 있다. 따라서 상태는 해당 상태를 사용하는 가장 낮은 레벨의 컴포넌트에 위치시키는 것이 좋다.
2.3 useEffect 최소화
useEffect는 render phase가 끝난 후인 commit phase에 실행된다. 하지만 useEffect 내부에서 setState를 호출하면 다시 render phase가 트리거된다. 이를 줄이기 위해 useEffect를 사용할 때, 해당 작업이 진정한 side effect인지 판단해야 한다. Side effect란 외부 값을 참조하거나 순수하지 않은 작업을 의미한다. 불필요한 useEffect 사용을 피하고 이벤트 핸들링과 side effect를 명확히 구분하는 것이 중요하다.
3. 경로 최적화
3.1 JSX와 React Element의 변환
React는 JSX를 React.createElement 또는 _jsx 함수로 변환하여 React Element를 생성한다. 이때 props는 객체로 전달되며, 객체는 참조형 데이터 타입이다. JavaScript에서 객체는 내용이 같더라도 서로 다른 참조를 가지므로, 빈 객체조차도 매번 새로운 객체로 인식된다. 이러한 이유로 props가 변경되지 않았더라도 빈 객체를 사용하는 경우 재렌더링이 발생할 수 있다.
3.2 memo를 활용한 최적화
React의 memo는 props의 얕은 비교를 통해 동일한 props를 가진 경우 컴포넌트의 재렌더링을 방지한다. memo를 사용하지 않으면, 상태가 변경된 컴포넌트의 서브트리는 모두 재렌더링된다. 하지만 props로 전달된 객체가 매번 새로운 참조를 가지면 memo의 얕은 비교를 통과하지 못한다. 이를 해결하려면 다음 두 가지 방법을 고려할 수 있다.
1. memo의 두 번째 인자로 커스텀 비교 함수를 전달하여 깊은 비교를 수행한다.
2. 부모 컴포넌트에서 빈 객체 대신 값이 변하지 않는 상수를 전달한다.
3.3 컴포넌트 생성 피하기
재렌더링 최적화 대상 컴포넌트를 부모 컴포넌트 바깥으로 분리하면, 부모의 재렌더링이 해당 컴포넌트에 영향을 미치지 않게 된다. 하지만 이 방법은 이벤트 핸들러나 상태를 클로저로 참조하는 경우에는 사용하기 어렵다. 클로저가 참조하는 값이 변할 때마다 새로운 함수가 생성되기 때문이다.
3.4 useCallback의 활용
useCallback은 이벤트 핸들러가 의존하는 값이 변경될 때에만 새로운 함수를 생성하도록 한다. 이를 통해 불필요한 함수 생성과 재렌더링을 방지할 수 있다. 예를 들어, 다음과 같은 방법으로 이벤트 핸들러를 최적화할 수 있다.
const MyComponent = ({ onClick }) => {
const handleClick = useCallback(() => {
onClick();
}, [onClick]);
return <button onClick={handleClick}>Click Me</button>;
};
4. 결론
React의 성능 최적화는 컴포넌트 재렌더링을 줄이는 데 초점이 맞춰져 있다. 이를 위해 상태 변화를 최소화하고, 경로를 최적화하는 전략을 활용해야 한다. memo, useCallback, useEffect와 같은 React의 기능을 적절히 사용하면 불필요한 재렌더링을 줄이고 애플리케이션의 성능을 크게 향상시킬 수 있다. React를 효율적으로 사용하기 위해선 이러한 최적화 방법들을 잘 이해하고 상황에 맞게 적용하는 것이 중요하다.
'React' 카테고리의 다른 글
React에서 batch 처리와 렌더링 주기: 무한 재렌더링 (1) | 2025.01.15 |
---|---|
React Hook이란 무엇인가 (0) | 2024.12.02 |
React useState의 동작방식과 Lazy Initialization (0) | 2024.11.16 |
React.memo에 대하여 (1) | 2024.11.16 |
React 18에서 변경된 Suspense (1) | 2024.11.04 |