#1 Promise
끔찍한 callback에서 벗어나 promise 적용하기
먼저 callback과 promise를 통해 짜여진 두 코드를 보자.
/* callback */
function add10(a, callback){
setTimeout(() => callback(a + 10), 100);
}
add10(5, res => {
console.log(res); // 15
});
/* promise */
function add20(a){
return new Promise(resolve => setTimeout(() => resolve(a + 20), 100));
}
add20(5)
.then(console.log); // 25
일단 가장 먼저 보이는 명확하게 보이는 차이는 promise에는 return 해준다는 것이다. 이 두 함수는 연속적으로 실행함에 있어 차이가 있다. 연속적으로 실행하는 코드를 타이핑하기엔 callback이 promise에 비해 많이 번거로운 것을 아래 코드를 통해 알 수 있을 것이다.
/* callback */
add10(5, res => {
add10(res, res => {
add10(res, res => {
console.log(res); //35
});
});
});
/* promise */
add20(5)
.then(add20)
.then(add20)
.then(console.log); //65
이처럼 promise를 활용하면, 훨씬 코드 사용이 간편하다.
그러나 가장 큰 차이는 promise에는 상태가 존재한다. promise가 호출되면 대기상태가 되며, 이행상태가 되면 then()을 이용해
처리 결과 값을 받을 수 있다.
Promise의 3가지 상태를 알아보자.
- Pending(대기): 비동기 처리 로직이 아직 미완료인 상태
- Fulfilled(이행): 비동기 처리가 완료되어 promise가 결과 값을 반환해준 상태
- Rejected(실패): 비동기 처리가 실패하거나 오류가 발생한 상태
Pending 상태
아래 처럼 메서드를 호출하면 Pending 상태가 된다.
new Promise();
이 후 new Promise() 메서드를 호출 시 콜백 함수의 인자로, resolve, reject에 접근할 수 있다.
new Promise(function (resolve, reject) {
// ...
});
Fulfilled 상태
다음 콜백 함수의 인자 resolve를 실행하면 Fulfilled상태가 된다.
new Promise(function (resolve, reject) {
resolve();
});
이 후 이행 상태가 되면 then()을 이용해 처리 결과 값을 받을 수 있다.
Rejected 상태
reject인자로 reject()를 실행하면 Rejected상태가 된다. 그리고 실패 상태가 되면, 실패한 처리 결과 값을 catch()로 받을 수 있다.
그럼 이제 다시 아래코드를 통해 callback과 promise 차이를 한번 더 보자.
/* callback */
var a = add10(5, res => {
add10(res, res => {
add10(res, res => {
console.log(res);
});
});
});
console.log(a); // undefined
/* promise */
var b = add20(5)
.then(add20)
.then(add20)
.then(console.log);
console.log(b); // Promise {<pending>}
위와 같이 각각 a, b 변수에 저장후 아래를 실행 해보자.
/* callback의 경우 */
add10(5, _ => _); // undefined
/* <더 이상 어떠한 일도 할 수 없다> */
/* promise의 경우 */
add20(5, _ => _); // Promise {<pending>}
var c = add20(5, _ => _);
console.log(c) // Promise {<resolved>: 25}
var d = c.then(a => a - 5);
console.log(d) // Promise {<resolved>: 20}
d.then(console.log); // 20
/* <이 후 계속해서 원하는 일을 다룰 수 있음 > */
이처럼 promise는 callback과는 다르게 상태값으로 연속적으로 실행할 수 있다.
값으로서의 Promise 활용
일급을 활용해서 값으로서의 Promise를 다뤄 볼 것이다. 먼저 아래와 같은 함수가 있다고 가정하자.
const go1 = (a, f) => f(a);
const add5 = a => a + 5;
console.log(go1(10, add5)); // 15
위 코드가 잘 동작하기 위해선 'f'라는 함수가 동기적으로 동작하는 함수여야 하고, a라는 값 역시 동기적으로 값을 알 수 있어야 한다. 다시말해서 비동기 상황이 아닌 즉, Promise가 아닌 값이 들어와야 위 함수에 값을 정상적 잘 적용할 수 있을 것이다.
const delay100 = a => new Promise(resolve =>
setTimeout(() => resolve(a), 100));
const go1 = (a, f) => f(a);
const add5 = a => a + 5;
console.log(go1(10, add5)); // 15
console.log(go1(delay100(10), add5)); // [object Promise]5
위 코드에서 Promise는 정상적으로 동작하지 않았음을 볼 수 있다. Promise가 일급이라는 성질을 이용해서 위 코드가 잘 동작 할 수 있도록 만들어 볼 것이다.
const delay100 = a => new Promise(resolve =>
setTimeout(() => resolve(a), 100));
const go1 = (a, f) => a instanceof Promise ? a.then(f) : f(a);
const add5 = a => a + 5;
var r = go1(10, add5);
console.log(r); // 15
var r2 = go1(delay100(10), add5);
r2.then(console.log); // Promise{<pending>}
instanceof 구문을 통해 go1에게 전달된 첫번째 인자가 promise라면, then을 한 후에 해당하는 값에 함수의 적용하는 방식으로 정상동작하게끔 표현할 수 있다. 위 두코드가 완전 똑같이 동작하도록 작성할 수도 있다.
const n1 = 10;
go1(go1(n1, add5), console.log); //15
const n2 = delay100(10);
go1(go1(n2, add5), console.log); //15
함수 합성 관점에서의 Promise와 모나드
이 챕터에선 함수합성을 안전하게 하는 관점에서 살펴 볼 것 이다.
const g = a => a + 1;
const f = a => a * a;
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)); /* 아무 동작 X */
위 코드에서 일반적인 함수 합성을 봤을때, 인자값으로 null값을 넣으면 NaN이 반환되지만, 모나드를 활용하면 null값일 경우 아무 값도 반환되지 않으므로, 좀 더 안전한 합성을 이룰 수 있는 것을 볼 수 있다.
Promise도 위에서의 모나드와 유사한데, 아래를 보면 null값을 넣으면 NaN을 반환되는 것을 볼 수 있다.
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
정리하자면, Promise는 인자값이 있고 없고를 따지는 것이 아니라, 비동기 상황속에서 안전하게 합성을 이룰 수 있는 가를 다루는 것이다. 아래를 보고 마지막으로 이해해보자.
new Promise(resolve =>
setTimeout(() => resolve(2), 100)
).then(g).then(f).then(r => log(r)); // 9
다시말해서, Promise는 합성 관점에서 보았을때 얼마만큼 딜레이가 필요한 상황에서도 함수를 적절한 시점에 평가해서 합성시키기 위한 도구로서 바라 볼 수 있다.
Kleisli Composition 관점에서의 Promise
kleisli composition는 오류가 있을 수 있는 상황에서의 함수 합성을 안전하게 하는 하나의 규칙이다.
아래와 같은 코드가 있다고 보자.
var users = [
{id: 1, name: 'aa'},
{id: 2, name: 'bb'},
{id: 3, name: 'cc'}
];
const getUserById = id =>
find(u => u.id == id, users);
const f = ({name}) => name;
const g = getUserById;
const fg = id => f(g(id));
console.log(fg(2)); // bb
위 코드 까지는 항상 오류 없는 동일한 결과를 나타낼 것이다.
console.log(fg(2) == fg(2)); // true
하지만, 외부에서 user의 상태가 변화되었다고 가정해보자.
/* 변화 전 */
const r = fg(2);
console.log(r); // bb
/* 변화 후 */
users.pop();
users.pop();
const r2 = fg(2);
console.log(r2); /* error발생 */
위와 같이 외부에 어떠한 변화가 일어난다면 상황에 따라 에러가 일어 날 수 있으므로, 합성 하면서 에러가 나지 않게 하는 것이 kleisli composition이라고 한다.
이를 구현하기 위해, 함수 합성을 Promise를 통해 하며, 값을 찾았을때 그 값이 없으면, Promise.reject()을 반환하면 된다.
const getUserById = id =>
find(u => u.id == id, users) || Promise.reject('없어요!');
const f = ({name}) => name;
const g = getUserById;
const fg = id => Promise.resolve(id).then(g).then(f);
fg(2).then(console.log); // bb
users.pop();
users.pop();
fg(2).then(console.log); /* 콘솔 출력 x : Promise {<rejected>: "없어요!"} */
위와같이 값이 엉뚱한 결과를 받지 않고 log값도 출력되지 않으므로, 안전한 합성을 이룰 수 있다.
이제 이 값을 catch를 통해 다음과 같은 값을 뽑아 낼 수 있을 것이다.
const getUserById = id =>
find(u => u.id == id, users) || Promise.reject('없어요!');
const f = ({name}) => name;
const g = getUserById;
const fg = id => Promise.resolve(id).then(g).then(f).catch(a => a);
fg(2).then(log); // bb
users.pop();
users.pop();
fg(2).then(log); // 없어요!
go, pipe, reduce에서 비동기 제어
go, pipe, reduce 상황에서 Promise를 제어해볼 것이다.
아래 코드를 보자.
go(1,
a => a + 10,
a => Promise.resolve(a + 100),
a => a + 1000,
console.log);
/* [object Promise]1000 */
위와 같은 상황에서는 "[Object Promise]1000" 라고 뜨며, 비정상적으로 코드가 실행되지 않을 것이다.
그럼, 앞에서 사용했던 go, pipe, reduce 함수를 보자.
const go = (...args) => reduce((a, f) => f(a), args);
const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs);
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;
});
pipe는 go를, go는 reduce를 사용하고 있기 때문에, reduce만 고치면 이 문제를 해결 할 수 있다.
reduce 함수에서 수정해야 되는 부분을 보자.
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); /* 이부분을 Promise 값을 기다려서 만들어지는 값으로 변환해야 함 */
}
return acc;
});
그럼 Promise의 값을 정상적으로 받을 수 있게 수정해보자.
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);
acc = acc instanceof Promise ? acc.then(acc => f(acc, a)) : f(acc, a);
}
return acc;
});
위와 같이 수정을 하면, 위에서 작성한 go 함수에서 정상적으로 "1111"값을 출력할 것이다.
go(1,
a => a + 10,
a => Promise.resolve(a + 100),
a => a + 1000,
console.log); // 1111
하지만, 여전히 불안한 상태의 코드이다. 왜냐하면, Promise 구문이 끝나고 다음 Promise 구문이 아닌 "a => a+ 1000" 구문에서는 계속해서 Promise 체인에다가 함수를 합성하기 때문이다. 만일 위 코드를 즉시 하나의 콜스택에서 실행하길 바랬다면, 코드를 수정해야 한다.
다음과 같이 go1함수를 추가해서 수정해보자.
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;
});
});
위와같이 코드가 구성되었다면, Promise가 go함수의 제일 첫 인자로와도 오류없이 잘 실행된다.
go(Promise.resolve(1),
a => a + 10,
a => a + 1000,
a => a + 10000,
console.log()); // 11111
Promise.then의 중요한 규칙
then 메서드를 통해 결과를 꺼냈을때의 값이 반드시 Promise가 아니라는 규칙이다.
Promise.resolve(Promise.resolve(Promise.resolve(1))).then(console.log); // 1
위와같이 Promise가 중첩되어 선언되어 있어도, 한번의 then으로 안에있는 결과값을 얻을 수 있다. 다시말해서, Promise 체인이 연속적으로 대기가 걸려있어도, 내가 원하는 곳에서 한번의 then으로 해당하는 결과를 얻을 수 있다는 말이다.
new Promise(resolve => resolve(new Promise(resolve => resolve(1)))).then(console.log); // 1
위와 같이 연속적으로 resolve를 한다고 하더라도, 한번의 then으로 값을 얻을 수 있다.
'Web[웹] > ES6+ 함수형 언어' 카테고리의 다른 글
[ES6+: 응용] 명령형에서 함수형으로 변환시키기 (0) | 2019.10.24 |
---|---|
[JS: ES6+] 비동기: 동시성 프로그래밍 (2) (0) | 2019.10.21 |
[JS: ES6+] 제너레이터/이터레이터 프로토콜로 구현하는 지연 평가 (2) (0) | 2019.10.14 |
[JS: ES6+] 제너레이터/이터레이터 프로토콜로 구현하는 지연 평가 (1) (0) | 2019.10.08 |
[JS: ES6+] 장바구니 예제로 코드를 줄이고 HTML로 표현해보기 (0) | 2019.10.07 |