※ 다음 글은 해당 포스트의 번역본입니다.
setTimeout
은 리액트에서든, 일반 자바스크립트에서든 동일하게 동작합니다.
하지만 리액트에서 setTimeout
을 사용하는 경우엔 인지해야 하는 몇 가지 주의사항이 있는데, 그에 대해 이번 튜토리얼에서 알아 보려고 합니다.
마지막 섹션에선 리액트 훅으로 timeout 기능을 더 잘 핸들링 할 수 있는 방법에 대해 알아 볼 거니깐 끝까지 읽어 주세요.
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>
);
}
}
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>
</>
)
}
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
가 변경되거나 컴포넌트가 언마운트될 때는 언제나 타이머가 클리어 될 것을 보장합니다.