인간 JS 엔진되기 2 promise
제로초님의 인간 js엔진되기 2를 보고 정리하였다.
Promise
promise는 실행된 결과를 나중에 쓸 수 있다고 생각하면 된다.
실행된 결과가 promise 내부에 저장되기 떄문이다.
아래의 코드는 1초뒤에 console.log 작업을 무조건 해야한다.
1 | setTimeout(() => { |
promise로 작성할 경우 결과값을 저장하다가 필요할 떄에 쓸 수 있다.
1 | const promise = new Promise((resolve, reject) => { |
promise를 이용하면 코드도 더 깔끔하게 작성할 수 있다.
콜백함수는 결과값을 바로 받아서 사용해야하기 떄문에 콜백지옥이 발생할 수 밖에 없다.
1 | axios.get("서버주소1", function (data1) { |
하지만 promise를 사용할 경우 결과값을 나중에 사용해도 되기 떄문에 같은 로직도 더 깔끔하게 작성할 수 있다.
1 | const p1 = axios.get("서버주소1"); |
하지만 PromiseAll은 여러 요청 중 하나만 실패해도 error블럭으로 가기 때문에 성공한 나머지 응답을 사용하지 못한다는 단점이 있다.
이를 보완하기 위해 나온 것이 Promise.allSettled이다.
1 | const p1 = axios.get("서버주소1"); |
Promise.allSettled를 사용하면 모든 요청을 이행한 후 각 promise에 대한 결과를 나타내는 객체 배열을 반환한다.
비동기
한 번 비동기는 영원히 비동기이다. 비동기로 하는 작업은 동기 작업으로 바꾸려는 시도를 하지 말아야 한다.
비동기는 동시가 아닌 순서의 문제다.
즉 코드순서와 실제 실행순서가 다르다.
때문에 비동기코드는 일반적인 호출스택으로 분석할 수 없다.
이벤트 루프를 생각하여 분석해야한다.
1 | setTimeout(() => { |
비동기함수들은 js엔진에서 실행하여 끝날 때 비동기함수의 반환값을 태스크 큐에 넣는다.
이벤트루프는 호출스택에서 하나의 노드가 pop 될 때 마다 태스크를 확인해서 호출스택에 넣는다.
마이크로 태스크는 매크로 태스크보다 더 높은 우선순위를 가진다.
마이크로 태스크엔 promise와 process.nextTick이 들어간다.
나머지는 매크로 태스크에 들어간다.
비동기, 동기 그리고 Promise의 관계
promise에도 동기가 있다. promise의 실행은 바로 하기 때문이다.
promise 내부의 코드를 실행한 후 결과값을 promise에서 저장하다가 원하는 타이밍에 resolve를 통해 실행한다.
promise가 아닌 값은 자동으로 resolve된다.
정리하자면 promise의 실행은 바로하지만 결과값은 resolve 될 떄에 promise 객체에 저장된다. 저장된 결과값은 then에서 더 나중에 사용할 수 있다.
만약 결과값이 늦게 나온다면 then, await, Promise.all 이런 함수들은 결과값을 기다린 후에 실행된다.( axios.then 하는 상황을 생각하면 된다.)
1 | const p = new Promise((resolve, reject) => { |
위의 코드는 다음과 같은 순서로 동작한다.
- promise p를 만들며 내부의
제일먼저
를 출력한다. - setTimeout을 실행한다.
- 익명함수에서
딴짓
을 출력한다. - p.then을 실행한다.( 프로미스 객체에 then을 등록한다.)
- 동기 작업들은 모두 완료되었으므로 호출스택에서 익명함수가 제거된다.
- 다른 스레드에서 작업하던 setTimeout 내부 콜백을 매크로 태스크에 넣는다.
- 현재 호출스택이 비어있으므로 이벤트루프에서 setTimeout 내부 콜백을 호출스택에 삽입하여 실행하고
5
가 출력된다. 이후 promise p의 resolve를 호출한다. 그리고 호출스택에서 setTimeout 내부 콜백이 제거된다. - resolve가 호출되었으므로 다른 스레드에서 마이크로 태스크 큐에 p.then 내부 콜백을 전달한다.
- 현재 호출스택이 비어있으므로 이벤트 루프에서 p.then 내부 콜백을 호출스택에 삽입하여 실행하고
result 5
가 출력된다. - 호출스택과 큐가 비어있고 js 엔진의 스레드에서도 하는 작업이 없기 때문에 프로그램이 종료된다.
async/await을 Promise로 바꾸기
then에서 에러가 발생하면 아래의 then은 실행되지 않고 바로 reject되어서 catch블록으로 이동하게 된다.
아래의 코드에서 첫 번쨰 then에서 에러가 발생했따면 이후의 then은 무시된다. 마지막 catch 블록에서 전부 담당해야 한다.
1 | p.then((result) => { |
때문에 아래와 같이 작성하는 것이 바람직하다.
1 | p.then((result) => { |
await에도 똑같이 적용된다.
다음과 같이 promise 분기를 적용할 수 있다.
1 | function delay(ms) { |
또한 then 내부의 반환값에 따라 다음 then 블럭에서의 값이 달라진다.
1 | p.then((result) => { |
마찬가지로 await에서도 똑같이 적용된다.
1 | async function a() { |
위 두가지 코드를 참조하여 async/await을 promise로 바꿀 수 있다.
1 | async function a() { |
하지만 실행컨텍스트가 다르기 떄문에 구조는 같지만 완전히 같은 코드는 아니다.
실행컨텍스트와 스코프체인을 고려하여 async/await은 제너레이터 문법으로 구현되었다.
await을 연달아 쓰면 안된다.
promise는 결과값을 나중에 사용할 수 있는 것이다.
병렬로 처리해야할 경우에는 await을 연달아 쓰는 것보다 Promise.allSettled를 사용하여 처리하는 것이 훨씬 낫다.
1 | function delay(ms) { |
Promise의 다양한 활용
순서가 보장되어야 할 경우와 그렇지 않은 경우에 대해 다르게 작성할 수 있다.
1 | const results = await Promise.all([p1, p2, p3]); |