서론
React는 효율적인 UI 업데이트를 위해 가상 DOM과 상태 관리 시스템을 사용한다. 그러나 상태 업데이트와 렌더링 과정에서 발생하는 Reconciliation은 비용이 큰 작업이다. 이를 최소화하려면 React의 batch 처리와 렌더링 주기를 잘 이해하고 활용해야 한다. 특히 React 18부터 도입된 비동기 배치 처리는 성능 최적화를 위한 핵심 도구이지만, 이 동작을 잘못 이해하면 무한 재렌더링과 같은 문제를 초래할 수 있다.
1. React에서 Reconciliation과 Batch 처리의 중요성
Reconciliation은 React가 변경된 가상 DOM을 실제 DOM에 반영하기 위해 차이를 계산하는 과정이다. 이는 매우 비용이 크므로, React는 setState 호출을 최소한의 렌더링 주기로 묶어 처리한다. 이를 batch 처리라고 하며, React 18 이전과 이후에서 동작 방식이 다르다:
• React 18 이전: React 이벤트 핸들러 내부에서만 batch 처리가 이루어졌고, 비동기 코드(예: setTimeout, Promise)에서는 각 setState 호출마다 렌더링이 발생했다.
• React 18 이후: React는 비동기 코드에서도 batch 처리를 지원한다. 따라서 동일한 비동기 흐름 내에서 여러 setState 호출이 하나의 렌더링으로 묶인다.
React 18부터는 모든 setState 호출을 배치 처리하여 Reconciliation 과정을 최적화한다. 하지만, 이를 잘 활용하기 위해선 React의 렌더링 주기와 JavaScript의 실행 스코프에 대한 이해가 필요하다.
2. 동기 vs 비동기 코드의 렌더링 주기 차이
React의 렌더링 과정은 크게 Render Phase와 Commit Phase로 나뉜다:
• Render Phase: 컴포넌트를 렌더링하고 가상 DOM을 생성한다. 이 단계에서는 실제 DOM 변경이 이루어지지 않는다.
• Commit Phase: 가상 DOM 변경 사항을 실제 DOM에 반영한다. 이 단계에서 useEffect와 같은 side effect가 실행된다.
2.1 동기 코드에서의 Batch 처리
동기 코드에서 여러 setState 호출은 동일한 렌더링 주기 내에서 묶여서 배치 처리된다.
if (someCondition) {
setState1(value1); // 배치 처리됨
setState2(value2); // 배치 처리됨
}
이 경우 React는 한 번의 Reconciliation으로 상태 업데이트를 처리한다.
2.2 비동기 코드에서의 Batch 처리
React 18부터는 비동기 코드에서도 batch 처리가 지원되지만, useEffect 내부의 setState는 새로운 렌더링 주기를 트리거한다. 즉, useEffect가 실행된 후 호출된 setState는 새로운 Reconciliation 과정을 거친다.
useEffect(() => {
setState1(value1); // 새로운 렌더링 주기 트리거
}, []);
여기서 발생하는 추가적인 Reconciliation은 성능에 영향을 미칠 수 있다.
3. 무한 재렌더링의 원인: 렌더링 주기의 순환
다음은 무한 재렌더링에 빠질 수 있는 코드 예제이다:
if (isActive && userStatus === 'running') {
setIsRendered(false);
}
useEffect(() => {
setIsActive(false);
}, []);
이 코드에서 무한 재렌더링이 발생하는 이유는 Render Phase에서 상태 변경(setState)이 발생하면, 새로운 Render Phase가 즉시 시작되기 때문이다. React는 setState를 호출하면 Render Phase → Commit Phase의 렌더링 주기를 시작한다. 하지만 위 코드에서는 조건문 내부에서 setIsVisible(false)가 호출되면서 새로운 Render Phase를 트리거한다. 이로 인해 React는 Commit Phase로 넘어가기 전에 다시 Render Phase를 실행하게 되고, Commit Phase 이후에 실행되어야 할 useEffect는 실행되지 않는다.
life cycle 최적화를 하면서 동시에 이 무한 재렌더링 문제를 해결하려면 useEffect가 새로운 렌더링 주기를 트리거하더라도, 조건문이 반복적으로 상태를 변경하지 않도록 방지해야 한다.
if (isActive && userStatus === 'ready' && isRendered) {
setIsRendered(false);
}
이 코드는 다음과 같은 방식으로 동작한다. isReasonWaitRendered가 true일 때만 setIsReasonWaitRendered(false)가 호출되며, 이로 인해 상태가 한 번만 변경된다. 이후 useEffect에서 setWorkMode(false)가 호출되더라도, 조건문이 재평가될 때 이미 isReasonWaitRendered가 false로 변경된 상태이기 때문에 조건이 다시 참으로 평가되지 않는다. 따라서 추가적인 상태 변경이 발생하지 않으며, 무한 재렌더링이 방지된다.
이 코드의 장점은 조건문 내부에서 setState를 한 번만 호출하므로 불필요한 렌더링을 방지할 수 있다는 점이다. 또한, useEffect로 인해 새로운 렌더링 주기가 발생하더라도 상태가 안정적으로 유지되어 React의 렌더링 흐름에 영향을 주지 않는다. 이를 통해 성능을 최적화하면서 안정적인 상태 관리를 구현할 수 있다.
'React' 카테고리의 다른 글
React에서 경로 최적화를 통한 성능 향상 (0) | 2025.01.14 |
---|---|
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 |