ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JS/DOM] 바닐라 자바스크립트로 스톱워치(STOP WATCH)구현하기
    Frontend 2019. 4. 4. 11:53

     

     

     

    만드는데 거의 3일 정도 걸렸던 스톱워치이다.. 휴..

    처음 간단하게 시작 / 스톱 버튼을 만들었을 때는 그렇게 어렵지 않았다.

    문제는 재시작, 기록, 리셋 등등의 기능을 넣으면서부터 꼬이기 시작하고 시간이 뒤죽박죽 난리가 나고..;;

     

     


     

     

    일단 HTML태그는 다음과 같다.

    <div>
    	<div>
    		<span id="postTestMin">00</span><!-- 분 -->
    		<span>:</span>
    		<span id="postTestSec">00</span><!--초-->
    		<span>.</span>
    		<span id="postTestMilisec">00</span><!--밀리초-->
    	</div>
    	<div>
    		<ul id="testRecordList"></ul><!--중간 기록할 리스트-->
    	</div>
    	<div>
    		<button type="button" id="testStartBtn">START</button><!--시작/재시작/기록 버튼-->
    		<button type="button" id="testStopBtn">STOP</button><!--스톱 버튼-->
    	</div>
    </div>

     

     

    내가 최초로 생각한 방법은

    자바스크립트의 Timestamp값을 이용하는 것이었다.

     

    내가 생각한 스톱워치 알고리즘을 적어보면 다음과 같다.

     

     

     

    시작 버튼을 누르는 순간에 변수A에 현재시간을 불러오고 그 시간을 timestamp값으로 저장한다.

    그리고 시작 시점부터 1ms 마다 계속해서 현재시간의 timestamp값을 새로 불러와 B에 저장한다.

    B에서 A를 뺀 timestamp값을 분, 초, 밀리초로 변환하여 화면에 출력하면 된다.

     

    그리고 이걸 코드로 표현하면 다음과 같이 표현할 수 있다.

    var stTime
    var timerStart
    
    document.getElementById('testStartBtn').addEventListener('click', function() {
        if(! stTime) {
            stTime = new Date().getTime()	//클릭한 시점의 현재시간 timestamp를 stTime에 저장
        }
        
        timerStart = setInterval(function() {
            var nowTime = new Date().getTime()	//1ms당 한 번씩 현재시간 timestamp를 불러와 nowTime에 저장
            var newTime = new Date(nowTime - stTime)	//(nowTime - stTime)을 new Date()에 넣는다
            
            var min = newTime.getMinutes()	//분
            var sec = newTime.getSeconds()	//초
            var milisec = Math.floor(newTime.getMilliseconds() / 10)	//밀리초
            
            document.getElementById('postTestMin').innerText = addZero(min)
            document.getElementById('postTestSec').innerText = addZero(sec)
            document.getElementById('postTestMilisec').innerText = addZero(milisec)
        }, 1)
    })
    
    document.getElementById('testStopBtn').addEventListener('click', function() {
        if(timerStart) {
            clearInterval(timerStart)
        }
    })
    
    function addZero(num) {
        return (num < 10 ? '0'+num : ''+num)
    }

     

    여기까지는 잘 구현되었는데 문제는 '재시작'이었다.

     

     

     

    예를 들어서 12시 00분 00초에 START버튼을 눌렀다.

    A변수(전역변수)에 12:00:00시간을 저장한다.

     

    B변수(지역변수)에 흐르는 시간을 담는다.

    12:00:01, 12:00:02, 12:00:03, ... 

     

    B-A를 한다.

    00:00:01. 00:00:02. 00:00:03, ...

     

    12:00:10에 STOP버튼을 누른다.

     

    12:00:40에 START버튼을 눌러 재시작한다.

    A변수에는 아까 저장해둔 12:00:00이 담겨있으나

    B변수에는 12:00:41, 12:00:42, 12:00:43, ...이 담기게 된다.

     

    B-A를 하면

    00:00:41, 00:00:42, 00:00:43, ...

     

    이런 문제가 생겨버린다.

    00:00:10에서 STOP을 눌렀으니 재시작을 눌렀을 때 00:00:11, 00:00:12, ... 가 되어야 하는데

    STOP을 누르고 흘러간 시간 때문에 00:00:41, 00:00:42, ... 가 출력되는 것이다.

     

     

    그렇다고 해서 만약에 A변수에 재시작 클릭 시점의 시간을 새로 저장하면

    기존 데이터가 날아가고 00:00:00부터 새로 시작하게 된다.

     

     

    그래서 시행착오끝에 생각해낸 방법은 다음과 같다.

     

     

    STOP버튼을 눌렀을 때 전역변수에 STOP시점의 시각을 timestamp로 저장하고

    재시작 버튼을 눌렀을 때의 시각에서 STOP시점의 시각을 뺀다. (=> STOP을 누르고 흘러간 시간 C이다.)

     

    이 흘러간 시간을 A에 더해주고 A+C한 값을

    1ms마다 호출한 시간에서 빼주면

    원하는 시점부터 재시작을 할 수 있다.

     

     

     

    그런데 문제는 시작시간의 timestamp값을 가져오고 stop하면 다시 timestamp값을 저장하고

    그걸 더하고 빼고 하는 과정에서 아주 미묘하게 약간의 오차가 생긴다는 점이다.

    시간을 가져올 때마다 timestamp를 사용하려면 new Date()를 생성하고

    거기에서 timestamp값을 가져오다보니 그런거 같은데

    이 문제를 해결하려고 여러가지 알아보다가 Date.now()라는 메소드를 알게 되었다.

     

    var time01 = new Date().getTime()
    var time02 = Date.now()

    위 2개의 메소드는 기능이 똑같다.

     

    둘 다 1970년 1월 1일 00:00:00 UTC 시각으로부터 현재까지 흐른 밀리세컨즈를 가져온다.

    우리는 이 값들을 변수에 저장해 더하고 빼는 연산을 하면서

    특정 시점에서 특정 시점까지 얼마나 시간을 흘렀는지 계산할 수 있다.

     

    근데 Date.now()가 new Date().getTime()보다 훨씬 더 빠르다!

    인터넷을 찾아보니 약 2배 정도 빠르다고 한다.

     

    new Date()로 현재 시간을 생성하고 그 값에서 getTime() 메소드로 또 시간을 변환하는 것보다

    한 번에 값을 가져오는 것이 빠를 수밖에 없겠지.

     

    다만 Date.now()는 익스플로러9 이상의 브라우저에서만 지원한다.

     

    하, 이 놈의 익스플로러..;;

     

     

    하여튼 Date.now()를 이용하여 위의 알고리즘으로 스톱워치를 구현하니

    스톱워치가 잘 작동한다!

    var stTime = 0
    var endTime = 0
    var timerStart
    
    var min
    var sec
    var milisec
    
    var startBtn = document.getElementById('testStartBtn')
    var stopBtn = document.getElementById('testStopBtn')
    
    startBtn.addEventListener('click', function() {
    	if(!stTime) {
    		stTime = Date.now()	// 처음 시작할 때
    	} else {
    		stTime += (Date.now() - endTime)	// 재시작할 때
    	}
    	
    	timerStart = setInterval(function() {
    		var nowTime = new Date(Date.now() - stTime)
    
    		min = addZero(nowTime.getMinutes())
    		sec = addZero(nowTime.getSeconds())
    		milisec = addZero(Math.floor(nowTime.getMilliseconds() / 10))
    
    		document.getElementById('postTestMin').innerText = min
    		document.getElementById('postTestSec').innerText = sec
    		document.getElementById('postTestMilisec').innerText = milisec
    	}, 1)
    })
    
    stopBtn.addEventListener('click', function() {
    	if(timerStart) {
    		clearInterval(timerStart)
    		endTime = Date.now()	// STOP시점의 시간 저장
    	}
    })
    
    function addZero(num) {
    	return (num < 10 ? '0'+num : ''+num)
    }

     

     

     

    이제 좀 더 완성도를 높이기 위해 버튼을 클릭할 때마다 버튼 이름과 기능을 바꿔주는 코드를 추가했다.

     

     

    • START버튼 클릭 시 시간 카운트 / START버튼이 RECORD버튼으로 변환
    • 카운트 동작 중 RECORD버튼 클릭 시 기록된 시각이 화면에 출력 (나중에 클릭한 기록이 맨 위로 가도록 출력)
    • STOP버튼 클릭 시 시간 카운트 멈춤 / STOP버튼은 RESET버튼으로, START버튼은 RESTART버튼으로 변환
    • RESET버튼 클릭 시 모두 초기화 / RESTART버튼 클릭 시 재시작

     

    var stTime = 0
    var endTime = 0
    var timerStart
    
    var min
    var sec
    var milisec
    
    var startBtn = document.getElementById('testStartBtn')
    var stopBtn = document.getElementById('testStopBtn')
    var recordList = document.getElementById('testRecordList')
    
    startBtn.addEventListener('click', function() {
    	// RECORD
        if(this.innerText == 'RECORD' && milisec) {
            console.log(min, sec, milisec)
            var li = document.createElement('li')
            li.style.color = "#fff"
            li.innerText = min + ' : ' + sec + ' : ' + milisec
            if(! recordList.firstChild) {
                recordList.append(li)
            } else {
                recordList.insertBefore(li, recordList.firstChild)
            }
            return false
        }
        this.innerText = 'RECORD'
    	
        if(!stTime) {
            stTime = Date.now()	// 최초 START
        } else {
            stopBtn.innerText = 'STOP'
            stTime += (Date.now() - endTime)	// RESTART
        }
    
        timerStart = setInterval(function() {
            var nowTime = new Date(Date.now() - stTime)
    
            min = addZero(nowTime.getMinutes())
            sec = addZero(nowTime.getSeconds())
            milisec = addZero(Math.floor(nowTime.getMilliseconds() / 10))
    
            document.getElementById('postTestMin').innerText = min
            document.getElementById('postTestSec').innerText = sec
            document.getElementById('postTestMilisec').innerText = milisec
        }, 1)
    })
    
    stopBtn.addEventListener('click', function() {
        if(timerStart) {
            clearInterval(timerStart)	// STOP
    
            if(this.innerText == 'STOP') {
                endTime = Date.now()
                this.innerText = 'RESET'
                startBtn.innerText = 'RESTART'
            } else {	// RESET
                stTime = 0
                min = 0
                sec = 0
                milisec = 0
                document.getElementById('postTestMin').innerText = '00'
                document.getElementById('postTestSec').innerText = '00'
                document.getElementById('postTestMilisec').innerText = '00'
                startBtn.innerText = 'START'
                this.innerText = 'STOP'
                timerStart = null
                recordList.innerHTML = ''
            }
        }
    })
    
    function addZero(num) {
        return (num < 10 ? '0'+num : ''+num)
    }

     

    완성!

     

     

    00 : 00 . 00

     

     

    반응형

    COMMENT