프로젝트 소개
Reaction Time Test는 사용자의 반응 속도를 측정하는 웹 게임입니다. 화면이 변화하는 순간부터 사용자가 클릭할 때까지의 시간을 측정하여 밀리초 단위로 결과를 보여줍니다.
5개 언어(한국어, 영어, 일본어, 중국어, 스페인어)를 지원하며, 전 세계 사용자가 이용할 수 있습니다.
초기 구현의 문제점
1. 부정확한 시간 측정
처음에는 Date.now()를 사용하여 시간을 측정했습니다:
const startTime = Date.now();
// ... 사용자가 클릭 ...
const reactionTime = Date.now() - startTime;
하지만 Date.now()는 밀리초 단위로만 측정할 수 있어, 더 정밀한 측정이 필요했습니다.
2. 복잡한 상태 관리
게임의 상태(대기, 준비, 측정 중, 결과)를 여러 변수로 관리하다 보니 코드가 복잡해졌습니다:
let isWaiting = false;
let isReady = false;
let isMeasuring = false;
// ... 복잡한 if-else 체인 ...
해결 방법 1: performance.now() API
performance.now()는 마이크로초 단위(소수점 5자리까지)로 시간을 측정할 수 있습니다.
const startTime = performance.now();
// ... 사용자가 클릭 ...
const reactionTime = performance.now() - startTime;
성능 비교
| 메서드 | 정밀도 | 용도 |
|---|---|---|
Date.now() |
밀리초 | 날짜/시간 표시 |
performance.now() |
마이크로초 | 성능 측정 |
구현 예시
class ReactionTimer {
constructor() {
this.startTime = 0;
this.endTime = 0;
}
start() {
this.startTime = performance.now();
}
stop() {
this.endTime = performance.now();
return this.endTime - this.startTime;
}
reset() {
this.startTime = 0;
this.endTime = 0;
}
}
해결 방법 2: State Machine 패턴
State Machine 패턴을 적용하여 게임 상태를 명확하게 관리합니다.
상태 정의
const GameState = {
IDLE: 'idle',
WAITING: 'waiting',
READY: 'ready',
MEASURING: 'measuring',
RESULT: 'result'
};
State Machine 구현
class GameStateMachine {
constructor() {
this.state = GameState.IDLE;
this.timer = new ReactionTimer();
this.listeners = {};
}
// 상태 변경
setState(newState) {
const oldState = this.state;
this.state = newState;
this.emit('stateChange', { oldState, newState });
}
// 이벤트 리스너 등록
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
// 이벤트 발생
emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}
// 게임 시작
startGame() {
this.setState(GameState.WAITING);
const delay = this.getRandomDelay(2000, 5000);
setTimeout(() => {
if (this.state === GameState.WAITING) {
this.setState(GameState.READY);
}
}, delay);
}
// 화면 클릭
handleClick() {
switch (this.state) {
case GameState.WAITING:
this.handleEarlyClick();
break;
case GameState.READY:
this.startMeasurement();
break;
case GameState.MEASURING:
this.finishMeasurement();
break;
case GameState.RESULT:
this.startGame();
break;
}
}
// 조기 클릭 처리
handleEarlyClick() {
this.setState(GameState.IDLE);
this.emit('earlyClick');
}
// 측정 시작
startMeasurement() {
this.setState(GameState.MEASURING);
this.timer.start();
}
// 측정 완료
finishMeasurement() {
const reactionTime = this.timer.stop();
this.setState(GameState.RESULT);
this.emit('measurementComplete', { reactionTime });
}
// 랜덤 지연 시간 생성
getRandomDelay(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}
UI와 통합
const game = new GameStateMachine();
// 상태 변경에 따른 UI 업데이트
game.on('stateChange', ({ oldState, newState }) => {
updateUI(newState);
});
// 측정 완료 이벤트 처리
game.on('measurementComplete', ({ reactionTime }) => {
displayResult(reactionTime);
});
// 조기 클릭 이벤트 처리
game.on('earlyClick', () => {
showEarlyClickMessage();
});
// 클릭 이벤트 연결
document.getElementById('game-area').addEventListener('click', () => {
game.handleClick();
});
function updateUI(state) {
const gameArea = document.getElementById('game-area');
const message = document.getElementById('message');
switch (state) {
case GameState.IDLE:
gameArea.style.backgroundColor = '#333';
message.textContent = '클릭하여 시작';
break;
case GameState.WAITING:
gameArea.style.backgroundColor = '#333';
message.textContent = '초록색이 되면 클릭하세요';
break;
case GameState.READY:
gameArea.style.backgroundColor = '#4CAF50';
message.textContent = '지금 클릭!';
break;
case GameState.MEASURING:
message.textContent = '';
break;
case GameState.RESULT:
gameArea.style.backgroundColor = '#333';
message.textContent = '다시 하려면 클릭하세요';
break;
}
}
최적화 결과
| 항목 | 개선 전 | 개선 후 |
|---|---|---|
| 시간 정밀도 | 1ms | 0.01ms |
| 상태 관리 | 복잡한 if-else | 명확한 State Machine |
| 코드 가독성 | 낮음 | 높음 |
| 유지보수 | 어려움 | 쉬움 |
추가 최적화
1. RequestAnimationFrame 사용
애니메이션과 타이머를 requestAnimationFrame을 사용하여 최적화합니다:
function updateTimer() {
if (game.state === GameState.MEASURING) {
const currentTime = performance.now();
const elapsed = currentTime - game.timer.startTime;
displayTime(elapsed);
requestAnimationFrame(updateTimer);
}
}
2. 이벤트 위임
여러 요소에 이벤트 리스너를 추가하는 대신 이벤트 위임을 사용합니다:
document.getElementById('game-container').addEventListener('click', (e) => {
if (e.target.matches('#game-area, #start-button')) {
game.handleClick();
}
});
결론
performance.now() API와 State Machine 패턴을 적용하여 Reaction Time Test의 정확도와 코드 품질을 크게 개선했습니다.
- 정확도: 마이크로초 단위의 정밀한 시간 측정
- 가독성: 명확한 상태 관리로 이해하기 쉬운 코드
- 유지보수: 새로운 기능 추가가 쉬워짐
프로젝트는 https://reactiontest.dreamurl.biz/에서 체험해볼 수 있습니다.