함수형 프로그래밍에 대한 탐구

UI SEOK YU·2023년 5월 18일
1

JavaScript

목록 보기
5/6
post-thumbnail

액션과 계산, 데이터


액션 / 계산 / 데이터

  • 액션 : 실행값이나 횟수에 의존

  • 계산 : 같은 입력 값에는 같은 출력

  • 데이터 : 이벤트에 대한 사실. 영구적

  • 액션을 포함한 함수는 액션이다.
    => 여러개로 중첩되거나 연달하 호출하는 함수안에 단 1개의 액션만 있더라도 그 모든 함수는 액션이 된다.

암묵적 입출력 제거

  • 입력과 출력은 암묵적일 수 있다.

  • 함수에 암묵적 입출력이 있으면 액션이 된다.

  • 함수에서 암묵적 입출력을 제거하면 계산이 된다.

  • 서브루틴 추출하기 : 코드의 일부를 별도의 서브루틴(또는 메서드)으로 분리.
    원래 코드에서는 분리한 메서드를 호출하여 사용

  • 공유변수(전역변수 등)를 포함한 암묵적 입력은 인자로, 암묵적출력은 리턴값으로 변경하여 추상화

  • 액션의 인자와 출력을 명시적으로 하더라도, 해당 인자들이 요구사항에 잘 부합하는지 (결과가 맞아 떨어지도록 만드는 것이 아니라, 의미적으로 흐름이 맞는지)

코드의 정리

  • 과한 함수의 분리?? 기준이 뭘까? 다른 함수에서 재사용 될 가능성?
    그렇다면 액션에서 계산을 분리하여 함수로 만드는 것은 다른 곳에서 재사용될 가능성이 높은 것들로 추출해야 할 듯. 코드가 길어보인다고 해서, 액션에 계산이 포함되어 있다고 해서 무작정 분리하는 건 좋지 못할 듯.

  • 의미있는 계층
    관심사에 따른 분리 ( cart, item, user ...)
    자신과 연관되어 있는 걸로만 구성하고, 나머지는 명시적 입력과 출력에 의해서.

  • 유틸리티 함수 생성
    특정 데이터명으로 쓰이는 것이 아니라 일반적인 네이밍으로 하여 도구로서 가치를 가지도록 함. ( add_item(cart,item) --> add_element(arr,el) )

  • 비즈니스 로직 : 내 서비스에서 운영되는 특별한 규칙. 정책이나 계획수립에 따라 회사마다 운영되는 논리적 흐름을 일컫는 듯.

  • 설계 : 엉커있는 것을 푼다. 함수가 하나의 일만 하도록 작성하면 개념을 중심으로 쉽게 구성할 수 있다. ( 클린코드에서도 강조한 내용이었다. )


불변성 유지

카피 온 라이트

  • 리턴값이 있다 == 읽기동작 == 불변성

  • '쓰는 동작' 은 이미 존재하는 데이터에 작성하는 것

  • 카피온 라이트는 이미 존재하는 데이터를 '읽고' 복사하여, 새로운 저장소에 초기화 함.

  • 즉, 쓰는 동작 (== 액션) 에서 읽기 (==계산)으로 변경

  • (121p) 장바구니에 물건을 추가하는 동작이 있을 때,
    장바구니 배열에 아이템을 push 하는 것이 아니라,
    장바구니 배열을 복사하여, 새로운 배열에 push하고 그 새로운 배열을 원래 배열을 가리키던 변수에 대입하는 것이다. (즉 새로운 배열의 주소로 변경한다)
    최종상태는 장바구니는 새로운 배열을 가리키고, 원래의 배열도 그대로 남아 있다.

  • shift() 와 같이 (1)원본배열을 변경하며, (2)첫번째 요소를 반환 하는 메소드는 현재 읽기와 쓰기가 결합되어 있다. 리턴값이 있으므로, 이미 이 메소드는 "읽기" 작업이다. => 배열을 복사하여 활용하는 함수를 만들고, 첫번재 값이 리턴하거나 복사한 배열 중 첫번째 값이 없어진 새로운 배열을 리턴하는 함수를 각각 만들어 분리하여 사용한다.

  • 액션 : 변경 가능한 값을 읽는 것 ( 고정적이지 않는 데이터..)

  • 계산 : 변경 불가능한 값을 읽는 것

  • 서비스를 개발하다보면 예측 불가능한 요소들이 많으므로, 불변데이터 구조로 작성하되, 속도가 느린부분에 대해서만 최적화를 하는게 낫다. 또한 불변데이터 구조를 취한다 하더라도, 얕은 복사가 대부분이므로 주소값만 복사되어 걱정보다 많이 메모리를 차지하지 않는다.메모리 관리를 하는 가비지 컬렉터도 성능이 좋으므로 불변데이터는 전체적인 서비스성능을 저하시키지 않는다.


function setPriceByName (cart, name, price){
  var cartCopy = cart.slice();  //---------------------------①
  for (var i = 0 ; i < cartCopy.length ; i++){
    if (cartCopy[i].name === name){
		cartCopy[i] = setPrice(cartCopy[i], price)  //-------②
    }
  }
  return cartCopy;
}

function setPrice(item, new_price){
  return objectSet(item, "price", new_price)
}

function objectSet(object, key, value){
  var new_obj = Object.assign({}, object);
  copy[key] = value    //-----------------------------------②
  return copy
}
  • 배열에 들어있는 객체 데이터를 수정하려고 할 때 :
    1. 배열을 불변데이터로 다루어야 함. ---①
    2. 객체를 불변데이터로 다루어야 함. ---②
  • 최하위부터 최상위까지 중첩된 데이터의 구조의 모든 부분이 불변형이어야 불변형 데이터가 된다.

방어적 복사

카피-온-라이트의 한계

  • 모든 코드는 카피-온-라이트 형태가 아니다. 즉 불변성을 지키는 코드가 아니다.
  • 필연적으로 불변성을 깨는 함수와 만나게 된다.
  • 하지만 (현실적으로) 모든 함수를 카피 온 라이트로 만들 수 없다.
  • 함수를 불변성을 깨지 않도록 만들 지 못한다면
    그 함수에 주는 값과 받는 값이 불변성을 깨지 않는 방법을 쓴다.

방어적 복사

  • 그 함수에 데이터를 줄 때, 깊은 복사해서 줌
    ( 주소를 알아서 영향을 주지 못하게 )
  • 그 함수로부터 데이터를 받을 때, 깊은 복사해서 따로 보관함
    ( 같은 주소로 인해 그 데이터가 바뀔 때 영향을 받지 않도록 )
functionadd_item_to_cart(name, price){
  ...
  var cart_copy = deepCopy(shopping_cart);  // -----①
  black_friday_promotion(cart_copy)         // -----②
  shopping_cart = deepCopy(cart_copy);      // -----③
}

① 레거시 코드인 black_friday_promotion 함수에 데이터를 넘기기 전, 데이터의 주소를 줘버리면 내것도 어떻게 바뀔 지 모르므로, 깊은 복사해서 사본을 줌.
black_friday_promotion는 데이터를 받았지만,
functionadd_item_to_cart가 가진 원래의 shopping_cart의 주소는 알 수 없음.
③ 아무튼 복사받은 데이터가 함수를 통과해서 다시 돌아왔을 때, 그걸 그대로 쓰면 그 데이터의 주소가 외부에 공유되었기 때문에 언제 어떻게 바뀔지 모르므로, 값 자체만 가져다 쓸 수 있도록 깊은 복사해서 저장함.


계층형 설계

직접구현 패턴

  • 함수들을 역할과 기능에 따라 계층을 나누어서, 각 계층이 바로 아래층에만 의존하도록 만들면, 전체적인 시스템의 구조가 단순해 진다.
  • 프로그램의 구조를 계층화 하여, 추상화 레벨 별로 층을 나눔.
  • 각 계층의 목적은 각 계층에 있는 함수들의 목적과 같다.
  • 각 층에 구성된 함수는 바로 아래층의 함수로'만' 구현이 되어 있어야 함.
  • 한 함수가 여러 계층의 함수에 의존할 경우, 코드의 명확성이 떨어짐.
  • 함수가 더 구체적인 내용을 다루지 않도록 함수를 일반적인 함수로 빼낸다.
  • 3단계 줌 레벨에서 보면, 전역-계층-함수 단위로 볼 수 있고, 함수 단위에서 한 함수가 하나의 계층에 있는 동작을 사용하는지 확인한다.
  • 어떤 함수가 어떤 계층에 있어야 하는지 판단할 때, 사용하는 함수들이 어느 계층에 있는지, 비슷한 역할을 함수가 어떤 계층에 있는 지 참고한다.

추상화 벽 패턴

  • 구체적 구현보단 역할과 기능을 통한 설계
  • 예를들면 자바스크립트 같은 언어는 기계어로 컴파일 되지만, 실제로 사용하는 사람들은 자바스크립트가 어떻게 기계어를 다루는 지 몰라도 된다. 따라서 프로그래머에겐 자바스크립트와 같은 언어가 추상화의 벽 이다.
  • API나 라이브러리와 같은 것들도 내부적으로 어떻게 동작하는지 몰라도 사용할 수 있다. 이 또한 추상화의 벽이다.
  • 계층형 설계에서 위의 계층이 아래계층의 동작방식이 어떤 형태로 이루어지는지 몰라도 되도록 추상화 한 것이 추상화의 벽이다.
  • 따라서 추상화 벽을 구성하는 함수는 그 위 계층의 함수에게 인터페이스이다.
  • 추상화의 벽을 사용하면 해결하려는 문제의 구체적인 부분을 무시할 수 있다. 이를 통해 코드의 실수를 줄이고, 지치지 않을 수 있다.

작은 인터페이스 패턴

  • 상위 계층의 함수를 만들 때, 가능한 현재 계층에 있는 함수들로 구현한다.
  • 그러기 위해서는 바로 아래층의 추상화의 벽에 있는 함수들은 (인터페이스들은) 작게분리되어 있어야 동작에 대한 선택이 가능하다.
  • 그리고 인터페이스를 자꾸 새로 만들지 말고 있는 걸 최대한 활용해야 한다.

편리한 계층패턴

  • 이상적인 설계는 사실상 어렵다. 현실과 이상 사이에서 절충해야한다.
  • 코드의 유지보수에 어려움이 없고, 편리하다면 어느정도 이상적인 코드이다.
  • 아키텍쳐와 패턴을 사용하는 목적은 효율적으로 개발하기 위함임을 잊지말자.
  • 유지보수성 : 아래에 있는 함수일 수록 이 함수에 의존하는 함수가 많아 수정하기 어렵다. 즉 자주바뀌는 코드일 수록 위쪽 계층으로 간다.
  • 테스트 가능성 : 아래쪽에 있는 함수는 기반이 되는 함수들이기 때문에 거의 바뀌지 않는다. 따라서 아래쪽에 있는 함수를 테스트 하는 것이 비용대비 얻는 게 많다.
  • 재사용성 : 아래쪽에 있는 함수는 다른 함수의 기반이 될 수 있는 함수들이다. 낮은수준의 단계는 재사용성이 높다.

일급 추상

일급함수

냄새나는 코드

  • 함수 구현이 거의 똑같다
  • 함수 이름에서 다른부분이 함수 본문에서 다른부분과 일치한다.
  • 즉 특정 부분만 다르고 나머지 다 똑같다. 겹치는 부분이 많다.

리팩토링 : 암묵적 인자 -> 명시적 인자

  • 일급 : 값. 인자로 다룰 수 있는 대상
  • 일급 함수 : 함수'자체'를 일급으로 다룰 수 있다는 말. 자바스크립트에서는 '함수를 실행시킨 값'과 '함수 자체'를 평가하여 '값'으로 다룰 수 있어 함수를 일급함수라고 한다. 반면에 어떤 다른언어에서는 '함수를 실행시킨 값'만 값으로 여기고, '함수 자체'를 값으로 쓸 수 없는 경우가 있다. 이런 경우는 함수가 일급이 아니다.
  • 자바스크립트를 기준으로, 일급이 아닌 것들을 찾아보면 +, * 와 같은 연산자와, if, for 과 같은 문법적 요소들도 일급이 아니다. 즉 그 자체로 값으로 사용될 수 없다.
  • 냄새나는 코드를 암묵적 인자-> 명시적 인자로 바꾸어 리팩토링 한다는 것은, 내부에 구현된 구체적 요소를 밖으로 빼어 인풋으로 만드는 것을 말한다.
    암묵적 인자는 함수내부에 인풋이 아닌 미리 박혀있는 '일급 값' 이라고 생각하면 된다. 보통은 그 함수의 구체적인 기능에 관련이 되어 있기 때문에 함수의 이름과 관련 지어두었을 가능성이 크다. 따라서 이 미리 박혀있는 요소를 빼어 그것을 인풋값으로 받아두도록 하면 (일급이므로 인자로 받을 수 있다. JS에서는 함수도 일급함수이므로 함수도 받을 수 있다는 말이 됨), 그 함수는 더이상 그 암묵적 요소에 얽메이지 않게된다. 비슷한 형태의 함수들은 이런 과정을 거치면 모두 서로 공통적인 함수를 만들어 낼 것이다. 이것이 추상화를 통해 코드에서 나는 냄새를 없앨 수 있는 방법이다.

  • 일급이 아닌 요소들도 일급처럼 다룰 수 있다.
    '+' 와 같은 더하기 연산자를 인자 a,b를 받아 a+b를 리턴하는 형태의 함수를 만들면 된다.

  • 이처럼 암묵적 인자를 명시적 인자로 바꾸는 리팩토링과, 일급이 아닌 요소를 일급으로 만드는 과정을 토대로, 함수가 함수를 인자로하여 고차함수를 만들어 낼 수 있는 준비가 된다.

콜백을 활용한 추상화

  • 고차함수 : 일급함수를 인자로 받는 함수.
  • 위에서는 암묵적 인자를 명시적 인자로 바꾸어 추상화 하는 과정을 설명했는데, 고차함수도 그 내용과 맥락이 같다. 단 암묵적 인자가 '함수' 라는 일급객체일 뿐이다.
  • 알맹이(일급)를 인자로, 껍데기를 함수로 만드는 과정이다.
  • 그렇게 만들어 낸 함수A를 인자로 받는 추상화된 함수B에서, 인자로 받는 함수를 콜백이라고 한다. 콜백인 함수를 실행시키지 않고 함수 자체로 넘기는 이유는 함수의 실행시점을 함수 B 내부 코드에서 제어할 수 있기 때문이다. ( 이 말을 완성된 고차함수를 보면 이해가 어려울 수 있는데, 역으로 고차함수가 만들어 지는 과정을 보면 왜 인자로 받는 함수를 실행시키지 않고 그 자체로 받는 지 이해할 수 있다.)
  1. 알맹이와 껍데기 확인하기 ( 책 : 본문과 앞부분 뒷부분 확인하기 )
  2. 껍데기를 빼내어 새 함수로 감싸기 ( 책 : 함수(로) 빼내기 )
  3. 알맹이를 새 함수의 콜백으로 빼기 ( 책 : 콜백 빼내기 )
  • 일급 값은 변수에 저장할 수 있고, 인자로 전달하거나 함수의 리턴값으로 활용이 가능하다.
  • 일급이 아닌 기능은 함수로 감싸 일급으로 만들 수 있다.
  • 고차함수는 다른 함수에 인자로 넘기거나 리턴 값으로 받을 수 있는 함수다.
  • 함수에서 나는 냄새를 없애기 위해, 암묵적 인자를 드러내고 알맹이를 콜백으로 바꾸는 과정을 사용할 수 있다.

원칙을 코드(함수)로 표현하기

function arraySet(arr, idx, val){
  var copy = arr.slice();
  copy[idx] = value;
  return copy;
}

⬇︎

function withArrCopy(arr, f){
  var copy = arr.slice();
  f(copy);
  return copy;
  
function arraySet(arr, idx, val){
  return withArrCopy(arr, f(copy){
         copy[idx] = value;
	});
}
  • 알맹이 함수 f(copy){copy[idx] = value;}를 받은
    껍데기 함수withArrCopy(arr, f)를 실행시켜(평가하여) 그 결과값을 반환한다.
  • 어떤 원칙(여기서는 카피-엔-라이트)을 코드로 표현하고 있다고 볼 수 있음.

함수로 함수를 변형하기

  • = 함수를 리턴하는 함수.
try{ saveuserData(user); } 
catch (error){ logErrors(error); }

⬇︎

function wrapLoggingError(f){
  return function (args){
    try{ f(args); } 
    catch (error){ logErrors(error); }
 }
  
saveuserDataWithLogging = wrapLoggingError(saveuserData(user))
  • 껍데기 함수wrapLoggingError가 알맹이 함수saveuserData 를 받아 함수 자체를 평가(생성)하여 반환하고 있다.
  • 함수를 일급으로 다루는 성질을 활용하여, 함수의 실행시점을 다룰 수 있다.
  • '함수를 리턴하는 (찍어내는?) 함수'를 팩토리 함수 라고 한다.
  • 정리하면 고차함수로 어떤 함수에 다른 행동이 추가된 함수로 바꿔줄 수 있음.
  • 고차함수로 패턴이나 원칙을 코드로 만들 수 있다.
  • 고차함수로 함수를 리턴하는 함수를 만들어 낼 수도 있다.
  • 고차함수로 코드의 중복을 상당히 줄일 수 있다. 그러나, 고차함수의 남용은 코드의 역할이 무엇인지 이해하기 어렵게 만들 수 있다. 적절하게 사용해야 한다.


function curry(f) {
  return (a, ..._) => (_.length ? f(a, ..._) : (..._) => f(a, ..._));
}
  • 위 코드는 fxjs 에서 특정함수를 curry화 하기 위해 사용하는 고차함수다.
    인자가 몇개 들어오느냐에 따라 함수가 어떻게 실행될지를 결정한다
    (바로 실행할지? 아니면 인자를 더 받는 함수로 리턴할지)
  • 함수를 리턴하는 방식으로 함수를 변형시키는 좋은 예제인 것 같아 같이 기록한다.


forEach, map, filter, reduce를 활용

  • for 반복문은 하나의 인자값에 대해 무언갈 하고, 치우고 를 반복한다.
  • 그러나 forEach, map, filter, reduce 와 같은 메서드는 데이터를 배열로 다룬다.
    배열로 하여금 한 번에 처리하여 쿼리를 작성하는 것과 같이 다룰 수 있다.

같은 함수가 계속 중첩되는경우 (nested) 재귀를 통해 정리할 수 있다.
재귀함수를 생성하게 될 경우, 일반적인 재귀 호출과정과 맨 끝단(더 이상 nested 되지 않는 시점) 을 정의한다.

  • 종료 조건 : 더 이상 재귀호출을 하지 않는 조건
  • 재귀 호출 : 반복적으로 진행하는 연결고리

함수의 이름에 구체적인 내용이 들어가 있는경우와 함수의 이름에서 냄새나는 것을 구분하기 : 추상화 된 함수를 활용하는 경우, 변수와 자료구조를 보고 어떤 동작인지 이해하기 어려울 수 있다. 추상화된 함수를 호출하는 함수를 만들어서, 특정 조건의 인자를 미리 인풋값에 넣어 만들어 둘 수 있다.

function updatePostById(category, id, modifyPost){
  return nestedUpdate(category, ['posts', id], modifyPost)
}

function nestedUpdate(object, keys, modify){
  if (keys.length === 0)
    return modify(object)
  var key1 = key[0]
  var restOfkeys = drop_first(keys);
  return update(object, keys, function(value1){
    return nestedUpdate(valie1, restOfkeys, modify)
  })
}
  • 배열을 다루는 고차함수 활용하기
  • 중첩된 데이터를 변경할 때, 특정 층의 데이터까지 내려가는 동안의 모든 데이터를 복사하여야 한다. (카피-온-라이트, 액션이 아닌 계산)
  • 함수가 일급임을 이용하여, 어떤 원칙을 코드로 표현하거나 어떤 함수를 다른 행동이 추가된 함수로 바꿀 수 있다. (후자는 데코레이터와 같은역할이라고 볼 수 있을 것 같다.)

타임라인 격리하기

타임라인 다이어그림

  • 함수의 액션을 타임라인으로 그린다. (계산은 필요없음)
  • 액션이 한 타임라인에서 분리되어 있다면, 시간과 순서에 영향을 받을 수 있음을 의미함.
  • 특히 공유하는 자원이 있을 때 시간에 의존적이므로 주의해야 함. (해당 자원을 초기화 하고나서 사용하려는데 다른 타임라인에 있는 동작이 개입하는 등)
  • 자원을 공유하지 않도록 수정하여 시간에 독립적으로 만들 수 있다. => 이는 액션에서 계산이 된다.
    자원을 공유하지 않도록 수정 : 전역변수 -> 지역변수로 변경 등

비동기 콜백

  • 프로미스가 없다고 가정할 때, 비동기동작을 다루어 보자.
function get_pets_ajax() {
    var pets = 0;
    return dogs_ajax(function (dogs) {
        cats_ajax(function (cats) {
            pets = dogs + cats;
            return pets;
        });
    });
}

위와 같은 코드를 실행하면 정상적으로 pets를 받을 수 있지 않을까?
하지만, dogs_ajax는 undefined 를 반환한다.
그렇다면 비동기적으로 계산되는 pets를 어떻게 활용할 수 있을까?

function get_pets_ajax(callback) {
    var pets = 0;
    dogs_ajax(function (dogs) {
        cats_ajax(function (cats) {
            pets = dogs + cats;
            callback(pets);
        });
    });
}

내가 pets를 계산하려는 것은, 어디선가 활용하려고 하기 때문이다.
활용할 어딘가를 함수자체에 넘겨주면 된다. 함수는 일급이므로 인자로 넘겨줄 수 있고,
위 코드에서는 cats_ajax가 실행되어 pets를 계산하고 나면 그 때 비로소 callback 부분을 실행할 것이다.
이렇게 보니 프로미스가 나오기 전, 콜백으로 비동기를 어떻게 다루었는지 이해가 된다.


타임라인 사이에 자원공유

  • 안전하게 공유 : 올바른 순서대로 자원을 사용하고 반환한다.

  • queue를 통해서 자원에 대해 한번에 하나의 작업만 사용이 가능하게 만들 수 있다.
    굳이 큐가 아니더라도 다양한 방식으로 구현 가능함.

  • 동시성 기본형 : 자원을 안전하기 공유할 수 있는 재사용 가능한 코드. 직접 만들어서 쓸 수도.

function add_item_to_cart(item){
  cart = add_item(cart,item)
  // add_item은 객체를 새로 생성해서 반환함.
  calc_cart_total(cart, update_total_dom);
}

function calc_cart_total (cart, callback){
  var total = 0;
  cost_ajax(cart, function(cost){
    total += cost
    shipping_ajax(cart, function(shipping){
		total += shipping
      	callback(total);
    });
  });
}

  • 위 코드는 카트에 아이템 담기를 눌러서 아이템 가격과 배송비 가격이 계산되고 DOM에 출력되는 코드이다.
  1. DOM 이라는 공용자원을 제어하도록, 큐를 생성하여 첫번째 있는 작업을 실행하도록 만든다.
  2. 큐에 있는 내용을 연속적으로 중복 실행하지 않도록 (1번칸에 있는게 완료되지 않았는데 2번이 시작되지 않도록) 제어
  3. 큐에서 꺼낸 걸 끝내고 나서 다음 걸 진행하도록 재귀형태로 만듬. 없을 때는 멈추는 설정도 추가
  4. 변수를 공유하지 않도록 함수로 감싸 전역변수를 지역번수로 만들고, 그 함수는 원래 실행부를 함수객체로 만들어 리턴
  5. 단, 함수 내부에 실행되는 함수들은 실행 순서를 준수해야하는 경우, 콜백으로 넘겨 뒤섞이지 않도록 제어함.
  6. 뭉쳐있는 함수를 기능별로 분리하여, 다형성을 띌 수 있는 부분은 콜백으로 빼서 추상화 함.
  7. 그러면 큐에서 하나 뽑아서 할 일과, 그 할 일이 끝나면 할 것 이 두개를 콜백과, 콜백의 콜백으로 넘김으로써, 큐 자체는 하나씩 뽑아서 실행시키는 기능만 남았다.
  8. 완료되었을 때의 콜백을 같이 전달하도록 추가하여, 큐에 있는 것이 실행되고 완료될 때마다 할 것을 지정함.
  9. 또한 중복되어 실행될 필요가 없는 경우, 최신 것만 실행시키고 나머지는 드랍이 가능하도록 큐를 수정함.
  • 아래는 위 과정을 거쳐 수정된 코드이다.
function DroppingQueue(max, worker) {
    var queue_items = [];
    var working = false;

    function runNext() {
        if (working) { return }
        if (queue_items.length == 0) { return }

        working = true
        var item = queue_items.shift();
        worker(item.data, function (val) {  	//<----- 근데 이러려면 worker의 형태가 
          										//미리 지정되어 있으니, 잘 맞춰서 써야할 듯. 
          										//(타입스크립트에서는 타입을 지정해버리면 좀 나을듯?)
            working = false;
            setTimeout(item.callback, 0, val)
            runNext()
        })

        return function (data, callback) {
            queue_items.push({
                data: data,
                callback: callback || function () { }
            });
            while (queue_items.length > max) {
                queue_items.shift();
            }
            setTimeout(runNext, 0);
        }
    }
}

function calc_cart_worker(cart, done) {
    calc_cart_total(cart, function (total) {
        update_total_dom(total);
        done(total);
    });
}

var update_total_queue = DroppingQueue(1, calc_cart_worker)

// cart에 아이템을 추가하고, cart 전체의 금액을 계산하는 함수.
// 따라서 여러번 클릭해도 최신 cart 만 받아서 실행하면 되므로, 중간 것들은 버려도 됨.

시간을 일급으로 다루기 (시간모델 만들기)

  • 함수의 동작을 일렬로만 처리한다면, 비효율적인 시간활용으로 인해 전체 작업속도가 느릴 수 있다.
  • 이럴 때 서로에게 영향을 미치지 않는 함수를 분리해 내어 병렬적으로 처리가 가능한데,
  • 하나의 함수를 완료해서 바로 다음 동작을 실행하면, 아직실행중인 함수에 영향을 끼칠 수도 있다.
  • 이 외에도 다양한 사례가 있겠지만, 위와 같은 상황이 발생한다고 할 때 병렬처리를 보장하는 함수를 작성해보면 아래와 같다.
function Cut(num, callback){
  var num_finished = 0;
  return function(){
    num_finished += 1;
    if (num_finished == num)
      callback();
  };
}
  • 함수자체를 리턴하는 형태가 지속적으로 나오고 있다.
  • 함수 자체를 반환하는 함수로 함수를 생성하면, 반환된 함수들은 클로저로 변수를 공유할 수 있다.
  • 위에서는 num_finished가 공유될 것이다.
function JustOnce(action){
  var alreadyCalled = false;
  return function(a,b,c){
    if (alreadyCalled) return;
    alreadyCalled = true;
    return action(a,b,c);
  }
}
  • 아니면 이렇게 하나의 함수가 전체적으로 한번 만 실행되게 할 수도 있다.

이처럼 프로그램에서는 여러 작업이 난무하여 진행되는데, 이 질서를 유지시킬 수 있도록 시간모델과 동시성 기본형을 만들어 사용해야 한다.


반응형 아키텍쳐

  • 순차적 액션 : X 하고 Y 하고 Z 하고..
  • 반응형 액션 : X 하면 Y 하고, Y 하면 Z 하고..

🙋 항상 반응형 아키텍쳐가 더 좋을까?

  • 그렇지 않다. 적합한 아키텍쳐는 프로그램에 따라 다르다.
  • 문서를 검증하고 서명 후 보관함에 저장하는 시스템은 원인과 효과가 잘 나타나지 않는다. 이런경우는 순차적인 아키텍쳐가 더 적합할 수 있다.
  • 반면에 고객의 행동에 따라 다른 서비스를 제공해야하는 프로그램의 경우, 원인과 효과가 일어나기 충분하므로 반응형 아키텍쳐가 적합할 수 있다.

단계 1) 레거시 코드

var shopping_cart = {};

function add_item_to_cart(name, price) {
    var item = make_cart_item(name, price)
    shopping_cart = add_item(shopping_cart, item)
    var total = calc_total(shopping_cart)
    set_cart_total_dom(total)
    update_shipping_icons(shopping_cart)
    update_tax_dom(total)
}

모든 액션이 뭉쳐있는 레거시 코드 예제
(제품추가를 눌러, 쇼핑카트를 업데이트하고, 연달아 아이콘과 DOM을 업데이트 하는 것)

  • 장바구니 변경 방법 : 제품추가, 제품삭제, 장바구니 비우기, 수량변경, 할인코드 적용
  • 장바주니 변경 시 반영대상 : 배송 아이콘 업데이트, 세금 표시, 합계 표시, 장바구니 개수 업데이트
  • 원인과 결과가 각각 m개, n개라고 할 때 (위 예제는 m = 5, n = 4)
    • 순차적 액션 : m * n 개 (각각의 액션마다 장바구니가 바뀌는 액션을 지정해야 하므로)
    • 반응형 액션 : m + n 개 (각각의 액션은 장바구니만 수정하면 되고, 각각의 효과는 장바구니만 보면 되므로)
  • 반응형 액션은 원인과 효과를 분리하여 액션을 관리하는 것.

단계 2) 셀을 활용한 반응형 액션

function ValueCell(initialValue) {
    var currentValue = initialValue;
    var watchers = []
    return {
        val: function () {
            return currentValue
        },
        update: function (f) {
            var oldValue = currentValue
            var newValue = f(oldValue)
            if (oldValue !== newValue) {
                currentValue = newValue
                forEach(watchers, function (watchers) {
                    watchers(newValue);
                })
            }
        },

        addWatcher: function (f) {
            watchers.push(f)
        }

    }
}

function FormulaCell(upstreamCell, f) {
    var myCell = ValueCell(f(upstreamCell.val()))
    upstreamCell.addWatcher(function (newUpStreamValue) {
        myCell.update(function (oldValueOfmyCell) {
            return f(newUpStreamValue)
        })
    })

    return {
        val: myCell.val,
        addWatcher: myCell.addWatcher
    }
}

셀을 만들어 반응형으로 관리.

  • ValueCell

    • 값을 뱉는 메서드
    • 값을 업데이트 하는 메서드,
    • 업데이트하면 연달아 실행할 함수를 추가하는 메서드 포함.
  • FormulaCell

    • 선행될 셀을 연결하여 셀을 생성하는 함수.
    • 이 셀의 값을 선행된 셀의 값을 받아 초기화
    • f는 선행될 셀의 값을 가지고 어떻게 이걸 활용할지 나타내는 함수
    • 선행될 셀의 와쳐에 '선행 셀의 새로운 값을 받아서 후행셀의 업데이트를 f를 활용하여 진행하겠다'는 함수를 추가함.
    • 최종 리턴 값은 선행된 셀에 와쳐를 추가한 후행 셀이 탄생.

이제 셀을 활용하여 레거시 코드를 고쳐보자.

단계 3) 반응형 액션으로 리팩토링

var shopping_cart = ValueCell({});
var cart_total = FormulaCell(shopping_cart, calc_total)

function add_item_to_cart(name, price) {
    var item = make_cart_item(name, price)
    shopping_cart.update(function (cart) {
        return add_item(cart, item);
    })
}

shopping_cart.addWatcher(update_shipping_icons)
cart_total.addWatcher(set_cart_total_dom)
cart_total.addWatcher(update_tax_dom)

코드 설명

  • 아이템을 장바구니에 추가
  • 쇼핑카트가 업데이트됨
  • 쇼핑카트가 업데이트 됨에 따라 와쳐인 set_cart_total_domupdate_tax_dom 이 실행됨.
  • 쇼핑카트 업데이트에 따른 동작은 언제든지 추가하고 제거할 수 있음.

어니언 아키텍쳐

  • 어니언 아키텍쳐 구성
    • 인터렉션 계층 : 바깥세상에 영향을 주거나 받는 액션
    • 도메인 계층 : 비즈니스 규칙을 정의하는 계산
    • 언어 계층 : 언어 유틸리티와 라이브러리

전통적인 계층형 아키텍쳐는 데이터베이스가 최하단에 존재하여, 모든 것이 액션이 된다.

그러나 어니언 아키텍쳐는 함수형 프로그래밍에서 액션과 계산을 분리하는 방법과 같이,
외부세계에 영향을 주고 받는 층과, 주어진 값에 대해 특정 동작을 한 후 다시 반환해주는 도메인 층으로 구분할 수 있다.

  • 어니언 아키텍쳐에서 인터렉션 계층은, 계층의 최상위에 있어 이 층에 의존하는 층이 없으므로 변경하기 쉽다.
  • 만약 데이터베이스에서 장바구니 내역을 불러와 계산해야 한다면, 인터렉션 계층의 핸들러가 데이터베이스를 불러오고, 도메인에 전달하는 역할을 한다. 도메인이 직접 데이터베이스를 가져오지 않는다.

도메인 계층을 액션이 아닌 계산으로 유지하는 것이 중요함.

0개의 댓글