본문으로 건너뛰기
개발 머꼬
개발 노트React
hohyeon.dev18

의존성 배열을 정직하게 채웠더니 Effect가 무한히 도는 이유

  • #Common Pitfall
  • #Engineering Note
  • #React

문제 발생

린터가 지적한 대로 의존성을 채웠더니 요청이 멈추지 않았습니다.

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를 계속 쓰는 다른 버그로 바뀝니다.

해결 방안

  1. 바뀌지 않는 값이면 컴포넌트 바깥으로 뺍니다. 문서가 첫 번째로 제시하는 방법입니다 — props나 state에 의존하지 않는 객체는 매 렌더 다시 만들 이유가 없습니다.

  2. 동적인 객체는 Effect 안에서 만듭니다. 그러면 의존성이 원시값만 남습니다.

useEffect(() => {
  const options = { query, page };
  fetchItems(options).then(setItems);
}, [query, page]);
  1. props로 객체를 받는다면 필요한 값만 꺼냅니다. 문서의 세 번째 방법입니다.
const { query, page } = options;
useEffect(() => {
  fetchItems({ query, page }).then(setItems);
}, [query, page]);
  1. useMemo/useCallback은 마지막 선택지입니다. 부모에서 객체를 메모이제이션해 넘기는 것도 동작하지만, 부모가 그 사실을 계속 기억해야 하는 결합이 생깁니다. 원시값으로 좁히는 쪽이 대개 더 단순합니다.

  2. Effect 자체가 필요한지 먼저 묻습니다. 사용자의 입력에 반응해 데이터를 가져오는 코드는 이벤트 핸들러나 프레임워크의 데이터 로딩(예: Server Component, 라우터 로더)이 더 맞는 자리일 때가 많습니다.

  3. 정리 함수를 잊지 않습니다. 이 Effect는 요청 경합에도 노출되어 있습니다. 응답이 도착 순서와 다르게 오면 오래된 결과가 화면에 남습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.