Ver3.5 ⏱️ 자바스크립트 타이머 완전정복: setTimeout과 setInterval의 모든 것

⏱️ 자바스크립트 타이머 완전정복: setTimeout과 setInterval의 모든 것
비동기 타이밍 제어의 핵심 — 원리부터 실전 패턴까지 친구처럼 쉽게 파헤쳐보자!
안녕! 오늘은 자바스크립트 개발하다 보면 무조건 마주치게 되는 두 친구, setTimeout과 setInterval에 대해 제대로 파헤쳐볼 거야. 🎉
"어, 나 이거 써봤는데?" 싶은 사람도 있을 거고, "이름은 들어봤는데 정확히 어떻게 돌아가는지 모르겠어" 하는 사람도 있을 거야. 둘 다 환영이야! 오늘 끝나고 나면 이 두 함수를 완전히 내 것으로 만들 수 있을 거거든.
단순히 "몇 초 뒤에 실행해줘" 수준을 넘어서, 이벤트 루프와의 관계, 실전에서 자주 쓰는 패턴, 그리고 흔히 빠지는 함정까지 — 전부 다 커버할 거야. 준비됐어? 그럼 고고! 🚀
🕰️ 타이머, 왜 필요한 거야?
먼저 왜 타이머가 필요한지부터 생각해보자. 자바스크립트는 기본적으로 싱글 스레드(Single Thread) 언어야. 한 번에 하나의 작업만 처리할 수 있다는 뜻이지.
그런데 현실 세계의 웹 앱을 보면 어때? 3초 뒤에 팝업 띄우기, 1초마다 시계 업데이트, 사용자가 타이핑 멈추면 0.5초 뒤에 검색 실행... 이런 것들이 넘쳐나잖아.
이걸 가능하게 해주는 게 바로 비동기 타이머 API야. 자바스크립트 엔진 자체가 아니라, 브라우저(또는 Node.js)가 제공하는 Web API를 통해 타이밍을 제어하는 거지.
이 사실이 왜 중요하냐고? 나중에 이벤트 루프 얘기할 때 이게 핵심이 되거든. 일단 계속 가보자!
⏳ setTimeout — "나중에 한 번만 실행해줘"
기본 문법
setTimeout은 지정한 시간(밀리초) 이후에 콜백 함수를 딱 한 번 실행해주는 함수야.
// 기본 형태
const timerId = setTimeout(콜백함수, 지연시간ms, ...추가인자);
// 예시 1: 2초 뒤에 실행
setTimeout(function() {
console.log("2초가 지났어!");
}, 2000);
// 예시 2: 화살표 함수로
setTimeout(() => {
console.log("안녕, 나는 1.5초 뒤에 나타났어!");
}, 1500);
// 예시 3: 추가 인자 전달
setTimeout((name, age) => {
console.log(`${name}는 ${age}살이야`);
}, 1000, "철수", 25);
// 출력: 철수는 25살이야
clearTimeout(timerId)로 취소할 수 있어!
지연시간 0ms의 비밀 🤫
이거 진짜 많이들 헷갈려하는 부분이야. setTimeout(fn, 0)을 쓰면 즉시 실행될 것 같지? 근데 실제로는 그렇지 않아!
console.log("1번");
setTimeout(() => {
console.log("2번 - setTimeout 0ms");
}, 0);
console.log("3번");
// 출력 순서:
// 1번
// 3번
// 2번 - setTimeout 0ms
왜 이런 일이 벌어지냐고? 이게 바로 이벤트 루프(Event Loop)의 마법이야! 잠깐 이 개념을 짚고 넘어가자.
이벤트 루프와 타이머의 관계 🔄
위 그림을 보면서 이해해보자. 자바스크립트 엔진이 코드를 실행하는 방식은 이래:
console.log("1번"), console.log("3번") 같은 것들.setTimeout을 만나면 브라우저의 Web API에 "이 콜백을 N밀리초 뒤에 태스크 큐에 넣어줘"라고 위임해.setTimeout(fn, 1000)은 "정확히 1초 뒤에 실행"이 아니라 "최소 1초 뒤에 실행"이야! 콜 스택이 바쁘면 더 늦게 실행될 수 있어. 이걸 타이머 드리프트(Timer Drift)라고 해.
setTimeout 취소하기 — clearTimeout
타이머를 등록했다가 취소하고 싶을 때는 clearTimeout을 써!
// 타이머 등록
const timerId = setTimeout(() => {
console.log("이건 실행 안 될 거야!");
}, 3000);
// 1초 뒤에 타이머 취소
setTimeout(() => {
clearTimeout(timerId);
console.log("타이머 취소됨!");
}, 1000);
// 결과: "타이머 취소됨!" 만 출력됨
clearTimeout을 호출해도 에러는 안 나. 그냥 아무 일도 안 일어나. 안전하게 써도 돼!
🔁 setInterval — "계속 반복해서 실행해줘"
기본 문법
setInterval은 지정한 시간마다 콜백을 반복 실행해주는 함수야. 멈추라고 할 때까지 계속!
// 기본 형태
const intervalId = setInterval(콜백함수, 간격ms, ...추가인자);
// 예시: 1초마다 카운트
let count = 0;
const intervalId = setInterval(() => {
count++;
console.log(`${count}초 경과`);
if (count >= 5) {
clearInterval(intervalId);
console.log("5초 완료! 타이머 종료");
}
}, 1000);
// 출력:
// 1초 경과
// 2초 경과
// 3초 경과
// 4초 경과
// 5초 경과
// 5초 완료! 타이머 종료
setInterval 취소하기 — clearInterval
const intervalId = setInterval(() => {
console.log("반복 중...");
}, 500);
// 3초 뒤에 반복 중지
setTimeout(() => {
clearInterval(intervalId);
console.log("반복 종료!");
}, 3000);
clearTimeout으로 setInterval을 취소하거나 그 반대도 가능해. 하지만 명확성을 위해 각각 맞는 함수를 쓰는 게 좋아!
⚔️ setTimeout vs setInterval — 뭘 써야 해?
둘 다 반복 작업에 쓸 수 있는데, 어떤 차이가 있을까? 이게 진짜 중요한 포인트야!
| 구분 | setTimeout | setInterval |
|---|---|---|
| 실행 횟수 | 1회 | 무한 반복 (취소 전까지) |
| 반복 구현 | 재귀 호출로 가능 | 기본 기능 |
| 실행 간격 | 콜백 실행 시간 포함 후 다음 예약 | 콜백 실행 시간 무관하게 고정 간격 |
| 드리프트 위험 | 낮음 (재귀 패턴 사용 시) | 높음 (콜백이 오래 걸릴 때) |
| 취소 함수 | clearTimeout | clearInterval |
setInterval의 숨겨진 문제점 😱
setInterval을 쓸 때 조심해야 할 게 있어. 콜백 함수가 실행되는 시간이 간격보다 길면 어떻게 될까?
// 문제 상황: 콜백이 2초 걸리는데 1초마다 실행하려고 함
setInterval(() => {
// 이 작업이 2초 걸린다고 가정
doHeavyWork(); // 2000ms 소요
}, 1000);
// setInterval은 1초마다 콜백을 태스크 큐에 넣으려 하지만
// 이전 콜백이 아직 실행 중이면 큐에 쌓이게 됨
// → 콜백들이 연속으로 실행되는 현상 발생!
재귀 setTimeout 패턴 — 더 안전한 반복 🛡️
setInterval 대신 setTimeout을 재귀적으로 호출하는 패턴이야. 이게 더 안전하고 유연해!
// 재귀 setTimeout 패턴
function repeat() {
// 작업 수행
doSomething();
// 작업 완료 후 다음 실행 예약
setTimeout(repeat, 1000);
}
// 시작
setTimeout(repeat, 1000);
// 이 패턴의 장점:
// 1. 이전 콜백이 완전히 끝난 후 다음 실행을 예약
// 2. 콜백 실행 시간에 따라 간격이 자동 조정됨
// 3. 조건에 따라 간격을 동적으로 변경 가능!
// 더 실용적인 예시: 동적 간격 조정
function smartRepeat() {
const startTime = Date.now();
doWork();
const elapsed = Date.now() - startTime;
const nextDelay = Math.max(0, 1000 - elapsed); // 최소 0ms
setTimeout(smartRepeat, nextDelay);
}
setTimeout(smartRepeat, 1000);
- 재귀 setTimeout: 무거운 작업, 이전 작업 완료 후 다음 실행이 중요할 때 (API 폴링, 데이터 동기화 등)
🎯 실전 활용 패턴 모음
이론은 충분히 했으니까 이제 실제로 어떻게 쓰는지 패턴들을 살펴보자! 개발하다 보면 이런 패턴들을 정말 자주 쓰게 돼.
패턴 1: 디바운스(Debounce) 🏀
사용자가 연속으로 이벤트를 발생시킬 때, 마지막 이벤트 후 일정 시간이 지나면 실행하는 패턴이야. 검색창에서 타이핑할 때 매 키 입력마다 API 호출하면 서버가 힘들잖아? 그걸 방지하는 거야!
function debounce(func, delay) {
let timerId;
return function(...args) {
// 이전 타이머 취소
clearTimeout(timerId);
// 새 타이머 등록
timerId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// 사용 예시
const searchInput = document.getElementById('search');
const handleSearch = debounce((event) => {
console.log('검색 실행:', event.target.value);
// API 호출 등
}, 500);
searchInput.addEventListener('input', handleSearch);
// 사용자가 타이핑을 멈추고 0.5초 후에만 검색 실행!
패턴 2: 쓰로틀(Throttle) 🚦
일정 시간 동안 최대 한 번만 실행되도록 제한하는 패턴이야. 스크롤 이벤트처럼 엄청 자주 발생하는 이벤트에 유용해!
function throttle(func, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// 사용 예시
const handleScroll = throttle(() => {
console.log('스크롤 위치:', window.scrollY);
}, 200);
window.addEventListener('scroll', handleScroll);
// 200ms에 한 번만 실행됨!
패턴 3: 폴링(Polling) 📡
서버에서 주기적으로 데이터를 가져오는 패턴이야. WebSocket이 없을 때 실시간 업데이트를 구현할 때 써!
class Poller {
constructor(fetchFn, interval = 5000) {
this.fetchFn = fetchFn;
this.interval = interval;
this.timerId = null;
this.isRunning = false;
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this.poll();
}
async poll() {
try {
await this.fetchFn();
} catch (error) {
console.error('폴링 에러:', error);
}
if (this.isRunning) {
this.timerId = setTimeout(() => this.poll(), this.interval);
}
}
stop() {
this.isRunning = false;
clearTimeout(this.timerId);
}
}
// 사용 예시
const poller = new Poller(async () => {
const response = await fetch('/api/notifications');
const data = await response.json();
updateUI(data);
}, 3000);
poller.start();
// 나중에 멈출 때
// poller.stop();
패턴 4: 지연 실행 (Lazy Execution) 😴
무거운 초기화 작업을 페이지 로드 직후가 아니라 약간 뒤에 실행해서 초기 렌더링 성능을 높이는 패턴이야!
// 페이지 로드 후 즉시 실행 (무거운 작업)
window.addEventListener('load', () => {
// 핵심 UI 먼저 렌더링
renderCriticalUI();
// 무거운 작업은 100ms 뒤에 실행
setTimeout(() => {
initHeavyAnalytics();
loadNonCriticalAssets();
initChatWidget();
}, 100);
});
패턴 5: 타임아웃 처리 ⏰
비동기 작업에 타임아웃을 걸고 싶을 때 사용하는 패턴이야!
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`${ms}ms 타임아웃 초과!`));
}, ms);
});
return Promise.race([promise, timeout]);
}
// 사용 예시
async function fetchWithTimeout() {
try {
const data = await withTimeout(
fetch('https://api.example.com/data'),
5000 // 5초 타임아웃
);
console.log('데이터 받음:', data);
} catch (error) {
console.error('실패:', error.message);
}
}
fetchWithTimeout();
패턴 6: 애니메이션 루프 🎨
간단한 애니메이션을 setInterval로 구현하는 패턴이야. (실제 복잡한 애니메이션은 requestAnimationFrame을 쓰는 게 더 좋지만!)
// 간단한 카운트다운 타이머 UI
function createCountdown(seconds, onTick, onComplete) {
let remaining = seconds;
onTick(remaining); // 즉시 첫 번째 틱
const intervalId = setInterval(() => {
remaining--;
onTick(remaining);
if (remaining <= 0) {
clearInterval(intervalId);
onComplete();
}
}, 1000);
// 취소 함수 반환
return () => clearInterval(intervalId);
}
// 사용
const cancel = createCountdown(
10,
(remaining) => {
document.getElementById('timer').textContent = remaining;
},
() => {
console.log('카운트다운 완료!');
}
);
// 필요하면 취소
// cancel();
🕳️ 자주 빠지는 함정들
이제 실수하기 쉬운 부분들을 짚어볼게. 이거 알아두면 나중에 디버깅 시간 엄청 아낄 수 있어!
함정 1: this 바인딩 문제 😵
class Timer {
constructor() {
this.count = 0;
}
start() {
// ❌ 잘못된 방법 - this가 undefined 또는 window가 됨
setInterval(function() {
this.count++; // 에러! this가 Timer 인스턴스가 아님
console.log(this.count);
}, 1000);
// ✅ 올바른 방법 1: 화살표 함수 사용
setInterval(() => {
this.count++; // 화살표 함수는 this를 상위 스코프에서 가져옴
console.log(this.count);
}, 1000);
// ✅ 올바른 방법 2: bind 사용
setInterval(function() {
this.count++;
console.log(this.count);
}.bind(this), 1000);
// ✅ 올바른 방법 3: 변수에 저장
const self = this;
setInterval(function() {
self.count++;
console.log(self.count);
}, 1000);
}
}
const timer = new Timer();
timer.start();
함정 2: 클로저와 루프 🔄
// ❌ 흔한 실수: 모든 타이머가 같은 i 값을 참조
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i); // 5, 5, 5, 5, 5 출력됨!
}, i * 1000);
}
// ✅ 해결책 1: let 사용 (블록 스코프)
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i); // 0, 1, 2, 3, 4 출력됨!
}, i * 1000);
}
// ✅ 해결책 2: IIFE로 클로저 생성
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(() => {
console.log(j); // 0, 1, 2, 3, 4 출력됨!
}, j * 1000);
})(i);
}
// ✅ 해결책 3: setTimeout의 추가 인자 활용
for (var i = 0; i < 5; i++) {
setTimeout((j) => {
console.log(j); // 0, 1, 2, 3, 4 출력됨!
}, i * 1000, i);
}
함정 3: 메모리 누수 💧
// ❌ 컴포넌트가 제거돼도 타이머가 계속 실행됨
class MyComponent {
constructor() {
this.data = new Array(1000000).fill('데이터'); // 큰 데이터
// 이 타이머는 컴포넌트가 제거돼도 계속 실행됨!
setInterval(() => {
this.updateData(); // this.data 참조 → 메모리 해제 안 됨
}, 1000);
}
}
// ✅ 올바른 방법: 정리(cleanup) 함수 구현
class MyComponent {
constructor() {
this.data = new Array(1000000).fill('데이터');
this.intervalId = null;
}
mount() {
this.intervalId = setInterval(() => {
this.updateData();
}, 1000);
}
unmount() {
// 컴포넌트 제거 시 타이머 정리!
clearInterval(this.intervalId);
this.intervalId = null;
}
}
// React에서의 예시
// useEffect(() => {
// const id = setInterval(doSomething, 1000);
// return () => clearInterval(id); // cleanup 함수
// }, []);
함정 4: 문자열 인자 사용 (절대 하지 마!) 🚫
// ❌ 절대 이렇게 하지 마! (eval과 동일한 보안 위험)
setTimeout("console.log('위험!')", 1000);
setTimeout("doSomething()", 1000);
// ✅ 항상 함수를 직접 전달
setTimeout(() => console.log('안전!'), 1000);
setTimeout(doSomething, 1000);
eval()처럼 동작해. XSS 공격에 취약해지고 성능도 나빠져. 절대 쓰지 마!
🔬 타이머의 정밀도와 한계
최소 지연 시간
브라우저는 타이머에 최소 지연 시간을 강제해. HTML 명세에 따르면 중첩된 타이머(5단계 이상)는 최소 4ms의 지연이 적용돼!
// 중첩 타이머 테스트
let depth = 0;
function nestedTimer() {
depth++;
const start = Date.now();
setTimeout(() => {
const actual = Date.now() - start;
console.log(`깊이 ${depth}: 요청 0ms, 실제 ${actual}ms`);
if (depth < 10) nestedTimer();
}, 0);
}
nestedTimer();
// 출력 예시:
// 깊이 1: 요청 0ms, 실제 1ms
// 깊이 2: 요청 0ms, 실제 1ms
// 깊이 3: 요청 0ms, 실제 1ms
// 깊이 4: 요청 0ms, 실제 1ms
// 깊이 5: 요청 0ms, 실제 4ms ← 여기서부터 최소 4ms!
// 깊이 6: 요청 0ms, 실제 4ms
// ...
백그라운드 탭 스로틀링 🔋
브라우저는 배터리 절약과 성능을 위해 백그라운드 탭의 타이머를 제한해!
Firefox 백그라운드 탭: 최소 1000ms 간격으로 제한
Safari 백그라운드 탭: 더 엄격한 제한 적용
해결책: 정확한 타이밍이 필요하면
Web Workers를 사용해. 워커는 백그라운드 탭 제한을 받지 않아!
requestAnimationFrame과의 비교 🎬
애니메이션 구현할 때 setInterval 대신 requestAnimationFrame을 쓰는 게 더 좋은 경우가 많아!
// setInterval로 애니메이션 (비추천)
let position = 0;
setInterval(() => {
position += 5;
element.style.left = position + 'px';
}, 16); // 약 60fps를 목표로 하지만 정확하지 않음
// requestAnimationFrame으로 애니메이션 (추천!)
let position = 0;
function animate() {
position += 5;
element.style.left = position + 'px';
if (position < 500) {
requestAnimationFrame(animate); // 다음 프레임에 실행
}
}
requestAnimationFrame(animate);
- 백그라운드 탭에서 자동으로 일시 정지
- 배터리 소모 최소화
- 부드러운 애니메이션 보장
🧪 Promise와 async/await로 타이머 다루기
현대 자바스크립트에서는 타이머를 Promise로 감싸서 async/await와 함께 쓰는 패턴이 정말 많이 쓰여!
sleep 함수 만들기 😴
// Promise로 감싼 sleep 함수
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// async/await와 함께 사용
async function main() {
console.log("시작!");
await sleep(2000); // 2초 대기
console.log("2초 후...");
await sleep(1000); // 1초 대기
console.log("3초 후!");
}
main();
취소 가능한 sleep 만들기 🛑
function cancellableSleep(ms) {
let timerId;
const promise = new Promise((resolve, reject) => {
timerId = setTimeout(resolve, ms);
});
const cancel = () => {
clearTimeout(timerId);
};
return { promise, cancel };
}
// 사용 예시
async function example() {
const { promise, cancel } = cancellableSleep(5000);
// 2초 뒤에 취소
setTimeout(cancel, 2000);
await promise;
console.log("이건 실행 안 될 수도 있어!");
}
AbortController와 함께 사용하기 🎮
function sleepWithAbort(ms, signal) {
return new Promise((resolve, reject) => {
const timerId = setTimeout(resolve, ms);
signal.addEventListener('abort', () => {
clearTimeout(timerId);
reject(new DOMException('Aborted', 'AbortError'));
});
});
}
// 사용 예시
async function fetchWithDelay() {
const controller = new AbortController();
// 3초 뒤에 취소
setTimeout(() => controller.abort(), 3000);
try {
await sleepWithAbort(5000, controller.signal);
console.log("5초 완료!");
} catch (error) {
if (error.name === 'AbortError') {
console.log("취소됨!");
}
}
}
재시도 로직 구현하기 🔄
async function retry(fn, options = {}) {
const {
maxAttempts = 3,
delay = 1000,
backoff = 2, // 지수 백오프 배수
} = options;
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
console.log(`시도 ${attempt}/${maxAttempts} 실패:`, error.message);
if (attempt < maxAttempts) {
const waitTime = delay * Math.pow(backoff, attempt - 1);
console.log(`${waitTime}ms 후 재시도...`);
await sleep(waitTime);
}
}
}
throw lastError;
}
// 사용 예시
async function main() {
try {
const result = await retry(
() => fetch('https://api.example.com/data'),
{ maxAttempts: 3, delay: 1000, backoff: 2 }
);
console.log('성공!', result);
} catch (error) {
console.error('모든 재시도 실패:', error);
}
}
// 재시도 간격: 1초 → 2초 → 4초 (지수 백오프)
🧩 Node.js에서의 타이머
Node.js에서도 setTimeout과 setInterval을 쓸 수 있어. 브라우저와 거의 같지만 몇 가지 차이점이 있어!
Node.js 전용 타이머 함수들
clearImmediate(id) setImmediate 취소
process.nextTick(fn) 현재 작업 완료 직후, 이벤트 루프 다음 단계 전에 실행
timers/promises Promise 기반 타이머 모듈 (Node.js 15+)
// Node.js 실행 순서 테스트
console.log("1: 동기");
process.nextTick(() => console.log("2: nextTick"));
Promise.resolve().then(() => console.log("3: Promise"));
setImmediate(() => console.log("4: setImmediate"));
setTimeout(() => console.log("5: setTimeout 0ms"), 0);
console.log("6: 동기");
// 출력 순서:
// 1: 동기
// 6: 동기
// 2: nextTick ← 가장 먼저!
// 3: Promise ← 마이크로태스크
// 4: setImmediate ← I/O 이후
// 5: setTimeout 0ms
// Node.js 15+ timers/promises 모듈
import { setTimeout, setInterval } from 'timers/promises';
// Promise 기반 setTimeout
async function main() {
console.log("시작");
await setTimeout(2000); // 2초 대기
console.log("2초 후!");
}
// setInterval을 async iterator로 사용
async function* generateTicks(interval) {
for await (const _ of setInterval(interval)) {
yield Date.now();
}
}
// 사용
const ticker = generateTicks(1000);
for await (const timestamp of ticker) {
console.log('틱:', timestamp);
// 5번 후 break
}
process.nextTick은 마이크로태스크 큐보다도 먼저 실행되는 특별한 함수야!
🧪 타이머 테스트하기
타이머가 포함된 코드를 테스트할 때는 실제로 기다리면 테스트가 너무 느려지잖아. 그래서 가짜 타이머(Fake Timers)를 사용해!
Jest의 가짜 타이머
// Jest 테스트 예시
describe('타이머 테스트', () => {
beforeEach(() => {
jest.useFakeTimers(); // 가짜 타이머 활성화
});
afterEach(() => {
jest.useRealTimers(); // 실제 타이머 복원
});
test('setTimeout 테스트', () => {
const callback = jest.fn();
setTimeout(callback, 3000);
// 아직 실행 안 됨
expect(callback).not.toHaveBeenCalled();
// 3초 앞으로 빨리감기
jest.advanceTimersByTime(3000);
// 이제 실행됨
expect(callback).toHaveBeenCalledTimes(1);
});
test('setInterval 테스트', () => {
const callback = jest.fn();
setInterval(callback, 1000);
jest.advanceTimersByTime(5000); // 5초 앞으로
expect(callback).toHaveBeenCalledTimes(5); // 5번 실행
});
test('모든 타이머 즉시 실행', () => {
const callback = jest.fn();
setTimeout(callback, 99999); // 엄청 긴 타이머
jest.runAllTimers(); // 모든 타이머 즉시 실행
expect(callback).toHaveBeenCalled();
});
});
📊 성능 최적화 팁
타이머 배칭(Batching) 🎯
여러 개의 타이머를 따로따로 만드는 것보다 하나로 묶어서 관리하면 성능이 좋아져!
// ❌ 비효율적: 타이머 여러 개
setTimeout(updateHeader, 100);
setTimeout(updateSidebar, 100);
setTimeout(updateFooter, 100);
// ✅ 효율적: 하나로 묶기
setTimeout(() => {
updateHeader();
updateSidebar();
updateFooter();
}, 100);
타이머 매니저 클래스 만들기 🏗️
class TimerManager {
constructor() {
this.timers = new Map();
}
setTimeout(key, fn, delay) {
// 같은 키의 기존 타이머 취소
this.clearTimeout(key);
const id = setTimeout(() => {
this.timers.delete(key);
fn();
}, delay);
this.timers.set(key, { type: 'timeout', id });
return id;
}
setInterval(key, fn, interval) {
this.clearInterval(key);
const id = setInterval(fn, interval);
this.timers.set(key, { type: 'interval', id });
return id;
}
clearTimeout(key) {
const timer = this.timers.get(key);
if (timer && timer.type === 'timeout') {
clearTimeout(timer.id);
this.timers.delete(key);
}
}
clearInterval(key) {
const timer = this.timers.get(key);
if (timer && timer.type === 'interval') {
clearInterval(timer.id);
this.timers.delete(key);
}
}
clearAll() {
this.timers.forEach(({ type, id }) => {
if (type === 'timeout') clearTimeout(id);
else clearInterval(id);
});
this.timers.clear();
}
}
// 사용 예시
const tm = new TimerManager();
// 같은 키로 여러 번 호출해도 마지막 것만 실행됨 (자동 디바운스!)
tm.setTimeout('search', () => doSearch(), 500);
tm.setTimeout('search', () => doSearch(), 500); // 이전 것 자동 취소
// 페이지 이탈 시 모든 타이머 정리
window.addEventListener('beforeunload', () => tm.clearAll());
🌟 실전 예제: 완성된 타이머 컴포넌트
지금까지 배운 내용을 종합해서 실제로 쓸 수 있는 타이머 컴포넌트를 만들어보자! 재능넷 같은 플랫폼에서 경매나 한정 시간 이벤트에 쓸 수 있는 카운트다운 타이머야.
class CountdownTimer {
constructor(options = {}) {
this.duration = options.duration || 60; // 초 단위
this.onTick = options.onTick || (() => {});
this.onComplete = options.onComplete || (() => {});
this.onPause = options.onPause || (() => {});
this.onResume = options.onResume || (() => {});
this.remaining = this.duration;
this.intervalId = null;
this.isPaused = false;
this.isRunning = false;
this.startTime = null;
this.pausedAt = null;
}
start() {
if (this.isRunning) return this;
this.isRunning = true;
this.isPaused = false;
this.startTime = Date.now();
this._tick();
this.intervalId = setInterval(() => this._tick(), 100); // 100ms마다 체크
return this;
}
_tick() {
if (this.isPaused) return;
const elapsed = (Date.now() - this.startTime) / 1000;
this.remaining = Math.max(0, this.duration - elapsed);
this.onTick({
remaining: this.remaining,
elapsed: elapsed,
progress: elapsed / this.duration,
formatted: this._format(this.remaining)
});
if (this.remaining <= 0) {
this.stop();
this.onComplete();
}
}
pause() {
if (!this.isRunning || this.isPaused) return this;
this.isPaused = true;
this.pausedAt = Date.now();
this.onPause({ remaining: this.remaining });
return this;
}
resume() {
if (!this.isPaused) return this;
// 일시정지된 시간만큼 시작 시간 조정
const pausedDuration = Date.now() - this.pausedAt;
this.startTime += pausedDuration;
this.isPaused = false;
this.pausedAt = null;
this.onResume({ remaining: this.remaining });
return this;
}
stop() {
clearInterval(this.intervalId);
this.intervalId = null;
this.isRunning = false;
return this;
}
reset() {
this.stop();
this.remaining = this.duration;
this.isPaused = false;
this.startTime = null;
this.pausedAt = null;
return this;
}
_format(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) {
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}
}
// 사용 예시
const timer = new CountdownTimer({
duration: 300, // 5분
onTick: ({ formatted, progress }) => {
document.getElementById('display').textContent = formatted;
document.getElementById('progress').style.width = `${progress * 100}%`;
},
onComplete: () => {
console.log('시간 종료!');
alert('경매가 종료되었습니다!');
}
});
// 시작
document.getElementById('startBtn').onclick = () => timer.start();
// 일시정지
document.getElementById('pauseBtn').onclick = () => timer.pause();
// 재개
document.getElementById('resumeBtn').onclick = () => timer.resume();
// 초기화
document.getElementById('resetBtn').onclick = () => timer.reset();
setInterval을 100ms 간격으로 사용해서 부드러운 업데이트-
Date.now()를 기준으로 실제 경과 시간 계산 → 드리프트 방지- 일시정지/재개 기능 지원
- 메서드 체이닝 지원 (
timer.start().pause() 이런 식으로)- 재능넷 같은 플랫폼의 경매, 이벤트 타이머에 바로 적용 가능!
📚 핵심 내용 총정리
🎓 마무리하며
자, 이제 setTimeout과 setInterval에 대해 정말 깊이 파헤쳐봤어! 처음에는 단순히 "몇 초 뒤에 실행해주는 함수" 정도로만 알고 있었다면, 이제는 이벤트 루프와의 관계, 정밀도의 한계, 실전 패턴까지 완전히 이해했을 거야. 🎉
자바스크립트 개발을 하다 보면 타이머는 정말 어디서나 쓰이거든. 디바운스, 쓰로틀, 폴링, 애니메이션, 카운트다운... 이 모든 것의 기반이 바로 오늘 배운 두 함수야.
프로그래밍 실력을 키우고 싶다면 이런 기초 개념들을 제대로 이해하는 게 정말 중요해. 재능넷에서도 JavaScript 관련 재능을 가진 분들이 많이 활동하고 있으니, 더 깊은 내용이 궁금하다면 전문가의 도움을 받아보는 것도 좋은 방법이야!
2️⃣ 둘 다 Web API — 이벤트 루프를 통해 비동기로 실행
3️⃣ 지연 시간은 "최소" 보장, 정확한 시간은 보장 안 됨
4️⃣ 무거운 반복 작업엔 재귀 setTimeout 패턴이 더 안전
5️⃣ 컴포넌트 정리 시 반드시 clearTimeout/clearInterval 호출
6️⃣ 디바운스, 쓰로틀, sleep 등 실전 패턴을 적극 활용하자!
이제 타이머 관련 코드를 보면 "아, 이게 이렇게 돌아가는 거구나!" 하고 바로 이해할 수 있을 거야. 코딩 즐겁게 해! 🚀
관련 키워드
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.