혼자 적어보는 노트

프로그래머스 데브코스 TIL - Day 10 본문

스터디

프로그래머스 데브코스 TIL - Day 10

jinist 2022. 4. 3. 21:36

 

 학습 목차

- [DAY 10] VanillaJS를 통한 자바스크립트 기본 역량 강화 (2)

- 함수형 프로그래밍과 ES6+ (4)

 

✅ 새롭게 학습한 부분

- 값으로써의 promise 활용

- 함수 합성 / 모나드 / Kleisli Composition

- 함수의 비동기 제어

- 지연평가 + Promise의 효율성

- 지연된 함수의 병렬 평가 (Concurrency)

- CallStack에 쌓인 catch 에러 해결

 


💻 비동기/ 동시성 프로그래밍

 

promise와 callback의 차이란?

promise는 대기, 성공, 실패의 값을 만들고 프로미스 객체가 리턴된다.
값으로써 다루어지기 때문에 일급이다.

 

const delay100 = a => new Promise(resolve =>
  setTimeout(() => resolve(a), 100));

const go1 = (a, f) => a instanceof Promise ? a.then(f) : f(a);
// 전달된 값이 프로미스라면 than메소드와 함께 함수를 실행하고
// 아니라면 전달된 함수에 값을 전달한다.

const add5 = a => a + 5;

console.log(go1(10, add5)) // 15
console.log(go1(delay100(10), add5)) // Promise {<pending>}
go1(delay100(10), add5).then(console.log) // 15

promise를 사용할 경우 값이 담긴 Promise 객체를 리턴한다.

 

go1 함수를 사용하여 일반 함수와 Promise를 똑같이 사용하고싶다면

아래와 같이 작성을 하면 된다.

go1(go1(10, add5), console.log); // 15
go1(go1(delay100(10), add5), console.log); // 15

 

함수 합성

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)); // 아무 값도 없음
[1, 2, 3].map(g).map(f).forEach(r => console.log(r)); // 4  9  6

array를 사용하여 빈 값을 전달할 경우에도 안전하게 결과를 만들 수 있다.

 

promise의 안전한 합성

Promise.resolve(1).then(g).then(f).then(r => console.log(r)); // 4
Promise.resolve().then(g).then(f).then(r => console.log(r)); // NaN

new Promise(resolve =>
  setTimeout(() => resolve(2), 100)
).then(g).then(f).then(r => console.log(r)); // 9

Promise는 then 메소드를 사용해서 합성을 하는데

빈 값을 넣었을 때 잘못된 값이 나타난다.

Promise는 내부에 값이 있고 없고에 대한 안전한 합성이 아니라

비동기 상황(대기)에서 연속적으로 안전한 합성이 가능하게 만들어 준다.

 

즉, Promise는 합성의 관점에서 보았을 때 비동기적인 프로그래밍에서 안전한 합성을 해준다는 의미를 가진다.

 

Kleisli Composition 관점에서의 Promise

- 오류가 있을 수 있는 함수의 합성을 안전하게 할 수 있는 하나의 규칙

- 함수를 합성 했을 때 한쪽의 함수에서 에러가 발생 했을 때 같은 결과가 나타나는 것.

  ex) f(g(x)) = g(x)

 

var users = [
  { id: 1, name: "Jay"},
  { id: 2, name: "Hey"},
  { id: 3, name: "May"},
];

const getUserId = id => {
  console.log(find(u => u.id == id, users), "id")
  return find(u => u.id == id, users) || Promise.reject("값이 없습니다.")
};

const f = ({ name }) => name;
const g = getUserId;

const fg = id => Promise.resolve(id).then(g).then(f).catch(a => a);
// f와 g를 합성한 fg함수

fg(5).then(console.log); // 값이 없습니다.

g의 결과가 없다면 Promise에서 reject를 반환하고
.catch를 사용하여 이후의 연결되는 함수를 진행하지 않고

바로 catch문을 진행함으로써 f(g(x)) = g(x)가 성립된다.

 

이러한 결과로 인해 promise는 Kleisli Composition을 지원한다고 볼 수 있다.

 

go, reduce 함수의 비동기제어

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;
  }
  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);

go(
  1,
  a=> a + 100,
  a=> Promise.resolve(a + 1000),
  a=> a + 1000,
  console.log // 2101
)

 

go 함수에서 Promise를 반환하는 값을 사용할 경우 다음 평가에서 오류가 생기게 되는데

중간에 Promise를 반환하는 함수를 만났을 때 reduce에서 재귀함수를 사용하여

Promise를 처리하고 go1함수를 사용하여 처음에 들어오는 acc값을 처리 함으로써

비동기나 동기 값이 들어오더라도 효율적으로 처리가 되게 할 수 있다.


promise.then의 규칙

Promise.resolve(Promise.resolve(1)).then(console.log) // 1

 

지연평가

Promise 값을 지연평가 할 경우 오류가 생기는 부분 수정해보기!

go(
  [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)],
  L.map(a => a + 10),
  take(3),
  console.log
);

Promise 값을 반환 받았을 때에도 정상적으로 실행을 할 수 있게

중간에 처리하는 함수들의 코드를 수정해주어야 한다.

 

L.map = curry(function* (f, iter) {
  for (const a of iter) {
    // yield f(a);
    yield go(a, f);
  }
});

const take = curry((l, iter) => {
  let res = [];
  console.log(iter);
  iter = iter[Symbol.iterator]();
  return function recur(){
    let cur;
    while (!(cur = iter.next()).done) {
      const a = cur.value;
      if(a instanceof Promise) return a.then(a=> {
      
        return  (res.push(a), res).length === l ? res : recur();
        // a가 Promise라면 then으로 꺼내서 push
        // 이미 return이 된 상태기 때문에 재귀를 사용하여 while문을 다시 실행시킨다.
      })
      res.push(a);
      if (res.length === l) return res;
    }
    return res;
    }();
});

L.map함수는 이전에 만든 go1함수로 처리를 하고

reduce에서 진행한 것과 같이 take함수에서는 Promise라면 then으로 꺼내서 담고,

재귀함수로 while문을 다시 실행시켜서 나머지 값들도 담을 수 있다.

* (res.push(a), res).length 처럼 두 개의 내용을 한 줄로 처리할 수 있다는 것을 알게되었다.

 

filter의 지연을 위한 kleisli composition (+ nop)

go(
  [1,2,3,4,5],
  L.map(a => Promise.resolve(a * a)),
  L.filter(a=> a % 2),
  L.map(a => a * a),
  take(3),
  console.log
)

L.filter 또한 Promise 값이 들어올 경우 잘못된 값이 출력된다.

 

const nop = Symbol('nop');

L.filter = curry(function* (f, iter) {
  for (const a of iter) {
    const b = go1(a, f);
    if(b instanceof Promise) yield b.then(b=> b ? a : Promise.reject(nop))
    // 처리한 값이 false라면 reject를 사용하여 catch로 이동
    else if (b) {
      console.log(b, "b")
      yield a;
    }
  }
});

Promise값은 true로 구분이 되기 때문에 then을 이용해서 꺼내서 확인을 하고 

처리한 값이 false라면 다음에 계산할 함수에 값을 전달하면 안된다.

Promise.reject를 통해 catch로 넘기면 다음 함수를 건너뛸 수 있다!

 

하지만 reject를 할 경우 실제 에러로 인한 사용인지 값을 전달하지 않기 위한 구분자로써의 사용인지

알 수 없기 때문에 reject에 symbol('nop') 값을 담아서 전달한다.

 

// take함수 일부

while (!(cur = iter.next()).done) {
      const a = cur.value;
      if(a instanceof Promise)
      return a
        .then(a => (res.push(a), res).length === l ? res : recur())
        .catch(e => e == nop ? recur() : Promise.reject(e));
      // take로 전달된 값이 nop으로 전달되었다면 무시하고 다음 while문을 돌리고
      // 실제 에러라면 reject로 에러를 반환
      res.push(a);
      if (res.length === l) return res;
    }

take에서 catch를 사용하여 이어서 처리를 마무리 해주면 된다.

 

❗❗ 여기서 catch를 다른 함수에 보내서 이어서 처리할 수 있다는 것을 알게 되었다.

결국 then은 reject를 만나면 이후의 then을 무시하고 catch로 넘어가기 때문에
그 원리를 이용한 방식이였던 것이다.

 

 

지연평가 + Promise의 효율성

go(
  [1,2,3,4,5],
  L.map(a => new Promise(resolve => setTimeout(() => resolve(a * a), 1000))),
  L.filter(a=>new Promise(resolve => setTimeout(() => resolve(a % 2), 1000))),
  take(2),
  console.log
)

setTimeout을 사용하여 시간이 소요되는 비동기 로직을 작성 했을 때

전부 평가를 하지 않고 실제 필요한 평가만 진행을 하는 것을 알 수 있었다.

 

 

지연된 함수의 병렬 평가 (Concurrency)

위의 지연평가를 보면 실제 필요한 평가만 진행을 하지만 map과 filter가 각각 실행이 되는데

reduce를 사용하여 한번에 동시적으로 처리를 할 수 있다.

const delay500 = a => new Promise(resolve =>
  setTimeout(() => resolve(a), 500));

const C = {};
C.reduce = curry((f, acc, iter) => iter ?
  reduce(f, acc, [...iter]) :
  reduce(f, [...acc]))
  // 비동기가 이뤄지는 것을 기다리지 않고 전부 실행

go(
  [1,2,3,4,5],
  L.map(a => delay500(a * a)),
  L.filter(a=>a % 2),
  C.reduce((a, b) => a + b),
  console.log
)

 

CallStack에 쌓인 catch 에러 해결

중첩해서 비동기 코드를 실행할 경우 마지막에 평가되는 값은 같지만

catch로 보낸 reject가 처리되지 않아서 console이 지저분해지는데

이 부분은 빈 값의 함수를 통해 해결할 수 있다.

function noop() {}
const catchNoop = arr =>
  (arr.forEach(a => a instanceof Promise ? a.catch(noop) : a), arr);

C.reduce = curry((f, acc, iter) => {
  return iter ?
    reduce(f, acc, catchNoop([...iter])) :
    reduce(f, catchNoop([...acc]));
})

* C.take도 동일한 방식으로 작성했다.

 

+ catch 다루기

var a = Promise.reject('hi');
a = a.catch(a => a);

// a는 catch된 promise이고 이후에 catch를 할 수 없다.
// var a = Promise.reject('hi').catch(a => a); 와 같다.

var a = Promise.reject('hi');
a.catch(a => console.log(a, "catch"));

// a를 담지 않으면 a는 catch가 되지 않은 상태이므로
// 원하는 때에 catch를 할 수 있다.

a.catch(a => console.log(a, "catch")); // hi catch

reject한 promise를 바로 catch할 수 있다.

❗❗ catch후에 다시 catch를 하면 원래 있던 promise를 리턴하기만 한다.

 

 

C.filter / C.map

C.take = curry((l, iter) => take(l, catchNoop(iter)));
C.takeAll = C.take(Infinity);
C.map = curry(pipe(L.map, C.takeAll));
C.filter = curry(pipe(L.filter, C.takeAll));


C.map(a => delay500(a * a), [1, 2, 3, 4, 5]).then(console.log);
C.filter(a => delay500(a % 2), [1, 2, 3, 4, 5]).then(console.log);

C.filter와 C.map 또한 위와 같이 만들 수 있고,

즉시, 지연, Promise, 병렬성을 원하는 대로 조합하여 사용할 수 있다.

 

 


✍ 느낀 점

then, catch에 대해 조금은 알고 있었지만 이번에 좀 더 활용하는 방법을 배운 것 같다.
또한 한 줄로 코드를 처리하는 방법과 변수 안에서 삼항연산자를 사용하는 것을 알게 되었고
들어오는 값이 어떤 값이냐에 따라 구분해서 처리 함으로써 함수 합성 시 동일한 결과 값을 내거나
같은 결과지만 다른 효율성을 보이는 것들을 보았을 때 코드 자체는 간단하지만 생각을 깊게 하게 만들었다.💡😲
사실 이해하는것 자체에도 벅찼기 때문에 직접 활용하기에는 아직 많이 부족하지만

생각했던 것 보다는 재미있었고 더 알고 싶은 마음이 생겼다! 

Comments