-
함수형 프로그래밍(FP) - 액션에서 계산 빼내기CS지식/함수형 프로그래밍 2024. 3. 27. 12:10
액션에서 계산 빼내기
MeaMart 예시
항상 가득 차있는 장바구니
MegaMart는 온라인 쇼핑몰이다. 경쟁력을 유지하고 있는 중요한 기능 중 하나는 쇼핑 중 장바구니에 담겨 있는 제품의 금액 합계를 볼 수 있는 기능이다.
//장바구니 제품과 금액 합계를 //담고 있는 전역변수 let shoppingCart = []; let shppingCartTotal = 0; function addItemToCart(name, price){ shopingCart.push({ name, price }); calcCartTotal(); } function calcCartTotal(){ shoppingCartToal = 0; for(const sc of shoppingCart){ shoppinCartTotal += sc.price; } //금액 합계를 반영하기 위해 DOM 업데이트 setCartTotalDom(); }
새로운 요구사항
MegaMart는 구매 합계가 20달러를 초과하면 무료 배송을 해주려고 합니다.그래서 현재 장바구니에 넣을시 20달러가 넘게 되는 제품의 구매 버튼 옆에 무료 배송 아이콘을 표시해 주려고한다.
절차적인 방법으로 구현하기
구매 버튼에 무료 배송 아이콘을 표시하기 위한 함수를 만든다. 우선 절차적인 방법으로 코드를 작성하지만 뒤에서 함수형 방식으로 리팩토링한다.
function updateShippingIcons(){ //페이지에 있는 모든 구매 버튼을 //가져와 반복문을 적용한다. const buyButtons = getBuyButtonDom(); for(const bb of buyButtons){ //무료 배송이 가능한지 확인한다. if(bb.item.price + shoppingCartTotal >= 20) bb.showFreeShippingIcon(); else bb.hideFreeShippingIcon(); } }
합계 금액이 바뀔 때마다 모든 아이콘을 업데이트하기 위해 calcCartTotal()함수 마지막에 updateShippingIcon()함수를 불러준다.
function calcCartTotal(){ shoppingCartToal = 0; for(const sc of shoppingCart){ shoppinCartTotal += sc.price; } //금액 합계를 반영하기 위해 DOM 업데이트 setCartTotalDom(); updateShippingIcons(); }
세금 계산하기
장바구니의 금액 합계가 바뀔 때마다 세금을 다시 계산해야 한다. 학습을 위해 함수형 프로그래밍을 적용하지 않고 이 기능을 구현해보자.
//세금 값을 변경하는 함수를 만든다. function updateTaxDom(){ //금액 합계에 10%곱하고, DOM을 업데이트 한다. setTaxDom(shoppingCartTotal * 0.10); }
앞에서 한 것처럼 calCartTotal() 함수 마지막에 새로 만든 함수를 불러준다.
function calcCartTotal(){ shoppingCartToal = 0; for(const sc of shoppingCart){ shoppinCartTotal += sc.price; } //금액 합계를 반영하기 위해 DOM 업데이트 setCartTotalDom(); updateShippingIcons(); update_tax_dom(); }
현재코드의 테스트 용이성
지금 코드는 비지니스 규칙을 테스트하기 어렵다.
구체적으로 왜 지금의 코드는 테스트하기 여려운지 조목조목 살펴보자
- 브라우저 설정하기
- 페이지 로드하기
- 장바구니 제품 담기 버튼 클릭
- DOM 업데이트 될 때까지 기다리기
- DOM에서 값가져오기
- 가져온 문자열 값을 숫자로 바꾸기
- 예상하는 값과 비교하기
현재 코드 설명
테스트를 쉽게 할려면 필요한 조건
- DOM 업데이트와 비지니스 규칙은 분리되어야 한다.
- 전역변수가 없어야 한다.
현재코드의 재사용 용이성
결제팀과 배송팀이 현재작성한 코드를 사용할려고한다.
결제팀과 배송팀에서 현재 만든 코드를 재사용하려고 했지만, 다음과 같은 이유로 재사용할 수 없었다.
- 장바구니 정보를 전역변수에서 읽어오고 있지만, 결제팀과 배송팀은 데이터베이스에서 장바구니 정보를 읽어 와야한다.
- 결과를 보여주기 위해 DOM을 직접 바꾸고 있지만, 결제팀은 영수증을, 배송팀은 운송장을 출력해야한다.
코드설명
- 결제팀과 배송팀에서 이 비지니스 규칙을 사용하려고 한다. (>=20)
- 위 함수는 전역변수인 shoppingCartTotal값이 있어야 실행할 수 있다.
- buyButtons.showFreeShippingIcon()은DOM이 있어야 실행할 수 있다.
- return 값이 없기 때문에 결과를 받을 방법이 없다.
개선점 해야하는 점 살펴보기
- 전역변수에 의존하지 않아야 한다.
- DOM을 사용할 수 있는 곳에서 실행된다고 가정하면 안 된다.
- 함수가 결과값을 리턴해야한다.
//변경 가능한 전역변수는 액션이다. let shoppingCart = []; //A let shppingCartTotal = 0; //A function addItemToCart(name, price){ //A //전역변수를 바꾸는 것은 액션이다. shopingCart.push({ name, price }); calcCartTotal(); } function updateShippingIcons(){ //A //DOM에서 읽는 것은 액션이다. const buyButtons = getBuyButtonDom(); for(const bb of buyButtons){ //DOM을 바꾸는 것은 액션이다. if(bb.item.price + shoppingCartTotal >= 20) bb.showFreeShippingIcon(); else bb.hideFreeShippingIcon(); } } function updateTaxDom(){ //A //DOM을 바꾸는 것은 액션이다. setTaxDom(shoppingCartTotal * 0.10); } function calcCartTotal(){ //A //전역변수를 바꾸는 것은 액션이다. shoppingCartTotal = 0; for(const sc of shoppingCart){ shoppinCartTotal += sc.price; } setCartTotalDom(); updateShippingIcons(); updateTaxDom(); }
현재의 코드는 계산이나 데이터가 없고 모든 코드가 액션이다. 이제 이 코드를 함수형 프로그래밍을 사용해서 어떻게 개선 시킬수 있는지 알아보자.
함수에는 입력과 출력이 존재
모든 함수는 입력과 출력이 있다. 입력은 함수가 계산을 하기 위한 외부 정보이다. 출력은 함수 밖으로 나오는 정보나 어떤 동작이다. 함수를 부르는 이유는 결과가 필요하기 때문이다. 그리고 원하는 결과를 얻으려면 입력이 필요하다.
입력과 출력 예제
let total = 0; //인자는 입력이다. function addToTotal(amount){ //전역변수를 읽는 것은 입력이다. //콘솔에 뭔가를 찍는 것은 출력이다. console.log("Old totla: " + total); //전역변수를 바꾸는 것은 출력이다. total += amount; //리턴값은 출력이다. return total; }
입력과 출력은 명시적이거나 암묵적일 수 있다.
파라미터는 명시적인 입력이다. 그리고 return값은 명시적인 출력이다. 하지만 암묵적으로 함수로 들어가거나 나오는 정보도 있다.
let total = 0; function addToTotal(amount){ console.log("Old totla: " + total); total += amount; return total; }
- 인자는 명시적 입력이다.
- 전역변수를 읽는 것은 암묵적 입력이다.
- 콘솔에 찍는 것은 암묵적 출력이다.
- 전역변수를 바꾸는 것도 암묵적 출력이다.
- return 값은 명시적 출력이다.
함수에 암묵적 입력과 출력이 있으면 액션
함수에서 암묵적 입력과 출력을 없애면 계산이 된다. 암묵적 입력은 함수의 임자로 바꾸고, 암묵적 출력은 함수의 return 값으로 바꾸면 된다.
현재코드 개선 방안
- DOM 업데이트와 비즈니스 규칙은 분리되어 한다.
- DOM을 업데이트 하는것은 암묵적 출력이다.
- 사용자가 정보를 볼 수 있어야 하기 때문에 DOM 업데이트를 어디선가 해야한다.
- 전역변수가 없어야 한다.
- 전역변수를 읽은 것은 암묵적인 입력이다.
- DOM을 사용할 수 있는 곳이라고 해서 바로 실행하지 않아도 된다.
- 현재 함수는 DOM을 직접 사용하고 있다.
- DOM을 직접사용하면 암묵적 출력이 되므로 명시적 출력으로 바꾼다.
서브루틴 추출하기(extract subroutine)
calcCartTotal함수 액션에서 계산 빼내기
암묵적 입력값과 출력값을 명시적으로 바꾸기위해 액션코드에서 계산하는 코드를 분리한다.
바꾼코드
funcfion calcCartTotal(){ calcTotal(); setCartTotalDom(); updateShippingIcons(); updateTaxDom(); } function calcTotal(){ shopping_cart_total = 0; for(const item of shoppingCart){ shoppingCartTotal += item.price; } }
빼낸 코드를 새로운 함수로 만들고 이름을 붙여줬다. 그리고 원래 코드에서 빼낸 부분은 새로 만든 함수를 호출하도록 고쳤다. 하지만 분리해서 새로만든 함수역시 아직 액션이다. 계속해서 새 함수를 계산으로 바꿔가자.
방금한 리팩토링은 서브루틴 추출하기(extract subroutine)라고 할 수 있다. 기존 코드에서 동작은 바뀌지 않는다.
새로 만든 함수는 아직 액션익 때문에 계산으로 바꿔야 한다. 이 함수에는 출력 두 개와 입력 하나가 있다.shoppingCartTotal 전역변수를 익어오는 것은 입력이고, 해당 전역변수를 바꾸는 건 출려이다. 이 암시적인 입력과 출력을 모두 명시적으로 바꿔보자.
암묵적 출력을 없앤 코드
function calcCartTotal(){ //리턴값을 받아 전역변수에 할당한다. shoppingCartTotal = calcTotal(); setCartTotalDom(); updateShippingIcons(); updateTaxDom(); } function calcTotal(){ //지역변수로 바꾼다. let total = 0; for(const item as shoppingCart){ total += item.price; } //지역변수를 리턴한다. return total; }
암묵적 입력을 없앤 코드
function calcCartTotal(){ shoppingCartTotal = calcTotal(shoppingCart); setChartTotalDom(); updateShippingIcons(); updateTaxDom(); } function calcTotal(cart){ let total = 0; //전역벼수 대신 인자를 만들어 사용한다. for(const item of cart){ total += item.price; } return total; }
calcTotal()함수는 이제 계산이다. 모든 입력은 인자로 바꾸었고 모든 출력값은 return으로 반환된다.
addItemToCart 함수 액션에서 계산빼내기
바뀐 코드
function addItemToCart(name, price){ //입력:전역변수를 인자로 넘긴다. //출력: 원래 함수에서 return을 받아 전역변수에 할당 addItem(shoppingCart,name,price); calcCartTotal(); } //인자를 추가한다. function addItem(cart, name, price){ //입력:전역변수 대신 인자를 사용하도록 한다. //출력: 복사본을 만들어 지역변수에 할당한다. let newCart = cart.slice(); //입력:복사본을 변경한다. newCart.push({ name, price }); //출력:복사본을 return한다. return newCart; }
변경전 코드의 존재했던 암묵적 출력은 shoppingCart에 있는 배열은 요소를 추가하는 부분이다. 이 값을 바꾸는 대신 복사본을 만들고 복사본에 추가해 return 해야한다.
복사본을 만들고 복사본에 제품을 추가해서 return했다. 그리고 호출하는 코드에서는 리턴값을 받아 전역변수에 할당한다.
계산으로 바꾸는 작업이 끝났다. addItem() 함수는 암묵적 입력이나 축력이 없는 계산이다.
🤔 생각해보기
복사본을 만들어서 인자 cart 값을 변경하지 않아도 되었다. 만약 인자로 전달한 값을 직접 변경한다면 그 함수는 계산일까? 계산이 아니라면 왜 아닌것일까?[출처- 쏙쏙 들어오는 함수형 코딩, 저 에릭 노먼드]
'CS지식 > 함수형 프로그래밍' 카테고리의 다른 글
함수형 프로그램밍(FP) - 더 좋은 액션 만들기1(설계고민, 암묵적 입출력 줄이기) (0) 2024.03.27 함수형 프로그래밍(FP) - 액션과 계산, 데이터 (0) 2024.03.26 함수형 프로그래밍(FP) - 현실에서 함수형 사고 (2) 2024.03.26 함수형 프로그래밍(FP) - FP 프롤로그 (0) 2024.03.25 함수형 프로그래밍 - 평가와 일급, 고차함수, 리스트 순회 (0) 2024.03.25