Next 페이지에서 react에 알맞게 구현된 Hook인 useInterval 사용하기

우디·2024년 3월 4일
0
post-thumbnail

안녕하세요:) 개발자 우디입니다! 아래 내용 관련하여 작업 중이신 분들께 도움이되길 바라며 글을 공유하니 참고 부탁드립니다😊
(이번에 벨로그로 이사오면서 예전 글을 옮겨적었습니다. 이 점 양해 부탁드립니다!)

작업 시점: 2022년 2월

배경

  • 프로젝트에서 타이머 구현이 필요해서 setInterval을 사용할 일이 있었음.
  • setInterval은 함수와 delay를 인자로 받아서 특정 시간마다 함수를 실행해주는 메소드임.
    • 매개 변수
      • func
        • delay(밀리초)마다 실행되는 function. 첫 번째 실행은 delay(밀리초) 후에 발생.
      • code
        • 선택적 구문을 사용하면 함수 대신 문자열을 포함할 수 있음. 모든 delay(밀리초)마다 컴파일되고 실행됨.
        • 주의!) 이 구문은 eval()을 사용하는 것과 같은 이유로 보안상 위험하므로 "권장하지 않음".
      • delay (Optional)
        • 타이머가 지정된 함수 또는 코드 실행 사이에 지연해야 하는 밀리초(1/1000초) 단위의 시간. 지정하지 않으면 기본 값은 0임.
      • arg0, …, argN (Optional)
        • 타이머가 만료되면 func 에서 지정한 함수로 전달되는 추가 인수.
    • 반환 값
      • 반환된 intervalID는 setInterval() 호출로 생성된, 타이머를 식별하는 0이 아닌 숫자 값. 이 값은 clearInterval() (en-US)에 전달되어 interval을 취소할 수 있음.
  • 그런데 react 에서 setInterval이 제대로 작동하지 않았고, 그래서 이를 해결하는 과정을 정리해보려고 함

react 에서 제대로 작동하지 않는 setInterval, 왜 그럴까?

import { useState, useEffect } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1);
    }, 3000);

    return () => clearInterval(timer);
  });

  return <span>{count}</span>;
}

export default App;
  • 이는 리액트와 자바스크립트의 근본적인 차이에서 발생한 문제임
    • 리액트 컴포넌트의 state와 props는 계속해서 변화할 수 있는 값이며, 해당 값들이 변화할 경우 리액트 컴포넌트는 새로 변경된 값을 가지고 리렌더링을 발생시킴.
    • 자바스크립트의 setInterval() 함수는 한 번 설정되면 clearInterval() 함수를 호출하지 않는 이상 변화되지 않으며 초기의 값을 계속해서 기억하고 참조함.
  • => 이렇게 작동 방식이 다르다보니 setInterval이 가장 최신 state와 props를 참조한다고 보장할 수 없는 것임.
  • 또한, 어떤 내부 함수를 감싸는 외부 함수가 실행되고나서 종료되었다 하더라도, 내부 함수에서 외부 함수의 값에 접근할 수 있는 현상인 Closure 때문에 나를 감싸는 외부 함수(setInterval)는 이미 종료되어 사라졌는데, 내(setCount)가 계속 그 값을 기억하고 있어서 제대로 count가 되지 않는 것임.
  • 또한, 브라우저에서 자바스크립트의 실행 과정(아래와 같은)을 보면, 이 과정에서도 setInterval은 종료되었지만, setInterval의 내부 함수인 setCount가 실행될 때마다 그 초기값이었던 0을 기억하고 계속 0+1 만을 함.
    • 자바스크립트는 single thread 언어이기 때문에 자바스크립트 엔진은 한 번에 하나의 작업만 가능함 → 그렇기 때문에 실행 시간이 오래 걸리는 함수를 호출하게 되면 화면이 멈추게 됨 → 브라우저에서는 이러한 문제점을 Web API를 사용해 해결함 → Call Stack 에서 비동기 함수가 호출되면 Web API를 통해 Callback Queue에 쌓이게 되고, 이 Queue는 Call Stack이 비면 쌓아뒀던 함수들을 차례로 실행하는 것임 → setInterval 메소드도 브라우저에서 제공하는 Web API 중 하나임 → 그렇기 때문에 호출되면 바로 실행되지 않고 우리가 등록한 delay 시간을 기다렸다가 Callback Queue에 쌓임 → 그리고 Call Stack이 비면 그때 실행됨 → setInterval은 1회 실행 후 사라짐 → 초기값을 기억하고 있는 setCount가 반복적으로 실행됨.

대표적인 해결 방안

  1. useState의 updater function 형식 활용

    const App = () => {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        let timer = setInterval(() => {
          // setCount(count + 1);
          setCount(count => count + 1);
        }, 1000);
        
        return () => clearInterval(id);
      }, []);
    
      return <span>{count}</span>;
    }
    • setCount(count + 1)을 setCount(count => count + 1) 으로 수정함.
    • setState 내부에 콜백 함수를 사용하는 방식으로 변수가 항상 새로운 상태를 읽어들일 수 있음.
    • 비동기로 동작하는 setState의 변경이 순서대로 일어나게 하기 위한 방법임
    • 하지만 props의 최신 값은 보장하지 못함.
  2. useInterval 활용

    import { useState, useEffect, useRef } from 'react';
    
    function useInterval(callback, delay) {
      const savedCallback = useRef(); // 최근에 들어온 callback을 저장할 ref를 하나 만든다. ref는 저장소임.
    
      useEffect(() => {
        savedCallback.current = callback; // callback이 바뀔 때마다 ref를 업데이트 해준다. 새로운 값을 참조하는 callback으로 업데이트 해준 것.
      }, [callback]);
    
      useEffect(() => {
        function tick() {
          savedCallback.current(); // tick이 실행되면 callback 함수를 실행시킨다.
        }
        if (delay !== null) { // 만약 delay가 null이 아니라면 
          let id = setInterval(tick, delay); // delay에 맞추어 interval을 새로 실행시킨다.
          return () => clearInterval(id); // unmount될 때 clearInterval을 해준다.
        }
      }, [delay]); // delay가 바뀔 때마다 새로 실행된다.
    }
    • 가장 많이 사용되는 듯함.

배우고 느낀 점

  • 핵심적으로 작동 방식이 다르다는 점은 이해가 되는데, 깊게 들어가는 설명들은 다소 낯설게 느껴진다. 리액트와 자바스크립트에 대해 공부하다가 관련 개념이 나오면 다시 이해해보려고 시도해 봐야겠다.
  • 작동 원리나 기본 개념들에 대해 더 파고들며 공부하는 것이 중요한 것 같다. 앞으로 더 파고드는 습관을 길러야겠다.
profile
넓고 깊은 지식을 보유한 개발자를 꿈꾸고 있습니다:) 기억 혹은 공유하고 싶은 내용들을 기록하는 공간입니다

0개의 댓글