본문 바로가기

Web/JavaScript

함수형 자바스크립트 11. 실전코드조각2, 비동기

함수형 자바스크립트 10일차, 마지막 강의

9일차에 이은 실전 코드 조각과 비동기에 관한 내용이다.


이번엔 쇼핑몰에서 장바구니 정보를 조회하는 상황에 대한 예제를 살펴봤다.


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

var products = [

   {

       is_selected: true, // <--- 장바구니에서 체크 박스 선택

       name: "반팔티",

       price: 10000, // <--- 기본 가격

       sizes: [ // <--- 장바구니에 담은 동일 상품의 사이즈 별 수량과 가격

           { name: "L", quantity: 4, price: 0 },

           { name: "XL", quantity: 2, price: 0 },

           { name: "2XL", quantity: 3, price: 2000 } // <--- 옵션의 추가 가격

       ]

   },

   {

       is_selected: true,

       name: "후드티",

       price: 21000,

       sizes: [

           { name: "L", quantity: 2, price: -1000 },

           { name: "XL", quantity: 4, price: 2000 }

       ]

   },

   {

       is_selected: false,

       name: "맨투맨",

       price: 16000,

       sizes: [

           { name: "L", quantity: 10, price: 0 }

       ]

   },

];

Colored by Color Scripter

cs


데이터는 위와 같이 구성된다.

장바구니에 담은 상품이 존재하며, 해당 상품에는 선택 여부, 이름, 가격과 고객이 선택한 사이즈, 개수, 추가금액에 대한 정보가 존재한다.



1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

// 1. 모든 수량

var total_quantity = _.reduce(function(tq, product) {

   return _.reduce(product.sizes, function(tq, size) {

       return tq + size.quantity;

   }, tq);

}, 0);

_.go(products,

   total_quantity,

   console.log

);

// 2. 선택 된 총 수량

/* 1번의 total_quantity를 함수로 빼서 조합하면서 사용 */

_.go(products,

   _.filter(_get('is_selected')),

   total_quantity,

   console.log

);

// 3. 모든 가격

var total_price = _.reduce(function(tp, product) {

   return _.reduce(product.sizes, function(tp, size) {

       return tp + (product.price + size.price) * size.quantity;

   }, tp);

}, 0);

_.go(products,

   total_price,

   console.log

);

// 4. 선택 된 총 가격

_.go(products,

   _.filter(_get('is_selected')),

   total_price,

   console.log

);

Colored by Color Scripter

cs


이러한 데이터를 기반으로 4개의 문제를 풀어나간다.

  1. 장바구니의 모든 품목의 수량의 합 구하기

    1. 장바구니의 품목을 순회한다.

    2. 각 품목 내부에 존재하는 사이즈정보의 개수만큼 순회한다.

    3. 각 사이즈정보의 quantity 속성의 값을 누적한다.

  2. 장바구니에서 선택된 품목의 수량의 합 구하기

    1. 장바구니의 품목을 순회한다.

    2. 각 품목의 is_selected 속성이 true인 값을 필터링한다.

    3. 필터링 된 품목 내부에 존재하는 사이즈정보의 개수만큼 순회한다.

    4. 각 사이즈정보의 quantity 속성의 값을 누적한다.

  3. 장바구니의 모든 품목의 가격의 합 구하기

    1. 장바구니의 품목을 순회한다.

    2. 각 품목 내부에 존재하는 사이즈정보의 개수만큼 순회한다.

    3. (품목의 기본 가격 + 사이즈 추가금액)을 계산한 뒤 사이즈 수량을 곱한 값을 누적한다.

  4. 장바구니에서 선택된 품목의 수량의 합 구하기

    1. 장바구니의 품목을 순회한다.

    2. 각 품목의 is_selected 속성이 true인 값을 필터링한다.

    3. 필터링 된 품목 내부에 존재하는 사이즈정보의 개수만큼 순회한다.

    4. (품목의 기본 가격 + 사이즈 추가금액)을 계산한 뒤 사이즈 수량을 곱한 값을 누적한다.


맨 처음 1번 문제를 해결한 코드는 다음과 같았다.


1

2

3

4

5

6

7

8

9

10

_.go(

products,

_.reduce(function(tq, product) {

   return _.reduce(product.sizes, function(tq, size) {

       return tq + size.quantity;

   }, tq);

}, 0),

   console.log

);

Colored by Color Scripter

cs


1번 문제와 2번 문제를 살펴보면, 필터링을 하는 부분을 제외하고는 모두 같은 로직이다.

수량을 누적하는 reduce를 함수로 만들어 2번 문제에서 재활용을 하고, 코드를 좀 더 보기 좋게 만들었다.

이와 같이 데이터를 어떻게 순회할 것인지에 대한 명령적 코드를 지우고 함수를 나열하게 되면,

주어진 과제에 집중 할 수 있으며 문제 해결이 좀 더 쉬워진다.

3번 문제와 4번 문제도 마찬가지로, 금액을 누적하는 reduce를 함수로 만들어 명령적 코드를 감췄다.


값을 누적하는 부분을 reduce가 아닌 반복문으로 구현하면 어떻게 될까?

for each를 이용하는 것도 꽤나 단순한 모양이 될 거 같긴 하다.

하지만, reduce보다 명령적인 코드가 많이 드러나게 되는 부분은 어쩔 수 없을 것 같다.


1

2

3

4

5

6

7

8

9

10

var total_quantity = function() {

   var tq = 0;

   for ( i in products ) {

       for ( j in products[i].sizes ) {

           var sizes = products[i].sizes[j];

           tq += sizes.quantity;

       }

   }    

   return tq;

}

Colored by Color Scripter

cs


그럼 위와 같은 for문의 문제는 무엇일까?
이전에 for문의 문제는 상태 변이와 부수효과를 조장한다는 웹서핑 결과를 기록했었다.

for 루프는 상태 변이와 부수효과을 조장합니다. 그 결과  각종 버그와 예측할 수없는 코드의 잠재적인 원인이 되곤 합니다. 우리는 모두 전역 상태가 나쁘다는 말을 들었습니다. (지역상태도 전역상태와 같이 악을 공유하고 있지만, 규모가 작기 때문에 큰 문제가 되진 않았습니다.) 하지만 우리는 실제로 문제를 해결하지 못했으며 단지 그것을 최소화 하는데 주력 하였지요.

가변적인 상태에서 - 알 수 없는 특정 시점에 - 변수가 알려지지 않은 이유로 - "변경" 되어, 그  값이 변경된 이유를 디버깅하고 검색하는 데 몇 시간을 소비하게됩니다. 뭐 머리 좀 쥐어 뜯으면 되겠지요. 하지만 머지않아..머리카락이 그리워질 거에요.

출처: http://hamait.tistory.com/889 [HAMA 블로그]


그러니까, 결국 비동기처럼 함수가 어느시점에 평가될 지 알 수 없는 상황에서 products나 다른 변수들이 변경되면, 추후 개발자는 어디에서 문제가 생겼는지 발견하기가 어렵다는 것이 가장 큰 문제인 것 같다. (const키워드를 사용한다 해도 products와 같은 배열이 참조하고 있는 대상을 변경할 수 있다)

나는 아직 비동기를 다루는 코드를 작성해 보질 못해서 확 와닿지가 않는다.

물론 reduce는 고차 함수, 함수형 함수로서, 부수효과를 제어하여 비동기 상황에서 많은 이점을 가져다 줄 것이다.

단지 reduce라는 의미가 명확한 함수를 사용한 것, 코드의 길이가 줄었다는 것, i와 j같은 인덱스용 변수의 사용이 사라졌다는 점이 그나마 나에게 와닿는 장점인 것 같다.


다시 강의 내용으로, 이번엔 비동기에 대한 내용이다.


1

2

3

4

console.log(1);

console.log(2);

console.log(3);

console.log(4);

cs

위와 같은 코드를 실행하면 콘솔에는 당연히 1 -> 2 -> 3 -> 4 순서로 출력될 것이다.

비동기는 위와 달리 위에서부터 아래로 평가되는 것이 아니라 1 -> 3 -> 2 -> ....와 같이 순서와 상관없이 동작한다.


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

function square(a) {

   return new Promise(function(resolve) {

       setTimeout(function() {

           resolve(a * a);

       }, 500);

   });

}

console.clear();

/* 위에서부터 아래로 평가되는 것이 아니라 1 -> 3 -> 2 -> res처럼 순서와 상관없이 동작하는 것이 비동기 */

console.log(1);

square(10).then(function(res) {

   console.log(2);

   console.log( res );

});

console.log(3);

Colored by Color Scripter

cs


square는 파라미터로 받은 변수의 제곱을 0.5초 뒤에 반환하는 함수로,

위 코드의 결과는 1 -> 3 -> 2 -> 100 가 출력 될 것이다.


이러한 상황에서 개발자가 순서를 제어하기 위해서는 많은 노력이 소요되며, 일반 콜백함수만으로는 관리가 어렵다.

자바스크립트는 이러한 상황을 Promise 객체를 이용해서 일정부분 해결한다.

Promise 객체로 비동기를 제어하면 콜백을 만나지 않고 아래와 같이 간결한 코드를 만들 수 있다.


1

2

3

4

5

square(10)

   .then(square)

   .then(square)

   .then(square)

   .then(console.log);

cs


Promise는 마치 go와 같이 써내려갈 수 있다.

하지만, square와 같은 함수가 Promise 객체가 아닌 즉시 값을 반환하는 함수라면,

Promise 객체에 속해있는 then 메소드를 사용할 수 없게된다.

즉, Promise는 동기상황에서는 사용할 수 없는 객체가 된다.

비동기와 동기 상황에서 모두 사용하기 위해서는 단순히 go 함수를 사용하면 된다.

go 함수는 함수를 받아서 내부적으로 원하는 시점에 평가할 수 있도록 하는 함수이며, Promise와 동일한 비동기제어와 표현련의 이점을 가질 수 있다.

go 함수가 Promise 객체보다 나은 이점은 square가 즉시 값을 반환하는 함수여도 문제 없이 수행된다. 즉, 비동기 뿐 아니라 동기 상황에서도 사용할 수 있다.


1

2

3

4

5

_.go(square(10),

   square,

   square,

   square,

   console.log);

cs


코드를 보면 알겠지만, Promise 객체의 then을 써내려가는 것이나, go 함수에 결과를 전달해 나가는 것이나 표현력의 차이는 거의 없다고 봐도 무방한 것 같다.


함수가 평가되는 시점을 함수가 받아서 다루게 되면, 동기 코드나 비동기 코드나 차이가 없이 동작시킬 수 있게 된다. Promise 객체를 생성할 때 내부 구현이 복잡해서 제어가 힘든 경우가 있다.


1

2

3

4

5

6

7

8

9

10

11

var list = [2, 3, 4];

new Promise(function(resolve) {

   (function recur(res) {

       if ( list.length == res.length ) return resolve(res);

       square(list[res.length]).then(function(val) {

           res.push(val);

           recur(res);

       });

   })([]);

}).then(console.log);

Colored by Color Scripter

cs


list의 요소를 순환하면서 각각 요소를 square 함수로 평가한다.

익명 함수와 재귀를 이용해서 해결하며, 읽기 어려운 코드가 되었다.


위와 같이 특정한 일을 하기 위해서 항상 필요한 로직을 구현하기 보다,

이미 잘 만들어져있는 함수형 함수나 고차 함수를 이용해 프로그래밍 하는 것이 훨씬 간결하고 쉽다.

만약, 위와 같은 비동기를 제어하는 고차함수가 준비되어잇다면 위와 동일한 로직을 훨씬 간결하게 표현할 수 있다.

결론은, go와 map 함수를 이용하면 위와 같은 상황을 훨씬 쉽게 제어할 수 있다.


1

2

3

4

5

6

_.go(list,

   _.map(square),

   _.map(square),

   _.map(square),

   console.log

);

cs

익명함수니, 재귀니 하는 구현은 눈을 씻고 찾아봐도 볼 수 없다.

역시 매번 무언가를 만들기보다 기존에 잘 만들어져 있는 것들을 조합하면서 만드는 쪽이 가독성과 품질 향상에 큰 도움을 주는 것 같다.

자바에서 JSON과 같은 데이터를 받아 엑셀을 만들 때, 프로젝트마다 매번 새로운 인터페이스를 만든다던가 하는 일을 굉장히 비효율적이다. POI를 사용하면 간단한 코드로 엑셀 파일을 만들 수 있고, 매번 새로운 버전이 릴리즈되면서 더욱 탄탄해지는 이점을 공짜로 얻을 수 있다.