인간 JS 엔진되기 2 promise

제로초님의 인간 js엔진되기 2를 보고 정리하였다.

Promise

promise는 실행된 결과를 나중에 쓸 수 있다고 생각하면 된다.

실행된 결과가 promise 내부에 저장되기 떄문이다.

아래의 코드는 1초뒤에 console.log 작업을 무조건 해야한다.

1
2
3
4
5
setTimeout(() => {
console.log("a");
}, 1000);

//'a' 1초뒤

promise로 작성할 경우 결과값을 저장하다가 필요할 떄에 쓸 수 있다.

1
2
3
4
5
6
7
8
9
10
11
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 1000);
});

//...기타 작업

promise.then(() => {
console.log("a");
});

promise를 이용하면 코드도 더 깔끔하게 작성할 수 있다.

콜백함수는 결과값을 바로 받아서 사용해야하기 떄문에 콜백지옥이 발생할 수 밖에 없다.

1
2
3
4
5
axios.get("서버주소1", function (data1) {
axios.get("서버주소2", function (data2) {
axios.axios.get("서버주소3", function (data3) {});
});
});

하지만 promise를 사용할 경우 결과값을 나중에 사용해도 되기 떄문에 같은 로직도 더 깔끔하게 작성할 수 있다.

1
2
3
4
5
6
7
8
9
10
const p1 = axios.get("서버주소1");
const p2 = axios.get("서버주소2");
const p3 = axios.get("서버주소3");
const p4 = axios.get("서버주소4");
const p5 = axios.get("서버주소5");

Promise.All([p1, p2, p3, p4, p5, p6])
.then((result) => {})
.catch((error) => {})
.finally(() => {});

하지만 PromiseAll은 여러 요청 중 하나만 실패해도 error블럭으로 가기 때문에 성공한 나머지 응답을 사용하지 못한다는 단점이 있다.

이를 보완하기 위해 나온 것이 Promise.allSettled이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const p1 = axios.get("서버주소1");
const p2 = axios.get("서버주소2");
const p3 = axios.get("서버주소3");
const p4 = axios.get("서버주소4");
const p5 = axios.get("서버주소5");

Promise.allSettled([p1, p2, p3, p4, p5, p6])
.then((result) => {
//실패한 것만 필터링해서 다시 시도할 수 있다.
})
.catch((error) => {})
.finally(() => {});

// [
// {status: "fulfilled", value: 33},
// {status: "fulfilled", value: 66},
// {status: "fulfilled", value: 99},
// {status: "rejected", reason: Error: an error}
// ]

Promise.allSettled를 사용하면 모든 요청을 이행한 후 각 promise에 대한 결과를 나타내는 객체 배열을 반환한다.

비동기

  1. 한 번 비동기는 영원히 비동기이다. 비동기로 하는 작업은 동기 작업으로 바꾸려는 시도를 하지 말아야 한다.

  2. 비동기는 동시가 아닌 순서의 문제다.

즉 코드순서와 실제 실행순서가 다르다.

때문에 비동기코드는 일반적인 호출스택으로 분석할 수 없다.

이벤트 루프를 생각하여 분석해야한다.

1
2
3
4
5
6
7
8
9
10
11
setTimeout(() => {
console.log("a");
}, 0);

setTimeout(() => {
console.log("b");
}, 1000);

setTimeout(() => {
console.log("c");
}, 2000);

비동기함수들은 js엔진에서 실행하여 끝날 때 비동기함수의 반환값을 태스크 큐에 넣는다.

이벤트루프는 호출스택에서 하나의 노드가 pop 될 때 마다 태스크를 확인해서 호출스택에 넣는다.

마이크로 태스크는 매크로 태스크보다 더 높은 우선순위를 가진다.

마이크로 태스크엔 promise와 process.nextTick이 들어간다.
나머지는 매크로 태스크에 들어간다.

비동기, 동기 그리고 Promise의 관계

promise에도 동기가 있다. promise의 실행은 바로 하기 때문이다.

promise 내부의 코드를 실행한 후 결과값을 promise에서 저장하다가 원하는 타이밍에 resolve를 통해 실행한다.

promise가 아닌 값은 자동으로 resolve된다.

정리하자면 promise의 실행은 바로하지만 결과값은 resolve 될 떄에 promise 객체에 저장된다. 저장된 결과값은 then에서 더 나중에 사용할 수 있다.

만약 결과값이 늦게 나온다면 then, await, Promise.all 이런 함수들은 결과값을 기다린 후에 실행된다.( axios.then 하는 상황을 생각하면 된다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const p = new Promise((resolve, reject) => {
console.log("제일먼저");
setTimeout(() => {
a = 5;
console.log(a);
resolve(a);
}, 0);
});

// ... 기타 작업

console.log("딴짓");

p.then((result) => {
console.log("result", result);
});

// 제일먼저
// 딴짓
// 5
// result 5

위의 코드는 다음과 같은 순서로 동작한다.

  1. promise p를 만들며 내부의 제일먼저를 출력한다.
  2. setTimeout을 실행한다.
  3. 익명함수에서 딴짓을 출력한다.
  4. p.then을 실행한다.( 프로미스 객체에 then을 등록한다.)
  5. 동기 작업들은 모두 완료되었으므로 호출스택에서 익명함수가 제거된다.
  6. 다른 스레드에서 작업하던 setTimeout 내부 콜백을 매크로 태스크에 넣는다.
  7. 현재 호출스택이 비어있으므로 이벤트루프에서 setTimeout 내부 콜백을 호출스택에 삽입하여 실행하고 5가 출력된다. 이후 promise p의 resolve를 호출한다. 그리고 호출스택에서 setTimeout 내부 콜백이 제거된다.
  8. resolve가 호출되었으므로 다른 스레드에서 마이크로 태스크 큐에 p.then 내부 콜백을 전달한다.
  9. 현재 호출스택이 비어있으므로 이벤트 루프에서 p.then 내부 콜백을 호출스택에 삽입하여 실행하고 result 5가 출력된다.
  10. 호출스택과 큐가 비어있고 js 엔진의 스레드에서도 하는 작업이 없기 때문에 프로그램이 종료된다.

async/await을 Promise로 바꾸기

then에서 에러가 발생하면 아래의 then은 실행되지 않고 바로 reject되어서 catch블록으로 이동하게 된다.

아래의 코드에서 첫 번쨰 then에서 에러가 발생했따면 이후의 then은 무시된다. 마지막 catch 블록에서 전부 담당해야 한다.

1
2
3
4
5
6
7
8
9
10
11
p.then((result) => {
console.log("result", result);
})
.then(() => {
// 에러 발생
})
.then(() => {})
.then(() => {})
.then(() => {})
.then(() => {})
.catch((e) => {});

때문에 아래와 같이 작성하는 것이 바람직하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
p.then((result) => {
console.log("result", result);
})
.then(() => {
// 에러 발생
})
.then(() => {})
.catch((e) => {})
.then(() => {})
.catch((e) => {})
.then(() => {})
.catch((e) => {})
.then(() => {})
.catch((e) => {});

await에도 똑같이 적용된다.
다음과 같이 promise 분기를 적용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function delay(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
});
}

async function a() {
try {
await delay(1000);
} catch (error) {
console.error(error);
}
try {
await delay(1000);
await delay(1000);
await delay(1000);
} catch (error) {
console.error(error);
}
}

또한 then 내부의 반환값에 따라 다음 then 블럭에서의 값이 달라진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
p.then((result) => {
console.log("result", result);
})
.then(() => {
//return undefined
})
.then((result) => {
console.log(result); // undefined
return 1;
})
.then((result) => {
console.log(result); // 1
return Promise.resolve(1);
})
.then((result) => {
console.log(result); // 1
})
.then(() => {})
.then(() => {})
.catch((e) => {});

마찬가지로 await에서도 똑같이 적용된다.

1
2
3
4
async function a() {
const a = await 1; //1
const b = await Promise.resolve(1); // 1
}

위 두가지 코드를 참조하여 async/await을 promise로 바꿀 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
async function a() {
const a = await 1;
console.log("a", a);
console.log("hmm");
await null;
const b = await Promise.resolve(1);
console.log("b", b);
return b;
}

a()
.then((result) => {
console.log(result);
})
.then((result2) => {
console.log(result2);
});

// a 1
// hmm
// b 1
// 1

Promise.resolve(1) // await 1이기 때문에 promise를 만들어주어야 한다.
.then((a) => {
console.log("a", a); //await 아래 함수들은 첫 번째 await이 resolve된 이후 실행된다.
console.log("hmm");
return null;
})
.then(() => {
Promise.resolve(1);
})
.then((b) => {
console.log("b", b);
return b;
});

// a 1
// hmm
// b 1
// 1

하지만 실행컨텍스트가 다르기 떄문에 구조는 같지만 완전히 같은 코드는 아니다.

실행컨텍스트와 스코프체인을 고려하여 async/await은 제너레이터 문법으로 구현되었다.

await을 연달아 쓰면 안된다.

promise는 결과값을 나중에 사용할 수 있는 것이다.

병렬로 처리해야할 경우에는 await을 연달아 쓰는 것보다 Promise.allSettled를 사용하여 처리하는 것이 훨씬 낫다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function delay(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
});
}

async function a() {
await delay(3000);
await delay(6000);
await delay(9000);
} // 토탈 18초

async function b() {
const p1= delay(3000);
const p2 delay(6000);
await Promise.allSettled([p1,p2]);
await delay(9000);
} // 토탈 15초

Promise의 다양한 활용

순서가 보장되어야 할 경우와 그렇지 않은 경우에 대해 다르게 작성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
const results = await Promise.all([p1, p2, p3]);
// 동시에 된다.

result.map(async () => {
//await result....
}, []);
// 동시에 된다.

for (let result of results) {
//await result....
}
// 순서가 보장된다. p1 후 p2 후 p3

댓글