본문 바로가기

Web[웹]/ES6+ 함수형 언어

[ES6+: 응용] 시간을 이터러블로 다루기

 

#1 take, takeWhile, takeUntil로 코드 다루기

 

takeWhile, takeUntil

 

먼저 range와 take를 다시 알아보자.

위 두 개는 즉시 평가되는 것과 지연 평가되는 것으로 나뉘는데 이를 해석해보면 다음과 같다.

 

_.go(
    _.range(10), /* 0 ~ 9 까지의 배열 */
    _.take(3), /* 앞에서부터 3개만 자르기 */
    _.each(console.log)); // 0 1 2

_.go(
    L.range(10), /* 0 ~ 9 까지의 이터러블, 최대 10번 수행 */
    L.take(3), /* 최대 3개의 값이 필요하고, 최대 3번의 일을 수행 */
    _.each(console.log)); // 0 1 2

 

위 항목에서 range이후에 delay(1000)이 들어간다면, 즉시 평가의 경우 10초 정도 걸릴 것이며, 지연 평가의 경우엔 L.take에서 단 3개의 값 만들 필요하기 때문에, 3초 정도가 걸릴 것이다.

 

이 챕터에선 이와 같은 시간적인 측면을 다룰 것이며, 이어서 takeWhiletakeUntil을 알아보자. takeWhile이나 takeUntil은 좀 더 동적으로 일어나는 일을 효과적으로 제한하면서 다룰 수 있다.

 

takeWhile은 받아 오는 값이 true일 때까지만 받는 것이다.

 

/* false값을 만나면 더이상 받지않음 */
_.go(
    [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0],
    _.takeWhile(a => a),
    _.each(console.log)); // 1 2 3 4 5 6 7 8

/* 이후에 true값을 만나도 false전에 종료 */
_.go(
    [1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2],
    _.takeWhile(a => a),
    _.each(console.log)); // 1 2 3 4 5 6 7 8

 

takeUntil은 반대로 받아오는 값이 처음 만족하는 값일 때까지 받고 종료된다.

 

_.go(
    [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0],
    _.takeUntil(a => a),
    _.each(console.log)); // 1

_.go(
    [0, false, undefined, null, 10, 20, 30],
    _.takeUntil(a => a),
    _.each(console.log)); // 0 false undefined null 10

 

 


 

할 일들을 이터러블(리스트)로 바라보기

 

const track = [
  { cars: ['철수', '영희', '철희', '영수'] },
  { cars: ['하든', '커리', '듀란트', '탐슨'] },
  { cars: ['폴', '어빙', '릴라드', '맥컬럼'] },
  { cars: ['스파이더맨', '아이언맨'] },
  { cars: [] }
];

 

위 코드를 통해 만들어 볼 것은 자동차 경주이다.  "트랙 안에 각 조가 있으며, 각 조에 해당하는 사람들끼리 경쟁을 하는 것이고, 4명이 다 찬 조만 출발시키는다는 스케줄러"이다.

만약, 4조같은 경우엔 4명이 다 차지 않아서 출발을 시키지 않는다. 또 1조를 출발시킨 이후에 딜레이를 주고 2조를 출발시킨다.

 

위 내용을 통해 코드를 짜 보면 다음과 같이 나온다.

 

_.go(
   L.range(Infinity),
   L.map(i => track[i]),                 /* 각 조를 꺼냄 */
   L.map(({cars}) => cars),
   L.map(_.delay(2000)),                 /* 2초 딜레이 */
   L.takeWhile(({length: l}) => l == 4), /* 4명인 조만 출발시킴 */
   L.flat,                               /* 배열에서 값을 꺼내줌 */
   L.map(car => `${car} 출발!`),         /* 출발 문구 */
   _.each(console.log));
   
/*
(2초딜레이)
철수 출발!
영희 출발!
철희 출발!
영수 출발!
(2초딜레이)
하든 출발!
커리 출발!
듀란트 출발!
탐슨 출발!
(2초딜레이)
폴 출발!
어빙 출발!
릴라드 출발!
맥컬럼 출발! 
*/

 

만약 위코드에서, 4명이 다 찬 조가 아닌 4조까지만 출발을 시키려고 할 때는 takeUntil을 사용해서 나타내면 될 것이다.

 

_.go(
   L.range(Infinity),
   L.map(i => track[i]),                 /* 각 조를 꺼냄 */
   L.map(({cars}) => cars),
   L.map(_.delay(2000)),                 /* 2초 딜레이 */
   L.takeWhile(({length: l}) => l == 4), /* 4명이 아닌 조까지만 출발시킴 */
   L.flat,                               /* 배열에서 값을 꺼내줌 */
   L.map(car => `${car} 출발!`),         /* 출발 문구 */
   _.each(console.log));
   
/*
(2초딜레이)
철수 출발!
영희 출발!
철희 출발!
영수 출발!
(2초딜레이)
하든 출발!
커리 출발!
듀란트 출발!
탐슨 출발!
(2초딜레이)
폴 출발!
어빙 출발!
릴라드 출발!
맥컬럼 출발!
(2초딜레이)
스파이더맨 출발!
아이언맨 출발!
*/

 

 


 

 

#2 아임포트 결제 누락 처리 스케줄러

 

결제된 내역 가져오기

 

이터러블 프로그래밍으로 좀 더 실무적인 코드 중 결제 모듈을 붙이는 아임포트 서비스를 다뤄 볼 것이다. 그중 결제 누락이 된 후 처리하는 스케줄러를 볼 것이다.

 

const Impt = {
  payments: {
    1: [
      { imp_id: 11, order_id: 1, amount: 15000 },
      { imp_id: 12, order_id: 2, amount: 25000 },
      { imp_id: 13, order_id: 3, amount: 10000 }
    ],
    2: [
      { imp_id: 14, order_id: 4, amount: 25000 },
      { imp_id: 15, order_id: 5, amount: 45000 },
      { imp_id: 16, order_id: 6, amount: 15000 }
    ],
    3: [
      { imp_id: 17, order_id: 7, amount: 20000 },
      { imp_id: 18, order_id: 8, amount: 30000 }
    ],
    4: [],
    5: [],
    //...
  },
  getPayments: page => {
    console.log(`http://..?page=${page}`);
    return _.delay(1000 * 1, Impt.payments[page]);
  },
  cancelPayment: imp_id => Promise.resolve(`${imp_id}: 취소완료`)
};

const DB = {
  getOrders: ids => _.delay(100, [
    { id: 1 },
    { id: 3 },
    { id: 7 }
  ])
};

 

위 코드는 가상으로 작성된 코드이며, 최대 1page당 3개까지 값을 가져올 수 있으며, 3개보다 값이 적거나 없으면 더 이상 진행을 안 하는 식으로 동작이 된다.

imp_id는 결제 모듈 측 id이며, order_id는 가맹점에서 만들었던 id, amount는 가격이다.

 

getPayments는 해당하는 페이지에 요청을 하고 값을 꺼내오는 것이다.

 

Impt.getPayments(2).then(console.log)
/*
http://..?page=2
Promise {<pending>}
(딜레이 2초)
▼(3) [{…}, {…}, {…}]
 ▶0: {imp_id: 14, order_id: 4, amount: 25000}
 ▶1: {imp_id: 15, order_id: 5, amount: 45000}
 ▶2: {imp_id: 16, order_id: 6, amount: 15000}
   length: 3
 ▶__proto__: Array(0)
*/

 

cancelPayment는 승인된 결제내역을 취소하는 것으로 imp_id를 지정해 취소하면 실제론 사용자에게 문자도 가고, 신용카드도 취소도 되는 식으로 동작되는 식으로 동작이 된다.

 

Impt.cancelPayment(17) // Promise {<resolved>: "17: 취소완료"}

 

그리고 DB는 가맹점에서 사용하고 있는 DB모듈이다.

 

이제 결제된 결제 모듈 측 payments를 가져와서, 하나로 합쳐보자.

 

async function job(){
    const payments = _.go(
        L.range(1, Infinity), /* 언제까지 값을 꺼낼지 모르기때문에 Infinity */
        L.map(Impt.getPayments),
        L.takeUntil(({length}) => length < 3),
        _.each(console.log);
}

/*
http://..?page=1
▶(3) [{…}, {…}, {…}]
http://..?page=2
▶(3) [{…}, {…}, {…}]
http://..?page=3
▶(2) [{…}, {…}]
*/

 

위와 같이 나타낸다면, 결제 데이터가 있을 때마다 계속 출력하므로 한 번에 출력하기 위해선 아래처럼 해줘야 한다.

 

async function job(){
    // 결제된 결제모듈측 payments 가져온다.
    // page 단위로 가져오는데,
    // 결제 데이터가 있을 때까지 모두 가져와서 하나로 합친다.
    
    const payments = await _.go(
        L.range(1, Infinity),
        L.map(Impt.getPayments),
        L.takeUntil(({length}) => length < 3),
        _.flat);
    
    console.log(payments);
}

job();
/*
http://..?page=1
http://..?page=2
http://..?page=3
▼(8) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
 ▶0: {imp_id: 11, order_id: 1, amount: 15000}
 ▶1: {imp_id: 12, order_id: 2, amount: 25000}
 ▶2: {imp_id: 13, order_id: 3, amount: 10000}
 ▶3: {imp_id: 14, order_id: 4, amount: 25000}
 ▶4: {imp_id: 15, order_id: 5, amount: 45000}
 ▶5: {imp_id: 16, order_id: 6, amount: 15000}
 ▶6: {imp_id: 17, order_id: 7, amount: 20000}
 ▶7: {imp_id: 18, order_id: 8, amount: 30000}
   length: 8
 ▶__proto__: Array(0)
*/

 

 


 

가맹점의 DB의 주문서 가져오기

 

이번엔 가맹점 측에 있는 결제가 성공된 id값을 뽑아 볼 것이다.

 

async function job(){
    ...
    // 결제가 실제로 완료된 가맹점 측 주문서 id들을 뽑는다.
    
    const order_ids = await _.go(
        payments,
        _.map(p => p.order_id), /* order_id 뽑기 */
        DB.getOrders,           /* 실제로 결제된 id 반환 */
        _.map(({id}) => id));   /* id 뽑아내기*/

    console.log(order_ids);
}

job();
/*
http://..?page=1
http://..?page=2
http://..?page=3
(3) [1, 3, 7]
*/

 

 


 

비교 후 결제 취소 API 실행하기

 

payments와 order_Ids를 비교해서, 실제로 결제가 완료된 것만 남기고, 가맹점 측 id에서 알 수 없는 것들은 취소 API를 통해 취소를 하려고 한다. 즉, payments 중에 정말 결제가 실제로 된 것들을 걸러주게끔 하면 된다.

 

async function job(){
	...
    // 결제모듈의 payments와 가맹점의 주문서를 비교해서
    // 결제를 취소해야할 id들을 뽑아서
    // 결제 취소 api를 실행
    await _.go(
      payments,
      L.reject(p => order_ids.includes(p.order_id)),
      L.map(p => p.imp_id),
      L.map(Impt.cancelPayment),
      _.each(console.log));
}
/*
http://..?page=1
http://..?page=2
http://..?page=3
12: 취소완료
14: 취소완료
15: 취소완료
16: 취소완료
18: 취소완료
*/

 


 

스케줄러 반복 실행하기

 

스케줄러를 실행할 때 반복 실행이 되게 할 때 아래처럼 짤 수가 있다.

 

(function recur(){
   job().then(recur);
})();

 

하지만, 위와 같이 짠다면 너무 부하가 많이 걸릴 것이어서, delay를 통해 실행시간을 조정해준다.

 

// 7초에 한 번만 한다.
// 그런데 만일 job 7초보다 더 걸리면, job이 끝날 때까지
(function recur() {
  Promise.all([
    _.delay(7000, undefined),
    job()
  ]).then(recur);
}) ();

 

이렇게 짜면, job 작업이 7초 전에 끝나면 7초가 될 때까지 기다렸다가, 반복 실행이 되며, 또 job시간이 7초를 넘어섰다면, 그 작업이 끝나자마자 바로 반복실행이 된다.

 

전체 코드를 정리하자면 다음과 같이 나온다.

 

const Impt = {
  payments: {
    1: [
      { imp_id: 11, order_id: 1, amount: 15000 },
      { imp_id: 12, order_id: 2, amount: 25000 },
      { imp_id: 13, order_id: 3, amount: 10000 }
    ],
    2: [
      { imp_id: 14, order_id: 4, amount: 25000 },
      { imp_id: 15, order_id: 5, amount: 45000 },
      { imp_id: 16, order_id: 6, amount: 15000 }
    ],
    3: [
      { imp_id: 17, order_id: 7, amount: 20000 },
      { imp_id: 18, order_id: 8, amount: 30000 }
    ],
    4: [],
    5: [],
    //...
  },
  getPayments: page => {
    console.log(`http://..?page=${page}`);
    return _.delay(500 * 1, Impt.payments[page]);
  },
  cancelPayment: imp_id => Promise.resolve(`${imp_id}: 취소완료`)
};

const DB = {
  getOrders: ids => _.delay(100, [
    { id: 1 },
    { id: 3 },
    { id: 7 }
  ])
};

async function job(){
    const payments = await _.go(
        L.range(1, Infinity), /* 언제까지 값을 꺼낼지 모르기때문에 Infinity */
        L.map(Impt.getPayments),
        L.takeUntil(({length}) => length < 3),
        _.flat);
    
    const order_ids = await _.go(
        payments,
        _.map(p => p.order_id), /* order_id 뽑기 */
        DB.getOrders,           /* 실제로 결제된 id 반환 */
        _.map(({id}) => id));   /* id 뽑아내기*/


    await _.go(
      payments,
      L.reject(p => order_ids.includes(p.order_id)),
      L.map(p => p.imp_id),
      L.map(Impt.cancelPayment),
      _.each(console.log));
}

/* 반복실행 */
(function recur() {
  Promise.all([
    _.delay(7000, undefined),
    job()
  ]).then(recur);
}) ();