의존성 배열을 정직하게 채웠더니 Effect가 무한히 도는 이유
문제 발생
린터가 지적한 대로 의존성을 채웠더니 요청이 멈추지 않았습니다.
function SearchResults({ options }) {
const [items, setItems] = useState([]);
useEffect(() => {
fetchItems(options).then(setItems); // setItems → 리렌더 → options 새 객체 → 다시 실행
}, [options]);
}부모는 이렇게 쓰고 있었습니다.
<SearchResults options={{ query, page: 1 }} />원인 분석
의존성 비교는 값이 아니라 동일성으로 합니다. React 문서가 이유를 그대로 적습니다 — JavaScript에서 새로 만들어진 객체와 함수는 다른 모든 것과 구별되는 별개로 취급됩니다. 안의 내용이 같은지는 상관없습니다.
const options1 = { serverUrl: "https://localhost:1234", roomId: "music" };
const options2 = { serverUrl: "https://localhost:1234", roomId: "music" };
Object.is(options1, options2); // false부모가 렌더될 때마다 { query, page: 1 } 리터럴이 새 객체를 만들고, 자식의 Effect는 "의존성이 바뀌었다"고 판단합니다. 문서도 불필요한 의존성은 Effect를 너무 자주 실행시키거나 심지어 무한 루프를 만들 수 있다고 경고합니다.
린터를 끄는 것은 해법이 아닙니다. 그러면 오래된 options를 계속 쓰는 다른 버그로 바뀝니다.
해결 방안
-
바뀌지 않는 값이면 컴포넌트 바깥으로 뺍니다. 문서가 첫 번째로 제시하는 방법입니다 — props나 state에 의존하지 않는 객체는 매 렌더 다시 만들 이유가 없습니다.
-
동적인 객체는 Effect 안에서 만듭니다. 그러면 의존성이 원시값만 남습니다.
useEffect(() => {
const options = { query, page };
fetchItems(options).then(setItems);
}, [query, page]);- props로 객체를 받는다면 필요한 값만 꺼냅니다. 문서의 세 번째 방법입니다.
const { query, page } = options;
useEffect(() => {
fetchItems({ query, page }).then(setItems);
}, [query, page]);-
useMemo/useCallback은 마지막 선택지입니다. 부모에서 객체를 메모이제이션해 넘기는 것도 동작하지만, 부모가 그 사실을 계속 기억해야 하는 결합이 생깁니다. 원시값으로 좁히는 쪽이 대개 더 단순합니다. -
Effect 자체가 필요한지 먼저 묻습니다. 사용자의 입력에 반응해 데이터를 가져오는 코드는 이벤트 핸들러나 프레임워크의 데이터 로딩(예: Server Component, 라우터 로더)이 더 맞는 자리일 때가 많습니다.
-
정리 함수를 잊지 않습니다. 이 Effect는 요청 경합에도 노출되어 있습니다. 응답이 도착 순서와 다르게 오면 오래된 결과가 화면에 남습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.