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],
);