Published on

[회고록][트러블슈팅] Stale Closure

Authors
  • avatar
    Name
    Jihwan Seong
    Twitter

배경

  • react에서 예상치 않게 상태가 동작하는 상황이 발생
  • 상태가 예상치 않게 동작한 원인을 파악하고 해결법을 찾아보는 시간을 갖는다.

트러블 슈팅

Stale Closure 이슈

상황

  • 컴포넌트 함수에서 정의한 object state를 클로저 함수에서 사용.
  • 클로저 함수가 호출되면, object state를 사용 한뒤, 갱신한다.

문제

  • 클로저에 의해 object state를 갱신하지만, 이후에 다시 호출된 클로저 함수는 갱신된 state를 사용하지 않고 갱신전 state를 계속해서 사용함.
    const [findResult , setFindResult] = useState<FindPaintingResult>(props.findResult);
    const searchParam = useSearchParams();

    const loadMorePainting = async ()  => {

        if(!findResult.isMore || isLoadingRef.current){
            console.log(`return [loadMorePainting]`);
            return;
        }


        isLoadingRef.current = true;

        ...

        const result : FindPaintingResult = await findPainting(searchTitle,searchArtist,searchTags,searchStyles,findResult.pagination+1);
        console.log(`update setFindResult, page=${findResult.pagination}, update to ${result.pagination}`);

        setFindResult(result);

        setSearchPaintings(prev=> [...prev, ...result.data]);

        console.log(`setIsLoading : false. previous : ${isLoadingRef.current}`);
        isLoadingRef.current = false;
    }

      // 스크롤 이벤트 핸들러
    useEffect(() => {

        const handleScroll = () => {
            console.log(`call handleScroll`);
        if (
            window.innerHeight + window.scrollY >= document.body.offsetHeight - 500 &&
            !isLoadingRef.current
        ) {
            console.log(`call loadMorePainting`);
            loadMorePainting();
        }
        };

        console.log('[useEffect] : for config scroll event');
        window.addEventListener("scroll", handleScroll);
        return () => window.removeEventListener("scroll", handleScroll);
    }, [findResult]);

해결방법

1. useState 대신 useRef 사용

  • 직접적으로 JSX 컴포넌트 렌더링에 필요치 않은 데이터이므로, useRef를 사용한다.
  • 특징
    • 클로저 함수가 ref를 캡처하므로, 데이터가 갱신되도 클로저는 동일한 ref를 통해 갱신된 값을 사용 가능
    • state일 때보다 즉각적으로 값이 반영됨
    • 리렌더링을 발생시키지 않음

2. useEffect dependency 활용

  • useEffect를 활용하여 상태가 변경될 때 마다, 클로저 함수가 변경된 상태를 다시 캡처하도록 한다.
  • 특징
    • .. 솔직히 무슨 장점이 있는지는 모르겠음...

3. setState 함수에 콜백 함수 넘기기

  • setState 함수에 콜백을 넘기면, 콜백함수의 인자는 항상 최신 상태를 참조하게 됨
  • 현재 로직은 상태 변경시 이전상태에 의존하므로, 항상 최신 상태를 참조하게되면 문제가 해결됨.

선택

  • 1번 방법을 선택하였다.
  • 스크롤링 다운 이벤트는 매우빠르게 여러번 발생하므로, 즉각적으로 데이터가 변경될 필요가 있다.
  • 2번 방법 적용시, 상태(데이터)가 변경되지 않았지만 isLoadingRef는 즉각적으로 변경되어 loadMorePainting() 함수가 호출된다.

Ref