이전에 JS의 이벤트 루프에 대해 정리해봤다. 이번에는 조금 더 깊게 공부해보자.
JS는 싱글스레드 언어
싱글 스레드는 한 번에 하나의 작업만 수행할 수 있다. Python과 Java는 멀티 스레드를 지워해 여러 코드 로직은 동시에 실행할 수 있지만, JS는 싱글 스레드 언어이기 때문에 하나의 코드만 실행할 수 있다.
하지만 웹 애플리케이션에서는 네트워크 요청, 타이머, 이벤트 처리와 같은 작업을 비동기적으로 병렬 처리하는 경우가 있다. JS가 하나의 동작씩 동작한다면 위와 같은 동작을 하는 웹 애플리케이션은 굉장히 느릴것이다.
이러한 속도 저하 문제를 해결하기 위해서 JS 언어 엔진에서가 아닌 브라우저 내부의 멀티 스레드인 Web APIs에서 비동기 및 논블로킹으로 작업을 처리한다. 위와 같은 방법은 메인 스레드(싱글 스레드)에서 다른 곳으로 작업을 이전해 동작한 뒤 이벤트나 콜백함수를 받아 결과를 실행하며 동작한다.
비동기로 동작하는 핵심 요소는 JS 엔지 자체적으로 갖지 않고 브라우저가 갖고있다.
Node.js의 경우에는 libuv 내장 라이브러리가 비동기 동작을 처리한다.
이벤트 루프 Event Loop
이벤트 루프란 싱글 스레드인 JS의 작업을 멀티 스레드로 돌려 작업을 동시에 처리하거나 작업의 동작 우선 순위에 따라 동작의 순서를 결정하는 존재이다.
이벤트 루프는 브라우저 내부의 Call Stack, Callback Queue, Web APIs 등의 요소들을 모니터링하며 비동기 작업을 관리하며, 이러한 비동기 작업의 순서에 따른 실행 흐름을 제어한다.
브라우저의 동작 타이밍을 제어하는 관리자
JS에서는 setTimeout이나 fetch와 같은 비동기 JS 코드는 브라우저의 WebAPIs에 위임해 비동기 코드의 동작이 끝나면 Callback Queue에 넣는다. Call Stack에 잔여 실행 코드가 없으면 Callback Queue에 있는 작업들을 먼저 들어온 순서대로 Call Stack으로 옮기면서 실행한다.
이렇게 프로그램의 흐름이 이벤트에 의해 결정되는 이벤트 루프를 이용한 프로그래밍을 이벤트 기반 프로그래밍이라 한다.
이벤트 기반 프로그래밍은 비동기 작업을 비교적 쉽게 처리할 수 있으며, 멀티 스레드 언어에 비해 직관적인 코드 작성이 가능하고, 브라우저 환경에서 안정적으로 사용자 상호작용을 구현할 수 있다. 이러한 장점으로 봤을 때, JS를 이용한 웹 애플리케이션 개발은 중요하다고 볼 수 있다.
브라우저에서 이벤트 루프 구성
브라우저에서는 비동기 코드 동작을 구현하기 위해 Web APIs, Callback Queue, Event Loop 등이 있다.
- Call Stack : JS 엔진이 코드 실행을 위해 사용하는 메모리 구조
- Heap : 동적으로 생성된 JS 객체가 저장되는 공간
- Web APIs : 브라우저에서 제공하는 API 모음으로써 비동기적으로 실행되는 작업들을 전담해 처리한다.
(AJAX, fetch, 타이머 함수, DOM 조작 등) 링크 - Callback Queue : 비동기 작업이 완료되면 실행되는 함수들이 저장되는 공간
- Event Loop : 비동기 함수들을 적절한 타이밍에 실행시키는 존재
JS 이벤트 루프 동작 과정
위의 사진을 참고하며 진행해 보자.
싱글 스레드 언어인 JS는 Call Stack에 실행할 코드들을 담는다. 하나씩 실행 하다가 Web API와 같은 비동기 동작이 필요한 코드를 마주하면, 이 동작들의 콜백함수는 Task Queue로 이동된다. 이때 Call Stack에 더이상 실행할 코드가 남아 있지 않았을 때 Task Queue로 이동된 동작들이 실행된다.
위의 사진을 보면 Task Queue가 있고 Microtask Queue가 있다. 이는 코드에 따라 동작이 저장되는 Queue가 결정된다.
Task Queue의 경우 setTimeout, fetch, addEventListener와 같은 비동기로 처리되는 함수들의 콜백 함수가 들어가는 큐이다.
Microtask Queue는 Promise.then, async/await, process.nextTick, MutationObserver와 같이 우선적으로 비동기 처리되는 함수들의 콜백 함수가 들어가는 큐이다.
Callback Queue에서 Task Queue보다 Microtask Queue가 동작에서 우선순위가 높다. 즉, Microtask Queue에 저장된 콜백 함수가 Task Queue에 저장 콜백함수보다 먼저 실행된다. Microtask Queue에 저장된 콜백함수가 모두 실행되면 Task Queue에 있는 콜백함수가 실행된다.
async / await
우선 참고 애니메이션을 보자.
위의 자료에서 프로미스로 동작하는 one 함수가 가장 마지막에 실행되는 모습을 볼 수 있는데, 이전까지 정리한 내용에서 Promise와 async/await 동작은 Microtask Queue에 순차적을 저장되어 Call Stack이 비었을 때 차례대로 실행된다고 정리했다.
정리한 대로 동작한다면 분명 Promise가 먼저 실행 완료되고, 그 다음 async의 myFunc 함수가 실행 완료가 되어야 한다. 하지만 myFunc 함수가 먼저 실행 완료되고 Promise가 다음에 실행 완료되었다.
사실 async/await의 진짜 동작 과정을 아래에서 알아보자.
let x = await foo(); // 임의의 foo() 함수는 정의를 생략한다.
console.log(x);
console.log('끝');
사실 await 키워드 다음에 작성된 동일 라인의 코드는 모두 await foo()의 then 핸들러의 콜백 함수로 들어간다.
foo().then(x => {
console.log(x);
console.log('끝');
});
async/await 키워드는 promise.then() 메서드를 사용하지 않고 비동기 코드를 편하게 작성할 수 있게 해주는 문법적 편의 기능(Syntactic Sugar)이다. 실제로는 promise.then() 메서드를 간단하게 작성할 수 있게 해주는 것이다.
const one = () => Promise.resolve('One!');
async function myFunc(){
console.log('In function!');
const res = await one();
console.log(res);
}
console.log('Before Function!');
await myFunc();
console.log('After Function!');
/* ---------------- ↓↓↓ 변환 ↓↓↓ ---------------- */
const one = () => Promise.resolve('One!');
function myFunc(){
console.log('In function!');
return one().then(res => {
console.log(res);
});
}
console.log('Before Function!');
myFunc().then(() => {
console.log('After Function!');
});
'FE 이모저모 공부' 카테고리의 다른 글
JS의 호이스팅 발생 이유 (0) | 2024.07.26 |
---|---|
동기와 비동기 (Synchronous & Asynchronous) (1) | 2024.07.23 |
HTML의 iframe이 무엇이고 왜 iframe의 접근을 금지하지? (0) | 2024.07.17 |
TanstackQuery의 캐싱 (0) | 2024.05.15 |
프론트엔드 개발자가 하는 업무 (0) | 2024.04.26 |