반응형

React를 배우다 보면 useEffect의 두 번째 인자인 의존성 배열을 자주 만납니다. 특히 []를 넣으면 “처음 한 번만 실행된다”고 외우기 쉽지만, 실무에서는 이 이해만으로 부족합니다.

useEffect는 렌더링 이후 React 바깥의 시스템과 컴포넌트를 동기화할 때 쓰는 Hook입니다. 여기서 외부 시스템은 타이머, 이벤트 리스너, 네트워크 구독, 브라우저 API처럼 React 밖에서 살아 있는 대상을 뜻합니다.

의존성 배열은 실행 횟수 옵션이 아니다

의존성 배열은 effect가 어떤 값에 반응해야 하는지를 React에 알려주는 목록입니다. props, state, 컴포넌트 안에서 만든 변수나 함수처럼 렌더마다 바뀔 수 있는 값이 effect 안에서 쓰인다면, 그 값은 의존성 후보입니다.

따라서 []는 단순히 “한 번만 실행”이라는 뜻보다, “이 effect는 렌더 스코프의 reactive value 변화에 맞춰 다시 실행될 필요가 없다”는 선언에 가깝습니다.

stale closure는 왜 생길까

문제는 effect 안의 콜백이 오래된 렌더의 값을 붙잡을 때 생깁니다. 예를 들어 interval 안에서 count를 직접 읽으면, 콜백은 최신 count가 아니라 effect가 만들어진 시점의 count를 계속 기억할 수 있습니다.

useEffect(() => {
  const id = window.setInterval(() => {
    setCount(count + 1);
  }, 1000);

  return () => window.clearInterval(id);
}, []);

이 코드는 겉보기에는 1초마다 count를 올릴 것 같지만, count가 오래된 값으로 고정될 수 있습니다. 이것이 stale closure입니다.

functional update로 해결하기

이 경우에는 count를 직접 읽지 않고 functional update를 쓰는 편이 더 적절합니다.

useEffect(() => {
  const id = window.setInterval(() => {
    setCount((current) => current + 1);
  }, 1000);

  return () => window.clearInterval(id);
}, []);

setCount((current) => current + 1) 형태로 작성하면 React가 최신 상태 값을 전달해 줍니다. effect 내부에서 count를 직접 읽지 않으므로 []와도 자연스럽게 맞습니다.

의존성을 넣을지, 패턴을 바꿀지 판단하기

좋은 기준은 하나입니다. 이 값이 바뀌면 외부 시스템도 다시 맞춰야 하는가?

예를 들어 채팅방 ID가 바뀌면 새 채팅방에 다시 연결해야 합니다. 이런 경우에는 의존성에 값을 넣어야 합니다. 반대로 interval처럼 이전 상태를 기준으로 누적만 하면 되는 경우에는 functional update가 더 단순합니다.

객체와 배열 의존성도 조심해야 한다

React는 의존성을 Object.is로 비교합니다. 객체나 배열을 컴포넌트 렌더링 중에 새로 만들면 내용이 같아도 참조가 달라집니다. 그래서 객체를 의존성 배열에 그대로 넣으면 effect가 필요 이상으로 반복될 수 있습니다.

이때는 객체를 effect 안으로 옮기거나, 필요한 원시값만 의존성으로 분리하거나, 정말 필요한 경우에만 참조를 안정화하는 방식으로 정리해야 합니다.

마무리

useEffect의 의존성 배열은 성능을 위해 실행 횟수를 억지로 줄이는 장치가 아닙니다. effect가 읽는 값과 외부 시스템 사이의 동기화 계약입니다.

[]를 무조건 “처음 한 번”으로 외우기보다, effect가 실제로 어떤 값을 읽는지 먼저 봐야 합니다. 값 변화에 맞춰 다시 동기화해야 하면 의존성에 넣고, 최신 상태를 기준으로 누적만 하면 functional update 같은 React의 상태 갱신 패턴을 쓰면 됩니다.

참고

반응형