IT/React & Next.js

[React/TypeScript] setInterval 안에서 state가 변경되지 않을 때 (useInterval 사용법)

땅일단 2023. 12. 16. 23:04
// Timer.tsx

const [currentSeconds, setCurrentSeconds] = useState<number>(0);
const [isReverse, setIsReverse] = useState<boolean>(false);

useEffect(() => {
    setInterval(() => {
        if (!isReverse) {
            setCurrentSeconds(prevState => prevState + 1);
        } else {
            setCurrentSeconds(prevState => prevState - 1);
        }
    }, 1 * 1000);
});

 

위처럼 isReverse가 false일 때는 1초마다 currentSeconds가 1씩 증가하고, true일 때는 1씩 감소하는 코드가 있습니다.

 

isReverse가 false에서 true로 변하면 변한 순간부터 1씩 감소하리라 생각되지만,

실제로 결과를 보면 setInterval 외부에서 isReverse의 값을 false에서 true로 변경했더라도 currentSeconds는 계속 1씩 증가합니다.

 

 

 

게다가 불편한 점은 또 있는데요, setCurrentSeconds에서 prevState라는 값을 사용했습니다.

 

위 코드에서

setCurrentSeconds(prevState => prevState + 1); 이란 코드를

setCurrentSeconds(currentSeconds + 1); 로 바꾸면 어떻게 될까요?

 

이전 루프에서 갱신했던 state 값이 아닌 초기의 state 값을 불러왔기 때문에 currentSeconds라는 state 값은 계속 0을 유지할 겁니다.

이렇듯 setInterval 안에서는 변경된 state의 값이 반영되지 않는다는 불편한 점이 존재합니다.

 

 

 

왜 이런 문제가 발생할까요?

이는 자바스크립트의 클로저(Closure)라는 특징 때문입니다. 외부 변수(여기선 currentSeconds)의 값을 가져올 때는 최초에 참조했던 값인 '클로저'(여기선 0) 를 계속해서 참조하려는 특성 때문인데요, prevState라는 콜백 함수를 currentSeconds 대신 파라미터로 넘겨 주면 0이라는 값 대신 콜백 함수가 클로저가 되기 때문에 변경된 값을 가지고 올 수 있는 것이라고 합니다.

 

 

 

이런저런 불편요소가 많은 setInterval입니다. 그래서 Dan abramov라는 분이 setInterval의 단점을 보완한 useInterval이라는 커스텀 훅을 만들었다고 합니다.

 

Making setInterval Declarative with React Hooks — overreacted

If you played with React Hooks for more than a few hours, you probably ran into an intriguing problem: using setInterval just doesn’t work as you’d expect. In the words of Ryan Florence: I’ve had a lot of people point to setInterval with hooks as som

overreacted.io

잘 생기셨군요.

 

useInterval은 setInterval의 또 하나의 단점 중 하나인 렌더링도 보완했다고 하는데요,

계속해서 리렌더링을 하는 setInterval과 달리, useInterval은 useRef를 사용함으로써 렌더링 횟수를 줄여준다고 합니다.

바로 한번 useInterval을 사용해 보았습니다.

 

 

 

// useInterval.tsx

import {useEffect, useRef} from "react";

export const useInterval = (callback: () => void, interval: number) => {
    const savedCallback = useRef<(() => void) | null>(null);

    useEffect(() => {
        savedCallback.current = callback;
    });

    useEffect(() => {
        function tick() {
            if (savedCallback.current) {
                savedCallback.current();
            }
        }

        let id = setInterval(tick, interval);
        return () => clearInterval(id);
    }, [interval]);
};

 

위와 같은 커스텀 훅을 정의해 주면 useInterval을 쓸 준비는 끝납니다.

이제 setInterval을 사용했던 코드를 useInterval로 바꿔 봅시다.

 

 

 

// Timer.tsx

const [currentSeconds, setCurrentSeconds] = useState<number>(0);
const [isReverse, setIsReverse] = useState<boolean>(false);

useInterval(() => {
    if (!isReverse) {
        setCurrentSeconds(currentSeconds + 1);
    } else {
        setCurrentSeconds(currentSeconds - 1);
    }
}, 1 * 1000);

 

prevState를 쓰지 않아도 되고, isReverse의 값이 변하면 분기도 잘 나눠지는 결과를 볼 수 있습니다.