본문 바로가기

Backend/함수형 자바스크립트

Partial.js (2)

이전 포스팅 참고

 

Partial.js 에서의 비동기 제어에 대해 좀 더 알아볼까 합니다.

사실 async-await syntax 를 사용하는 요즘의 코드에선 이 내용이 큰 의미가 없을 것 같다는 생각도 들지만,

동기와 비동기 코드 구조를 동일하게 가져간다는 관점에서 보도록 하겠습니다.

 

- 비동기 제어가 되는 고차 함수

 

Underscore.js 나 Lodash 의 _.each, _.map, _.reduce 등의 함수들은 비동기 함수를 iteratee 로 사용할 수 없습니다.

예시를 먼저 보도록 하겠습니다.

 

const _ = require('lodash');

const result = _.map([1, 2, 3], function() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(new Date());
    }, 1000);
  });
});

console.info(result); // [Promise { <pending> }, Promise { <pending> }, Promise { <pending> }]

(async() => {
  const test = await Promise.all(result);
  console.info(test); // [2021-10-01T10:19:00.197Z, 2021-10-01T10:19:00.197Z, 2021-10-01T10:19:00.197Z]
})();

 

 

_.map 은 각 요소에 iteratee 를 적용 후 새로운 배열을 만들어 내는 함수입니다. 따라서 위 코드에서 의도한 result 의 값은 [2021-10-01T10:19:00, 2021-10-01T10:19:01, 2021-10-01T10:19:02] 이정도로 각 요소마다 1초의 차이가 나는 아이템을 가진 길이 3의 배열입니다.

하지만 result 는 resolve 되지않은 pending 상태의 Promise 객체를 가진 배열이 되었고, 이를 전부 풀려면 결국 Promise.all 을 사용해야 했습니다. 하지만 그 결과도 의도한대로 동작하진 않았습니다. 이는 Lodash 가 아닌 Underscore.js 를 사용해도 동일합니다.

 

const _ = require('partial-js');

const result = _.map([1, 2, 3], function() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(new Date());
    }, 1000);
  });
});

(async() => {
  const test1 = await result;
  console.info(test1); // [2021-10-02T10:34:36.810Z, 2021-10-02T10:34:37.811Z, 2021-10-02T10:34:38.815Z]
})();

 

동일한 코드를 Partial.js 로 구현한 버전입니다. 우선 실행도 3초 정도의 시간이 걸려 완료 되었고, 의도한 대로 동작한 것을 결과값으로부터 알 수 있습니다. Partial.js 는 iteratee 의 결과가 Promise 객체일 경우 내부적으로 for문 대신 재귀로 변경됩니다.

iteratee 가 각각 모두 1초 정도의 시간이 걸렸기 때문에 배열 요소 마다 1초의 차이가 있고, 실행에도 총 3초정도 걸린 것입니다.

 

const _ = require('partial-js');

const result = _.map([1, 2, 3], () => new Date());

console.info(result); // [2021-10-02T10:42:15.972Z, 2021-10-02T10:42:15.972Z, 2021-10-02T10:42:15.972Z]

 

위 버전은 동기 상황의 코드입니다. setTimeout 이 들어간 버전과 비교하면 동일하게 _.map 함수로 작성은 했지만 상황에 따라 코드 스타일이 다릅니다. 이를 _.go, _.pipe 등을 이용해 동일한 구조로 갖도록 변경해보겠습니다.

 

const _ = require('partial-js');

const syncDate = () => new Date();

const promiseDate = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(new Date());
    }, 1000);
  })
};

_.go([1, 2, 3],
  _.map(syncDate),
  console.info); // [2021-10-02T10:47:00, 2021-10-02T10:47:00, 2021-10-02T10:47:00]

_.go([1, 2, 3],
  _.map(promiseDate),
  console.info); // [2021-10-02T10:47:01, 2021-10-02T10:47:02, 2021-10-02T10:47:03]

 

가독성을 고려해 Date 의 ms 부분은 제거했습니다. _.go 함수를 사용해서 동기와 비동기 상황에서 동일한 코드 구조를 갖게 되었습니다.

 

Partial.js 의 _.each, _.map, _.reduce, _.find, _.filter 등 주요 함수들은 .go 와 _.pipe 처럼 하나의 함수로 동기와 비동기 상황이 모두 대응되도록 되어 있습니다. iteratee 의 결과나 iteratee 의 속성에 따라 동기 로직과 비동기 로직을 알아서 선택하도록 되어 있습니다.

동기 로직으로 실행되어야 할 경우엔 for 문을 사용하고, 비동기 로직을 사용해야 할 경우에만 재귀로 변경되기 때문에 성능적으로도 문제가 없습니다.

 

여기서 성능 얘기가 나오는 이유는, 위에서 말한 것 처럼 비동기 함수를 iteratee 로 사용할 수 없는 Underscore.js 나 Lodash 의 경우 이 문제를 해결하기 위해 bluebird.js 를 사용하기도 합니다.

하지만 여기서 성능 문제가 발생할 수 있는데, bluebird.js 는 함수 본체에서 비동기가 아닌 상황에서도 비동기가 일어납니다. 또한 iteratee 가 실행될 때마다 큐나 재귀, 내부 이벤트 루프에 의해 제어되기 때문에 for 문으로 구현된 함수들과 비교했을 때는 확실히 느립니다. 그리고 무조건 Promise 객체를 리턴하기 때문에 동기 함수들과 함께 사용하기엔 약간의 어려움이 있습니다.

 

물론 비동기 상황에서만 bluebird.js 를 사용하면 되겠지만, 지금 내용에서 중점으로 두고 있는 동기-비동기 상황에서의 동일한 코드 구조는 가져갈 수 없게 됩니다. 결국 함수형 프로그래밍에서 동기 함수와 비동기 함수의 조합을 할 수 없다는 문제로 이어지게 됩니다.

(제가 알고 있는 bluebird.js, Q.js 와 같은 Promise 라이브러리의 성능에 대한 부분은 최근엔 확인해보지 않아 이러한 문제가 해결되었을 수도 있습니다)

 

- _ .if

 

이 함수는 생각보다 쓰임새가 많지는 않을 것 같기도 하지만, 알아보도록 하겠습니다.

Partial.js 의 _.if 함수는 비동기 결과를 기다립니다.

 

const _ = require('partial-js');

const isAsyncTest1 = (x) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x === 1);
    }, 1000);
  });
};

const isAsyncTest2 = (x) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x === 2);
    }, 1000);
  });
}

const result =
  _.if(isAsyncTest1, () => '1 맞음')
  .else_if(isAsyncTest2, () => '2 맞음')
  .else(() => '둘 다 아님');

result(2).then(console.info); // 2 맞음

 

만약 위 코드를 _.if 없이 해결하려 했다면 코드가 다소 보기 좋지는 않았을 것 같네요. 물론 제 개인적으론 _.if 도 그렇게 가독성이 좋다 라는 느낌은 들지 않지만, 역시나 동기 상황에서와 비교해본다면 거의 비슷한 구조를 잡을 수 있습니다.

마찬가지로 _.go 함수와 함께 사용해보겠습니다.

 

const _ = require('partial-js');

const isAsyncTest1 = (x) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x === 1);
    }, 1000);
  });
};

const isAsyncTest2 = (x) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x === 2);
    });
  });
}

_.go(2,
  _.if(isAsyncTest1, () => '1 맞음')
  .else_if(isAsyncTest2, () => '2 맞음')
  .else(() => '둘 다 아님'),
  console.info); // 2 맞음

 

여담으로 지금 버전의 partial-js 에선 실행하면 _.if 가 _.if2 로 대체될 예정이라는 문구가 같이 나오네요. 함수명이 설마 _.if2 로 나오려나.. 하는 생각을 잠시 가져봅니다.

 

동기와 비동기 상황을 동시에 대응하는 함수를 쉽게 만드는 방법은 함수 로직을 _.go 나 _.pipe 로 구현해 두는 것입니다.

그러면 해당 함수들이 자동으로 동기 상황과 비동기 상황에 맞춰 로직을 알아서 변경시키기 때문에 추가적인 작업이 필요 없습니다.

이러한 Partial.js 의 컨셉을 잘 활용하면 상황에 상관없이 동일한 코드 구조를 잡을 수 있을 것입니다.

'Backend > 함수형 자바스크립트' 카테고리의 다른 글

지연 평가 (1)  (0) 2021.10.17
Partial.js (3)  (0) 2021.10.09
Partial.js (1)  (0) 2021.09.25
함수 조립 (2)  (0) 2021.09.18
함수 조립 (1)  (0) 2021.09.11