호출 스케줄링
일반적으로 함수를 호출하면 코드가 실행되는 즉시 함수가 실행된다. 하지만 개발을 하다 보면 특정 시간이 경과한 후에 함수를 호출해야 하는 상황이 발생한다. 이런 경우에 타이머 함수를 사용하여 함수 호출을 예약할 수 있으며, 이러한 행위를 호출 스케줄링이라고 한다.
자바스크립트는 호출 스케줄링을 위해 두 가지 타이머 생성 함수와 두 가지 타이머 제거 함수를 제공한다.
타이머 관련 함수들
- 타이머 생성 함수
- setTimeout: 지정된 시간이 경과한 후에 콜백 함수를 단 한 번 실행한다.
- setInterval: 지정된 시간 간격마다 콜백 함수를 반복적으로 실행한다.
- 타이머 제거 함수
- clearTimeout: setTimeout으로 생성한 타이머를 제거한다.
- clearInterval: setInterval로 생성한 타이머를 제거한다.
이러한 타이머 함수들은 특이하게도 ECMAScript 사양에 정의된 빌트인 함수가 아니다. 대신 브라우저와 Node.js 같은 호스트 환경에서 제공하는 호스트 객체로서, 전역 객체의 메서드로 제공된다. 이는 실행 환경에 따라 타이머 함수의 구현이나 세부 동작이 다를 수 있다는 점을 시사한다.
타이머의 동작 방식
타이머의 동작을 이해하기 위해서는 자바스크립트 엔진의 동작 방식을 알아야 한다. 자바스크립트 엔진은 단 하나의 실행 컨텍스트 스택만을 사용하는 싱글 스레드 방식으로 동작한다. 이는 한 번에 하나의 작업만을 처리할 수 있다는 것을 의미한다.
이러한 제약 때문에 타이머 함수들은 비동기 처리 방식으로 동작한다. 즉, 타이머의 만료 시간이 되었다고 해서 반드시 그 즉시 콜백 함수가 실행되는 것이 아니라, 현재 실행 중인 작업이 있다면 그 작업이 완료된 후에 실행된다. 이러한 특성으로 인해 정확한 타이밍을 보장하지는 않는다.
실제 지연 시간의 편차
타이머 함수에 지정된 시간은 콜백 함수가 실행되기까지의 최소 대기 시간을 의미한다. 실제로는 다음과 같은 이유로 지정된 시간보다 더 지연될 수 있다:
- 현재 실행 중인 작업이 있는 경우 해당 작업이 완료될 때까지 대기해야 한다.
- 이벤트 루프와 태스크 큐의 상태에 따라 실행 시점이 달라질 수 있다.
- 렌더링 엔진의 작업이나 다른 비동기 작업들과의 경쟁이 발생할 수 있다.
타이머 함수
setTimeout / clearTimeout
setTimeout은 일정 시간이 경과한 후 콜백 함수를 단 한 번 실행하는 타이머를 생성하는 함수다. 함수의 기본적인 형태는 다음과 같다.
const timeoutId = setTimeout(func|code[, delay, param1, param2, ...])
setTimeout 함수는 세 가지 매개변수를 받을 수 있다. 첫 번째는 실행할 콜백 함수이며, 두 번째는 지연 시간(밀리초 단위)이다. 세 번째부터는 콜백 함수에 전달할 인수들을 순서대로 넣을 수 있다. 특히 지연 시간을 나타내는 두 번째 매개변수는 생략이 가능한데, 이 경우 기본값으로 0이 설정된다.
콜백 함수를 전달하는 방식은 다음과 같이 여러 가지가 있다.
// 기본적인 사용법
setTimeout(() => console.log('기본 타이머'), 1000);
// 콜백 함수에 인수 전달
setTimeout((name, age) => console.log(`이름: ${name}, 나이: ${age}`), 1000, '홍길동', 20);
// 지연 시간 생략
setTimeout(() => console.log('지연 시간 생략'));
setTimeout 함수는 생성된 타이머를 식별할 수 있는 고유한 id를 반환한다. 이 타이머 id는 실행 환경에 따라 다른 형태를 가진다. 브라우저에서는 숫자 형태의 id를 반환하고, Node.js 환경에서는 객체 형태의 id를 반환한다. 이 타이머 id는 clearTimeout 함수를 통해 타이머를 취소할 때 사용된다.
// 타이머 생성 및 id 저장
const timerId = setTimeout(() => {
console.log('이 메시지는 출력되지 않는다.');
}, 1000);
// 타이머 취소
clearTimeout(timerId);
clearTimeout 함수는 setTimeout으로 생성한 타이머를 제거하는 역할을 한다. 이 함수는 setTimeout이 반환한 타이머 id를 인수로 받아서 해당 타이머를 취소한다. 타이머가 취소되면 예약된 콜백 함수는 실행되지 않는다.
setInterval / clearInterval
setInterval은 일정 시간 간격으로 콜백 함수를 반복 실행하는 타이머를 생성하는 함수다. setTimeout이 한 번만 실행되는 것과 달리 setInterval은 타이머가 취소될 때까지 계속해서 실행된다. 함수의 기본 형태는 setTimeout과 동일하다.
const intervalId = setInterval(func|code[, delay, param1, param2, ...])
setInterval도 setTimeout과 마찬가지로 콜백 함수, 지연 시간, 그리고 콜백 함수에 전달할 인수들을 매개변수로 받는다. 다만 동작 방식에서 차이가 있는데, setInterval은 지정된 지연 시간마다 콜백 함수를 반복적으로 실행한다.
setInterval의 실제 사용 예시를 보면 다음과 같다.
let count = 1;
const max = 5;
const intervalId = setInterval(() => {
console.log(`${count}번째 실행`);
if (count >= max) {
console.log('타이머 종료');
clearInterval(intervalId);
}
count++;
}, 1000);
이 코드는 1초마다 카운트를 증가시키면서 메시지를 출력하고, 카운트가 5에 도달하면 타이머를 종료한다. clearInterval 함수는 setInterval로 생성된 타이머를 제거하는데 사용되며, setInterval이 반환한 타이머 id를 인수로 받아 해당 타이머를 취소한다.
setInterval을 사용할 때는 주의해야 할 점이 있다. setInterval은 이전 작업의 완료 여부와 관계없이 일정 간격으로 콜백 함수를 실행하려 한다. 따라서 콜백 함수의 실행 시간이 간격보다 길어지면 콜백 함수의 실행이 연속적으로 누적될 수 있다. 이런 경우에는 setTimeout을 재귀적으로 호출하는 방식을 대안으로 고려할 수 있다.
디바운스와 스로틀
현대의 웹 애플리케이션에서는 다양한 사용자 인터랙션을 처리해야 한다. 특히 scroll, resize, input, mousemove와 같은 이벤트들은 사용자의 동작에 따라 매우 짧은 시간 간격으로 연속해서 발생하는 특징을 가진다. 이러한 이벤트들에 바인딩된 이벤트 핸들러가 과도하게 호출되면 애플리케이션의 성능이 크게 저하될 수 있다. 이는 특히 이벤트 핸들러가 복잡한 계산이나 DOM 조작, API 호출 등을 수행할 때 더욱 문제가 된다.
이러한 성능 문제를 해결하기 위해 개발자들은 디바운스(Debounce)와 스로틀(Throttle)이라는 두 가지 프로그래밍 기법을 사용한다. 이 두 기법은 연속적으로 발생하는 이벤트들을 그룹화하여 제어함으로써 이벤트 핸들러의 실행 횟수를 적절하게 조절한다.
디바운스
디바운스는 연속해서 발생하는 이벤트를 제어하는 기법으로, 여러 번 발생하는 이벤트 중 가장 마지막 이벤트만을 처리하는 방식이다. 구체적으로, 연속된 이벤트가 발생하는 동안에는 어떠한 이벤트도 처리하지 않다가, 더 이상 이벤트가 발생하지 않는 상태가 특정 시간 동안 지속되면 마지막 이벤트를 처리하는 방식으로 동작한다.
const debounce = (callback, delay) => {
let timerId;
// 클로저를 사용하여 timerId를 기억하는 함수를 반환한다
return event => {
// 이전에 설정된 타이머가 있다면 취소한다
if (timerId) clearTimeout(timerId);
// 새로운 타이머를 설정하고 delay 시간 후에 콜백을 실행한다
timerId = setTimeout(() => {
callback(event);
timerId = null; // 타이머 id 초기화
}, delay);
};
};
위는 디바운스의 상세한 구현 예시다.
이러한 디바운스의 실제 활용 사례를 살펴보자.
const $input = document.querySelector('input');
const $msg = document.querySelector('.msg');
// 실시간 검색 기능에 디바운스 적용
const handleSearch = debounce(event => {
const searchTerm = event.target.value;
// API 호출이나 무거운 연산 수행
fetch(`/api/search?q=${searchTerm}`)
.then(response => response.json())
.then(data => {
$msg.textContent = `검색 결과: ${data.length}건`;
});
}, 500);
$input.addEventListener('input', handleSearch);
위 코드에서 사용자가 입력을 할 때마다 API를 호출하는 대신, 사용자가 타이핑을 멈추고 500ms 동안 추가 입력이 없을 때만 API를 호출한다. 이는 서버의 부하를 크게 줄일 수 있는 효과적인 방법이다.
디바운스는 특히 다음과 같은 상황에서 매우 유용하게 사용된다.
- 실시간 검색 기능: 사용자가 검색어를 입력할 때마다 API를 호출하는 대신, 입력이 완료된 후에만 호출함으로써 서버 부하를 줄인다.
- 브라우저 리사이즈 이벤트 처리: 창 크기 조절이 완료된 후에만 레이아웃을 재계산하여 불필요한 연산을 방지한다.
- 자동 저장 기능: 문서 편집기에서 사용자가 타이핑하는 동안에는 저장하지 않고, 입력이 잠시 멈춘 후에만 저장을 수행한다.
- 중복 클릭 방지: 버튼의 연속 클릭을 방지하여 동일한 작업이 여러 번 실행되는 것을 막는다.
실무에서는 직접 구현한 디바운스 함수보다는 Underscore나 Lodash와 같은 라이브러리에서 제공하는 debounce 함수를 사용하는 것이 권장된다. 이러한 라이브러리들의 구현은 엣지 케이스들을 잘 처리하고 있으며, 다양한 옵션을 제공하여 더 섬세한 제어가 가능하다.
스로틀
스로틀은 디바운스와 비슷하게 이벤트의 실행 횟수를 제어하는 기법이지만, 그 동작 방식에는 중요한 차이가 있다. 스로틀은 이벤트가 연속으로 발생하더라도 일정 시간 간격으로 이벤트 핸들러가 최대 한 번만 호출되도록 보장한다. 즉, 마지막 이벤트만을 처리하는 디바운스와 달리, 스로틀은 정해진 시간 간격마다 이벤트 핸들러의 실행을 보장한다.
const throttle = (callback, delay) => {
let timerId;
return event => {
// 이미 대기 중인 타이머가 있다면 추가 실행을 막는다
if (timerId) return;
// 지정된 시간이 지나면 콜백을 실행하고 새로운 타이머를 설정할 수 있게 한다
timerId = setTimeout(() => {
callback(event);
timerId = null; // 다음 실행을 위해 타이머 id를 초기화한다
}, delay);
};
};
위는 스로틀의 기본적인 구현 예시다.
스로틀의 실제 활용 예시를 살펴보자.
const $container = document.querySelector('.container');
const $normalCount = document.querySelector('.normal-count');
const $throttleCount = document.querySelector('.throttle-count');
// 일반적인 스크롤 이벤트
let normalCount = 0;
$container.addEventListener('scroll', () => {
$normalCount.textContent = ++normalCount;
});
// 스로틀이 적용된 스크롤 이벤트
let throttleCount = 0;
const handleScroll = throttle(() => {
$throttleCount.textContent = ++throttleCount;
// 스크롤 위치에 따른 추가 콘텐츠 로드 등의 작업 수행
const { scrollTop, scrollHeight, clientHeight } = $container;
if (scrollTop + clientHeight >= scrollHeight - 100) {
loadMoreContent();
}
}, 100);
$container.addEventListener('scroll', handleScroll);
스로틀은 특히 다음과 같은 상황에서 효과적으로 사용된다.
- 무한 스크롤(Infinite Scroll) 구현: 사용자가 스크롤할 때 매번 새로운 콘텐츠를 불러오는 대신, 일정 시간 간격으로만 추가 콘텐츠를 로드한다.
- 실시간 게임이나 애니메이션: 빠른 속도로 움직이는 요소의 위치를 업데이트할 때, 렌더링 성능을 최적화할 수 있다.
- 실시간 데이터 처리: 센서 데이터나 실시간 분석 데이터를 처리할 때, 처리 빈도를 제어하여 시스템 부하를 관리한다.
- 브라우저 리사이즈에 따른 레이아웃 계산: 창 크기가 변경될 때 일정 간격으로만 레이아웃을 재계산하여 성능을 최적화한다.
디바운스와 마찬가지로, 실무에서는 Underscore나 Lodash의 throttle 함수를 사용하는 것이 권장된다. 이러한 라이브러리들은 오랜 시간 동안 검증되었으며, 다양한 브라우저 환경에서 안정적으로 동작한다.
이러한 디바운스와 스로틀은 각각의 장단점이 있으며, 사용 사례에 따라 적절한 기법을 선택해야 한다. 디바운스는 연속된 이벤트의 마지막 동작만을 처리해야 할 때 유용하며, 스로틀은 일정 주기로 이벤트 처리를 보장해야 할 때 적합하다. 두 기법 모두 성능 최적화에 매우 중요한 도구이며, 현대 웹 개발에서 필수적인 기법이라고 할 수 있다.
요약
호출 스케줄링의 개요
- 호출 스케줄링은 함수를 바로 실행하지 않고 일정 시간 후에 실행되도록 예약하는 방식이다.
- 자바스크립트는 타이머 생성 함수(setTimeout, setInterval)와 제거 함수(clearTimeout, clearInterval)를 제공한다.
- 이러한 타이머 함수들은 ECMAScript 사양이 아닌 호스트 객체로 제공되며, 실행 환경에 따라 동작이 다를 수 있다.
타이머의 동작 특성
- 자바스크립트는 싱글 스레드로 동작하여 한 번에 하나의 작업만 처리할 수 있다.
- 타이머는 비동기적으로 동작하며, 정확한 지연 시간을 보장하지 않는다.
- 실제 실행 시점은 다음과 같은 요인들에 의해 지연될 수 있다:
- 현재 실행 중인 작업의 완료 대기
- 이벤트 루프와 태스크 큐의 상태
- 렌더링 작업이나 다른 비동기 작업과의 경쟁
setTimeout/clearTimeout의 특징
- setTimeout은 지정된 시간 후 콜백 함수를 한 번만 실행한다.
- 함수 형태: setTimeout(callback, delay, ...args)
- 반환값으로 타이머 식별자를 제공하며, 이는 환경에 따라 숫자 또는 객체일 수 있다.
- clearTimeout을 통해 예약된 타이머를 취소할 수 있다.
- delay 인수 생략 시 기본값 0이 적용된다.
setInterval/clearInterval의 특징
- setInterval은 주기적으로 콜백 함수를 반복 실행한다.
- 함수 형태는 setTimeout과 동일하다.
- 콜백 함수의 실행 시간이 delay보다 긴 경우 문제가 발생할 수 있다.
- 이러한 경우 setTimeout의 재귀적 호출을 대안으로 고려할 수 있다.
디바운스와 스로틀의 개요
- 둘 다 이벤트의 과도한 호출을 제어하는 프로그래밍 기법이다.
- scroll, resize, input, mousemove 등 연속적으로 발생하는 이벤트 처리에 유용하다.
- 성능 최적화와 서버 부하 관리에 중요한 역할을 한다.
디바운스의 특징과 활용
- 연속된 이벤트 중 마지막 이벤트만 처리한다.
- 이벤트 발생 후 일정 시간 동안 추가 이벤트가 없을 때 콜백을 실행한다.
- 주요 활용 사례:
- 실시간 검색 기능의 API 호출 최적화
- 창 크기 조절 시 레이아웃 계산
- 자동 저장 기능
- 중복 클릭 방지
스로틀의 특징과 활용
- 일정 시간 간격으로 이벤트 핸들러의 실행을 보장한다.
- 연속적인 이벤트를 일정 주기로 그룹화하여 처리한다.
- 주요 활용 사례:
- 무한 스크롤 구현
- 실시간 게임의 위치 업데이트
- 실시간 데이터 처리
- 리사이즈에 따른 레이아웃 계산
실무적 고려사항
- 직접 구현보다는 검증된 라이브러리(Underscore, Lodash) 사용이 권장된다.
- 상황에 맞는 기법 선택이 중요하다:
- 마지막 이벤트만 중요한 경우: 디바운스
- 주기적 실행이 필요한 경우: 스로틀
- 적절한 지연 시간 설정이 중요하다.
- 브라우저 호환성과 엣지 케이스 처리를 고려해야 한다.
예상문제 [🔥]
https://github.com/junh0328/prepare_frontend_interview?tab=readme-ov-file
호출 스케쥴링이 무엇인가요?
호출 스케쥴링은 함수를 명시적으로 당장 실행하는 것이 아니라, 일정 시간이 경과한 후에 호출되도록 예약하는 것을 말합니다.
자바스크립트에서는 이러한 호출 스케쥴링을 위해 타이머 함수들을 제공합니다. 이러한 타이머 함수들은 ECMAScript 사양에서 정의된 것이 아닌 호스트 환경(브라우저나 Node.js)에서 제공하는 호스트 객체입니다.
특히 중요한 점은 자바스크립트 엔진이 싱글 스레드로 동작하기 때문에, 타이머 함수들은 비동기로 동작한다는 것입니다. 이는 지정된 시간이 되었다고 해서 정확히 그 시점에 콜백 함수가 실행된다는 보장이 없다는 것을 의미합니다. 현재 실행 중인 작업이나 이벤트 루프의 상태에 따라 실제 실행 시점이 지연될 수 있습니다.
타이머 함수에는 어떤 것들이 있나요?
자바스크립트에서 제공하는 타이머 함수는 크게 네 가지입니다.
첫째, setTimeout은 일정 시간이 지난 후에 콜백 함수를 단 한 번 실행하는 타이머를 생성합니다. 기본 형태는 setTimeout(callback, delay, ...args)이며, delay는 밀리초 단위로 지정하고 생략하면 0이 기본값이 됩니다.
둘째, setInterval은 일정 시간 간격으로 콜백 함수를 반복해서 실행하는 타이머를 생성합니다. 형태는 setTimeout과 동일하지만, 콜백 함수가 주기적으로 반복 실행된다는 점이 다릅니다.
셋째와 넷째로, clearTimeout과 clearInterval이 있습니다. 이들은 각각 setTimeout과 setInterval로 생성한 타이머를 제거하는 함수입니다. 타이머 함수가 반환한 고유한 타이머 id를 인수로 받아 해당 타이머를 취소합니다.
특히 setInterval 사용 시에는 한 가지 주의할 점이 있는데, 콜백 함수의 실행 시간이 interval보다 길어지면 콜백 함수의 실행이 중첩될 수 있다는 것입니다. 이런 경우에는 setTimeout을 재귀적으로 호출하는 방식을 대안으로 사용할 수 있습니다.
이벤트가 과도하게 호출되어 성능에 문제를 일으킬 경우에 할 수 있는 어떤 일을 통해 해결할 수 있나요?
이벤트가 과도하게 호출되는 문제는 주로 scroll, resize, input, mousemove와 같은 이벤트에서 자주 발생하는데, 이를 해결하기 위한 대표적인 두 가지 기법으로 디바운스(Debounce)와 스로틀(Throttle)이 있습니다.
이 두 기법은 연속해서 발생하는 이벤트들을 그룹화하여 이벤트 핸들러의 과도한 호출을 방지합니다. 특히 이벤트 핸들러가 복잡한 계산이나 DOM 조작, API 호출 등을 수행할 때 매우 효과적입니다.
실무에서는 이러한 기법들을 직접 구현하기보다는 Underscore나 Lodash와 같은 검증된 라이브러리의 구현체를 사용하는 것이 권장됩니다. 이러한 라이브러리들은 엣지 케이스들을 잘 처리하고 있으며, 브라우저 호환성도 보장되어 있습니다.
디바운스에 대해서 알고 있나요?
디바운스는 연속해서 발생하는 이벤트를 제어하는 기법으로, 연속된 이벤트 중에서 마지막 이벤트만 처리하도록 하는 방식입니다.
구체적인 동작 방식을 설명하면, 이벤트가 발생했을 때 즉시 처리하지 않고 일정 시간을 기다립니다. 이 대기 시간 동안 추가적인 이벤트가 발생하면 이전 대기시간을 취소하고 새로운 대기시간을 설정합니다. 최종적으로 설정된 대기시간 동안 더 이상 이벤트가 발생하지 않으면 이벤트 핸들러가 딱 한 번 실행됩니다.
전형적인 사용 예시로는 실시간 검색 기능이 있습니다. 사용자가 검색어를 입력할 때마다 API를 호출하는 것은 비효율적이므로, 사용자가 타이핑을 멈추고 일정 시간(예: 500ms)이 지난 후에 최종 입력값으로 한 번만 API를 호출하는 것이 효율적입니다.
이외에도 브라우저 리사이즈에 따른 레이아웃 계산, 자동 저장 기능, 버튼 중복 클릭 방지 등에 유용하게 사용됩니다.
쓰로틀에 대해서 알고 있나요?
스로틀은 이벤트가 연속해서 발생하더라도 일정 시간 간격으로 이벤트 핸들러가 최대 한 번만 호출되도록 보장하는 기법입니다.
디바운스가 마지막 이벤트만 처리하는 것과 달리, 스로틀은 정해진 시간 간격마다 이벤트 처리를 보장한다는 점이 큰 차이점입니다. 예를 들어 스로틀 시간을 200ms로 설정하면, 이벤트가 아무리 연속해서 발생하더라도 200ms마다 한 번씩은 이벤트 핸들러가 실행됩니다.
대표적인 활용 사례는 무한 스크롤 구현입니다. 사용자가 스크롤할 때마다 매번 서버에 요청을 보내는 것이 아니라, 일정 시간 간격으로만 요청을 보내도록 제어할 수 있습니다.
또한 실시간 게임에서 캐릭터의 위치 업데이트나 차트 데이터의 실시간 업데이트와 같이 주기적인 업데이트가 필요한 경우에도 유용하게 사용됩니다. 브라우저 리사이즈에 따른 레이아웃 계산에도 스로틀을 적용하면 성능을 크게 개선할 수 있습니다.
'🧱 프론트엔드 주제 > JavaScript' 카테고리의 다른 글
[모던 자바스크립트 Deep Dive] 43장 - Ajax (1) | 2024.11.19 |
---|---|
[모던 자바스크립트 Deep Dive] 42장 - 비동기 프로그래밍 (0) | 2024.11.12 |
[모던 자바스크립트 Deep Dive] 40장 - 이벤트 (0) | 2024.11.11 |
[모던 자바스크립트 Deep Dive] 39장 - DOM (0) | 2024.11.11 |
[모던 자바스크립트 Deep Dive] 38장 - 브라우저의 렌더링 과정 (3) | 2024.10.31 |