React 컴포넌트의 렌더링 페이즈(함수 본문)에서 setState 호출, 비동기 요청, DOM 직접 조작과 같은 부수 효과를 발생시키면 무한 루프, 예측 불가능한 동작, 성능 저하를 야기합니다. 모든 부수 효과는 useEffect 훅이나 이벤트 핸들러 내에서 처리해야 합니다.
React 컴포넌트의 함수 본문(렌더링 페이즈)은 순수 함수처럼 동작해야 합니다. 즉, 동일한 props와 state가 주어졌을 때 항상 동일한 UI를 반환해야 하며, 외부 상태를 변경하거나 비동기 작업을 수행해서는 안 됩니다. 렌더링 페이즈에서 setState를 호출하거나, 전역 상태를 변경하거나, 비동기 요청을 보내거나, 직접 DOM을 조작하는 등의 '부수 효과(Side Effects)'를 발생시키면 다음과 같은 심각한 문제가 발생합니다:
setState와 같은 상태 변경 로직이 렌더링 중에 실행되면, 상태가 업데이트되고 컴포넌트가 다시 렌더링됩니다. 이 과정이 반복되면서 무한 루프에 빠져 애플리케이션이 멈추거나 충돌할 수 있습니다.React의 렌더링 페이즈는 '무엇을 렌더링할지' 결정하는 역할에만 집중해야 하며, '무엇을 해야 할지'에 대한 작업은 별도의 페이즈에서 처리되어야 합니다.
모든 부수 효과는 React의 useEffect 훅 또는 이벤트 핸들러 내에서 처리해야 합니다.
useEffect 훅: 컴포넌트가 렌더링된 이후에 실행되는 부수 효과를 관리하는 데 사용됩니다. 데이터 가져오기(데이터 페칭), 구독 설정, 타이머 설정, DOM 직접 조작 등 컴포넌트의 라이프사이클과 관련된 모든 부수 효과는 useEffect 내에서 이루어져야 합니다. useEffect의 의존성 배열을 적절히 사용하여 언제 효과를 재실행할지 제어할 수 있습니다.렌더링 페이즈에서는 오직 UI를 구성하는 JSX를 반환하고, props와 state를 기반으로 순수하게 계산된 값을 사용하는 데 집중해야 합니다.
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;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;