앞서 만들어 놓은 여러가지 이터러블 프로토콜 기반의 함수들을 실무적인 코드에 적용하는 방법을 알아보도록 하자
let users = [
{
name: 'a', age: 21, family: [
{name: 'a1', age: 53}, {name: 'a2', age: 47},
{name: 'a3', age: 16}, {name: 'a4', age: 15}
]
},
{
name: 'b', age: 24, family: [
{name: 'b1', age: 58}, {name: 'b2', age: 51},
{name: 'b3', age: 19}, {name: 'b4', age: 22}
]
},
{
name: 'c', age: 31, family: [
{name: 'c1', age: 64}, {name: 'c2', age: 62}
]
},
{
name: 'd', age: 20, family: [
{name: 'd1', age: 42}, {name: 'd2', age: 42},
{name: 'd3', age: 11}, {name: 'd4', age: 7}
]
}
];
go(users,
L.flatMap(u => u.family), // [{name: "a1" , age:53}, {name: "a2" , age:47}, ...]
L.filter(u => u.age > 20), // [{name: "a3" , age:16}, {name: "a4" , age:15}, ...]
L.map(u => u.age), // [16, 15, ...]
take(4), // [16, 15, 19, 7]
reduce(add), // 57
console.log
);
자바스크립트에서 비동기 동기성 프로그래밍을 하는 방법은 크게 두가지이다.
비동기 상황이 promise를 사용하면 일급,즉 값으로 다뤄질수 있기에 변수 또는 함수에 할당/전달될 수 있다는 점이 callback과 아주 큰 차이다.
// callback 패턴
function add10(a, callback) {
// return을 안하고 있다
setTimeout(() => callback(a + 10), 100);
}
let a = add10(5, res => {
add10(res, res => {
add10(res, res => {
// log(res);
});
});
});
console.log(a) // 35
// Promise 패턴
function add20(a) {
// return을 하고 있다
return new Promise(resolve => setTimeout(() => resolve(a + 20), 100));
}
let b = add20(5)
.then(add20)
.then(add20)
.then(console.log); // 65
프로미스가 일급 함수라는 점을 인지하고 활용해보도록 해보자
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;
const n1 = 10;
console.log(go1(go1(n1, add5), log));
const n2 = delay100(10);
console.log(go1(go1(n2, add5), log));
모나드는 함수 합성을 안전하게 하기 위한 도구
// f . g
// f(g(x))
const g = a => a + 1;
const f = a => a * a;
// 안전하지 않은 코드 합성
console.log(f(g(1))); // 4
console.log(f(g())); // 빈 값이 들어와도 함수를 실행해 NaN으로 평가된다
// 모나드를 활용한 안전한 코드 합성
Array.of(1).map(g).map(f).forEach(r => log(r)); // 4
[].map(g).map(f).forEach(r => log(r)); // 빈 값이 들어오면 값이 평가 되지 않는다
// Promise를 활용한 함수 합성
// Promise는 then을 통해 함수를 함성한다, 하지만 프로미스는 비동기 상황에서의 안전한 합성을 한다.
Promise.resolve(2).then(g).then(f).then(r => log(r)); // 4
new Promise(resolve =>
setTimeout(() => resolve(2), 100)
).then(g).then(f).then(r => log(r)); // 9
오류가 있을 수 있는 상황에서의 함수 합성을 안전하게 할 수 있는 규칙
// f . g
// f(g(x)) = f(g(x))
// f(g(x)) = g(x)
var users = [
{id: 1, name: 'aa'},
{id: 2, name: 'bb'},
{id: 3, name: 'cc'}
];
const getUserById = id =>
find(u => u.id == id, users) || Promise.reject('없어요!');
const f = ({name}) => name;
const g = getUserById;
const fg = id => f(g(id));
// catch로 인해 오류가 났을시 안전한 함수 합성이 가능하게 된다.
const fg = id => Promise.resolve(id).then(g).then(f).catch(a => a);
fg(2).then(log);
setTimeout(function () {
users.pop();
users.pop();
fg(2).then(log);
}, 10);
기존 go,pipe,reduce 함수는 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;
});
});
const go = (...args) => reduce((a, f) => f(a), args);
const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs);
개선된 함수들을 사용해보면 정상적으로 작동 되는 것을 알 수 있다.
go(Promise.resolve(1),
a => a + 10,
a => Promise.reject('error~~'),
a => console.log('----'),
a => a + 1000,
a => a + 10000,
log).catch(a => console.log(a));
await는 기본적으로 평가가 Promise를 내뱉는 함수 기반으로만 사용할 수 있다. 그렇기에 모든 비동기 프로그래밍에 async-await 문법이 만능템은 아닌 것이다. 즉 async-await를 써서 자유롭게 비동기 프로그래밍을 구현하고 싶다면 Promise를 리턴하는 함수를 직접 만들줄 알아야한다.
그리고 async 함수는 항상 Promise 값을 리턴하기에 리턴된 값을 평가받고 싶다면 await를 써야한다.
function delay(time) {
return new Promise(resolve => setTimeout(() => resolve(), time)
}
async function f1(){
// await를 안쓰면 delay는 Promise를 리턴한다
const a = delay(10); // Promise
const b = await delay(10) // 10
const c = await delay(10) // 10
return b + c;
console.log(a,b);
}
f1(); // Promise
f1().then(console.log); // 20
(async () => {
console.log(await f1()) // 20
})();
기본적으로 자바스크립트에서 지원되는데 map 메소드는 Promise로 리턴되는 값들을 다루지 못한다.
function delayI(a) {
return new Promise(resolve => setTimeout(() => resolve(a), 100));
}
async function f2() {
const list = [1, 2, 3, 4];
const temp = list.map(async a => await delayI(a * a));
console.log(temp); // [Promise, Promise, Promise, Promise]
const res = await temp;
console.log(res); // [Promise, Promise, Promise, Promise]
}
f2();
async function f3() {
const list = [1, 2, 3, 4];
const temp = map(a => delayI(a * a), list);
console.log(temp); // Promise {<pending>}
const res = await temp;
console.log(res); // [1,4,9,16]
}
f3();
function f4() {
return map(a => delayI(a * a), [1, 2, 3, 4]);
}
(async () => {
console.log(await f4());
})();
파이파라인과 async/await은 완전히 다른 장치이다.
// 파이프라인으로 코드 구현 시
function f5(list) {
return go(list,
L.map(a => delayI(a * a)),
L.filter(a => delayI(a % 2)),
L.map(a => delayI(a + 1)),
C.take(2),
reduce((a, b) => delayI(a + b)));
}
go(f5([1, 2, 3, 4, 5, 6, 7, 8]), a => log(a, 'f5'));
// 파이프라인 없이 코드 구현 시
async function f6(list) {
let temp = [];
for (const a of list) {
const b = await delayI(a * a);
if (await delayI(b % 2)) {
const c = await delayI(b + 1);
temp.push(c);
if (temp.length == 2) break;
}
}
let res = temp[0], i = 0;
while (++i < temp.length) {
res = await delayI(res + temp[i]);
}
return res;
}
go(f6([1, 2, 3, 4, 5, 6, 7, 8]), log);
function f7(list) {
try {
return list
.map(a => JSON.parse(a))
.filter(a => a % 2)
.slice(0, 2);
} catch (e) {
log(e);
return [];
}
}
log(f7(['0', '1', '2', '{']));
async function f8(list) {
try {
return await list
.map(async a => await new Promise(resolve => {
resolve(JSON.parse(a));
}))
.filter(a => a % 2)
.slice(0, 2);
} catch (e) {
// log(e, '----------------------');
return [];
}
}
f8(['0', '1', '2', '{']).then(log).catch(e => {
log('에러 핸들링 하겠다.');
});