[JavaScript] 비동기의 동작 원리
- -
이번 시리즈에서는 JavaScript에서 비동기의 개념을 알아보고 그 동작 원리를 파헤쳐보자.
동기와 비동기
프로그래밍에서 요청사항을 처리하는 방식으로 "동기적" 또는 "비 동기적인" 처리 라는 개념이 존재한다.
동기 처리
먼저 동기처리란 사용자가 요청을 하면 서버가 요청을 처리하고 응답을 받을 때까지 사용자는 잠시 대기하게 된다. 즉 서버에서 응답을 받아야만 다음 동작이 가능해진다.
일반적으로 동기처리는 "submit"을 처리할 때 발생하며 서버에서는 요청에 따라 화면을 그릴 HTML을 생성해서 회신하게 된다. 결과적으로 동일한 화면이더라도 전체를 새로 그리게 된다.
비 동기 처리
반면 비 동기처리는 사용자가 요청을 하고 서버의 처리 결과를 기다리지 않고 바로 다음 동작을 진행한다. 즉 서버에서의 응답 여부에 상관 없이 다음 동작이 가능해진다.
비동기 통신은 "submit" 동작이 아닌 javascript 이벤트를 처리하면서 발생하며 서버에서는 요청에 따라 데이터 조각만을 회신한다. 그럼 결과를 받은 javascript 단에서 데이터를 이용해서 화면을 그리게 된다. 당연히 동일환 화면을 새로 그릴 필요는 없고 필요한 부분만 갱신하므로 효율적이다.
다시한번 정리해보면
위 내용을 다시한번 정리해보면 아래와 같다.
구분 | 동기 (Synchronous) | 비동기 (Asynchronous) |
정의 | 작업이 완료될 때까지 다음 작업을 기다리는 방식 | 작업이 진행되는 동안 다른 작업을 수행할 수 있는 방식 |
실행 순서 | 순차적으로 실행, 이전 작업이 완료되어야 다음 작업 실행 | 동시에 실행, 작업이 완료되면 콜백이나 프로미스를 통해 결과 처리 |
블로킹 | 블로킹 방식으로, 작업이 완료될 때까지 다른 작업이 대기 | 비블로킹 방식으로, 다른 작업을 수행하면서 결과를 기다림 |
사용 예 | 간단한 계산, 파일 읽기 등 | AJAX 요청, 타이머, 이벤트 리스너 등 |
코드 가독성 | 코드 흐름이 직관적이지만, 긴 작업이 있을 경우 UI가 멈출 수 있음 | 코드 흐름이 복잡해질 수 있지만, UI가 부드럽게 유지됨 |
오류 처리 | 일반적인 try-catch 문을 사용하여 오류 처리 | Promise 또는 async/await를 사용하여 오류 처리 |
성능 | 긴 작업이 있을 경우 성능 저하 | 여러 작업을 동시에 처리할 수 있어 성능 향상 가능 |
비동기 동작의 원리
JavaScript는 기본적으로 Single Thread Model이다. 즉 한번에 하나의 동작만 처리할 수 있는데 비동기는 어떻게 동작하는 걸까?
JavaScript runtime 환경
비동기 동작을 이해하기 위해서는 먼저 JavaScript의 runtime 환경에 대한 이해가 필요하다.
JavaScript의 runtime 환경은 Heap과 CallStack, Web APIs, Event Loop, Task Queue, MicroTaskQueeu로 구분할 수 있다.
- Heap: 객체, 배열, 함둥 등의 데이터가 생성되는 공간으로 G.C에 의해 메모리가 관리된다.
- Call Stack: 함수의 컨텍스트를 관리하는 구조로 함수를 호출할 때마다 해당 함수의 실행 컨텍스트를 스택에 push 하고 함수 실행이 끝나면 pop한다. 이 영역은 동기적으로 실행되며 스택이 비어있지 않은 상태에서는 다른 코드는 실행되지 않는다.
- Web APIs: 브라우저가 제공하는 기능으로 JavaScript Engine은 Web API를 이용해서 브라우저와 상호작용할 수 있다. Web APIs는 JS 코드와 별도로 동작하며 요청이 완료되면(timeout 등) 연관된 콜백 함수를 Task Queue에 추가해서 비동기로 동작하도록 한다.
- (Macro) Task Queue: Web API에서 처리된 비동기 작업의 콜백 함수가 대기하는 큐이다.
- Micro Task Queue: Promise의 then, catch, finally헨들러와 같은 더 높은 우선순위를 가진 비동기 작업이 대기하는 큐로 Call Stack이 비어있을 때 Task Queue의 작업보다 먼저 수행된다.
- Event Loop: Call Stack이 비어있을 때 Micro Task Queue > Task Queue를 확인해서 대기중인 작업을 Call Stack에 추가하는 역할을 수행한다.
Call Stack의 동작
Call Stack은 일반적인 언어에서의 stack과 동일하다.
single thread인 JavaScript는 전역 코드를 call stack에서 실행하는데 코드는 단순히 위에서 아래로 순차적으로 실행된다. 이 과정에서 새롭게 호출된 함수가 있다면 기존 함수(A)를 pending 하고 새로운 함수(B)를 call stack에 push해서 먼저 처리한다. B가 동작을 끝내면 B를 call stack에서 pop 하고 A를 다시 resume 시킨다.
function tenth() { }
function ninth() { tenth() }
function eigth() { ninth() }
function seventh() { eigth() }
function sixth() { seventh() }
function fifth() { sixth() }
function fourth() { fifth() }
function third() { fourth() }
function second() { third() }
function first() { second() }
first();
위 메서드 진행은 단지 call stack 만 사용되며 Task Queue와 무관한다.
[동작 확인]
비동기 함수와 Task Queue
setTimeout 같은 Web API를 호출하면 JavaScript는 브라우저와 연동해서 동작한다. 다음의 코드를 실행한다고 생각해보자.
setTimeout( // 1. javascript에서 web api에 요청 전달
function a() { // 3. T.Q에서 event loop에 의해서 call stack으로 이동
console.log("callback");
}
, 1000 // 2. timeout 후 browser에서 a을 Task Queue에 추가
);
먼저 setTimeout이 동기적으로 실행되면 web api에게 callback과 timeout 시간이 전달된다. web api에서는 timeout 동안 callback을 관리하고 있다가 때가 되면 callback을 task queue에 전달한다. event queue는 무한 루핑 하며 만약 call stack이 비어있다면 task queue에서 하나의 작업을 빼서 call stack으로 push해준다.
setTimeout(function a() {}, 1000);// 1000ms 후 T.Q에 callback 등록
setTimeout(function b() {}, 500); // 500ms 후 T.Q에 Callback 등록
setTimeout(function c() {}, 0); // 즉시 T.Q에 callback 등록
function d() {} // 선언문 - runtime과 무관
d(); // 동기적으로 즉시 실행
// d 이후 T.Q에 등록된 순으로 c, b, a 가 동작
[동작확인]
JS Visualizer 9000의 동작은 Web API를 표현하지 않아서 Task Queue에 들어가는 시점표현에 오류가 있다.
Micro Task Queue
Micro Task Queue는 Task Queue 보다 높은 우선순위를 갖는다. 즉 Event Loop는 Call Stack 이 비어있을 때 먼저 Micro Task Queue를 살펴보고 처리할 작업이 있다면 먼저 call stack으로 이동시킨다. 만약 Mi.T.Q에 처리할 작업이 없다면 비로서 T.Q의 작업을 처리한다. Mi.T.Q와 상대적으로 일반 Task Queue를 Macro Task Queue라고 한다.
다음은 Micro Task Queue에 추가되는 작업은 대표적들이다.
- Promise: then(), catch(), finally() 에 전달되는 Callback 함수
- async/await: await 가 적용되는 함수
- queueMicroTask(): window의 함수인 queueMicrotask()에 전달되는 Callback 함수
- MutationObserver: DOM의 변화 관찰을 위한 API로 변화가 감지하는 객체로 객체 생성시 전달되는 Callback 함수
만약 다음과 같은 코드가 동작한다면 실행 순서는 어떻게 될까?
fetch('https://www.google.com') // promise인 fetch 요청 - 서버에서 결과가 오기 까지 대기
.then(function a() {
console.log("a")
});
Promise.resolve() // 즉시 성공 - then 콜백은 micro task queue로 이동
.then(function b() {
console.log("b")
});
Promise.reject() // 즉시 실패 - catch 콜백은 micro task queue로 이동
.catch(function c() {
console.log("c")
});
console.log("end"); // 동기로 진행
- fetch에 의해 서버로 요청을 날린다. 이후 서버에서 결과를 받을 때까지 web api에서 대기한다.
- Promise.resolve는 언제나 성공이므로 then의 callback이 Mi.T.Q로 이동한다.
- Promise.reject는 언제나 실패이므로 catch의 callback이 Mi.T.Q로 이동한다.
- end가 동기적으로 call stack에서 처리되고 call stack은 비워진다.
- Mi.T.Q에 있던 2, 3의 callback이 하나씩 Event Loop에 의해 call stack으로 이동되서 처리되고 이 와중에 1 의 then에 등록된 callback도 Mi.T.Q로 이동된 후 처리된다.
[동작확인]: 역시 web api에 대한 부분은 표시되지 않았다.
Macro Task Queue vs Micro Task Queue
Micro Task Queue의 우선순위가 Macro Task Queue의 우선 순위보다 높다고 이야기 했었는데 제대로 이해했다면 다음의 동작을 예상할 수 있다.
setTimeout(function a() {
console.log("timeout");
}, 0);
Promise.resolve().then(function b() {
console.log("Promise")
});
console.log("sync")
- setTimeout의 콜백은 timeout이 지나면 Macro Task Queue에 등록된다. 여기서는 0이므로 즉시 등록된다.
- Promise.resolve는 언제나 성공하므로 then의 callback은 즉시 Micro Task Queue에 등록된다.
- 동기적으로 sync가 출력되고 call stack이 비게 된다.
- Event Loop는 Mi.T.Q > Ma.T.Q의 순으로 살펴보기 때문에 먼저 2의 callback을 call stack으로 이동시켜 처리한다.
- 다시 call stack이 비면 1의 callback을 call stack으로 이동시켜 처리한다.
[동작확인]
'JavaScript' 카테고리의 다른 글
[새로운 기능] 강화된 객체 표현과 연산자 (0) | 2022.05.02 |
---|---|
[ajax] vanilla javascript를 이용한 ajax 처리 (0) | 2022.03.14 |
[javascript]cors 크롬 플러그인 사용 (0) | 2022.03.11 |
[javascript] this -1 (0) | 2021.11.22 |
[새로운 기능] Template Literal (0) | 2021.11.20 |
소중한 공감 감사합니다