자바스크립트에서 함수가 실행되려면 '함수 코드 평과 과정'에서 생서된 함수 실행 컨텍스트가 콜 스택에 푸시되어야 한다.
자바스크립트 엔진은 단 하나의 콜 스택을 갖는다. 따라서 함수를 실행할 수 있는 창구가 단 하나이고, 동시에 두 개 이상의 함수를 실행할 수 없다.
콜 스택에 쌓인 모든 실행 컨텍스트(현재 실행중인 실행 컨텍스트를 제외하고)는 모두 실행 대기중인 태스크(task)들이다.
자바스크립트 엔진은 한 번에 하나의 태스크만 실행할 수 있는 싱글 스레드 방식으로 동작한다.
🚨주의할 점은 내장된 자바스크립트 엔진이 싱글 스레드로 동작하는 것이지, 브라우저, node.js가 싱글 스레드 방식으로 동작하는 것이 아니다.
모든 자바스크립트 코드가 싱글 스레드 방식으로 동작하면 자바스크립트는 비동기로 동작할 수 없다.
자바스크립트 엔진은 싱글 스레드로 동작하지만, 브라우저는 멀티 스레드로 동작한다.
동기 처리 방식은 태스크를 순서대로 하나씩 처리하므로 실행 순서가 보장되지만 앞선 태스크가 종료할 때까지 이후 태스크들이 블로킹되는 단점이 있다.
비동기 처리 방식은 현재 실행중인 테스크가 종료되지 않아도 다음 태스크를 곧바로 실행해서 블로킹이 발생하지 않는다.
자바스크립트는 비동기 처리 싱글 스레드 방식으로 동작한다.
대부분의 자바스크립트 엔진은 크게 두 개의 영역으로 구분한다.
비동기 처리에서 소스코드의 평가와 실행을 제외한 모든 처리는 자바스크립트 엔진을 구동하는 환경인 브라우저 또는 Node.js가 담당한다.
이를 위해 태스크 큐와 이벤트 루프를 제공한다.
프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체다
ES6에 도입되었으며 new
연산자와 함께 생성자 함수 Promise
를 호출하면 프로미스(Promise 객체)를 생성한다.
Promise는 호스트 객체가 아닌 ECMAScript 사양에 정의된 표준 빌트인 객체다.
Promise 생성자 함수는 비동기 처리를 수행할 콜백 함수(executor)를 인수로 전달받으며 이 콜백 함수는 resolve
와 reject
함수를 인수로 전달받는다.
프로미스는 현재 비동기 처리가 어떻게 진행되고 있는지를 나타내는 상태 정보를 갖는다.
프로미스의 상태 정보 | 의미 | 상태 변경 조건 |
---|---|---|
pending | 비동기 처리가 아직 수행되지 않은 상태 | 프로미스가 생성된 직후 기본 상태 |
fulfilled | 비동기 처리가 수행된 상태(성공) | resolve 함수 호출 |
rejected | 비동기 처리가 수행된 상태(실패) | reject 함수 호출 |
프로미스의 상태는 resolve
또는 reject
함수를 호출하는 것으로 결정된다.
const fulfiled = new Promise(resolve => resolve(1));
또한 프로미스는 비동기 처리 상태 정보인 [[PromiseStatus]]
, 비동기 처리 결과 정보인 [[PromiseValue]]
내부 슬롯을 갖는다.
프로미스는 프로미스의 비동기 처리 상태가 변화되면 프로미스의 처리 결과를 가지고 후속 처리를 진행할 then, catch, finally
메서드를 제공한다.
프로미스의 비동기 처리 상태가 변화하면 후속 처리 메서드에 인수로 전달한 콜백 함수가 선택적으로 호출된다.
모든 후속 처리 메서드는 프로미스를 반환하며, 비동기로 동작한다. 프로미스를 반환하기 때문에
then 메서드는 언제나 프로미스를 반환한다. then 메서드가 프로미스가 아닌 값을 반환하면 이 값을 암묵적으로 resolve 또는 reject하여 프로미스를 생성해 반환한다.
catch
후속 처리 메서드를 모든 then
메서드 호출 이후에 호출하면 비동기 처리에서 발생한 에러(rejected 상태)뿐만 아니라 then 메서드 내부에서 발생한 에러까지 모두 캐치할 수 있다.
Promise 생성자 함수는 5가지의 정적 메서드를 제공한다.
이미 존재하는 값을 래픽해서 프로미스를 생성하기 위해 사용한다.
여러 개의 비동기 처리를 모두 병렬처리할 때 사용한다.
Promise.all()
메서드는 프로미스를 요소로 갖는 배열 등의 이터러블을 인수로 전달받는다. 인수로 전달받은 이터러블의 요소가 프로미스가 아닌 경우 암묵적으로 각 요소를 Promise.resolve
메서드를 통해 프로미스로 래핑한다.
인수로 전달받은 배열의 모든 프로미스가 모두 fullfiled 상태가 되면 각 프로미스의 처리 결과를 배열에 저장해 새로운 프로미스를 반환한다.
Promise.all()
메서드는 인수로 전달받은 배열의 프로미스 중 하나라도 rejected 상태가 되면 나머지 프로미스가 fulfilled 상태가 되는 것을 기다리지 않고 즉시 종료한다.
가장 먼저 rejected 된 프로미스를 반환한다.
프로미스를 요소로 갖는 배열 등의 이터러블을 인수로 전달받는 것은 갖지만, 프로미스 요소 중 가장 먼저 fulfilled 상태가 된 프로미스를 반환한다.
프로미스가 rejected 상태가 되면 Promise.all
과 마찬가지로 가장 먼저 rejected 된 프로미스를 즉시 반환한다.
Promise.allSettled
메서드는 프로미스를 요소로 갖는 배열 등의 이터러블을 인수로 전달받는다.
전달받은 이터러블의 모든 프로미스가 settled 상태(fulfilled 혹은 rejected)가 되면 각 프로미스의 처리 결과를 나타내는 객체들을 배열에 담아 새로운 프로미스를 반환한다.
프로미스의 처리 결과를 나타내는 객체는 다음과 같다.
status
프로퍼티와 처리 결과 value
프로퍼티를 갖는다.status
프로퍼티와 에러 reason
프로퍼티를 갖는다.태스크 큐와 마이크로태스크 큐는 별도의 큐다.
프로미스 후속 처리 메서드의 콜백함수는 마이크로 태스크 큐에 저장되고 그 외의 콜백함수나 이벤트 핸들러는 태스크 큐에 저장된다.
마이크로태스크 큐가 태스크 큐보다 우선순위가 높기 때문에 이벤트 루프는 콜 스택이 비면 마이크로태스크 큐에 대기하고 있는 함수를 먼저 콜 스택에 푸시하고, 마이크로태스크 큐가 비면 태스크 큐에 대기하고 있는 함수를 콜 스택에 푸시한다.
async/await은 프로미스를 기반으로 동작한다.
async/await을 사용하면 프로미스의 후속 처리 메서드 없이 동기 처리처럼 프로미스가 처리 결과를 반환하도록 구현할 수 있다.
async 함수는 언제나 프로미스를 반환한다. async 함수가 명시적으로 프로미스를 반환하지 않아도 async 함수는 암묵적으로 반환값을 resolve 하는 프로미스를 반환한다.
await 키워드는 반드시 async 함수 내부에서 사용해야 하며, 반드시 프로미스 앞에서 사용해야 한다.
await 키워드는 프로미스가 settled 상태가 될 때까지 대기하다가 resolve된 프로미스의 처리 결과를 반환한다.