[React]리액트 컴포넌트에서 setTimeout 사용하기

Jay·2022년 5월 2일
2

※ 다음 글은 해당 포스트의 번역본입니다.

setTimeout은 리액트에서든, 일반 자바스크립트에서든 동일하게 동작합니다.

하지만 리액트에서 setTimeout을 사용하는 경우엔 인지해야 하는 몇 가지 주의사항이 있는데, 그에 대해 이번 튜토리얼에서 알아 보려고 합니다.

마지막 섹션에선 리액트 훅으로 timeout 기능을 더 잘 핸들링 할 수 있는 방법에 대해 알아 볼 거니깐 끝까지 읽어 주세요.

목차

1. 리액트에서의 setTimeout 사용법

setTimeout 함수는 두 가지 인수를 필요로 합니다. 하나는 우리가 실행하려는 콜백함수이고, 다른 하나는 콜백함수가 호출되기 까지의 지연 시간을 밀리초(ms) 단위로 받습니다.

setTimeout(() => console.log('Initial timeout!'), 1000);

리액트에서도 동일한 방식으로 작성하면 됩니다. 하지만 함수 컴포넌트 내의 아무곳에나 위치하지 않도록 조심하세요. 그렇지 않으면, 리렌더링 때마다 작동하게 될 테니깐요.

만일 컴포넌트가 마운트 되는 시점에 딱 한 번 실행되기를 원한다면, useEffect를 통해 다음과 같이 사용하면 됩니다.

useEffect(() => {
  const timer = setTimeout(() => console.log('Initial timeout!'), 1000);
}, []);

언제나 타이머를 클리어하자

컴포넌트가 언마운트될 때 타이머를 클리어하지 않으면, 컴포넌트가 보이지 않는 상태임에도 불구하고 콜백함수가 작동할 지 몰라요.

이는 어플리케이션에서의 메모리 부족 현상으로 이어질 수 있습니다. 컴포넌트가 언마운트 된 이후에도 타이머는 활성화되어 있기 때문에, 가비지 콜렉터가 컴포넌트를 수집하지 않을 겁니다.

이런 경우, 콘솔창에 아래와 같은 에러 메시지가 나타날 겁니다.

타이머를 클리어하기 위해선, setTimeout의 반환값을 가지고 clearTimeout을 호출해야 합니다.

const timer = setTimeout(() => console.log('Initial timeout!'), 1000);
clearTimeout(timer);

함수형 컴포넌트

우리는 컴포넌트가 언마운트될 때 실행할 함수를 위해 useEffect 사용합니다. 언마운트될 때 실행할 함수는 콜백함수로써 반환시키면 됩니다. 우리는 이 함수를 이용해 컴포넌트가 마운트 될 때 생성됐던 타이머를 클리어할 겁니다.

useEffect(() => {
  const timer = setTimeout(() => console.log('Initial timeout!'), 1000);
  return () => clearTimeout(timer);	// 타이머 클리어
}, []);

하지만 useEffect의 밖에서 타이머를 생성하고 싶은 경우는 어떻게 해야 할까요?

아래와 같은 방식으로는 작동하지 않습니다. 왜냐하면 timer는 리렌더링 때마다 재정의될 거거든요.

let timer;
const sendMessage = (e) => {
  e.preventDefault();
  timer = setTimeout(() => alert('Hey ??'), 1000);
}

useEffect(() => {
  // 타이머는 다음 리렌더링 이후에 또다시 undefined가 됩니다.
  return () => clearTimeout(timer);
}, []);

리액트의 state를 이용하는 것도 불가능합니다. 왜냐하면 state의 변경이 리렌더링을 유발하니깐요. 우리는 state를 사용하지 않고도 어디서든 값을 유지할 변수가 필요합니다.

useRef가 딱 그러하죠.

timerRef.current에 타이머를 할당하고, 컴포넌트가 언마운트될 때 해당 값에 접근하도록 합니다.

const timerRef = useRef(null);
const sendMessage = (e) => {
  e.preventDefault();
  timerRef.current = setTimeout(() => alert('Hey ??'), 1000);
}

useEffect(() => {
  // 컴포넌트가 언마운트될 때 interval을 클리어합니다.
  return () => clearTimeout(timerRef.current);
}, []);

비로소 우리는 컴포넌트가 마운트되는 시점에 타이머 함수가 딱 한번 실행될 것을 확신할 수 있어요.

클래스형 컴포넌트

리렌더링 사이에 state값을 유지하기는 클래스형 컴포넌트가 훨씬 쉽습니다. 아래와 같이 타이머를 위한 새로운 클래스 속성을 생성했습니다. 그런 다음 componentWillUnmount라는 생명주기 함수를 이용해 타이머를 클리어하고 있죠.

class App extends Component {
  timer;

  sendMessage = (e) => {
    e.preventDefault();
    this.timer = setTimeout(() => alert('Hey ??'), 1000);
  }

  componentWillUnmount() {
    clearTimeout(this.timer);
  }

  render() {
    return (
      <button onClick={this.sendMessage}>
        Send message
      </button>
    );
  }
}

2. setTimeout 안에서의 state 사용하기

setTimeout 의 콜백함수 안에서 상태변수를 사용하는 건 직관적이지 않습니다.

아래의 예제코드를 봅시다. 당신이 input란에 메시지를 써 넣고, "Send message" 를 클릭하면 2초 후에 alert를 띄우죠.

const App = () => {
  const [message, setMessage] = useState('');
  const handleChange = (e) => {
    e.preventDefault();
    setMessage(e.target.value);
  };

  const sendMessage = (e) => {
    e.preventDefault();
    setTimeout(() => {
      alert(message);
    }, 2000);
  };

  return (
    <>
      <input onChange={handleChange} value={message} />
      <button onClick={sendMessage}>
        Send message
      </button>
    </>
  );
};

만약 "Send message"를 클릭한 직후 input값을 변경한다면, 타이머 함수는 업데이트된 값을 보여줄까요 아니면 당신이 버튼을 클릭했을 시점에 저장하고 있던 마지막 값을 보여줄까요?

이곳에서 테스트할 수 있습니다.

깃헙 이슈에서 볼 수 있듯이, setTimeout은 처음에 호출되었던 값을 사용합니다. 우리의 예제에선, 버튼을 클릭했을 때 보여진 메시지 외의 다른 메시지는 보내고 싶지 않을 때라고 이해해도 되겠네요.

가장 최신의 값을 갖고 오길 원한다면, 우리는 참조값을 사용해야 해요.

먼저 useRef 훅을 이용해 새로운 참조 변수를 생성하고, useEffect를 이용해서 message 변수의 변화를 감지하도록 합니다.

변수값이 바뀔 때마다, 참조값에 상태값을 할당할 겁니다.

그런 다음, 타이머가 가장 최신의 값을 가져올 수 있게 상태변수 대신 참조값을 사용하도록 합니다.

const App = () => {
  const messageRef = useRef('');
  const [message, setMessage] = useState('');

  useEffect(() => {
    messageRef.current = message;
  }, [message]);

  const handleChange = (e) => {
    e.preventDefault();
    setMessage(e.target.value);
  };

  const sendMessage = (e) => {
    e.preventDefault();
    setTimeout(() => {
      // 가장 최신의 값
      alert(messageRef.current);
    }, 2000);
  };

  return (
    <>
      <input onChange={handleChange} value={message} />
      <button onClick={sendMessage}>
        Send message
      </button>
    </>
  )
}

3. useTimeout 훅을 이용한 선언적 timeouts

useInterval 훅처럼 useTimeout 커스텀 훅을 생성하는 것은 리액트에서의 타이머 작업을 더욱 쉽게 만들어 줍니다.

타이머의 생성과 클리어 과정을 추상화하면 해당 함수를 더욱 관리하기 쉽고 안전하게 만듭니다. 타이머를 클리어해야 한다는 걸 매번 기억할 필요가 없어지기 때문이죠.

useTimeout(() => {
  // Do something
}, 5000);

setTimeout처럼, 이 훅은 콜백과 숫자를 받습니다.

숫자를 null 값으로 설정하는 건 타이머를 무효화하고, 컴포넌트가 언마운트될 때 자동적으로 타이머를 취소합니다.

이 훅의 코드는 아래와 같습니다.

import { useEffect, useRef } from 'react'

function useTimeout(callback, delay) {
  const savedCallback = useRef(callback)

  // callback이 변경될 시, 가장 최신의 것을 기억합니다.
  useEffect(() => {
    savedCallback.current = callback
  }, [callback])

  // 타이머 설정
  useEffect(() => {
    // 지연시간이 구체적이지 않다면, 스케쥴링을 하지 않습니다.
    if (delay === null) {
      return
    }

    const id = setTimeout(() => savedCallback.current(), delay)

    return () => clearTimeout(id)
  }, [delay])
}

useEffect가 반환하는 콜백함수 안에서 해당 훅이 어떻게 타이머를 클리어하는지 주목하세요. 이 방법은 delay가 변경되거나 컴포넌트가 언마운트될 때는 언제나 타이머가 클리어 될 것을 보장합니다.

profile
개발할래요💻

0개의 댓글