콘텐츠 대표 이미지 - ⏱️ 자바스크립트 타이머 완전정복: setTimeout과 setInterval의 모든 것
JavaScript / 프로그램개발

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

비동기 타이밍 제어의 핵심 — 원리부터 실전 패턴까지 친구처럼 쉽게 파헤쳐보자!

안녕! 오늘은 자바스크립트 개발하다 보면 무조건 마주치게 되는 두 친구, setTimeout과 setInterval에 대해 제대로 파헤쳐볼 거야. 🎉

"어, 나 이거 써봤는데?" 싶은 사람도 있을 거고, "이름은 들어봤는데 정확히 어떻게 돌아가는지 모르겠어" 하는 사람도 있을 거야. 둘 다 환영이야! 오늘 끝나고 나면 이 두 함수를 완전히 내 것으로 만들 수 있을 거거든.

단순히 "몇 초 뒤에 실행해줘" 수준을 넘어서, 이벤트 루프와의 관계, 실전에서 자주 쓰는 패턴, 그리고 흔히 빠지는 함정까지 — 전부 다 커버할 거야. 준비됐어? 그럼 고고! 🚀


🕰️ 타이머, 왜 필요한 거야?

먼저 왜 타이머가 필요한지부터 생각해보자. 자바스크립트는 기본적으로 싱글 스레드(Single Thread) 언어야. 한 번에 하나의 작업만 처리할 수 있다는 뜻이지.

그런데 현실 세계의 웹 앱을 보면 어때? 3초 뒤에 팝업 띄우기, 1초마다 시계 업데이트, 사용자가 타이핑 멈추면 0.5초 뒤에 검색 실행... 이런 것들이 넘쳐나잖아.

이걸 가능하게 해주는 게 바로 비동기 타이머 API야. 자바스크립트 엔진 자체가 아니라, 브라우저(또는 Node.js)가 제공하는 Web API를 통해 타이밍을 제어하는 거지.

💡 핵심 포인트: setTimeout과 setInterval은 자바스크립트 언어 자체의 기능이 아니라, 브라우저/Node.js 환경이 제공하는 Web API야. ECMAScript 명세에는 없고, HTML 명세에 정의되어 있어!

이 사실이 왜 중요하냐고? 나중에 이벤트 루프 얘기할 때 이게 핵심이 되거든. 일단 계속 가보자!


⏳ 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살이야
✅ 반환값 timerId: setTimeout은 타이머 ID(양의 정수)를 반환해. 이걸 저장해두면 나중에 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)의 마법이야! 잠깐 이 개념을 짚고 넘어가자.

자바스크립트 이벤트 루프 구조 콜 스택 (Call Stack) console.log("3번") console.log("1번") 전역 실행 컨텍스트 Web APIs (브라우저 제공) setTimeout 타이머 대기중... DOM 이벤트 / fetch 등 태스크 큐 (Task Queue / Callback Queue) setTimeout 콜백 대기 이벤트 루프 (Event Loop) 콜스택 비면 큐에서 꺼내 실행 등록 완료시 콜스택 비면 콜백 push 콜 스택이 완전히 비워진 후에야 태스크 큐의 콜백이 실행돼! 그래서 setTimeout(fn, 0)도 현재 실행 중인 코드가 끝난 뒤에 실행됨 1 동기 코드 실행 2 타이머 등록 3 큐 → 스택

이벤트 루프와 타이머의 관계 🔄

위 그림을 보면서 이해해보자. 자바스크립트 엔진이 코드를 실행하는 방식은 이래:

1
동기 코드 실행: 콜 스택에 쌓인 코드를 순서대로 실행해. console.log("1번"), console.log("3번") 같은 것들.
2
타이머 등록: setTimeout을 만나면 브라우저의 Web API에 "이 콜백을 N밀리초 뒤에 태스크 큐에 넣어줘"라고 위임해.
3
콜백 대기: 지정된 시간이 지나면 브라우저가 콜백을 태스크 큐(Task Queue)에 넣어.
4
이벤트 루프 확인: 이벤트 루프가 콜 스택이 비어있는지 계속 확인해. 비어있으면 태스크 큐에서 콜백을 꺼내 콜 스택에 올려.
⚠️ 중요한 사실: 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);
💡 재미있는 사실: 브라우저 내부적으로 setTimeout과 setInterval은 같은 타이머 ID 풀을 공유해. 그래서 이론적으로는 clearTimeout으로 setInterval을 취소하거나 그 반대도 가능해. 하지만 명확성을 위해 각각 맞는 함수를 쓰는 게 좋아!
setTimeout vs setInterval 비교 setTimeout (한 번만) 시작 2초 후 콜백 실행 — — — — — 이후 실행 없음 setInterval (반복) 시작 1초 2초 3초 4초 중지! = 콜백 함수 실행 = setTimeout = setInterval = clearInterval 1000ms 간격

⚔️ setTimeout vs setInterval — 뭘 써야 해?

둘 다 반복 작업에 쓸 수 있는데, 어떤 차이가 있을까? 이게 진짜 중요한 포인트야!

구분 setTimeout setInterval
실행 횟수 1회 무한 반복 (취소 전까지)
반복 구현 재귀 호출로 가능 기본 기능
실행 간격 콜백 실행 시간 포함 후 다음 예약 콜백 실행 시간 무관하게 고정 간격
드리프트 위험 낮음 (재귀 패턴 사용 시) 높음 (콜백이 오래 걸릴 때)
취소 함수 clearTimeout clearInterval

setInterval의 숨겨진 문제점 😱

setInterval을 쓸 때 조심해야 할 게 있어. 콜백 함수가 실행되는 시간이 간격보다 길면 어떻게 될까?

// 문제 상황: 콜백이 2초 걸리는데 1초마다 실행하려고 함
setInterval(() => {
  // 이 작업이 2초 걸린다고 가정
  doHeavyWork(); // 2000ms 소요
}, 1000);

// setInterval은 1초마다 콜백을 태스크 큐에 넣으려 하지만
// 이전 콜백이 아직 실행 중이면 큐에 쌓이게 됨
// → 콜백들이 연속으로 실행되는 현상 발생!
⚠️ setInterval의 함정: 콜백 실행 시간이 간격보다 길면, 콜백들이 큐에 쌓여서 연속으로 실행될 수 있어. 이걸 방지하려면 재귀 setTimeout 패턴을 써야 해!

재귀 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);
✅ 언제 뭘 써야 해? - setInterval: 가벼운 작업, 정확한 간격이 중요할 때 (시계 UI, 애니메이션 프레임 등)
- 재귀 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 함수
// }, []);
⚠️ 메모리 누수 주의! SPA(Single Page Application)에서 컴포넌트가 언마운트될 때 타이머를 정리하지 않으면 메모리 누수가 발생해. 특히 React, Vue, Angular 같은 프레임워크에서 컴포넌트 라이프사이클에 맞게 타이머를 정리해줘야 해!

함정 4: 문자열 인자 사용 (절대 하지 마!) 🚫

// ❌ 절대 이렇게 하지 마! (eval과 동일한 보안 위험)
setTimeout("console.log('위험!')", 1000);
setTimeout("doSomething()", 1000);

// ✅ 항상 함수를 직접 전달
setTimeout(() => console.log('안전!'), 1000);
setTimeout(doSomething, 1000);
⚠️ 보안 경고: setTimeout/setInterval에 문자열을 전달하면 내부적으로 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
// ...

백그라운드 탭 스로틀링 🔋

브라우저는 배터리 절약과 성능을 위해 백그라운드 탭의 타이머를 제한해!

🌐 브라우저별 백그라운드 탭 타이머 제한: Chrome 백그라운드 탭: 최소 1000ms (1초) 간격으로 제한
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);
💡 requestAnimationFrame의 장점: - 브라우저의 리페인트 주기에 맞춰 실행 (보통 60fps = 16.67ms)
- 백그라운드 탭에서 자동으로 일시 정지
- 배터리 소모 최소화
- 부드러운 애니메이션 보장

🧪 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 전용 타이머 함수들

🟢 Node.js에서만 사용 가능한 타이머 관련 함수들: setImmediate(fn) 현재 이벤트 루프 사이클이 끝나면 즉시 실행
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
}
✅ Node.js 팁: Node.js에서 타이머 함수들의 실행 순서는 브라우저와 다를 수 있어. 특히 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();
  });
});
💡 테스트 팁: Vitest, Sinon.js 등 다른 테스트 라이브러리도 비슷한 가짜 타이머 기능을 제공해. 타이머 테스트할 때는 항상 가짜 타이머를 활용하자!

📊 성능 최적화 팁

타이머 배칭(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 핵심 정리 ⏳ setTimeout • 지정 시간 후 1회 실행 • 반환값: 타이머 ID (정수) • clearTimeout(id)로 취소 • 재귀 호출로 반복 구현 가능 • 콜백 완료 후 다음 예약 → 드리프트 없음 • setTimeout(fn, 0) → 비동기 실행 • 최소 지연: 중첩 5단계+ 시 4ms • 무거운 작업 반복에 적합 🔁 setInterval • 지정 간격마다 반복 실행 • 반환값: 타이머 ID (정수) • clearInterval(id)로 취소 • 고정 간격 유지 (콜백 시간 무관) • 콜백이 오래 걸리면 큐에 쌓임 • 가벼운 반복 작업에 적합 • 시계, 슬라이드쇼, 폴링 등 • 백그라운드 탭에서 제한됨 ⚠️ 공통 주의사항 • Web API (브라우저/Node.js 제공) — ECMAScript 명세 아님 • 콜 스택이 비어야 실행됨 (이벤트 루프) • 지연 시간은 "최소" 보장 — 정확한 실행 시간 보장 안 됨 • 문자열 인자 절대 금지 (보안 위험) • 컴포넌트 언마운트 시 반드시 정리 (메모리 누수 방지) • this 바인딩 주의 (화살표 함수 권장) 🎯 실전 패턴 디바운스: 마지막 이벤트 후 N ms 뒤 실행 쓰로틀: N ms에 최대 1회 실행 폴링: 주기적 서버 데이터 조회 sleep: Promise 래핑으로 async/await 활용 재시도: 지수 백오프로 안정적 재시도 타임아웃: Promise.race로 시간 제한

🎓 마무리하며

자, 이제 setTimeout과 setInterval에 대해 정말 깊이 파헤쳐봤어! 처음에는 단순히 "몇 초 뒤에 실행해주는 함수" 정도로만 알고 있었다면, 이제는 이벤트 루프와의 관계, 정밀도의 한계, 실전 패턴까지 완전히 이해했을 거야. 🎉

자바스크립트 개발을 하다 보면 타이머는 정말 어디서나 쓰이거든. 디바운스, 쓰로틀, 폴링, 애니메이션, 카운트다운... 이 모든 것의 기반이 바로 오늘 배운 두 함수야.

프로그래밍 실력을 키우고 싶다면 이런 기초 개념들을 제대로 이해하는 게 정말 중요해. 재능넷에서도 JavaScript 관련 재능을 가진 분들이 많이 활동하고 있으니, 더 깊은 내용이 궁금하다면 전문가의 도움을 받아보는 것도 좋은 방법이야!

💡 오늘의 핵심 요약: 1️⃣ setTimeout = 한 번 실행, setInterval = 반복 실행
2️⃣ 둘 다 Web API — 이벤트 루프를 통해 비동기로 실행
3️⃣ 지연 시간은 "최소" 보장, 정확한 시간은 보장 안 됨
4️⃣ 무거운 반복 작업엔 재귀 setTimeout 패턴이 더 안전
5️⃣ 컴포넌트 정리 시 반드시 clearTimeout/clearInterval 호출
6️⃣ 디바운스, 쓰로틀, sleep 등 실전 패턴을 적극 활용하자!

이제 타이머 관련 코드를 보면 "아, 이게 이렇게 돌아가는 거구나!" 하고 바로 이해할 수 있을 거야. 코딩 즐겁게 해! 🚀

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.