June 27, 2025
렌더링 중 부수 효과 발생시키기: 예측 불가능한 UI의 시작 🌪️
React
JavaScript
성능
상태관리
에러처리
아키텍처
컴포넌트
비동기처리
Summary
React 컴포넌트의 렌더링 페이즈(함수 본문)에서 setState
호출, 비동기 요청, DOM 직접 조작과 같은 부수 효과를 발생시키면 무한 루프, 예측 불가능한 동작, 성능 저하를 야기합니다. 모든 부수 효과는 useEffect
훅이나 이벤트 핸들러 내에서 처리해야 합니다.
Why Wrong?
React 컴포넌트의 함수 본문(렌더링 페이즈)은 순수 함수처럼 동작해야 합니다. 즉, 동일한 props와 state가 주어졌을 때 항상 동일한 UI를 반환해야 하며, 외부 상태를 변경하거나 비동기 작업을 수행해서는 안 됩니다. 렌더링 페이즈에서 setState
를 호출하거나, 전역 상태를 변경하거나, 비동기 요청을 보내거나, 직접 DOM을 조작하는 등의 '부수 효과(Side Effects)'를 발생시키면 다음과 같은 심각한 문제가 발생합니다:
- 무한 렌더링 루프:
setState
와 같은 상태 변경 로직이 렌더링 중에 실행되면, 상태가 업데이트되고 컴포넌트가 다시 렌더링됩니다. 이 과정이 반복되면서 무한 루프에 빠져 애플리케이션이 멈추거나 충돌할 수 있습니다. - 예측 불가능한 동작: React는 렌더링을 최적화하기 위해 컴포넌트를 여러 번 렌더링하거나, 렌더링을 중단하고 다시 시작할 수 있습니다. 렌더링 페이즈에서 발생한 부수 효과는 이러한 React의 내부 동작과 충돌하여 의도치 않게 여러 번 실행되거나, 데이터 불일치를 야기하여 UI가 예측 불가능하게 변할 수 있습니다.
- 성능 저하: 불필요한 재렌더링과 반복적인 비동기 요청, DOM 조작은 애플리케이션의 성능을 크게 저하시킵니다.
- 디버깅 어려움: 상태 변경의 원인을 파악하기 어렵게 만들고, 버그를 추적하고 수정하는 데 많은 시간과 노력이 필요하게 됩니다.
React의 렌더링 페이즈는 '무엇을 렌더링할지' 결정하는 역할에만 집중해야 하며, '무엇을 해야 할지'에 대한 작업은 별도의 페이즈에서 처리되어야 합니다.
How to Fix?
모든 부수 효과는 React의 useEffect
훅 또는 이벤트 핸들러 내에서 처리해야 합니다.
useEffect
훅: 컴포넌트가 렌더링된 이후에 실행되는 부수 효과를 관리하는 데 사용됩니다. 데이터 가져오기(데이터 페칭), 구독 설정, 타이머 설정, DOM 직접 조작 등 컴포넌트의 라이프사이클과 관련된 모든 부수 효과는useEffect
내에서 이루어져야 합니다.useEffect
의 의존성 배열을 적절히 사용하여 언제 효과를 재실행할지 제어할 수 있습니다.- 이벤트 핸들러: 사용자 인터랙션(클릭, 입력 등)에 의해 트리거되는 부수 효과(예: 상태 업데이트, API 호출)는 해당 이벤트 핸들러 내부에서 처리하는 것이 적절합니다. 이는 사용자 행동에 명확히 연결되어 예측 가능한 동작을 보장합니다.
렌더링 페이즈에서는 오직 UI를 구성하는 JSX를 반환하고, props와 state를 기반으로 순수하게 계산된 값을 사용하는 데 집중해야 합니다.
Before Code (Bad)
import React, { useState, useEffect } from 'react';
function BadComponent() {
const [count, setCount] = useState(0);
// 🚨 안티패턴: 렌더링 중에 setState 호출
// 이로 인해 무한 렌더링 루프 발생! (count가 5 미만일 때마다)
if (count < 5) {
setCount(count + 1);
}
// 🚨 안티패턴: 렌더링 중에 비동기 요청 실행
// 컴포넌트가 렌더링될 때마다 API 호출이 반복될 수 있음
fetch('/api/data')
.then(res => res.json())
.then(data => console.log('Data fetched:', data));
// 🚨 안티패턴: 렌더링 중에 DOM 직접 조작
// React의 가상 DOM과 충돌할 수 있으며, 예측 불가능한 UI를 야기
document.title = `Count: ${count}`;
console.log('Rendering BadComponent');
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(prev => prev + 1)}>Increment</button>
</div>
);
}
export default BadComponent;
After Code (Good)
import React, { useState, useEffect } from 'react';
function GoodComponent() {
const [count, setCount] = useState(0);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
// ✅ `count` 값이 변경될 때만 특정 로직 실행 (필요하다면)
// 이 로직 자체는 상태 업데이트가 아니므로 무한 루프를 유발하지 않음
useEffect(() => {
if (count < 5) {
console.log(`Count is ${count}, less than 5.`);
}
}, [count]); // count가 변경될 때마다 이 효과 실행
// ✅ 비동기 요청은 `useEffect` 내에서, 컴포넌트 마운트 시 한 번만 실행
useEffect(() => {
console.log('Fetching data...');
const fetchData = async () => {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const fetchedData = await response.json();
console.log('Data fetched:', fetchedData);
setData(fetchedData); // 상태 업데이트는 useEffect 내에서
} catch (e) {
console.error('Error fetching data:', e);
setError(e);
}
};
fetchData();
}, []); // 빈 의존성 배열: 컴포넌트 마운트 시 한 번만 실행
// ✅ DOM 직접 조작(side effect) 역시 `useEffect` 내에서
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]); // count가 변경될 때마다 이 효과 실행
console.log('Rendering GoodComponent');
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(prev => prev + 1)}>Increment</button>
{data ? <p>Fetched Data: {JSON.stringify(data)}</p> : <p>Loading data...</p>}
</div>
);
}
export default GoodComponent;