Front-end/React

useInterval 커스텀 훅

NiceKHJ 2025. 11. 27. 17:44

useInterval 커스텀 훅 사용 이유

 

문제점 : 

  • React에서 setInterval을 그냥 쓰면 클로저 문제 발생 한다
    state가 업데이트되어도 interval 안에서는 옛날 값을 참조하기 때문에
  • 메모리 누수 발생할 수 있다.
    cleanup을 제대로 안 하면 컴포넌트가 unmount 되어도 interval이 계속 실행되기 때문에
  • 코드 복잡도가 증가함
    useEffect 안에서 setInterval과 clearInterval을 매번 작성해야하기 때문에

useInterval의 장점 :

  • state 최신값을 항상 참조
  • 자동 cleanup으로 메모리 누수 방지
  • delay를 null로 바꾸면 일시정지 가능
  • 재사용 가능한 깔끔한 코드

 

일반 setInterval의 문제점 (예시)

function Counter() {
	const [count, setCount] = useState(0);
    
    useEffect(()=>{
    	const id = setInterval(()=>{
        	console.log(count); // 항상 0만 출력
            setCount(count + 1) //count가 업데이트 안됨
        },1000)
    	
        return ()=> clearInterval(id)
    },[]) //빈 배열이면 처음 count 값만 기억함
	
    return <div>{count}</div>
}

 

  • useEffect의 의존성 배열이 [ ] 이면 처음 렌더링 때의 count 값만 기억
  • interval 내부는 계속 count = 0 을 참조

 

useInterval 커스텀 훅 코드

import { useEffect, useRef } from "react";

export default function useInterval(callback, delay) {
	const savedCallback = useRef();
    
    //callback이 바뀔 때마다 ref에 저장함
    useEffect(()=>{
    	savedCallback.current = callback;
    },[callback])
	
    // delay가 바뀔때마다 interval 재설정
    useEffect(()=>{
    	//delay가 null이면 interval 안 돌아감
    	if(delay === null){
        	return;
        }
    
    	function tick() {
        	savedCallback.current();
        }
    	
        const id = setInterval(tick, delay);
        
        // cleanup : interval 정리
        return () => clearInterval(id);
    
    },[delay])
}

 

  • useRef로 최신 callback을 저장 - 클로저 문제 해결
  • 첫번째 useEffect는 callback이 바뀔때마다 ref 업데이트
  • 두번째 useEffect는 delay가 바뀔때만 interval 재설정
  • return () => clearInterval(id) 로 자동 cleanup

 

사용 예시 (카운트)

import { useState } from 'react';
import useInterval from './useInterval';


function Counter() {
	
    const [count, setCount] = useState(0);
    const [timeGo, setTimeGo] = useState(true);
    
    useInterval(()=>{
    	setCount(prev => prev + 1);
    }, timeGo ? 1000 : null); // null이면 멈춘다
    
    return(
    	<div>
    		<h1>{count}</h1>
    		<button onClick={() => setTimeGo(!timeGo)}>
            	{timeGo ? '정지' : '시작'}
            </button>
    	</div>
    )
}