함수형 프로그래밍 - 비동기 제어(1)

jiseong·2022년 7월 7일
0

T I Learned

목록 보기
286/291

과거에는 callback만을 사용하여 비동기처리를 했지만 연속적인 비동기처리를 하다보면 가독성이 좋지 못하고 서드파티라이브러리인 경우 제어의 역전문제로 믿음에 대한 문제가 있었다.

그래서 Promise라는 것이 나오게 된 것이고 오늘 강의에서 프로미스가 1급 객체이기 때문에 값으로써 다뤄질 수 있어 이를 활용하여 비동기적인 코드를 제어하는 예시들을 보여주었다.

callback, promise가 반환하는 값

다음과 같이 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, 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


Reference

0개의 댓글