React useCallback 의존성 관리로 인해 벌어질 수 있는 상태 갱신 이슈

sham·2024년 12월 9일
0

SkyScope 개발일지

목록 보기
12/12

개요

React에서 useCallback을 사용할 때, 종종 의존성 배열에 필요한 값을 누락하여 예기치 않은 문제가 발생할 수 있다. 특히, 외부 상태를 참조하거나 초기화된 상태(null 등)를 참조하게 될 경우라던가…

useCallback을 남용하다가 피를 보았다. 의존은 내 생각보다 훨씬 중요한 개념이었다.

장애가 발생할 수 있는 코드

다음 코드는 문제가 있는 코드이다. 어떤 부분에서 장애가 발생할 수 있을까? 곰곰히 생각해보자.

const onClickFooterPlace = useCallback(
    async (clickedFooterPlace: LocateDataType) => {
      if (!map) return;
      const newPlace = { ...clickedFooterPlace } as KakaoSearchType;

      const result = await transLocaleToCoord(clickedFooterPlace.position);
      if (!result) {
        return;
      }

      const { nx, ny, province, city, localeCode } = result;
      const apiLocalPosition = { lat: ny, lng: nx };
      Object.assign(newPlace, { province, city, localeCode, apiLocalPosition, isBookmarked: false });

      dispatch(addToast(newPlace));

      if (currentPlaces.length || bookmarkPlaces.length) {
        // currentPlaces나 bookmarkPlaces에 이미 존재하면 순서를 바꾸고 종료
        // currentPlaces, mapMarkers에 추가하지 않는다.
        if (isSwapPlace(clickedFooterPlace.placeId) !== 0) return;
      }

      setCurrentPlaces(prevCurrentPlaces => [newPlace, ...prevCurrentPlaces]);
      changeOnMapMarker(clickedFooterPlace, 'search');
    },
    [currentPlaces, bookmarkPlaces, mapMarkers],
  );

장애가 발생할 수 있는 코드 - 해답

정답은 onClickFooterPlace의 useCallback 의존성 배열에 map이 없었기 때문이었다.

React는 useCallback의 의존성 배열에 포함된 값이 변경될 때에만 콜백 함수를 새로 생성한다. 하지만 map이 의존성에 포함되지 않아 해당 함수가 최초에 생성할 때의 값이었던 null을 참조하고, map이 업데이트 되더라도 더 이상 갱신하지 않고 있었던 것이다.

useCallback은 기본적으로 클로저의 성질을 이용해서 동작하기 때문에, 의존성 배열에 포함되지 않은 값은 콜백 함수가 최초로 생성될 당시의 스코프에서 캡처된 값을 참조한다. 이는 의존성 배열에 포함되지 않은 값이 state나 props여도 동일하게 적용된다.

const onClickFooterPlace = useCallback(
    async (clickedFooterPlace: LocateDataType) => {
      if (!map) return;
      const newPlace = { ...clickedFooterPlace } as KakaoSearchType;

      const result = await transLocaleToCoord(clickedFooterPlace.position);
      if (!result) {
        return;
      }

      const { nx, ny, province, city, localeCode } = result;
      const apiLocalPosition = { lat: ny, lng: nx };
      Object.assign(newPlace, { province, city, localeCode, apiLocalPosition, isBookmarked: false });

      dispatch(addToast(newPlace));

      if (currentPlaces.length || bookmarkPlaces.length) {
        // currentPlaces나 bookmarkPlaces에 이미 존재하면 순서를 바꾸고 종료
        // currentPlaces, mapMarkers에 추가하지 않는다.
        if (isSwapPlace(clickedFooterPlace.placeId) !== 0) return;
      }

      setCurrentPlaces(prevCurrentPlaces => [newPlace, ...prevCurrentPlaces]);
      changeOnMapMarker(clickedFooterPlace, 'search');
    },
    [currentPlaces, bookmarkPlaces, mapMarkers, map],
  );
profile
씨앗 개발자

0개의 댓글

관련 채용 정보