21. 8. 13(금) TIL(Promise 지연 평가, 지연성 함수의 병렬 실행)

배준형·2021년 8월 13일
1

TIL

목록 보기
10/21
post-thumbnail

Javascript Promise 지연 평가 및 지연성 함수의 병렬 실행

📌 Promise와 콜백함수의 차이점

Promise : 프로미스가 콜백 함수와 다른 점은 then 메소드를 통해서 결과를 꺼낸다는 것 뿐만 아니라 비동기적으로 일급 값(States:대기, 성공, 실패 등의 인스턴스)을 다룬다는 점에서 차이가 있다.

  • then 메소드를 사용하게 되면 그 뒤에 프로미스가 리턴되게 되고, 계속 then을 연결지어서 비동기로 값으로 다룰 수 있다.
  • 비동기 상황이 값으로 다뤄지고, 이 값을 어떤 변수에 할당하거나 전달하여 체이닝할 수 있다는 점이 콜백 함수와 다른 점이다.
  • 프로미스는 비동기적으로 일어나는 상황을 안전하게 합성하기 위한 도구이다. 비동기 상황에서의 합성을 안전하게 하려는 성질을 가지고 있다.


📌 Promise를 인자로 받는 go, reduce 비동기 제어

const go = (...args) => reduce((a, f) => f(a), args);

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

이 경우 중간에 Promise를 만나 정상적으로 실행되지 않는다.

go 함수는 reduce 함수의 영향을 받으므로 reduce에서 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() { // return fuction recur() 를 추가
    let cur;
    while (!(cur = iter.next()).done) {
      const a = cur.value;
      acc = f(acc, a);
      if (acc instanceof Promise) return acc.then(recur);
      /* 입력된 값이 Promise일 때 그 값을 그대로 return 하면서 재귀하여 다시 reduce 함수가 실행되게 했고,
      아닐 경우 그대로 while 반복문을 돌면서 하나의 콜 스택을 사용하도록 만들 수 있다. */
    }
    return acc;
  } ();
});

여기까지 수정한 후 가장 위에 go 함수를 실행시키면 정상적으로 실행은 되나, 만약 첫 번째 값이 Promise일 때 첫 번째 acc 값 자체가 Promise가 되어 버리기 때문에 제대로 동작하지 않는다. 그럴 때 go 함수를 수정하여 Promise 값을 다룰 수 있게 하고, 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;
  } else {
    iter = iter[Symbol.iterator]();
  }
  return go1(acc, function recur(acc) { // go1 함수를 새로 지정하고, 여기에 추가하였다.
    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;
  });
});

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

go(
  Promise.resolve(1),
  a => a + 10,
  a => Promise.reject("error"), // 만약 중간에 reject("error") 값이 전달되게 되면
  a => a + 1000,
  a => a + 10000,
  console.log
).catch(a => console.log(a)); // error >> .catch로 reject를 캐치할 수 있다.


📌 map, filter의 Promise 지원

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

중간에 Promise 값을 받아서 정상적으로 동작하지 않는다. L.map을 통해 L.filter로 전달된 값이 Promise이기 때문이다. 이 때 L.filter에 go1함수를 사용하여 로직을 변경하여 동작시킬 수 있다.

L.filter = curry(function* (f, iter) {
  for (const a of iter) {
    const b = go1(a, f);
    if (b) yield a;
  }
});

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

>> 그러나, 이 경우에도 홀수만 받아서 [1, 9]가 출력되어야 하나 [1, 4]가 출력되어 정상적으로 동작한 것은 아니다.
그래서

const nop = Symbol('nop'); // reject 값이 반환됐을 때 지정해 놓은 nop이 반환된다면 아무 처리 없이 그대로 넘겨줄 것이고, 만약 진짜 error가 발생했다면 error를 출력하도록 구분하기 위해 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))
    else if (b) yield a;
  }
});

const take = curry((l, iter) => {
  let res = [];
  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) => (res.push(a), res).length === l ? res : recur()
        ).catch(e => e === nop ? recur() : Promise.reject(e));
      /* 여기에 전달된 값이 reject를 통해 전달된 nop 일 경우 이후의 함수에 값을 전달하지 말고 다시 recur()을 실행하며,
      실제로 에러가 발생했을 경우 그대로 Promise.reject(e)로 전달해주면 filter에 Promise가 전달되어도 정상적으로 실행되게 된다.*/
      res.push(a);
      if (res.length === l) return res;
    }
    return res;
  } ();
});

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


📌 지연성 함수의 병렬적인 평가

자바스크립트는 싱글 스레드를 기반으로 비동기적으로 일을 처리해서 하나의 스레드에서도 효율적으로 작업을 진행할 수 있다. 그래서 이러한 특징으로 인해 병렬적인 프로그래밍이 필요하지 않다고 생각할 수 있는데 자바스크립트는 싱글 스레드로 제어할 뿐이지 병렬적인 프로그래밍도 필요하다.

아래의 코드는 delay500 함수를 정의하여 0.5s 뒤에 Promise 연산이 실행되게 했고, 5개의 값을 모두 평가한 후 모두 더하는(reduce) 과정이다.

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

go(
  [1, 2, 3, 4, 5],
  L.map(a => delay500(a * a)),
  L.filter(a => a % 2),
  reduce(add),
  console.log
); // 35 (exited with code=0 in 2.617 seconds)

위 상황에서는 모든 과정이 0.5s 뒤에 이루어 지므로 5개의 값이 평가가 모두 완료되면 약 2.5s 이상의 시간이 걸리게 된다.

위 상황을 병렬적으로 처리해야 한다고 생각하면 최초에 map, filter에서 진행 될 값들의 평가를 모두 동시에 실행시켜서 더 빠르게 결과를 도출할 수 있을 것이다.

아래는 병렬적으로 코드를 실행시킬 수 있는 C.*** 함수를 정의하고 적용한 코드이다.

const C = {};
C.reduce = curry((f, acc, iter) => iter ? reduce(f, acc, [...iter]) : reduce(f, [...acc]));
/* iter가 있을 때, 그대로 전달, 없을 때 acc를 전달하는 방식으로 reduce를 넘기는데
전개 연산자를 통해 넘기기 때문에 reduce를 동시에 모두 실행하게 된다. */

go(
  [1, 2, 3, 4, 5],
  L.map(a => delay500(a * a)),
  L.filter(a => a % 2),
  C.reduce(add),
  console.log
); // 35 (exited with code=0 in 0.587 seconds)

이런식으로 자바스크립트는 싱글 스레드이지만 제어만 병렬적으로 코드를 작성하여 효율성을 높일 수 있다.

즉시 평가 또는 지연 평가(Promise 포함) 함수의 병렬적 조합

이러한 개념들을 바탕으로 즉시 또는 지연 평가되는 함수들을 개발자가 원하는 만큼 병렬적으로 조합하여 원하는 수준의 성능 향상을 기대할 수 있다.

1. 모두 실행하는 방법

console.time("");
go(
  [1, 2, 3, 4, 5, 6, 7, 8],
  map(a => delay500(a * a)),
  filter(a => delay500(a % 2)),
  map(a => delay500(a + 1)),
  take(2),
  console.log,
  _ => console.timeEnd("")
); // [2, 10]: 10.648s

2. 필요한 부분 까지만 실행하는 방법

console.time("");
go(
  [1, 2, 3, 4, 5, 6, 7, 8],
  L.map(a => delay500(a * a)),
  L.filter(a => delay500(a % 2)),
  L.map(a => delay500(a + 1)),
  take(2),
  console.log,
  _ => console.timeEnd("")
); // [ 2, 10 ]: 4.079s

3. 지연 평가 하되, take에서 병렬적으로 모두 동시에 실행할 경우

console.time("");
go(
  [1, 2, 3, 4, 5, 6, 7, 8],
  L.map(a => delay500(a * a)),
  L.filter(a => delay500(a % 2)),
  L.map(a => delay500(a + 1)),
  C.take(2),
  console.log,
  _ => console.timeEnd("")
); [ 2, 10 ]: 1.527s


📌 배운점

며칠 전 Promise, async, await에 대해서 정리하여 포스팅 한 적 있다. 그 때는 각각의 개념, 용어 정리, 왜 사용하는지 등에 대해 정리했는데, 오늘 Promise를 인자로 받을 때의 지연 평가를 적용하면서 자바스크립트에서 거의 대부분의 값, 객체 들을 다루는 방법에 대해 배운 것 같다. 물론 앞으로도 시행착오가 있을것이고, 더 다양한 상황이 있겠지만 우선 오늘 배운 항목들을 중점적으로 학습해 나가면 기본을 탄탄하게 할 수 있을것이라 생각한다.


📌 앞으로

자바스크립트에서는 비동기적인 일의 처리가 필수적이다. 따라서 오늘 배운 내용들이 학습할때도, 실무에서도 다양한 상황속에서 사용될 것이라고 생각한다. 오늘 정리하여 포스팅 하더라도 머리속에 남아있으리라는 보장이 없기 때문에 학습하다가 또는 프로젝트를 진행하다가 생각이 안나더라도 오늘 배웠다는 사실만큼은 기억하여 더 찾아보거나, 오늘 포스팅한 내용을 다시 찾아보면서 나의 지식으로 만들고 싶다.


profile
프론트엔드 개발자 배준형입니다.

0개의 댓글