본문 바로가기
JavaScript

[FP&ES6+] go, pipe, reduce에서 비동기 제어

by _sweep 2022. 1. 8.

함수형 프로그래밍과 JavaScript ES6+ 강의를 듣고 정리한 내용입니다.

 

 

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

 

우선 go 함수를 보면 reduce를 실행하고 reduce 안에서는 즉시함수를 실행하는 것 외에 다른 작업을 하는 것이 없기 때문에 go 함수 안의 작업이 실행되는 모든 제어권을 reduce가 가지고 있다.

pipe 함수도 go 함수를 바탕으로 작성되어 있기 때문에 이또한 제어권을 reduce가 가지고 있다.

따라서 reduce 함수의 일부분만 바꾸어 작성하면 go, pipe, reduce에서도 비동기 제어가 가능하다.

 

 

✏️ 예제

 

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

// output
// [object Promise]1000

 

먼저 비동기 제어 처리를 하지 않은 go 함수에 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 = acc instanceof Promise ? acc.then((acc) => f(acc, a)) : f(acc, a);
  }
  return acc;
});

 

인자로 들어온 acc가 Promise인지 아닌지 구별해서 각 상황에 맞는 처리를 해주는 것이다.

Promise 값일 경우에는 then 함수를 사용하여 함수 f를 적용하고 아닌 경우에는 즉시 f를 적용한다.

 

하지만 이와 같이 작성했을 경우에는 Promise를 만나고 난 이후의 코드들이 Promise가 아님에도 불구하고 Promise로 감싸져서 매번 비동기가 일어나게 된다.

이에 따라 불필요한 작업들이 생기기 때문에 성능 저하가 반드시 일어난다.

 

이를 해결하기 위해서는 중간에 Promise를 만나도 이후 만난 것이 Promise가 아닐 경우에는 동기적으로 동작하게끔 해야 한다.

 

const reduce = curry((f, acc, iter) => {
  if (!iter) {
    iter = acc[Symbol.iterator]();
    acc = iter.next().value;
  } else iter = iter[Symbol.iterator]();

  return (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;
  })(acc);
});

 

위 코드에서는 recur 함수를 리턴하는데 즉시 실행을 한다.

우선 리턴하는 값이 recur 함수이다.

recur 함수는 함수를 값으로 다루며 함수에 이름을 짓는 유명함수이다.

recur 함수의 while문 안에서는 먼저 acc = f(acc, a)를 통해 go문의 함수들을 적용해본다.

그리고 acc의 값이 Promise인지 아닌지를 if문에서 상황을 나누어 판단하며 Promise인 경우 재귀적 호출로 다시 recur 함수에 넘기고 이때 recur 함수의 인자인 acc에 계산한 값이 들어가게 된다.

 

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

// output
// 1111
// [object Promise]101001000

 

그러나 이 또한 완벽히 작동하는 것이 아니다.

인자로 넘어가는 함수에 Promise가 있는 경우에는 제대로 동작하지만 while문 안에서 먼저 함수를 실행 후 값을 확인하고 있기 때문에 맨 처음 들어가는 인자가 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;
    });
  });

 

이를 고려해 최종적으로 reduce를 작성하자면 위와 같다.

go1문을 이용해 인자로 들어오는 acc 값도 Promise인지 아닌지 확인 작업을 거치는 것이다.

 

이렇게 작성한 경우 원하는 결과를 얻을 수 있다.

 

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

go(
  Promise.resolve(1),
  (a) => a + 10,
  (a) => Promise.reject("Error!!!"),
  (a) => console.log(a),
);

// output
// 1111
// Uncaught (in promise) Error!!!

 

 

 Promise.then()의 중요한 규칙

 

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

// output
// 1

 

Promise.then()의 중요한 규칙 중 하나는 then 메서드를 통해 결과를 꺼냈을 때의 값이 반드시 Promise 값이 아니라는 것이다.

위와 같이 Promise가 여러 개로 중첩이 되어 있는 경우에도 단 하나의 then 메서드만을 사용하여 값을 꺼낼 수 있다.

 

 

 

 

 

댓글