동기 처리와 비동기 처리
자바스크립트 엔진의 기본 동작 원리
자바스크립트 엔진은 함수가 호출되면 해당 함수의 코드를 평가하여 함수 실행 컨텍스트를 생성한다. 이렇게 생성된 함수 실행 컨텍스트는 실행 컨텍스트 스택(콜 스택)에 순차적으로 푸시되어 실행된다. 함수의 실행이 완료되면 해당 함수의 실행 컨텍스트는 콜 스택에서 팝되어 제거된다.
이러한 구조로 인해 자바스크립트 엔진은 단 하나의 콜 스택만을 사용하며, 이는 함수를 실행할 수 있는 창구가 단 하나라는 것을 의미한다. 결과적으로 자바스크립트는 동시에 2개 이상의 함수를 실행할 수 없는 싱글 스레드 방식으로 동작한다.
동기 처리(Synchronous)의 특징과 동작 방식
동기 처리는 현재 실행 중인 태스크가 완전히 종료될 때까지 다음 태스크가 실행되지 않고 대기하는 방식이다. 이는 마치 한 줄로 서서 차례를 기다리는 것과 같은 원리로 작동한다.
이러한 동기 처리 방식은 태스크를 순서대로 하나씩 처리하므로 실행 순서가 명확하게 보장된다는 장점이 있다. 하지만 앞선 태스크가 종료될 때까지 이후의 모든 태스크들이 블로킹되어 대기해야 하는 심각한 단점이 존재한다.
function sleep(func, delay) {
const delayUntil = Date.now() + delay;
// 현재 시간이 목표 시간에 도달할 때까지 루프를 돌며 블로킹을 발생시킨다
while (Date.now() < delayUntil);
func();
}
function foo() {
console.log('foo');
}
function bar() {
console.log('bar');
}
// sleep 함수는 3초 동안 실행을 블로킹한다
sleep(foo, 3 * 1000);
// bar 함수는 sleep 함수가 완전히 종료된 후에야 실행된다
bar();
위 예제에서 볼 수 있듯이, sleep 함수는 지정된 시간 동안 실행을 블로킹하고, 그 시간이 지난 후에야 다음 함수인 bar가 실행된다. 이는 동기 처리의 전형적인 예시로, 코드의 실행 순서는 보장되지만 성능상의 문제를 일으킬 수 있다.
비동기 처리(Asynchronous)의 특징과 활용
비동기 처리는 현재 실행 중인 태스크가 종료되지 않은 상태라도 다음 태스크를 즉시 실행하는 방식이다. 이는 블로킹이 발생하지 않아 전체적인 처리 속도가 향상되는 장점이 있지만, 태스크의 실행 순서가 보장되지 않는다는 특징이 있다. 대표적인 비동기 처리 방식으로는 타이머 함수(setTimeout, setInterval), HTTP 요청, 이벤트 핸들러 등이 있다.
function foo() {
console.log('foo');
}
function bar() {
console.log('bar');
}
// setTimeout은 비동기적으로 실행되어 블로킹을 발생시키지 않는다
setTimeout(foo, 3 * 1000);
// bar는 foo의 실행을 기다리지 않고 즉시 실행된다
bar();
비동기 처리는 전통적으로 콜백 패턴을 사용하여 구현되었다. 하지만 이러한 콜백 패턴은 비동기 처리가 중첩되는 경우 콜백 헬(Callback Hell)이라는 문제를 발생시킨다.
콜백 헬은 코드의 가독성을 크게 저하시키고, 비동기 처리 중 발생하는 에러의 예외 처리를 매우 어렵게 만든다. 또한 여러 개의 비동기 처리를 한꺼번에 처리하는 데에도 한계가 있다.
이벤트 루프와 태스크 큐
자바스크립트의 동시성 지원 구조
자바스크립트는 싱글 스레드 방식으로 동작하지만, 실제 브라우저에서 동작하는 모습을 보면 여러 작업이 동시에 처리되는 것처럼 보인다. 예를 들어 웹 페이지에서는 애니메이션이 동작하면서 동시에 사용자의 클릭 이벤트를 처리하고, HTTP 요청을 통해 서버로부터 데이터를 가져오면서 동시에 화면을 렌더링하는 것이 가능하다.
이러한 동시성(Concurrency)을 지원하는 것이 바로 이벤트 루프이며, 이는 브라우저에 내장된 핵심적인 기능 중 하나다.
자바스크립트 엔진의 구성 요소와 역할
자바스크립트 엔진(예: 구글의 V8)은 크게 두 가지 핵심 영역으로 구성된다.
콜 스택(Call Stack)
콜 스택은 소스코드 평가 과정에서 생성된 실행 컨텍스트가 추가되고 제거되는 스택 자료구조다. 함수가 호출되면 해당 함수의 실행 컨텍스트가 생성되어 콜 스택에 푸시되고, 함수의 실행이 종료되면 해당 실행 컨텍스트는 콜 스택에서 팝되어 제거된다. 콜 스택의 최상위 요소인 실행 중인 실행 컨텍스트를 제외한 모든 실행 컨텍스트는 실행 대기 중인 태스크들이다.
힙(Heap)
힙은 객체가 저장되는 메모리 공간이다. 원시 값과 달리 객체는 크기가 정해져 있지 않으므로, 할당해야 할 메모리 공간의 크기를 런타임에 결정해야 한다(동적 할당). 따라서 객체가 저장되는 메모리 공간인 힙은 구조화되어 있지 않은 더미와 같은 형태를 띤다. 콜 스택의 실행 컨텍스트는 힙에 저장된 객체들을 참조하는 형태로 동작한다.
브라우저의 동시성 지원 구조
태스크 큐(Task Queue)
태스크 큐는 setTimeout이나 setInterval과 같은 비동기 함수의 콜백 함수 또는 이벤트 핸들러가 일시적으로 보관되는 영역이다. 이는 이벤트 큐(Event Queue) 또는 콜백 큐(Callback Queue)라고도 불린다. 태스크 큐와는 별도로 프로미스의 후속 처리 메서드의 콜백 함수가 일시적으로 보관되는 마이크로태스크 큐도 존재하는데, 이는 태스크 큐와는 다른 우선순위로 처리된다.
이벤트 루프(Event Loop)의 동작 방식
이벤트 루프는 콜 스택과 태스크 큐를 지속적으로 모니터링하는 역할을 한다. 구체적으로는 다음과 같은 순서로 동작한다.
- 콜 스택에 현재 실행 중인 실행 컨텍스트가 있는지 확인한다.
- 태스크 큐에 대기 중인 함수(콜백 함수, 이벤트 핸들러 등)가 있는지 확인한다.
- 만약 콜 스택이 비어 있고 태스크 큐에 대기 중인 함수가 있다면, 가장 앞에 있는 함수를 콜 스택으로 이동시킨다.
- 이동된 함수는 실행되고, 실행이 완료되면 콜 스택에서 제거된다.
이러한 과정을 통해 태스크 큐에 일시 보관된 함수들은 비동기적으로 처리된다. 특히 중요한 점은 태스크 큐에 있는 함수들은 콜 스택이 완전히 비어있을 때만 실행된다는 것이다.
비동기 처리의 실제 동작 과정 분석
자바스크립트의 비동기 처리는 자바스크립트 엔진의 영역과 브라우저(혹은 Node.js)의 영역이 협력하여 동작한다. 비동기 처리의 실제 동작을 이해하기 위해 다음 예제를 통해 자세히 살펴보자.
function foo() {
console.log('foo');
}
function bar() {
console.log('bar');
}
setTimeout(foo, 0); // 0초(실제로는 4ms) 후에 foo 함수가 호출된다
bar();
이 코드의 실행 과정은 다음과 같이 진행된다.
- 전역 코드의 평가
- 전역 코드가 평가되어 전역 실행 컨텍스트가 생성되고 콜 스택에 푸시된다.
- setTimeout 함수의 실행
- setTimeout 함수가 호출되면서 setTimeout 함수의 실행 컨텍스트가 생성되어 콜 스택에 푸시된다.
- 브라우저의 Web API인 타이머 함수도 함수이므로 실행 컨텍스트를 생성한다.
- setTimeout 함수는 브라우저에게 타이머 설정과 콜백 함수 등록을 요청한 후 종료되어 콜 스택에서 제거된다.
- 브라우저의 타이머 설정과 자바스크립트 엔진의 실행이 병행 처리
- 브라우저는 타이머를 설정하고 만료를 기다린다. 타이머가 만료되면 콜백 함수 foo를 태스크 큐에 푸시한다.
- 예제의 경우 지연 시간이 0이지만, 브라우저는 최소 지연 시간으로 4ms를 적용한다.
- 이와 동시에 자바스크립트 엔진은 bar 함수를 호출하여 실행한다.
- bar 함수의 실행
- bar 함수가 호출되어 실행 컨텍스트가 생성되고 콜 스택에 푸시된다.
- bar 함수가 실행을 마치고 콜 스택에서 제거된다.
- 이 시점에서 타이머가 만료되었더라도 foo 함수는 아직 태스크 큐에서 대기 중이다.
- 전역 코드 실행 종료
- 전역 코드의 실행이 완료되면 전역 실행 컨텍스트가 콜 스택에서 제거된다.
- 이제 콜 스택이 완전히 비어있는 상태가 된다.
- 이벤트 루프에 의한 콜백 함수 실행
- 이벤트 루프는 콜 스택이 비어있음을 감지하고, 태스크 큐에서 대기 중인 foo 함수를 콜 스택으로 이동시킨다.
- foo 함수의 실행 컨텍스트가 생성되어 실행되고, 실행이 완료되면 콜 스택에서 제거된다.
브라우저와 자바스크립트 엔진의 관계
중요한 점은 자바스크립트 엔진은 싱글 스레드로 동작하지만, 브라우저는 멀티 스레드로 동작한다는 것이다. 만약 모든 자바스크립트 코드가 자바스크립트 엔진에서만 싱글 스레드로 동작한다면, 비동기 처리는 불가능할 것이다. 예를 들어, setTimeout 함수의 타이머 설정도 자바스크립트 엔진이 수행한다면, 타이머가 완료될 때까지 다른 어떤 코드도 실행할 수 없을 것이다.
따라서 브라우저는 자바스크립트 엔진 외에도 렌더링 엔진과 Web API를 제공하여 멀티 스레드로 동작하면서 이러한 비동기 작업들을 처리한다. Web API는 DOM API, 타이머 함수, HTTP 요청(Ajax)과 같은 비동기 처리를 포함하며, ECMAScript 사양이 아닌 브라우저에서 제공하는 API다. 이러한 방식으로 브라우저와 자바스크립트 엔진이 협력하여 효율적인 비동기 처리를 가능하게 한다.
요약
자바스크립트 엔진의 기본 구조와 동작 원리
- 자바스크립트 엔진은 단 하나의 콜 스택을 가지며, 이는 싱글 스레드 방식으로 동작한다.
- 함수가 호출되면 실행 컨텍스트가 생성되어 콜 스택에 푸시되고, 실행이 완료되면 팝된다.
- 한 번에 하나의 태스크만 처리할 수 있어, 동시에 2개 이상의 함수를 실행할 수 없다.
- 콜 스택과 힙으로 구성되어 있으며, 힙은 객체가 저장되는 메모리 공간이다.
동기 처리의 특징과 한계
- 현재 실행 중인 태스크가 종료될 때까지 다음 태스크가 대기하는 방식이다.
- 실행 순서가 보장되어 예측 가능한 코드 실행이 가능하다.
- 앞선 태스크가 종료될 때까지 모든 태스크가 블로킹되는 심각한 단점이 있다.
- 특히 오래 걸리는 작업의 경우 전체 애플리케이션의 성능을 저하시킬 수 있다.
비동기 처리의 특징과 장단점
- 현재 태스크가 종료되지 않아도 다음 태스크를 즉시 실행하는 방식이다.
- 블로킹이 발생하지 않아 전체적인 처리 속도가 향상된다.
- 태스크의 실행 순서가 보장되지 않는 단점이 있다.
- 대표적인 비동기 처리로는 setTimeout, setInterval, HTTP 요청, 이벤트 핸들러가 있다.
- 전통적으로 콜백 패턴을 사용했으나, 콜백 헬과 에러 처리의 어려움이 있다.
이벤트 루프와 태스크 큐의 동작 구조
- 이벤트 루프는 자바스크립트의 동시성을 지원하는 핵심 메커니즘이다.
- 콜 스택과 태스크 큐를 지속적으로 모니터링한다.
- 태스크 큐는 비동기 함수의 콜백이 대기하는 공간이다.
- 콜 스택이 비어있을 때만 태스크 큐의 함수가 콜 스택으로 이동한다.
마이크로태스크 큐의 특징과 처리 순서
- 프로미스의 후속 처리 메서드의 콜백이 저장되는 별도의 큐이다.
- Promise의 then/catch/finally, process.nextTick, MutationObserver 등이 여기에 포함된다.
- 일반 태스크 큐보다 우선순위가 높아 먼저 처리된다.
- 마이크로태스크 큐의 모든 태스크가 처리된 후에야 태스크 큐의 태스크가 처리된다.
브라우저 환경에서의 비동기 처리
- 자바스크립트 엔진은 싱글 스레드로 동작하지만, 브라우저는 멀티 스레드로 동작한다.
- Web API를 통해 타이머 설정, 이벤트 처리, HTTP 요청 등을 처리한다.
- 비동기 작업의 콜백은 태스크 큐나 마이크로태스크 큐에 보관된다.
- 이벤트 루프가 큐들을 감시하며 적절한 시점에 콜백을 실행한다.
실행 순서와 우선순위
- 동기 코드는 즉시 실행되어 콜 스택에서 처리된다.
- 비동기 코드는 Web API에서 처리된 후 해당하는 큐로 이동한다.
- 마이크로태스크 큐의 태스크가 먼저 실행된다.
- 마이크로태스크 큐가 비어있을 때 태스크 큐의 태스크가 실행된다.
최적화 고려사항
- 과도한 비동기 작업은 태스크 큐의 폭발적 증가를 초래할 수 있다.
- 복잡한 비동기 처리는 프로미스나 async/await를 활용하여 관리한다.
- 비동기 작업의 실행 순서를 잘 이해하고 설계해야 한다.
- 중요한 작업은 마이크로태스크 큐를 활용하여 우선순위를 높일 수 있다.
예상문제 [🔥]
https://github.com/junh0328/prepare_frontend_interview?tab=readme-ov-file
동기와 비동기의 차이점에 대해서 설명해줄 수 있나요? 🔥🔥
동기와 비동기는 태스크 처리 방식의 차이를 나타냅니다.
동기 처리는 현재 실행 중인 태스크가 완료될 때까지 다음 태스크가 대기하는 방식입니다. 예를 들어, 은행 창구에서 한 명의 고객 업무가 완전히 끝나야만 다음 고객의 업무를 처리하는 것과 같습니다. 이는 실행 순서가 보장된다는 장점이 있지만, 앞선 태스크가 길어지면 모든 후속 태스크들이 블로킹되어 대기해야 하는 단점이 있습니다.
반면 비동기 처리는 현재 실행 중인 태스크가 완료되지 않은 상태라도 다음 태스크를 실행하는 방식입니다. 예를 들어, 식당에서 주문을 받은 직원이 주방에 주문을 전달하고, 음식이 준비되는 동안 다른 손님의 주문을 받는 것과 같습니다. 이는 태스크가 블로킹되지 않아 전체적인 처리 속도가 향상되는 장점이 있지만, 태스크의 실행 순서가 보장되지 않는다는 특징이 있습니다.
대표적인 비동기 처리의 예로는 타이머 함수(setTimeout, setInterval), HTTP 요청, 이벤트 핸들러 등이 있습니다.
동기와 비동기의 차이점을 한 줄 요약
동기는 작업을 순차적으로 하나씩 처리하는 방식이고, 비동기는 현재 작업이 완료되지 않아도 다음 작업을 실행하는 방식입니다.
이벤트 루프와 태스크 큐에 대해서 알고 있나요? 🔥🔥🔥
이벤트 루프와 태스크 큐는 자바스크립트의 동시성을 지원하는 핵심 메커니즘입니다.
자바스크립트는 싱글 스레드로 동작하지만, 브라우저에서는 여러 작업이 동시에 처리되는 것처럼 보입니다. 이를 가능하게 하는 것이 바로 이벤트 루프 시스템입니다.
자바스크립트 엔진은 크게 두 영역으로 나뉩니다. 하나는 함수의 호출과 실행을 담당하는 콜 스택이고, 다른 하나는 객체가 저장되는 메모리 공간인 힙입니다.
태스크 큐는 비동기 함수의 콜백 함수나 이벤트 핸들러가 임시로 보관되는 영역입니다. 예를 들어 setTimeout의 콜백 함수나 DOM 이벤트의 핸들러 함수들이 여기에 저장됩니다.
이벤트 루프는 다음과 같은 순서로 동작합니다.
- 콜 스택에 실행 중인 함수가 있는지 확인합니다.
- 태스크 큐에 대기 중인 함수가 있는지 확인합니다.
- 콜 스택이 비어있고 태스크 큐에 함수가 있다면, 태스크 큐의 첫 번째 함수를 콜 스택으로 이동시킵니다.
- 이 과정을 계속 반복합니다.
중요한 점은 태스크 큐의 함수는 콜 스택이 완전히 비어있을 때만 실행된다는 것입니다. 이는 자바스크립트의 'Run-to-completion' 특성을 보장합니다.
마이크로태스크 큐에 대해서 알고 있나요? 🔥🔥
마이크로태스크 큐는 태스크 큐와는 별도로 존재하는 큐로, 주로 프로미스의 후속 처리 메서드의 콜백 함수가 저장되는 영역입니다.
마이크로태스크 큐에 들어가는 작업의 예시로는...
- Promise의 then, catch, finally 메서드의 콜백
- process.nextTick (Node.js 환경)
- MutationObserver의 콜백
...등이 있습니다.
일반 태스크 큐(매크로태스크 큐라고도 함)에 들어가는 작업의 예시로는...
- setTimeout/setInterval의 콜백
- setImmediate (Node.js 환경)
- requestAnimationFrame
- I/O 작업의 콜백
- UI 렌더링 작업
...등이 있습니다.
마이크로태스크의 특징은 태스크 큐보다 우선순위가 높다는 점입니다. 이벤트 루프는 콜 스택이 비었을 때, 태스크 큐의 작업을 실행하기 전에 마이크로태스크 큐의 모든 작업을 먼저 처리합니다.
태스크 큐와 마이크로태스크 큐 중 어떤 것이 먼저 실행되나요? 🔥🔥
마이크로태스크 큐가 태스크 큐보다 먼저 실행됩니다. 구체적인 실행 순서는 다음과 같습니다.
- 콜 스택의 실행 중인 태스크가 완료됩니다.
- 콜 스택이 비면, 마이크로태스크 큐의 모든 태스크가 콜 스택으로 이동하여 순차적으로 실행됩니다.
- 마이크로태스크 큐가 비면, 태스크 큐의 첫 번째 태스크가 콜 스택으로 이동하여 실행됩니다.
- 위 과정이 반복됩니다.
예를 들어 보겠습니다.
console.log('1'); // 동기 처리
setTimeout(() => console.log('2'), 0); // 태스크 큐에 추가
Promise.resolve()
.then(() => console.log('3')) // 마이크로태스크 큐에 추가
.then(() => console.log('4')); // 마이크로태스크 큐에 추가
console.log('5'); // 동기 처리
이 코드의 실행 결과는 '1', '4', '3', '2' 순서로 출력됩니다.
실행 과정을 설명하면...
- '1'이 바로 출력됩니다 (동기 코드)
- setTimeout 콜백은 태스크 큐로 들어갑니다
- Promise의 then 핸들러는 마이크로태스크 큐로 들어갑니다
- '4'가 출력됩니다 (동기 코드)
- 콜 스택이 비워진 후 마이크로태스크 큐의 작업이 실행되어 '3'이 출력됩니다
- 마지막으로 태스크 큐의 작업이 실행되어 '2'가 출력됩니다
이러한 우선순위 체계는 Promise와 같은 중요한 비동기 작업들이 setTimeout과 같은 일반적인 타이머나 이벤트 핸들러보다 먼저 처리되도록 보장합니다.
'🧱 프론트엔드 주제 > JavaScript' 카테고리의 다른 글
[모던 자바스크립트 Deep Dive] 44장 - REST API (2) | 2024.11.24 |
---|---|
[모던 자바스크립트 Deep Dive] 43장 - Ajax (1) | 2024.11.19 |
[모던 자바스크립트 Deep Dive] 41장 - 타이머 (0) | 2024.11.12 |
[모던 자바스크립트 Deep Dive] 40장 - 이벤트 (0) | 2024.11.11 |
[모던 자바스크립트 Deep Dive] 39장 - DOM (0) | 2024.11.11 |