ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Countdown Timer ⏱
    Web/JS30 2021. 1. 2. 03:13
    반응형

    이번에는 정해진 시간만큼 카운트다운을 해주는 타이머를 만들어보는 예제이다.

    맨 처음 예제였던가, 시계를 만드는 예제가 있었는데

    이번에도 거기서 사용되었던 Date 객체를 이용한다.

    우선, HTML 코드는 다음과 같다.

    <div class="timer">
      <div class="timer__controls">
        <button data-time="20" class="timer__button">20 Secs</button>
        <button data-time="300" class="timer__button">Work 5</button>
        <button data-time="900" class="timer__button">Quick 15</button>
        <button data-time="1200" class="timer__button">Snack 20</button>
        <button data-time="3600" class="timer__button">Lunch Break</button>
        <form name="customForm" id="custom">
          <input type="text" name="minutes" placeholder="Enter Minutes">
        </form>
      </div>
      <div class="display">
        <h1 class="display__time-left"></h1>
        <p class="display__end-time"></p>
      </div>
    </div>

    우선, 사용될 예정인 HTML 태그들을 변수에 담아준다.

    const timerDisplay = document.querySelector('.display__time-left');
    const endTime = document.querySelector('.display__end-time');
    const buttons = document.querySelectorAll('[data-time]');

    위의 GIF에서 봤을 때, timerDisplay는 말 그대로 카운트다운 되는 시간을 출력하는 div이다.

    endTime은 그 아래에 있는, 몇 시에 카운트다운이 끝나는지를 보여주는 작은 div이고

    buttons는 상단에 있는, 몇 분만큼 카운트다운 할 것인지를 설정하는 버튼들이다.

    얘네들이 바로 buttons

    우선, 버튼을 클릭하면 실행될 함수인 timer 함수를 만들어보자.

    function timer(seconds) {
      setInterval(function() {
        seconds--;
      }, 1000);
    }

    만약, 이렇게 setInterval을 사용해서 간단하게 1초마다 초를 나타내는 변수를 감소시킨다고 쳐보자.

    매우 간단하게 구현이 될 것 같지만, 사실 setInterval에는 표면적으로 드러나지 않는 문제가 좀 있다.

     

    우선, 브라우저의 다른 탭을 사용하는 경우, setInterval 이 동작하지 않는 경우가 있다.

    그리고 iOS의 경우에는 스크롤을 할 때 브라우저의 동작을 잠깐 멈추기 때문에 GIF가 멈추는 것과 마찬가지로

    스크롤을 할 때 setInterval 함수가 잠깐동안 동작하지 않는다.

    만약 3초동안 스크롤을 하면, 타이머는 3초만큼 더 늦게 시간이 카운트 되는 것.

    (아마 스크롤을 더 부드럽게 하기 위한 방법이라고 생각된다)

     

    그래서 우리가 사용해줄 객체가 바로 Date 객체이다.

    Date 객체에는 now라는 메소드가 있는데, 바로 현재 시간을 반환해주는 함수이다.

    매 1초마다 '(처음 시점의 Date.now() + 지정했던 시간(초)) - 매 초마다의 Date.now()'를 계산해주면 된다.

    그러면 매 초마다의 Date.now()가 1초씩 증가하기 때문에, 계산 결과가 곧 '남은 시간'을 의미하게 되는 것.

     

    참고로, Date.now()의 단위는 밀리초이고 우리가 timer 함수에 인자로 넣어줄 seconds는 초 단위이므로

    now과 seconds를 더할 때는 seconds에 1000을 곱해서 같은 단위로 만들어주도록 한다.

    function timer(seconds) {
      const now = Date.now();
      const then = now + seconds * 1000;
      
      setInterval(() => {
        const secondsLeft = Math.round((then - Date.now()) / 1000);
        if(secondsLeft < 0) {
          return;
        }
      }, 1000);
    }

    secondsLeft에다가 Math.round를 해준 것은, Date.now()를 해줬을 때 밀리초 단위로 계산되어서

    약간의 오차가 날 수 있기 때문에 정수 단위로 초를 만들어주기 위해 반올림을 해준 것.

     

    그리고 위에서 남은 시간(secondsLeft)이 0보다 작아지면 return을 하라고 해놨는데

    사실 return을 한다고 해서 setInterval을 벗어날 수는 없다.

    setInterval을 벗어나기 위해서는 clearInterval을 해줘야 하기 때문에 다음과 같이

    setInterval을 변수에 담아준 후 clearInterval을 해준다.

    let countdown;
    
    function timer(seconds) {
      const now = Date.now();
      const then = now + seconds * 1000;
      
      countdown = setInterval(() => {
        const secondsLeft = Math.round((then - Date.now()) / 1000);
        if(secondsLeft < 0) {
          clearInterval(countdown);
          return;
        }
        displayTimeLeft(secondsLeft);
      }, 1000);
    }

    이제 다음은 이렇게 계산되는 시간을 1초마다 출력해주는 함수인 displayTimeLeft 함수를 작성해보자.

    function displayTimeLeft(seconds) {
        const minutes = Math.floor(seconds / 60);
        const remainderSeconds = seconds % 60;
        const display = `${minutes}:${remainderSeconds < 10 ? '0' : ''}${remainderSeconds}`;
        document.title = display;
        timerDisplay.textContent = display;
    }
    

     

    간단하다. seconds는 남은 시간을 초단위로 계산해놓은 것이기 때문에

    60으로 나누면 몇 분이 남았는지를 구할 수 있고, 60으로 나눈 나머지몇 초가 남았는지를 의미한다.

     

    그리고 이를 문자열로 나타낼 때, 한 자리 숫자일 때는 앞에 0이 붙어서 출력되도록 해줘야 한다.

    document.title은 그냥 홈페이지 제목에도 숫자가 출력되도록 하기 위해 그냥 보너스로 해놓은 것.

     

    여기서 위의 timer 함수를 다시 볼 필요가 있는데, displayTimeLeft를 setInterval 내부에서만 호출하면

    맨 처음에 1초는 건너뛰고 1초마다 호출이 된다.

    만약 10초를 설정하면, 맨 처음의 1초는 건너뛰고 9초부터 출력되면서 1초씩 줄어든다.

    아마 setInterval이 바로 실행되지 않고, 우리가 설정한 시간(여기서는 1000ms)만큼

    한 번 기다린 후에 호출이 되기 때문인 것 같다.

     

    그래서, setInterval에 들어가기 전에 미리 한 번 displayTimeLeft를 호출해주고 들어가야

    맨 처음 10초때부터 온전히 출력하면서 카운트를 할 수 있다.

    let countdown;
    
    function timer(seconds) {
      const now = Date.now();
      const then = now + seconds * 1000;
      
      displayTimeLeft(seconds);
      displayEndTime(then);
      
      countdown = setInterval(() => {
        const secondsLeft = Math.round((then - Date.now()) / 1000);
        if(secondsLeft < 0) {
          clearInterval(countdown);
          return;
        }
        displayTimeLeft(secondsLeft);
      }, 1000);
    }

    그리고 displayEndTime, 즉 언제 끝날지도 출력을 해줘야되니깐 관련된 함수를 작성해보자.

    function displayEndTime(timestamp) {
      const end = new Date(timestamp);
      const hour = end.getHours();
      const adjustedHour = hour > 12 ? hour - 12 : hour;
      const minutes = end.getMinutes();
      endTime.textContent = `Be Back At ${adjustedHour}:${minutes < 10 ? '0' : ''}${minutes}`;
    }
    

     

    이 함수는 인자로 timestamp를 받는데, 이 함수를 호출하는 곳으로 돌아가서 보면

    인자로 'then'을 넣어주는 것을 볼 수 있다.

     

    즉, 맨 처음에 호출해준 Date.now()에다가 우리가 카운트하기를 원하는 시간 seconds만큼을 더한 시간이

    인자로 들어와서 시간 카운트가 언제 끝나는지를 의미하는 변수 end에 들어가게 되는 것.

    (seconds는 밀리초 단위인데, 이것을 다시 새로운 Date 객체로 만들면 날짜와 시간대를 가진 객체로 재탄생한다)

     

    만약 우리가 오후 2:00에 5분을 카운트 하기를 원한다고 입력하면 end는 2:05가 되는 것이다.

    그래서 hour에는 2가 들어가고, minutes에는 5가 들어가게 된다.

    역시나 textContent를 이용하여 한 자리 시간일 때는 앞에 0이 들어가게끔 출력을 해주면 된다.

     

    이제 각 시간대 버튼을 클릭했을 때 해당 시간만큼의 timer 함수가 실행되도록 코드를 짜보자.

    function startTimer() {
      const seconds = parseInt(this.dataset.time);
      timer(seconds);
    }
    
    buttons.forEach(button => button.addEventListener('click', startTimer));

    맨 위의 HTML 코드를 보면, 각 버튼마다 'data-time'이라는 attribute가 설정되어 있다.

    그것을 this.dataset.time으로 뽑아서 정수로 변환해준 후에 seconds 변수에 넣어서 timer 함수를 실행해줬다.

     

    이제 버튼을 누르면 해당 시간만큼 타이머가 실행되는데,

    버튼을 이리저리 누르다보면 문제점을 하나 발견하게 된다.

    우리가 새로운 타이머를 시작할 때, 이전의 타이머를 정리해주지 않았기 때문

    이전의 타이머와 새로운 타이머가 겹치게 되면서 위와 같은 현상이 나타나게 된다.

     

    해결책은 간단히, 새로운 타이머를 시작할 때마다 이전의 타이머를 정리해주면 된다.

    function timer(seconds) {
        // 요기
        clearInterval(countdown);
    
        const now = Date.now();
        const then = now + seconds * 1000;
        displayTimeLeft(seconds);
        displayEndTime(then);
    
        countdown = setInterval(() => {
            const secondsLeft = Math.round((then - Date.now()) / 1000);
            if(secondsLeft < 0) {
                clearInterval(countdown);
                return;
            }
            displayTimeLeft(secondsLeft);
        }, 1000);
    }

    문제가 깔끔하게 해결되었다.

    마지막으로 기능 하나만 더 넣어보자면

    버튼들 옆에 'Enter Minutes'라고 적혀있는 input form이 하나 있다.

    여기에 숫자를 입력했을 때, 해당 분만큼 타이머가 작동되도록 하는 기능이다.

    꽤나 간단하고, 코드는 다음과 같다.

    document.customForm.addEventListener('submit', function(e) {
        e.preventDefault();
        const mins = this.minutes.value; // this는 customForm을 가리킴.
        timer(mins * 60); // 분을 입력하게 되어있으므로 초로 변환
        this.reset(); // form의 input들을 비운다.
    });

    해당 form의 input에 숫자를 입력하고 엔터키를 눌렀을 때,

    일단은 submit 이벤트의 기본값인 새로고침을 e.preventDefault로 없애줘야 한다.

     

    여기서 한 가지 짚고 넘어가야 할 기능.

    만약 특정 HTML Element가 'abcd'라는 name attribute를 가지고 있다고 하면,

    우리는 거기에 document.abcd로 접근할 수가 있다는 사실을 알고 있는가?

    나는 몰랐다. ㄴㅇㄱ

     

    만약 그 abcd가 form이라고 치면, 그 안에 'def'라는 name을 가진 input에는

    document.abcd.def로 접근할 수가 있다! ㄴㅇㄱ!!

     

    위에서 보면 form의 name이 'customForm'이기 때문에 document.customForm으로 접근한 것을 볼 수 있고

    그 안에 있는 'minutes'라는 input에는 this.minutes(= document.customForm.minutes)로 접근한 모습을 볼 수 있다.

    입력된 분에다가 60을 곱해서 초 단위로 바꿔준 후에 timer 함수를 호출하면 되고

    input 태그는 입력 후에 비워주는 것이 좋으므로 this.reset()(= document.customForm.reset())을 호출해서

    해당 form의 모든 input(하나 뿐이지만)을 비워주면 된다.

     

     

    전체 자바스크립트 코드

    let countdown;
    const timerDisplay = document.querySelector('.display__time-left');
    const endTime = document.querySelector('.display__end-time');
    const buttons = document.querySelectorAll('[data-time]');
    
    function timer(seconds) {
        clearInterval(countdown);
    
        const now = Date.now();
        const then = now + seconds * 1000;
        displayTimeLeft(seconds);
        displayEndTime(then);
    
        countdown = setInterval(() => {
            const secondsLeft = Math.round((then - Date.now()) / 1000);
            if(secondsLeft < 0) {
                clearInterval(countdown);
                return;
            }
            displayTimeLeft(secondsLeft);
        }, 1000);
    }
    
    function displayTimeLeft(seconds) {
        const minutes = Math.floor(seconds / 60);
        const remainderSeconds = seconds % 60;
        const display = `${minutes}:${remainderSeconds < 10 ? '0' : ''}${remainderSeconds}`;
        document.title = display;
        timerDisplay.textContent = display;
    }
    
    function displayEndTime(timestamp) {
        const end = new Date(timestamp);
        const hour = end.getHours();
        const adjustedHour = hour > 12 ? hour - 12 : hour;
        const minutes = end.getMinutes();
        endTime.textContent = `Be Back At ${adjustedHour}:${minutes < 10 ? '0' : ''}${minutes}`;
    }
    
    function startTimer() {
        const seconds = parseInt(this.dataset.time);
        timer(seconds);
    }
    
    buttons.forEach(button => button.addEventListener('click', startTimer));
    
    document.customForm.addEventListener('submit', function(e) {
        e.preventDefault();
        const mins = this.minutes.value; 
        timer(mins * 60); 
        this.reset();
    });

     

    반응형

    'Web > JS30' 카테고리의 다른 글

    Whack a Mole Game 🎮  (0) 2021.01.02
    Video Speed Controller UI  (0) 2020.12.27
    Click and Drag to Scroll  (0) 2020.12.26
    Stripe Follow Along Nav  (0) 2020.12.26
    Event Capture, Propagation, Bubbling and Once  (0) 2020.12.24

    댓글

Designed by Tistory.