useEffect 무한 루프에 빠졌습니다.안녕하세요, React를 공부하고 있는 학생입니다.
컴포넌트가 마운트될 때 외부 API를 호출해서 데이터를 가져오고, 그 데이터를 state에 저장하려고 useEffect를 사용하고 있습니다.
그런데 코드를 실행하면 브라우저가 거의 멈추고, 네트워크 탭을 확인해보니 동일한 API 요청이 초당 수십 번씩 계속해서 보내지고 있습니다. 명백한 무한 루프 상황인 것 같은데, 원인을 잘 모르겠습니다.
아래는 문제가 되는 코드입니다.
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [data, setData] = useState(null);
const [options, setOptions] = useState({ limit: 10 }); // API 요청에 사용될 옵션
const fetchData = async () => {
console.log('Fetching data...');
const response = await fetch(`https://api.example.com/data?limit=${options.limit}`);
const result = await response.json();
setData(result);
};
useEffect(() => {
fetchData();
}, [options]); // 의존성 배열에 options 객체를 넣었습니다.
return (
<div>
{data ? <p>Data loaded!</p> : <p>Loading...</p>}
</div>
);
}
export default MyComponent;
제가 생각하기에, options 객체가 변경될 때만 fetchData가 실행되도록 의도했는데, 왜 계속해서 재실행되는지 이해가 가지 않습니다. setOptions를 호출하지도 않았는데 말이죠.
options가 정말 변경될 때만 리페칭(re-fetching)하고 싶다면 어떻게 접근해야 하는지도 궁금합니다.자세한 설명과 코드 예시를 부탁드립니다. 감사합니다!
안녕하세요. 질문자님께서 겪고 계신 문제는 React 개발자들이 흔히 마주치는 참조 동등성(Referential Equality) 문제입니다.
JavaScript에서 객체나 배열과 같은 참조 타입은 변수에 값이 아닌 메모리 주소(참조)가 저장됩니다.
const obj1 = { a: 1 };
const obj2 = { a: 1 };
console.log(obj1 === obj2); // false, 내용은 같지만 참조(메모리 주소)가 다름
React 컴포넌트는 렌더링될 때마다 함수 내부의 모든 코드가 다시 실행됩니다. 질문자님의 코드에서 MyComponent가 렌더링될 때마다 아래 라인이 실행됩니다.
const [options, setOptions] = useState({ limit: 10 });
useState는 초기 렌더링 시에만 { limit: 10 }을 사용하고, 이후 렌더링에서는 현재 options 상태 값을 반환합니다. 하지만 useEffect의 의존성 배열에 options를 직접 넣는 것이 문제입니다.
React는 리렌더링이 발생할 때마다 useEffect의 의존성 배열에 있는 값들을 이전 렌더링 시점의 값과 비교합니다. 이 비교는 Object.is() (=== 와 거의 동일)를 통해 이루어집니다.
문제의 흐름은 이렇습니다.
options 객체가 메모리에 생성됩니다.useEffect가 실행됩니다. fetchData()를 호출합니다.fetchData 내부에서 setData()가 호출되면서 컴포넌트의 state가 변경됩니다.MyComponent 함수가 다시 실행되면서, 새로운 options 객체가 또 생성됩니다. 이 객체는 내용물은 { limit: 10 }으로 이전과 같지만, 메모리 주소(참조)는 다릅니다.useEffect는 의존성 배열 [options]를 확인합니다. "이전 렌더링의 options"와 "현재 렌더링의 새로운 options"를 비교합니다. 두 객체의 참조가 다르므로, React는 의존성이 변경되었다고 판단합니다.useEffect의 콜백 함수 (fetchData())가 다시 실행됩니다.컴포넌트가 마운트될 때 딱 한 번만 API를 호출하는 것이 목적이라면, 의존성 배열을 빈 배열 [] 로 만드세요. 이는 "어떤 것에도 의존하지 않으니, 맨 처음 한 번만 실행해달라"는 의미입니다.
useEffect(() => {
fetchData();
}, []); // 빈 배열로 변경
만약 options 객체의 특정 값, 예를 들어 limit가 바뀔 때만 실행하고 싶다면, 객체 전체가 아닌 해당 원시 타입(primitive type) 값을 의존성 배열에 넣어주세요.
const [limit, setLimit] = useState(10);
useEffect(() => {
// fetchData는 이제 limit을 직접 사용하도록 수정해야 합니다.
const fetchData = async () => {
console.log('Fetching data...');
const response = await fetch(`https://api.example.com/data?limit=${limit}`);
const result = await response.json();
setData(result);
};
fetchData();
}, [limit]); // 객체가 아닌 원시 타입 값을 의존성으로!
부득이하게 객체를 의존성 배열에 넣어야 한다면, 렌더링마다 새로운 객체가 생성되는 것을 막아야 합니다. 이때 useMemo 훅을 사용할 수 있습니다.
const options = useMemo(() => ({
limit: 10
}), []); // options 객체를 메모이제이션
useEffect(() => {
fetchData();
}, [options]);
이러면 useMemo의 의존성 배열([])이 변경되지 않는 한, React는 렌더링이 반복되어도 이전에 생성했던 options 객체를 재사용하므로 무한 루프가 발생하지 않습니다.
결론부터 말씀드리면, useEffect의 두 번째 인자인 의존성 배열을 빈 배열([]) 로 설정하시면 문제가 해결됩니다.
useEffect(() => {
fetchData();
}, []); // <--- 바로 이 부분입니다.
useEffect의 의존성 배열은 "이 배열 안의 값이 바뀔 때만, 첫 번째 인자인 함수를 실행해주세요"라는 규칙을 따릅니다.
[]): "바뀌는 것을 감시할 변수가 없으니, 컴포넌트가 처음 화면에 나타날 때 딱 한 번만 실행해주세요" 라는 의미가 됩니다.질문자님의 원래 코드에서는 [options]와 같이 객체를 배열에 넣었습니다.
문제는 JavaScript에서 객체는 내용이 같아도 매번 새로 만들어질 때마다 다른 것으로 취급된다는 점입니다. 컴포넌트가 렌더링될 때마다 새로운 options 객체가 만들어지고, useEffect는 "어? 이전이랑 다른 객체가 들어왔네? 다시 실행해야지!" 라고 착각하게 되어 무한 루프가 발생한 것입니다.
따라서, 컴포넌트가 처음 로딩될 때 한 번만 데이터를 불러오는 것이 목적이라면 의존성 배열은 항상 []로 비워두는 것이 가장 일반적이고 확실한 방법입니다.