과거에는 callback만을 사용하여 비동기처리를 했지만 연속적인 비동기처리를 하다보면 가독성이 좋지 못하고 서드파티라이브러리인 경우 제어의 역전문제로 믿음에 대한 문제가 있었다.
그래서 Promise라는 것이 나오게 된 것이고 오늘 강의에서 프로미스가 1급 객체이기 때문에 값으로써 다뤄질 수 있어 이를 활용하여 비동기적인 코드를 제어하는 예시들을 보여주었다.
다음과 같이 setTimeout메서드를 활용하여 비동기적인 상황을 발생시켰을 때 변수에 담기는 값으로 callback은 undefined를 가리키지만 Promise의 경우에는 값으로써 상태값을 가진 프로미스 객체를 반환하는 것을 볼 수 있다. 그렇기 때문에 프로미스는 상태(대기,성공,실패)를 값으로 사용할 수 있어 응용과 표현력이 좋다.
// callback의 경우
function add10(a, cb){
setTimeout(() => cb(a + 10), 100);
}
const cb = add10(5, console.log);
console.log(cb); // undefined
// Promise의 경우
function add20(a) {
return new Promise((resolve) => setTimeout(() => resolve(a+20), 100));
}
const promise = add20(5).then(console.log);
console.log(promise); // Promise { <pending> }
먼저 callback과 값으로써 다룰 수 있는 promise의 차이를 이용한 예시이다.
다음과 같은 코드가 있을 때, 우리가 기대한바는 a라는 값이 동기적으로 평가되고 f라는 함수가 동기적으로 동작하여 15라는 결과 값을 얻어내는 것이다.
const go1 = (a, f) => f(a);
const add5 = a => a + 5;
console.log(go1(10, add5));
하지만 다음과 같이 평가를 지연시키는 delay100이 존재할 때 a라는 값을 동기적으로 평가할 수 없어 그 결과 값을 출력했을 때 기대했던 바와 다른 값을 얻게 된다.
const delay100 = a => new Promise(resolve =>
setTimeout(() => resolve(a), 100));
const r1 = go1(delay100(10), add5);
console.log(r1);
// [object Promise]5
// 기대했던 결과와 다름
그래서 이러한 경우에 promise가 1급 객체라는 특징을 활용해서 수정해본다면 인자의 값, 일급으로 다뤄질 수 있는 값이 비동기 상황인지 체크하여 비동기를 제어해주면 된다.
const go2 = (a, f) => a instanceof Promise ? a.then(f) : f(a);
const r2 = go2(delay100(10), add5);
console.log(r2); // Promise { <pending> }
r2.then(console.log); // 15
모나드는 어떤 특정 상황에서 안전하게 함수를 합성하기 위한 기법이라고 한다.
다음과 같은 코드가 있다고 가정해보자.
const g = a => a + 1;
const f = a => a * a;
우리가 f와 g를 합성하여 결과값을 도출한다고 했을 때, 상황에 따라서는 인자에 값을 전달해주지 않는 경우도 있다.
그런데 일반적인 함수 합성을 봤을 때는 다음과 같이 그 이후의 함수까지 동작되어 NaN과 같이 원하지 않는 값을 리턴하는 것을 볼 수 있다. 따라서, (f⋅g)(x)는 안전하게 합성되지 않는 함수라고 할 수 있다.
// 일반적인 합성함수
console.log(f(g(1))); // 4
console.log(f(g())); // NaN
그래서 [배열표현]
을 모나드라고 상상하고 모나드를 활용한 합성함수를 사용한다고 하면 아래와 같이 인자로 전달되는 값의 유무에 따라 다음 함수가 실행되지 않게 할 수 있어 보다 안전하게 함수를 합성할 수 있게 한다.
[1].map(g).map(f).forEach(r => console.log(r)); // 4
[].map(g).map(f).forEach(r => console.log(r)); // 동작안함
프로미스에서의 모나드는 조금 다르다.
인자값이 있고 없고를 따지는 것이 아니라 비동기적인 상황에서 안전하게 처리한다는 관점의 차이가 있다.
Promise.resolve(2).then(g).then(f).then(r => console.log(r)); // 9
Promise.resolve().then(g).then(f).then(r => console.log(r)); // NaN
앞서 작성했던 go, reduce를 사용하여 비동기 제어를 하려고 하면 아래와 같은 결과값을 얻게 된다.
go(1,
a => a + 10,
a => Promise.resolve(a + 100),
a => a + 1000,
console.log);
/* [object Promise]1000 */
결과값 [object Promise]1000
은 우리가 구하고자 했던 값이 아니기 때문에 Promise를 값으로 다룰 수 있는 특징을 이용하여 기존의 go, reduce를 수정해야 한다.
const go = (...args) => reduce((a, f) => f(a), args);
const reduce = curry((f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
} else {
iter = iter[Symbol.iterator]();
}
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
acc = f(acc, a);
}
return acc;
});
go는 reduce를 사용하고 있기 때문에, reduce만 고치면 이 문제를 해결 할 수 있다. 위에서 작성했던 go2와 마찬가지로 비동기 상황인지를 체크하여 Promise라면 Promise 값을 기다려서 만들어지는 값으로 변환하는 과정을 추가하여 작성해주면 된다.
const go1 = (a, f) => a instanceof Promise ? a.then(f) : f(a);
const reduce = curry((f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
} else {
iter = iter[Symbol.iterator]();
}
return go1(acc, function recur(acc) {
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
acc = f(acc, a);
if (acc instanceof Promise) return acc.then(recur);
}
return acc;
});
});
이후에 다시 코드를 실행했을 때는 정상적으로 1111란 값을 얻을 수 있게 된다.
추가적으로 같이 공부하면 좋은 키워드이다.
event loop
macrotask
microtask