회사 프로젝트의 대부분은 비동기 통신으로 API연결 작업을 합니다. 찍어내듯이 배웠지만, Promise 객체와 async/await에 대해서 알아보고자 합니다.
우선 이를 이해하기 앞서
1. 동기와 비동기의 차이
2. 싱글 스레드 기반 JS의 비동기 처리 방법에 대해서 알아보도록 하자.
동시에 일어난다는 뜻입니다. 요청과 결과가 동시에 일어난다는 약속인데, 바로 요청을 하면 시간이 얼마가 걸리던지 요청한 자리에서 결과가 주어져야 합니다. 즉 리턴해주기 전까지 사용자는 다른 활동이 불가능하며 기다려야 합니다.
동시에 일어나지 않는다는 뜻입니다. 요청과 결과가 동시에 일어나지 않을 거라는 약속이죠.
JavaScirpt는 싱글 스레드 프로그래밍언어 때문에 비동기 처리가 필수적이다! 하지만 비동기 처리는 그 결과가 언제 반환될 지 알 수 없기 때문에 동기식으로 처리하는 기법들이 사용되어야 한다. 대표적으로 setTimeout, callback, Promise 객체가 있습니다. 비동기 코드를 동기식으로 처리하기에 좋은 기법들이지만, 약간의 문제점을 지녔고 이를 보완하기 위해 async, await이 이를 보완해준다.
싱글 : 하나의 스레드를 갖는 프로세스. 첫번째 작업 마무리 후 두번째 작업을 시작한다.
멀티 : 하나 이상의 스레드를 갖는 프로세스. 두개의 스레드가 두개의 작업을 짧은 시간 동안 번갈아가며 수행하기 때문에 두개의 작업이 동시에 처리되는 것으로 보인다.
C, Java, Python을 사용하면 상식적으로 별도의 스레드나 프로세스를 사용하지 않는 이상 먼저 작성된 순서로, 즉 동기적으로 코드가 실행된다. 하지만 자바스크립트는 먼저 실행된 코드의 작업이 끝나기 전에 더 나중에 실행된 코드의 작업이 끝날 수도 있다.(전말 자바스크립트는 왜 이 모양일까)
function first(){
setTimeout(()=>{
console.log('the first function has been called');
}, 1000)
}
function second(){
setTimeout(()=>{
console.log('the second function has been called');
}, 500)
}
first();
second();
console 창을 확인해보면 second 함수가 먼저 실행된 게 보인다. 이것이 JavaScript의 비동기성이다. 하지만 JavaScript는 하나의 스레드(Single Thread)기반의 언어이다. 즉, 한번에 하나의 작업밖에 수행하지 못한다는 의미이다. 하지만 Ajax로 데이터를 불러오며 Mouseover 이벤트를 처리하면서 애니메이션도 동작시킨다. 이런 동시성이 어떻게 가능한 것일까?
JavaScript 엔진은 메모리 힙과 단일 호출 스택(Call Stack)을 가지고 있다. 하나의 호출 스택만 가지고 있으므로 단 한번에 단 하나의 함수만 처리가 가능하다. 호출된 함수를 추가(push), 제거(pop)하는 형태를 지니고 있다. 이렇게 JavaScript는 다른 함수가 실행되고 있을 때는 종료 직전까지 다른 작업이 중간에 끼어들 수 없다. 이것을 Run-to-completion이라 한다. 그렇다면 동시 실행이 불가능할까? 하지만 JavaScript는 엔진으로만 돌아가는 것이 아니다.
JavaScript 엔진 밖에서도 실행에 관여하는 요소들이 존재한다. WebApi(DOM, AJAX, SetTimeout) 와 Task Queue, EventLoop들이 있다.
브라우저에서 제공되는 API이며, AJAX나 Timeout등의 비동기 작업을 실행한다.
1. JavaScirpt에서 SetTimeout과 같은 함수를 실행
2. JavaScript엔진은 WebAPI에 SetTimeout을 요청하는 동시에 Callback까지 전달.
3. CallStack에서는 WepAPI요청 이후 SetTimeout작업이 완료되어 제거.
WebAPI는 방금 요청받은 setTimeout을 완료 후 동시에 전달받은 Callback을 Task Queue라는 곳에 넘겨준다.
Task Queue는 Callback Queue라고도 한다. 큐 형태로 WebAPI에서 넘겨받은 Callback함수를 저장한다. 이 Callback함수들은 자바스크립트 엔진의 CallStack의 모든 작업이 완료되면 순서대로 CallStack에 추가된다.
이 때
1. CallStack이 비어있지 않은지 (실행중인 작업이 존재하는지)
2. TaskQueue에 Task가 존재하는지를 판단하고
3. Task Qqueue의 작업을 CallStack에 옮기는 일을 EventLoop가 작업한다.
EventLoop는 이 작업을 처음부터 끝까지 반복하여 실행한다. 그래서 EventLoop인 것!
while (queue.waitForMessage()){
queue.processNextMessage();
}
MDN은 EventLoop의 작업을 위와 같은 가상의 코드로 설명하고 있다.
setTimeout(()=>{
console.log('all task was done');
}, 5000);
5초 뒤에 문장을 출력하는 간단한 코드다. 어떻게 비동기적으로 작동하는지 알아보자!
결국 여기서 알 수 있는 점은, JavaScript 엔진은 그저 주어진 코드를 실행하는 온디맨드(on demand)실행 환경이라는 것이다. 그 코드 실행의 스케줄링은 JavaScirpt엔진이 호스팅된 런타임 환경에 맡게되는 것이다.
setTimeout(function(){console.log(1);}, 0);
console.log(2)
위와 같은 경우, 2->1 순서로 실행되는 것을 확인할 수 있다.
코드가 실행될 경우
1. setTimeout이 먼저 실행된 후 CallStack에는 setTimeout이 등록
2. Web API에 setTimeout 작업을 요청함과 동시에 CallStack에는 setTimeout이 삭제되고 console.log(2)가 추가.
3. console.log(2) 작업이 완료되면, TaskQueue에서 대기중이던 console.log(1)(WebAPI에서 TaskQueue로 넘겨진 callback 함수) 작업이 CallStack으로 전달(EventLoop에 의해서)
4. 실행 후 프로그램이 종료된다.
엄밀히 말하자면 자바스크립트 엔진(메인 스레드)이 싱글 스레드인 것이다. (Call Stack) 이는 독립적으로 실행되지 않고, 웹 브라우저나 노드js와 같은 멀티 스레드 환경에 임베디드 되어 실행된다. 때문에 자바스크립트와 Web API, event loop등을 분리하여 말하기 어렵다.
간단하게 정리하자면 자바스크립트는 엔진이 싱글 스레드로 작동, 런타임 환경이 멀티 스레드를 제공하기 때문에 자바스크립트라는 언어는 멀티 스레드 프로세스 작업을 다룰 수 있다!
프로미스는 비동기 작업을 조금 더 편하게 처리할 수 있도록 ES6에 도입된 기능입니다. 이전에는 콜백함수로 처리했지만, 콜백함수로 처리할 경우 비동기 작업이 많아지면 쉽게 난잡해집니다.
function increaseAndPrint(n, callback){
setTimeout(()=>{
const increased = n+1;
console.log(increased);
if(callback){
callback(increased);
}
}, 1000);
}
increaseAndPrint(0, n=>{
increaseAndPrint(n, n=>{
increaseAndPrint(n, n=>{
...console.log('end!');
});
});
});
비동기적으로 처리해야 하는 일이 많아질수록, 코드의 깊이가 계속 깊어지는 현상이 있습니다. Promise를 사용하게 되면 코드의 깊이가 깊어지는 현상을 방지할 수 있어요!