은 사용이 안 된다.
setinterval은 interval 주기를 원하는대로 정해서 일정한 간격마다 지정한 함수나 원하는 동작을 실행하게 만들어준다. 자바스크립트에서는 원래 사용하던대로 사용하니 정상적으로 작동이 됐었다... 그리고 아무 생각 없이 그것을 그대로 React에 적용하니, 원하는 결과를 얻을 수 없었다.
사용자가 콘텐츠를 결제하고 들어오면, 영상 강의를 볼 수 있는데 이때 영상에 play 이벤트가 실행되면 10초마다 patch로 서버를 호출해 사용자가 그동안 얼만큼 해당 강의를 들었는지 체크하는 로직이었다.
정말 정말... 열받는 상황이었다. 10초 마다 setinterval을 실행 했는데, 어떤 때는 원하는대로 동작이 됐고 어떤 때는 pause를 눌렀음에도 setinterval이 계속 실행되고 있었다.
그 '어떤 때'를 찾기 위해 정말 여러번 setinterval과 clearinterval을 실행했다.
그리고 그 어떤 때를 찾지 못 했다.... 어떤 때라도 알았더라면 접근이 쉬웠을텐데 정말 랜덤하게 됐다, 안 됐다 하는 것이 아닌가!
'어떤 때' 찾기 실패!
리액트의 리렌더링 때문이다. setinterval에 의해 변화가 일어나면 리액트는 리렌더링을 하는데 이것 때문에 clearinterval이 제대로 작동하지 않는 것이다.
useEffect를 사용하고 의존성 배열을 비워 초기 렌더링 됐을 때만, setinterval이 실행될 수 있도록 한다.
=> 결과적으로 실패했다. 처음의 setinterval이 제대로 clear 되지 않고 setinterval이 계속 중첩되어 쌓여갔다.
원인조차 제대로 파악이 어려워 며칠을 붙잡고 있으면서 useInterval 커스텀 훅이란 것을 발견했다.
기본적인 사용 방법과 실제로 내가 쓴 방법을 같이 보여주고자 한다.
먼저 useInterval 커스텀 훅을 위한 파일을 하나 만들었다.
그리고 한줄 한줄 살펴보며 코드를 해석해보자.
import {useRef, useEffect} from "react"
export function useInterval(callback:()=>void, delay:number | null) {
const savedCallback = useRef<typeof callback>(callback);
useEffect(()=>{
savedCallback.current = callback;
},[callback]);
useEffect(()=>{
const tick = () => {
savedCallback.current();
}
if(delay !== null){
let interval = setInterval(tick, delay);
return () => clearInterval(interval);
}
},[delay])
}
- useinterval의 기본구조는 인자로 callback과 delay를 받는다.
- savedCallback은 왜 있을까?
useRef를 사용하여 리렌더링을 방지하기 위해 존재한다. useRef를 사용하면 ref로 초기화한 ref 객체를 반환하는데, 이는 useRef로 관리하는 값이 변경되어도 리렌더링 되지 않는다.
만약 useState로 callback을 저장한다면, savedCallbacK의 값이 변경될 때마다 리렌더링이 일어나게 되고, 계속해서 초기값만 가져오게 된다.- useRef에 저장한 savedCallback을 왜 useEffect로 또 실행하지?
savedCallback 함수는 callback의 데이터가 바뀔 때마다 실행되어 새로운 callback으로 savedCallback이 대체된다. 즉, callback이 바뀔 때마다 새로운 callback으로 대체하기 위해 사용한다고 생각하면 된다.
왜죠?
- useEffect의 의존성 배열에 callback이 없으면 리렌더링 될때마다 실행된다. 원하는 결과가 아니다.
- useEffect의 의존성 배열에 빈 배열이 있다면, 초기에만 실행된다. 바뀔 때마다 실행이 안 된다.
- useRef에 저장한 savedCallback을 useEffect내에서 실행하자!
조건문을 활용하여 delay가 null이 아닐 때만, savedCallback이 setinterval로 실행될 수 있도록한다.
얻을 수 있는 결과는 다음과 같다.
- callback이 바뀔 때마다 새로운 callback으로 갱신되기 때문에, setinterval이 중첩되지 않는다.
- delay가 의존성 배열로 있기 때문에 useEffect가 계속 실행되는 것을 방지한다.
useInterval을 찾아보면서 고생했던 점은 세 가지였다.
useInterval에 대한 이해 / 타입스크립트에 함께 적용하기 / event와 함께 실행되게 하기
대부분의 자료나 블로그는 이벤트가 아닌 일정 숫자가 되면 clear 되게 하는 예제가 많았기 때문이다.
const [isPlaying, setIsPlaying] = useState(false);
useInterval(()=> {
return axios.patch(`api`)
.then(res=>res)
.catch(error => error)
}, isPlaying ? 10000 : null)
const play = useCallback(() => {
setIsPlaying(true);
}, [])
const stop = useCallback(() => {
setIsPlaying(false);
}, [])
return (
<video
play={play}
stop={stop}/>
위에서 정의한 useInterval을 비디오플레이하는 컴포넌트에 import 한 다음 위에 처럼 활용하였다.
useState로 playing 상태값을 관리하여 비디오가 플레이되면 delay가 10000이 되고 플레이 되지 않으면(pause, ended) playing을 false로 바꿔 delay가 null이 된다.
그래서 useInterval이 null일 때는 실행이 되지 않는 점을 이용하였다!