이번에도 스톱워치를 만들다가.. 말로만 듣던 setTimeout과 setInterval의 시간보장문제를 직접 맞닥뜨리게 되었습니다.

말로만 듣던걸 간단한 프로그램을 만들면서 직접 겪으니까 참 오묘하네요 ㅎㅎ

 

목차


    0. 타이머 함수

    타이머 함수가 뭔지 모르는 사람들을 위해 간략한 설명을 하고 넘어가겠습니다!

    자바스크립트의 타이머 함수는 일정 시간이 지난 후에 특정 코드나 함수가 실행될 수 있도록 해주는 장치입니다.

    대표적으로 setTimeout과 setInterval이 있습니다.

     

    0.0. setTimeout

    setTimeout은 실행 함수와 기다리는 시간을 매개변수로 받습니다.

    뒤의 arg들은 function에 전달할 매개변수를 뜻합니다.

    const timeoutID = setTimeout(function[, delay, arg1, arg2, ...]);

     

    만약 타이머가 진행되는 도중에 타이머를 멈추고 함수를 실행하고 싶지 않다면 clearTimeout을 실행하면 됩니다.

    대신 이걸 위해서는 setTimeout을 미리 변수에 등록해 둬야겠죠?

    clearTimeout(timeoutId);

     

    0.1. setInterval

    setInterval 역시 동일합니다.

    setTimeout과의 차이점은 일회성이 아니라 계속해서 실행된다는 것이죠.

    const timeoutID = setTimeout(function[, delay, arg1, arg2, ...]);

     

    만약 인터벌이 진행되는 도중에 타이머를 멈추고 함수를 실행하고 싶지 않다면 clearInterval을 실행하면 됩니다.

    setTimeout은 한번 실행되면 끝이지만, setInterval은 clearInterval하기 전까지는 계속해서 실행되므로 사용하지 않는다면 반드시 clearInterval해줘야 합니다!

     

    0.2. 예제

    setTimeout의 경우 JS엔진이 아닌 WebAPI라는 별도의 곳에서 처리를 합니다.

    이것을 통해 싱글스레드인 JS에서 비동기적인 처리가 가능하게 되는 것이죠.

     

    setTimeout(() => {console.log("첫 번째 메시지")}, 5000);
    setTimeout(() => {console.log("두 번째 메시지")}, 3000);
    setTimeout(() => {console.log("세 번째 메시지")}, 1000);
    
    // 콘솔 출력:
    
    // 세 번째 메시지
    // 두 번째 메시지
    // 첫 번째 메시지

     


    1. 그래서 뭐가 문제인데?

    특정 시간 후에 함수가 동작하게 하는 것은 이해했습니다.

    하지만 도대체 그게 뭐가 문제가 될까요?

     

    JS파일에서 5000ms 후에 함수가 작동하도록 WebAPI에 명령을 전달했습니다.

    그러면 WebAPI는 5000ms를 기다리고 그 이후 해당 함수를 task queue에 전달합니다.

    그런데 task queue에서 이벤트루프를 통해 바로 call stack으로 전달하면 좋은데, 그게 마음대로 안될 수가 있습니다.

    만약 call stack에서 작업을 하고 있는게 있다면 event loop는 task queue에서 대기하는 명령을 바로 call stack으로 올리지 않고 대기하게 됩니다.

    5000ms가 지났더라도 task queue에서 기다리는 시간이 추가로 발생하게 되는 것이죠!!

     

    위 내용을 더 잘 이해하고 싶다면 이벤트 루프의 동작에 대해 검색해보시면 됩니다!

     

    딜레이가 지정한 값보다 더 긴 이유에 대해서는 아래 MDN에도 잘 설명되어 있으니 참고하세요!

     

     

    setTimeout() - Web API | MDN

    전역 setTimeout() 메서드는 만료된 후 함수나 지정한 코드 조각을 실행하는 타이머를 설정합니다.

    developer.mozilla.org

     


    2. 인생은 실전

    사진은 이전 useEffect에서 보여드렸던 그 코드 맞습니다!

    이걸 그대로 살려서 millisecond라는 변수만 추가하고 반복시간을 10ms로 줄여서 실행했는데요...!!

     

     

    아... 분명 제가 만든 스톱워치를 먼저 눌렀는데, 시간이 뒤쳐지는 모습이 보이죠?

    제가 원하는 대로 10ms마다 useEffect 내부 함수가 동작하는게 아니라 조금씩 밀리기 때문에 이런 현상이 발생하고 있습니다.

     

     


    3. 해결책은 Date

    Date()는 흔히 시간을 뽑아내고 싶을 때 많이 사용하는 함수입니다.

    추가로 Date.now()는 UTC 기준으로 1970년 1월 1일 0시 0분 0초부터 현재까지 경과된 밀리 초(Number)를 반환하는 함수입니다.

    그럼 이걸 어떻게 사용할 수 있을까요?

     

    저는 startTime과 pauseTime이라는 요소를 2개 만들었습니다.

    각각의 요소는 렌더링과는 무관하기 때문에 useRef를 이용해서 만들었죠.

    이제 시간 데이터들은 startTime.current, pauseTime.current에 저장될 것입니다.

    (앞으로 변수명을 언급할 때 .current는 생략하고 말하도록 하겠습니다!!)

     

     

    startTime이 null인 상황에서 시작 버튼을 누르면 startTime을 현재 시간으로 바꿉니다.

    만약 startTime이 null이 아니라면 (한번 시작을 했고, 중지버튼을 누르지 않았다면) 현재 시간에서 pause를 누른 시간을 뺀 만큼의 시간을 startTime에 더해줍니다.

     

     

    pauseTime은 pause를 누른 시간을 저장하고 있습니다.

     

     

    그리고 중지 버튼을 누르면 startTime과 pauseTime이 리셋됩니다.

     

     

    여기까지 완료했으면, 실제로 잘 동작하는지 console.log로 찍어봅시다.

    Date.now() - startTime.current를 하게 되면 시작 버튼을 누르고 지나간 Number 형태의 시간만 남게 되겠죠?

    Date 객체를 만들 때 이 값을 넣어주면 자동으로 시간으로 변환해줍니다.

     

    그리고 Date 객체에서 시간을 추출하기 위해서는 getUTCHours, getUTCMinutes, getUTCSeconds, getUTCMilliseconds라는 메서드를 사용하면 됩니다.

    그냥 getHours가 아니라 getUTCHours를 사용한 이유는 국제 시간 기준으로 가져오기 위해서입니다.

    UTC가 붙어있지 않다면 자동으로 현지시간 기준으로 반환하기 때문에 한국 기준 9시간이 자동으로 잡히기 때문이죠!

     

     

    이제 브라우저에서 돌려보면? 실제 시간과 현재 스톱워치의 괴리감이 느껴집니다.

    겨우 5초 돌렸는데 벌써 1초 이상 밀리고있네요?!!

     

     

    우리가 원하는 결과를 얻었죠? 그렇다면 이걸 상태값에 그대로 적용시켜주면 끝입니다.

     

    기존의 복잡했던 코드들을 오히려 더 깔끔하게 줄일 수 있게 되었네요!!

    그리고 millisecond까지 보여주기 때문에 1/100의 정확도를 가진 것처럼 페이크(?)를 줄 수도 있습니다. (실제로는 1ms마다 처리가 되지는 않겠지만 말이죠..!)

     

    before
    after

    반응형
    • 네이버 블로그 공유하기
    • 네이버 밴드에 공유하기
    • 페이스북 공유하기
    • 카카오스토리 공유하기