[React] useInterval 을 깊이 분석해보자

    728x90
    반응형

     

     

    useInterval

    useInterval은 callback, delay를 인자로 받아서 setInterval이 호출할 함수와 setInterval의 간격/멈춤을 조절할 수 있는 커스텀훅 입니다.

    이에 관해 강의를 보다가 그냥 이런게 있구나 하고 쓰려다가,

    왜? 이렇게 만들었지? 이렇게 만들면 코드도 짧고 편하지 않나? 라고 생각했다가

    그 이유를 알게 되면서

    아 이거는 좀 알아둬야 나중에 비슷한 경우가 생기더라도

    코드를 분석하고 이해할 수 있겠다라고 생각해서 블로그에 남기기로 했다.

     

     

     

    useInterval 코드 분석

    // useInterval.js
    function useInterval(callback, delay) {
    	const savedCallback = useRef();
    
        useEffect(() => {
        	savedCallback.current = callback;
        });
    
        useEffect(() => {
            function tick() {
        		savedCallback.current();
        	}
    
            if (delay !== null) {
                let id = setInterval(tick, delay);
                return () => clearInterval(id);
            }
        }, [delay]);
    
        return callbackF;
    }
    
    export default useInterval;

     

    위 코드가 useInterval 코드이다.

    코드가 어떻게 저렇게 짜여졌는지 알아보자.

    여기서 하나의 경우를 전재하고 가자. ==> callback 함수는 state를 바꿔주는 함수로 쓰인다.

     

    • 처음 useEffect에서 useRef로 만든 ref객체에 부모컴포넌트에서 내려받은 callback함수를 참조시킨다 ->
    • 두번째 useEffect에서 ref개체의 callback함수를 setInterval로 delay시간 마다 호출시킨다 ->
    • callback함수는 state를 업데이트해주는 함수기 때문에 부모의 컴포넌트를 리랜더링 시킨다 ->
    • 함수 컴포넌트는 리랜더링하게 되면 함수컴포넌트내의 모든것이 같이 리랜더되기 때문에 callback함수는 초기화 된다 or 다시 callback함수로 선언된다. ( === 1번 콜백함수와 새로 선언된 2번 callback 함수는 다른 함수가 된다.) ->
    • 새로 랜더링되면 useInterval함수도 최신의 callback함수를 내려받으며 실행된다 ->
    • 여기서는 delay에만 영향을 받는 2번째 useEffect를 제외한 어떤 리랜더링 상황에도 같이 재호출해주는 1번 useEffect의 콜백함수가 실행된다 ->
    • ref객체는 최신의 callback함수를 참조하게 된다 ->
    • setInterval에서 매 delay동안 호출해주고 있는 함수는 ref객체에 참조된 callback함수이므로 최신callback함수로 매번 호출 시키게 된다 ->
    • delay가 바뀌게 되어 최초기화 함수를 실행하는 상황이 있다고 가정했을때!
    • 2번 useEffect가 처음실행될때 -> 순서대로 로직 실행 -> 그 순간의 스코프저정한 초기화 함수 킵 ->
    • dealy 가 업데이트된다 -> 2번째 useEffect함수의 콜백함수가 재 실행되는데 초기화 함수가 먼저 발생 하고, 그다음 처음부터 내려가면서 실행된다. (useEffec에서 초기화 함수는 의존성배열에 있는 state가 바뀌게 되면 그때의 스코프를 저장해뒀다가 useEffec의 콜백함수가 재 실행될때 제일 먼저 실행되고 나머지 로직들디 순서대로 실행되게 된다.) ->
    • 즉 2번 useEffect 첫 실행 -> 순서대로 로직 실행 -> 초기화함수 킵 ->  delay 업데이트 -> 킵해뒀던 초기화 함수 실행 -> 2번 useEffect의 콜백함수 실행 -> 순서대로 로직 실행 -> setInterval함수 실행 -> 현재의 스코프를 저장한 클리어함수 킵 -> 혹시 한번더 바껴서 2번 useEffect 한번더 실행되게 되면 클리어함수 먼저 실행 후 나머지 로직 순서대로 실행
    • 그래서 만약 delay가 null로 업데이트되게 되면(처음 실행에서 setInterval 는 호출한 상황) -> 2번 useEffect 실행 -> 첫 호출에 킵해뒀던 클리어함수 실행(setInterval clear) -> 나머지 로직 순서대로 실행 -> delay가 null 이기 때문에 setInterval 재 호출 실패 -> setInterval 멈춤... 이렇게 된다.

    간단히 말해서 위 커스텀훅은 callback함수를 최신화 시켜줘서 리랜더링 되더라도 setInterval이 호출하는 callback 함수를 최신의 것으로 유지할 수있고, delay의 상태에 따라서 setInterval을 멈출수 있는 커스텀훅 입니다.

     

     

    와 이렇게 정리해서 순서대로 써보니 엄청 길다 하지만 이해해보면 아하고 넘어갈 수 있을 것 입니다.

    이걸 이해하게 되면 왜 아래의 코드들이 제대로 실행이 안되거나, 버그가 생기거나 하는지 알 수 있습니다.

    우선 중요한 개념인 ref를 먼저 알아보고 다른 코드들을 알아봅시다.

     

     

     

    useRef

    • useRef는 주로 DOM 요소에 대한 참조를 저장하거나, 데이터를 컴포넌트의 라이프사이클 동안 일관되게 유지할 필요가 있을 때 사용됩니다.
    • useRef는 .current 속성을 통해 참조되는 값에 대한 컨테이너로 작동합니다.
    • useRef로 생성된 객체가 업데이트되더라도, 이는 React의 렌더링 사이클에 영향을 주지 않습니다.
    • 즉, useRef는 렌더링 사이에 값이 유지되길 원할 때 사용하되, 그 값의 변경이 리랜더링을 발생시키지는 않습니다.
    • useRef는 컴포넌트의 전체 생명주기 동안 동일한 참조(ref 객체)를 유지합니다. 이를 통해 값이나 DOM 요소에 대한 참조를 저장할 수 있으며, 이 값들은 컴포넌트가 리렌더링되더라도 초기화되지 않습니다.
    • 리랜더링되더라도 useRef의 값은 그대로 유지되고, uesRef값을 수정하더라도 리랜더링 되지 않는다.
    • useRef를 사용하는 이유는 callback 같은 함수가 변경되어도 이전의 참조를 유지하면서 최신 함수를 참조할 수 있도록 하기 위함입니다. 이는 타이머나 이벤트 리스너 같은 곳에서 특히 유용합니다.

     

     

    반응형

     

     

    첫번째 잘못된 코드

    function useInterval(callback, delay) {
        useEffect(() => {
            if (delay !== null) {
                let id = setInterval(callback, delay);
                return () => clearInterval(id);
                }
            }, [delay]);
    
        return savedCallback.current;
    }
    
    export default useInterval;

     

    위 코드 callback함수를 받아와서 setInterval을 작동 시키고, delay를 받아와서 반복의 속도, 멈춤을 조절하기 위해 짠 코드입니다.

    그러나 여기서 문제는 callback함수가 state를 업데이트 시켜줘서, 리랜더링 되는 경우에

    callback은 새로운 함수로 선언되게 되고 useInterval은 그 새로운 callback을 다시 받아오게 됩니다.

    그러면 기존에 이미 setInterval로 반복시키고 있던 이전의 callback함수는 최신의 callback함수로 바꿀수 있는 기능이 없기 때문에, 기존의 callback함수를 계속 반복시키고 있게 됩니다.

    그렇게 되면,

    최신 callback함수가 state를 변경시켜주고 그로인해 부모컴포넌트가 리랜더링되면 화면을 바꿔주는데,

    이전 callback함수만 계속 실행되기 때문에 현재 부모컴포넌트의 state에 영향을 주지 않는다.

    그렇기에 반복문은 계속 실행되지만 아무 변화가 없는것처럼 보이게 됩니다.

     

     

     

     

     

    두번째 잘못된 코드

    function useInterval(callback, delay) {
        useEffect(() => {
            if (delay !== null) {
                let id = setInterval(callback, delay);
                return () => clearInterval(id);
            }
        }, [delay, callback]);
    
        return callback;
    }
    
    export default useInterval;

     

    위 코드는 첫번째 잘못 코드에서의 실수를 만회하기 위해서 useEffect에 종속되는 배열로 callback을 추가시켜주었습니다.

    그래서 callback함수가 state를 업데이트하고, 최신의 callback으로 재 선언되어 내려와도 setInterval이 호출시켜주고 있는 함수는 최신의 것으로 유지될 수 있습니다.

    그 이유는

    • 첫 랜더링 -> useEffect 실행 -> 코드 순서대로 실행 -> setInterval 실행 -> callback 실행 -> state 변경 -> 클리어함수 킵 -> state가 업데이트되면서 부모함수 리랜더링 -> callback함수 재선언 -> useInterval 재실행 -> 최신 callback 받아옴 -> 종속된 callback변화를 알아채고 useEffect의 콜백함수 재실행 -> 킵해둔 클리어함수 실행 -> 나머지 코드 순서대로 실행
    • 이런식으로 진행되면 setInterval 샐행 -> callbak함수 최신화  -> clear -> setInterval 샐행 -> callbak함수 최신화  -> clear -> ....
    • 반복적으로 일어나면서 화면을 기대한대로 랜더하게 됩니다.

    그렇지만 callbak함수가 최신화 되고, setInterval을 초기화 시켜주고 다시 실행하면서 딜레이가 발생할 수 있는데...

    처음 호출
    0초 뒤 setInterval 실행
    0.1초 뒤에 callback 새로운걸로 바뀜
    setInterval 멈춤
    
    2.1초 뒤 setInterval 실행
    2.2초 뒤에 callback 새로운걸로 바뀜
    setInterval 멈춤
    
    3.2초 뒤 setInterval 실행
    3.3초 뒤에 callback 새로운걸로 바뀜
    setInterval 멈춤

     

    위 코드처럼 딜레이가 발생할수 있다.

    이런 문제를 해결하는 가장 좋은 방법은 clearInterval 을 시키지 않는것다. (초기화 함수에 들어있는)

    매번 callback함수가 바뀌면서 set! callback Change! clear! set! callback Chang! clear! 이런 순서가되니까 딜레이가 안생기는것도... 이상할지도...?

     

    이렇게 useInterval이라는 커스텀훅을 깊에 알아봤는데

    덕분에 useRef와 useEffect함수의 실행, 초기화 함수 이런 부분을 좀 더 잘 알수있게 된

    계기가 된것 같다.

    다음번에 이런 함수가 나오면 해석하거나,

    이런식으로 커스텀훅을 짜보게 되면 좋겠다.

    중고신입화이팅!

     

     

     

     

     

     

     

     

     

     

    참조!

    https://www.inflearn.com/course/lecture?courseSlug=web-game-react&unitId=21608&category=questionDetail&tab=curriculum

     

    학습 페이지

     

    www.inflearn.com

     

    728x90
    반응형

    댓글