tag: js

$scope를 사용하는 더 나은 방법, angular.extend()

2015년 11월 8일

이 글은 Todd Motto의 글 A better way to $scope, angular.extend, no more “vm = this”를 번역한 글이다.

Angular에서 Controller As 문법을 사용하면 var vm = this;와 같이 this의 스코프 문제를 회피하는 방식으로 작성하는데 이 방법을 회피하기 위해 angular.extend를 활용하는 방식을 제안하고 있다. 댓글에도 많은 반론이 있는 글이라서 댓글도 따라 번역했다. 반론에는 문법이 익숙하지 않다고 하지만 이 문법이 더 명시적인 느낌이 드는 편이다. 다만 반론과 같이, angular에 대한 의존성을 높이는 방식이고 ES6 클래스와는 어울리지 않는 문법이란 부분에서 단점도 크게 느껴진다.


$scope를 사용하는 더 나은 방법, angular.extend, 더이상 "vm = this"는 없다

Angular 컨트롤러는 지난 1년간 발전해왔다. 이제는 많은 사람들이 가장 최근에 추가된 "컨트롤러" 문법인 controllerAs 스타일을 활용하고 있다. ($scope를 직접 바인딩하는 방식을 멀리 하고서 말이다.)

스타일에 대한 다양한 의견 중 내가 수용하고 있는 방식은, 컨트롤러 가장 상위에 var vm = this;를 우선적으로 선언하는 것이다. 최근에는 vm을 실제 자바스크립트 컨트롤러 내에서 사용하는 것을 멀리하고 있다. 대신 평범한 자바스크립트 변수와 함수로 작성한 후, 필요로 하는 부분을 "exports"와 같은 방식으로 외부에서 바인딩 할 수 있게 작성하고 있다.

내 이전 작업과 어떻게 다른지 살펴보기 위해 var vm = this; 부터 시작하자.

var vm = this;

이 방법은 컨트롤러를 변수와 바인딩하기 위한, 아주 유명한 방법이다. (결국 $scope 와 연결된다.) 단순한 예제를 살펴보자:

function MainCtrl () {

  var vm = this;

  function doSomething() {

  }

  // exports
  vm.doSomething = doSomething;

}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);

이 패턴은 굉장하며 Angular로 개발하는데 아주 유용하다. (여기에는 함수를 직접 선언하지 않고 vm.doSomething = function () {}와 같이 바로 바인딩하는 변형도 있다.) vm을 생성하는 이유는 다른 함수 내에서 올바른 문맥을 참조하기 위해서인데 this는 다른 변수와는 달리 어휘 스코핑(lexical scoping)를 따르지 않기 때문이다. 그래서 thisvm에 "참조"로 배정해놓고 사용하는 것이다.

많은 내용은 바인딩해야 할 때, vm을 엄청 많이 반복해서 사용하고 끝내 vm.* 참조가 코드 전반에 생기게 된다. 사실 잘 따져보면 모든 코드를 this에 직접 바인딩할 필요가 없고, JavaScript는 그 자체 인스턴스에 포함된 변수로도 충분히 동작할 수 있다. (예를 들면, 콜백에서 vm.foo를 사용하는 방식보다 var foo = {};와 같이 업데이트를 지역적으로 수행하는 방식이 낫다.) 다음은 vm.* 바인딩을 많이 사용한 경우에 대한 예시다:

function MainCtrl () {

  var vm = this;

  function doSomething1() {}
  function doSomething2() {}
  function doSomething3() {}
  function doSomething4() {}
  function doSomething5() {}
  function doSomething6() {}

  // exports
  vm.doSomething1 = doSomething1;
  vm.doSomething2 = doSomething2;
  vm.doSomething3 = doSomething3;
  vm.doSomething4 = doSomething4;
  vm.doSomething5 = doSomething5;
  vm.doSomething6 = doSomething6;
}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);

angular.extend 사용하기

angular.extend라고 알고 있는 이 방식은 새로운 아이디어는 아니지만, Modus Create의 글 AngularJS: Tricks with angular.extend()에서 아이디어를 얻게 되었고, 내 angular 컨트롤러 전략/패턴에서 vm 참조를 완전히 제거하게 되었다. 이 글에서는 angular.extend($scope, {...});를 사용하고 있지만, 내 예제에서는 controllerAs 문법으로 차용하고 있다.

다음은 vm을 버리고 this에 간단히 바인딩하는 간단한 예제다:

function MainCtrl () {
    this.someVar = {
      name: 'Todd'
    };
    this.anotherVar = [];
    this.doSomething = function doSomething() {
    };
}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);

angular.extend를 사용하면 깔끔하고 더욱 객체 주도적인 코드를 얻을 수 있고, 아이템 목록을 넘겨주는 대신 단순한 exports 객체를 넘겨줄 수 있다:

function MainCtrl () {
  angular.extend(this, {
    someVar: {
      name: 'Todd'
    },
    anotherVar: [],
    doSomething: function doSomething() {

    }
  });
}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);

이 방식이 this 키워드를 반복하지 않아도 되게 한다. (또는 $scope를 여전히 사용하고 있다면 $scope 또한 반복하지 않아도 된다.)

이 방식을 사용하면 "private" 메소드를 사용하는데도 좀 더 편리하고 명확하게 작성할 수 있다:

function MainCtrl () {
  
  // private
  function someMethod() {

  }

  // public
  var someVar = { name: 'Todd' };
  var anotherVar = [];
  function doSomething() {
    someMethod();
  }
  
  // exports
  angular.extend(this, {
    someVar: someVar,
    anotherVar: anotherVar,
    doSomething: doSomething
  });
}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);

이 방식에 대한 다른 생각이나 좋은 예제가 있는지 궁금하다.


댓글에 달린 다른 의견 1, 2에서 공감가는 부분을 옮겨보자면, 이 방식에는 다음과 같은 단점이 존재한다.

  1. 코드 복잡도가 증가하는 것처럼 보인다. (vm에 비해 복잡하게 보인다.)
  2. angular.extend() 을 사용하는 것으로 angular에 대한 강결합이 발생한다. ES6 클래스 등을 사용할 때 불편하다.
  3. doSomething에서 someVar를 참조하는 과정이 복잡해진다.
  4. 느리다.

Angular의 Controller As 문법 살펴보기

2015년 11월 8일

Todd Motto의 글 Digging into Angular’s “Controller as” syntax를 번역했다. Angular의 Controller As 문법에 대해 설명하고 있는 글이다. $scope를 분리하는 것으로 더 사용성 높은 컨트롤러를 만들 수 있고 최근 ES6에서 클래스를 만드는데 좋은 호환성을 보장하고 있다는 얘기를 듣고 번역하게 되었다.


AnularJS 컨트롤러는 최근 몇가지 변화가 있었다. (정확하게는 버전 1.2부터.) 스코프, 컨트롤러와 Angular 개발에 있어서 이 의미는 꽤 희미하면서도 아주 강력한 변화다. 이 변화는 구조를 향상하고 더 깔끔한 스코프와 똑똑한 컨트롤러를 만드는데 일조한다.

우리가 알고 있는 컨트롤러는 클래스 같은(class-like) 객체로 Model과 View를 변경하는데 사용되지만, 이 모든 과정이 수수께끼 같은 $scope 객체에 의해 이뤄진다. 많은 개발자가 this 키워드를 $scope 대신 사용하는 것을 추천하고 있어 Angular 컨트롤러에서 $scope가 선언되어 있는 방식을 변경하도록 압박하고 있다.

v1.2.0 이전의 컨트롤러는 다음과 같이 생겼다:

// <div ng-controller="MainCtrl"></div>
app.controller('MainCtrl', function ($scope) {
  $scope.title = 'Some title';
});

늘 컨트롤러에 $scope를 주입했었지만, 다음은 컨트롤러를 $scope로부터 분리한 개념이다. 이 방식이 더 낫다고 논의되었다:

app.controller('MainCtrl', function () {
  this.title = 'Some title';
});

별로 한 일은 없지만 이 과정으로 좀 멋진 결과를 얻을 수 있게 되었다.

클래스로서 컨트롤러

자바스크립트에서 "class"를 인스턴스화(instantiation) 하면, 다음과 같을 것이다:

var myClass = function () {
  this.title = 'Class title';
}
var myInstance = new myClass();

이렇게 선언 후 myInstance 인스턴스를 사용해 myClass의 메소드와 프로퍼티에 접근할 수 있다. Angular에서는 이와 비슷한 방식으로 접근하는 방법으로 Controller as 문법을 제공하게 되었다. 다음은 어떻게 선언하고 바인딩 하는지에 대한 예제다:

// 선언은 평소같이 하지만 `$scope` 대신 `this`를 사용
app.controller('MainCtrl', function () {
  this.title = 'Some title';
});

이 방법은 더 클래스 기반 설정을 사용할 수 있게 되어, 이 컨트롤러를 DOM에서 인스턴스화 할 때 쉽게 변수에 할당할 수 있게 된다:

<div ng-controller="MainCtrl as main">
  // MainCtrl은 존재하지 않고, 대신 `main` 인스턴스를 얻을 수 있음
</div>

this.title을 DOM에 반영하기 위해서는 새 인스턴스를 사용하면 된다:

<div ng-controller="MainCtrl as main">
   {% raw %}{{ main.title }}{% endraw %}
</div>

스코프를 네임스페이스로 처리할 수 있는 것은 아주 좋은 접근이라고 생각하며 Angular를 엄청나게 깔끔하게 한다고 생각한다. 난 항상 {% raw %}{{ title }}{% endraw %} 같이 "떠있는 변수(모호한 변수)"를 싫어했는데, {% raw %}{{ main.title }}{% endraw %} 처럼 인스턴스와 함께 작성할 수 있는 방식은 훨씬 마음에 든다.

중첩된 스코프

중첩된 스코프도 Controller as 문법에서 얻을 수 있는 결과인데, 가끔 현재 스코프의 $parent 프로퍼티에 접근해 상위 스코프에서 필요한 부분을 받아와야 할 필요가 있었다.

다음 예제를 보자:

<div ng-controller="MainCtrl">
  {% raw %}{{ title }}{% endraw %}
  <div ng-controller="AnotherCtrl">
    {% raw %}{{ title }}{% endraw %}
    <div ng-controller="YetAnotherCtrl">
      {% raw %}{{ title }}{% endraw %}
    </div>
  </div>
</div>

먼저 {% raw %}{{ title }}{% endraw %} 를 반복적으로 사용하는데다 여러 스코프의 경계를 오가고 있기 때문에 이 값이 어디서 들어오는지 아주 모호하고 혼란스러운 인터폴레이션(interpolation) 이슈가 발생한다. 어느게 무엇이 될 지도 예측하기 어렵다. 스코프를 가로질러 변수에 접근하는 것은 이해하는데 훨씬 명확하다:

<div ng-controller="MainCtrl as main">
  {% raw %}{{ main.title }}{% endraw %}
  <div ng-controller="AnotherCtrl as another">
    {% raw %}{{ another.title }}{% endraw %}
    <div ng-controller="YetAnotherCtrl as yet">
      {% raw %}{{ yet.title }}{% endraw %}
    </div>
  </div>
</div>

또한 부모 스코프에 다음과 같이 작성하지 않고도 접근할 수 있다:

<div ng-controller="MainCtrl">
  {% raw %}{{ title }}{% endraw %}
  <div ng-controller="AnotherCtrl">
    Scope title: {% raw %}{{ title }}{% endraw %}
    Parent title: {% raw %}{{ $parent.title }}{% endraw %}
    <div ng-controller="YetAnotherCtrl">
      {% raw %}{{ title }}{% endraw %}
      Parent title: {% raw %}{{ $parent.title }}{% endraw %}
      Parent parent title: {% raw %}{{ $parent.$parent.title }}{% endraw %}
    </div>
  </div>
</div>

그리고 더욱 논리적이다:

<div ng-controller="MainCtrl as main">
  {% raw %}{{ main.title }}{% endraw %}
  <div ng-controller="AnotherCtrl as another">
    Scope title: {% raw %}{{ another.title }}{% endraw %}
    Parent title: {% raw %}{{ main.title }}{% endraw %}
    <div ng-controller="YetAnotherCtrl as yet">
      Scope title: {% raw %}{{ yet.title }}{% endraw %}
      Parent title: {% raw %}{{ another.title }}{% endraw %}
      Parent parent title: {% raw %}{{ main.title }}{% endraw %}
    </div>
  </div>
</div>

깔끔하지 않은 $parent 호출을 더이상 안해도 된다. 만약 컨트롤러의 위치가 DOM 또는 스택 내에서 변경된다면, $parent.$parent.$parent.$parent를 연쇄적으로 변경해야만 한다! 어휘적으로 스코프에 접근할 수 있는 것이 훨씬 편리하다.

watchers/watchers/scope 메소드

Controller as 문법을 맨 처음 사용하고서 "오, 대박!" 이랬지만, 스코프 관찰자(watchers)나 메소드를 사용하기 위해서는 $scope의 의존성을 주입할 필요가 있다. (예를 들면 $watch, $broadcast, $on 같은 것을 사용해야 할 때.) 웩, 이 부분을 얼마나 피하려고 노력했는데 말이다. 하지만 이조차도 대박인 것을 알게 되었다.

Controller as 문법이 동작하는 방식은 $scope 같은 클래스 같은 객체가 되는 것이 아니라, 컨트롤러가 현재 $scope바인딩 하도록 하는 방식이다. 나에게는 클래스와 Angular의 특별한 기능을 분리하는 핵심적인 방식이 되었다.

이 의미는 다음 같이 클래스 같은 컨트롤러를 갖고 있다는 뜻이다:

app.controller('MainCtrl', function () {
  this.title = 'Some title';
});

이 기능 이전에 또는 일반적인 바인딩 이상의 기능이 필요할 때, $scope를 의존성으로 넣어, 그냥 컨트롤러보다 훨씬 강력하고 특별한 기능을 활용할 수 있게 되었다.

이 특별한 기능은 $scope의 메소드로 모두 포함되어 있다. 다음 예제를 보자:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  $scope.$on('someEventFiredFromElsewhere', function (event, data) {
    // do something!
  });
});

꼬인 문제 다리미질 하기

이 코드는 $scope.$watch() 예제를 작성하는 동안 나타난 흥미로운 문제다. 아주 단순한 예제지만 Controller as 문법에서는 예상한대로 동작하지 않는다:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  // doesn't work!
  $scope.$watch('title', function (newVal, oldVal) {});
  // doesn't work!
  $scope.$watch('this.title', function (newVal, oldVal) {});
});

헤헤, 그래서 여기서 뭘 할 수 있나? 재밌게도 다른 날 이 코드를 읽었을 때, 이 부분에서 $watch()에게 첫 인자를 함수로 넘겨주면 해결할 수 있는 문제인 것을 알 수 있었다:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  // 음.. 함수로 쓰면,
  $scope.$watch(function () {}, function (newVal, oldVal) {});
});

그 의미는 여기서 작성한 this.title을 참조로 넘길 수 있다는 뜻이다:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  // 이러면 되겠군...
  $scope.$watch(function () {
    return this.title; // `this`가 위에서 말한 `this`가 아니네!!
  }, function (newVal, oldVal) {});
});

컨텍스트를 angular.bind()를 사용해 변경하자:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  // 짠
  $scope.$watch(angular.bind(this, function () {
    return this.title; // 이 `this`가 위 `this`와 같음
  }), function (newVal, oldVal) {
    // 이제 newVal과 oldVal의 변화를 잡을 수 있음
  });
});

역주. IE9 이상을 지원한다면 angular.bind 대신 Function#bind를 사용해도 되고, John Papa의 방식대로 var vm = this; 식으로 작성해 회피해도 된다.

$routeProvider/디렉티브/그 외 아무곳에나 선언하기

컨트롤러는 동적으로 배정될 수 있으므로 항상 어트리뷰트로 연결해둘 필요가 없다. 디렉티브 내에서 controllerAs: 프로퍼티를 사용할 수 있고, 이 프로퍼티는 쉽게 배정할 수 있다:

app.directive('myDirective', function () {
  return {
    restrict: 'EA',
    replace: true,
    scope: true,
    template: [].join(''),
    controllerAs: '', // 쉽고 편하다!
    controller: function () {}, // 이 컨트롤러를 위 controllerAs 의 이름으로 인스턴트화 할 것임
    link: function () {}
  };
});

$routeProvider 내에서도 동일하다:

app.config(function ($routeProvider) {
  $routeProvider
  .when('/', {
    templateUrl: 'views/main.html',
    controllerAs: '',
    controller: ''
  })
  .otherwise({
    redirectTo: '/'
  });
});

controllerAs 문법 테스트하기

controllerAs를 테스트하는데 미묘하게 다른데 고맙게도 $scope를 주입할 필요가 없다. 이 의미는 컨트롤러를 테스트할 때 참조하는 프로퍼티를 넣을 필요가 없다는 뜻이다. (vm.prop 같은 부분.) 이제 간단하게 $controller에 변수명을 지정하는 것만으로 테스트할 수 있다.

// controller
angular
  .module('myModule')
  .controller('MainCtrl', MainCtrl);

function MainCtrl() {
  this.title = 'Some title';
};

// tests
describe('MainCtrl', function() {
  var MainController;

  beforeEarch(function(){
    module('myModule');

    inject(function($controller) {
      MainController = $controller('MainCtrl');
    });
  });

  it('should expose title', function() {
    expect(MainController.title).equal('Some title');
  });
});

controllerAs 문법을 사용했을 때 $controller 함수로 인스턴스화 하는 것 대신에 $scope를 주입해야 할 필요가 있는 경우에는 $controller에 다음과 같이 객체로 넘겨주면 된다. (scope.main 인스턴스에서 사용될) 컨트롤러를 위한 이 alias는 $scope를 (실제 Angular 앱처럼) 추가하게 된다. 하지만 그다지 아름다운 해법은 아니다.

// Same test becomes
describe('MainCtrl', function() {
  var scope;

  beforeEarch(function(){
    module('myModule');

    inject(function($controller, $rootScope) {
      scope = $rootScope.$new();
      var localInjections = {
        $scope: scope,
      };
      $controller('MainCtrl as main', localInjections);
    });
  });

  it('should expose title', function() {
    expect(scope.main.title).equal('Some title');
  });
});

Angular 컨트롤러를 작성하는 두가지 방법

2015년 11월 8일

Johnpapa의 Do You Like Your Angular Controllers with or without Sugar?를 번역한 글이다. 원본 포스트는 CC BY 2.5 라이센스로 작성되어 있다.

그냥 읽을 때는 괜찮게 느껴졌는데 옮기고 나니 핵심적인 부분이 없는 감상문 느낌이라 아쉬웠다. 덕분에 다른 글도 번역하게 된 좋은 원동력(?)이 되었다. 1.2 이후로 소개된 Controller As에 대해 전통적인 방법과 어떻게 다른지에 대해 설명하고 있다.


Angular 컨트롤러를 작성하는 두가지 방법

Angular 문서만 읽고 왔더라도 $scope를 MVC의 C(컨트롤러)에서 미친듯이 사용하는 모습은 이상하게 보였을 것이다. $scope는 컨트롤러와 뷰 사이를 연결하는 풀과 같은 존재로 데이터 연결이 필요한 모든 경우를 돕는다. 최근 Angular 팀은 컨트롤러에서 $scope를 사용하는 새로운 방식을 공개했다. 이제 $scope(이 단어를 쓰면 전통적인 방식의 컨트롤러에서 쓰는걸 의미함)와 함께 this(Angular 팀과 내가 Controller-As로 사용하는 방식을 의미함)을 사용할 수 있게 되었다. 이 두 가지 기술에 대한 질문을 아주 많이 받았다. 모두가 선택을 좋아하고 동시에 그 선택에서 얻을 수 있는 것이 무엇인지 명확하게 알고 싶어한다. 그래서 Angular에서 컨트롤러를 생성할 때 사용할 수 있는 이 두 가지 방식($scope와 Controller As)에 대해 이야기하고 활용해보자.

전통적인 컨트롤러와 Controller As 모두 $scope를 갖고 있다. 이 점이 이해하는데 가장 중요하다. 어느 한 방식을 선택한다고 다른 장점을 포기하는 것이 아니다. 정말. 이 두가지 방법은 모두 사용된다.

먼저 알아야 할 과거

$scope는 "전통적인" 기법으로 "controller as"는 아주 최근에 나온 기술이다. (공식적으로 1.2.0 pre릴리스에서 나타나지만 불완전했음.) 둘 다 완벽하게 동작하기에 내가 줄 수 있는 지침은 둘 중 하나를 골라 일관되게 사용하라는 것이다. 하나의 앱에서 둘 다 섞어서 사용할 수 있지만, 일관적으로 사용해야 하는 이유는 놀라울 정도로 명확하다. 그러므로 하나를 고르고 주사위를 던져라. 가장 중요한 점은 일관성이다. 어느 것을 골라야 하나? 그 선택은 개발자에게 달렸다. $scope를 이용한 예가 훨씬 많지만 "controller as"도 흐름에 따라 잘 골라야 한다. 둘 중 어느 것이 더 나은가? 논쟁할 만한 주제다. 그렇다면 어떻게 골라야 할까?

"controller as"를 선호하면 숨기기 편하다

중개하는 역할을 하는 객체인 $scope를 사용하면 컨트롤러에서 사용하는 모든 맴버를 뷰에 공개하게 된다. this.*를 설정하는 것으로 컨트롤러에서 뷰에 공개하고 싶은 부분에 대해서만 노출하는 것이 가능하다. 물론 $scope를 사용해도 동일하게 쓸 수 있지만 표준 자바스크립트의 this를 사용하는 것을 선호한다. 종합적으로 보면 개인적인 선호에 따라 Controller As 기법을 더 선호한다. 다음과 같이 코드를 작성한다:

var vm = this;

vm.title = 'some title';
vm.saveData = function() { ... };

이 방식이 더 보기 쉽고 어떤 부분이 뷰에 노출되는지 쉽게 확인할 수 있다. "vm" 변수는 뷰모델(viewmodel)을 의미한다. 이 명칭은 단순하게 내 컨벤션이다. $scope를 사용할 때도 같은 방법을 쓸 수 있지만 $scope를 사용할 때는 그렇게 작성하지 않았다.

$scope.title = 'some title';
$scope.saveData = function() { ... };

결국 이 부분은 작성자에게 달려있다.

주입이 필요한 경우

$scope는 컨트롤러에 $scope를 주입할 필요가 있을 때 사용한다. 이 부분은 controller as 기법을 사용할 때는 필요 없는 부분이지만 몇가지 다른 이유에 의해 필요할 때가 존재한다. (가령 $broadcast가 필요하거나, watch를 사용할 필요가 있는데 컨트롤러 내에서 하는 것을 피하고 싶을 때.) 이 부분은 사실 Controller As 기법을 더 좋아하는 이유 중 하나다. $scope가 데이터 바인딩 등을 위해 정말 필요한 상황일 때만 명시적으로 선언하기 때문이다. broadcast 메시지를 듣기 위한 것도 한 예제다. watch는 다른 경우지만 컨트롤러 내에서 watch하고 싶지 않은 경우에 사용할 수 있다.

유행은?

명시적으로 $scope가 선언된 코드가 더 오래 사용한 방식이기 때문에 예제가 많다. 하지만 최근 예제는 Controller As를 사용한 경우가 많다. 이 예제를 원한다면 Visual Studio 플러그인인 SideWaffle을 사용할 수 있다. 이 두가지 기법 컨트롤러 모두를 지원한다. 설탕이 싫다면 전통적인 $scope 컨트롤러를 선택하라. 설탕을 원한다면 controller as 를 선택하라. Angular 팀은 이 두가지 선택지를 제공하고 있고 이 선택지 모두 마음에 든다. 개인적으로는 Controller As 기법이 마음에 든다. 이 두가지 방법 모두 데이터 바인딩을 할 수 있다. Controller As는 $scope와 개발하는데 더 편리하게 한다고 생각한다. 그러니 둘 중 어느 것을 선택하는가는 온전히 당신의 몫이다.

Express, Koa, Hapi 장단점 비교

2015년 10월 13일

nodejs로 개발을 한다면 Express, Koa, Hapi 중 하나는 꼭 접하게 된다. 내 경우는 Express를 맨 처음 접해서 가장 익숙하지만 generator를 지원하는 koa에 대한 이야기도 들어봤고 hapi도 최근 react나 angular와 함께 사용하는 얘기를 자주 들을 수 있었다.

어떤 차이가 있는지 검색하다가 간단하게 정리된 Jonathan Glock의 글 Node.js Framework Comparison: Express vs. Koa vs. Hapi을 접하게 되었고 장단점 부분만 간단하게 번역했다. 원문에는 비교 코드도 포함되어 있어서 코드를 보고 싶다면 원문을 살펴보길 권한다.

Thank you Airpair for giving me the opportunity to translate this article. If you want to check the original, please visit Node.js Framework Comparison: Express vs. Koa vs. Hapi page.

각 프레임워크의 장단점

Express

Node.js 프레임워크 중 커뮤니티가 가장 크다. 거의 5년 가량 개발되어 가장 성숙했고 StrongLoop에 의해 관리되고 있다. 서버를 쉽게 실행/운영할 수 있다. 내장된 라우터로 코드를 쉽게 재사용 가능하다.

수작업으로 해줘야 하는 부분이 많다. 내장된 에러 핸들링이 없어서 미들웨어를 잃어버릴 수 있다. 한 문제를 해결하기 위해 여러 방법으로 접근할 수 있다. Express는 스스로를 완고하다고 표현하는데 이 부분은 양날의 검이며 초보인 경우에는 단점으로 작용한다. 다른 프레임워크에 비해 메모리를 많이 차지한다.

Koa

메모리를 덜먹고 표현력이 좋다. 다른 프레임워크에 비해 미들웨어 작성이 쉽다. 기본적으로 뼈대 프레임워크라서 제공되는 미들웨어와 함께 사용해야만 하는 Express와 Hapi와 달리, 개발자가 필요한 미들웨어만 구성해 사용할 수 있다. ES6를 도입하고 있어 ES6 제너레이터를 사용할 수 있다.

여전히 불안정하고 많은 양의 개발이 진행중이다. ES6를 사용하기 위해 최신 버전의 node.js를 사용해야 한다. (주, 이 문제는 지금도 해당하는지 모르겠음.) 미들웨어를 직접 작성할 수 있는게 장점일 수 있지만 단점일 수도 있다. 예제서 살펴본 라우터는 훨씬 다양한 옵션을 다뤄야 한다.

Hapi

코드보다 설정을 더 많이 해야 해서 정말 좋은 프레임워크인지 말이 많다. 견고함과 재사용성을 요구하는 큰 규모 팀에서는 흔하게 사용한다. 월마트랩에서 만들고 이름있는 회사에서 많이 쓰고 있어서 검증되었다고 보는 편이다. 좋은 프레임워크로 계속 성장할 것으로 보인다.

Hapi는 크고 복잡한 어플리케이션에 특성화 되어 있다. 보일러플레이트로 작성해야 할 코드가 많아서 작은 웹앱에서는 쓰기 불편하고, 예제 및 hapi로 작성된 오픈소스 앱도 적다. 이 프레임워크를 선택하면 서드파티 미들웨어에 기대는 쪽보다 개발자가 직접 작성해야 할 부분이 더 많을 것이다.


위 프레임워크 중 Express만 경험해봐서 각각 예제 코드가 살펴보는데 도움되었다. Koa는 tj가 노드를 떠난다는 글 쓴 이후로 시들할줄 알았는데 (그 핑계로 Koa를 딱히 살펴보지 않았는데) 여전히 잘 관리되고 있었다. 다양한 라이브러리가 매일같이 쏟아져 나와 봐야할 것도 많긴 하지만 잘 정착하는 프레임워크도 늘어나고 있어 커뮤니티가 잘 성숙하고 있다는 인상을 준다.

각 프레임워크 웹사이트

JavaScript 모나드

함수형 프로그래밍에서의 모나드 JavaScript에서 살펴보기, Monads in JavaScript 번역

2015년 7월 22일

얼마 전 제이펍 출판사 베타리더스 3기에 선정되었다. 선정 되자마자 <함수 프로그래밍 실천 기술>이란 제목의 책을 베타리딩하게 되었는데 함수형 프로그래밍에 대해 전반적인 내용과 세세한 개념을 Haskell로 설명하는 책이었다. 함수형 프로그래밍에 대한 책을 처음 읽어봐서 생소한 개념도 많았지만 다른 언어로의 비교 코드를 많이 제시하고 있어 전체적인 이해에 도움이 많이 되었다. 조만간 출간된다고 하니 관심이 있다면 제목을 적어두는 것도 좋겠다 🙂

함수형 언어를 얘기하면 모나드가 꼭 빠지지 않는다. 이 포스트는 Monads in JavaScript의 번역글이다. 이 글이 모나드에 대해 세세하게 모든 이야기를 다룬 것은 아니지만 모나드의 아이디어를 JavaScript로 구현해서 이 코드에 익숙하다면 좀 더 쉽게 접근할 수 있는 글이라 번역으로 옮겼다. 쉽게 이해하기 어렵지만 이해하면 정말 강력하다는 모나드를 이 글을 통해 조금이나마 쉽게 이해하는데 도움이 되었으면 좋겠다.


모나드는 순서가 있는 연산을 처리하는데 사용하는 디자인 패턴이다. 모나드는 순수 함수형 프로그래밍 언어에서 부작용을 관리하기 위해 광범위하게 사용되며 복합 체계 언어에서도 복잡도를 제어하기 위해 사용된다.

모나드는 타입으로 감싸 빈 값을 자동으로 전파하거나(Maybe 모나드) 또는 비동기 코드를 단순화(Continuation 모나드) 하는 등의 행동을 추가하는 역할을 한다.

모나드를 고려하고 있다면 코드의 구조가 다음 세가지 조건을 만족해야 한다.

  1. 타입 생성자 – 기초 타입을 위한 모나드화된 타입을 생성하는 기능. 예를 들면 기초 타입인 number를 위해 Maybe<number> 타입을 정의하는 것.
  2. unit 함수 – 기초 타입의 값을 감싸 모나드에 넣음. Maybe 모나드가 number 타입인 값 2를 감싸면 타입 Maybe<number>의 값 Maybe(2)가 됨.
  3. bind 함수 – 모나드 값으로 동작을 연결하는 함수.

다음의 TypeScript 코드가 이 함수의 일반적인 표현이다. M은 모나드가 될 타입으로 가정한다.

interface M<T> {

}

function unit<T>(value: T): M<T> {
    // ...
}

function bind<T, U>(instance: M<T>, transform: (value: T) => M<U>): M<U> {
    // ...
}

Note: 여기에서의 bind 함수는 Function.prototype.bind 함수와 다르다. 후자의 bind는 ES5부터 제공하는 네이티브 함수로 부분 적용한 함수를 만들거나 함수에서 this 값을 바꿔 실행할 때 사용하는 함수다.

JavaScript와 같은 객체지향 언어에서는 unit 함수는 생성자와 같이 표현될 수 있고 bind 함수는 인스턴스의 메소드와 같이 표현될 수 있다.

interface MStatic<T> {
    new(value: T): M<T>;
}

interface M<T> {
    bind<U>(transform: (value: T) => M<U>):M<U>;
}

또한 여기에서 다음 3가지 모나드 법칙을 준수해야 한다.

  1. bind(unit(x), f) ≡ f(x)
  2. bind(m, unit) ≡ m
  3. bind(bind(m, f), g) ≡ bind(m, x => bind(f(x), g))

먼저 앞 두가지 법칙은 unit이 중립적인 요소라는 뜻이다. 세번째 법칙은 bind는 결합이 가능해야 한다는 의미로 결합의 순서가 문제가 되서는 안된다는 의미다. 이 법칙은 덧셈에서 확인할 수 있는 법칙과 같다. 즉, (8 + 4) + 2의 결과는 8 + (4 + 2)와 같은 결과를 갖는다.

아래의 예제에서는 화살표 함수 문법을 사용하고 있다. Firefox (version 31)는 네이티브로 지원하고 있지만 Chrome (version 36)은 아직 지원하지 않는다.

Identity 모나드

identity 모나드는 가장 단순한 모나드로 값을 감싼다. Identity 생성자는 앞서 살펴본 unit과 같은 함수를 제공한다.

function Identity(value) {
  this.value = value;
}

Identity.prototype.bind = function(transform) {
  return transform(this.value);
};

Identity.prototype.toString = function() {
  return 'Identity(' + this.value + ')';
};

다음 코드는 덧셈을 Identity 모나드를 활용해 연산하는 예시다.

var result = new Identity(5).bind(value =>
                 new Identity(6).bind(value2 =>
                     new Identity(value + value2)));

Maybe 모나드

Maybe 모나드는 Identity 모나드와 유사하게 값을 저장할 수 있지만 어떤 값도 있지 않은 상태를 표현할 수 있다.

Just 생성자가 값을 감쌀 때 사용된다.

function Just(value) {
  this.value = value;
}

Just.prototype.bind = function(transform) {
  return transform(this.value);
};

Just.prototype.toString = function() {
  return 'Just(' + this.value + ')';
};

Nothing은 빈 값을 표현한다.

var Nothing = {
  bind: function() {
    return this;
  },
  toString: function() {
    return 'Nothing';
  }
};

기본적인 사용법은 identity 모나드와 유사하다.

var result = new Just(5).bind(value =>
                 new Just(6).bind(value2 =>
                      new Just(value + value2)));

Identity 모나드와 주된 차이점은 빈 값의 전파에 있다. 중간 단계에서 Nothing이 반환되면 연관된 모든 연산을 통과하고 Nothing을 결과로 반환하게 된다.

다음 코드에서는 alert가 실행되지 않게 된다. 그 전 단계에서 빈 값을 반환하기 때문이다.

var result = new Just(5).bind(value =>
                 Nothing.bind(value2 =>
                      new Just(value + alert(value2))));

이 동작은 수치 표현에서 나타나는 특별한 값인 NaN(not-a-number)과도 유사하다. 결과 중간에 NaN 값이 있다면 NaN은 전체 연산에 전파된다.

var result = 5 + 6 * NaN;

Maybe 모나드는 null 값에 의한 에러가 발생하는 것을 막아준다. 다음 코드는 로그인 사용자의 아바타를 가져오는 예시다.

function getUser() {
  return {
    getAvatar: function() {
      return null; // 아바타 없음
    }
  }
}

빈 값을 확인하지 않는 상태로 메소드를 연결해 호출하면 객체가 null을 반환할 때 TypeErrors가 발생할 수 있다.

try {
  var url = getUser().getAvatar().url;
  print(url); // 여기는 절대 실행되지 않음
} catch (e) {
  print('Error: ' + e);
}

대안적으로 null인지 확인할 수 있지만 이 방법은 코드를 장황하게 만든다. 코드는 틀리지 않지만 한 줄의 코드가 여러 줄로 나눠지게 된다.

var url;
var user = getUser();
if (user !== null) {
  var avatar = user.getAvatar();
  if (avatar !== null) {
    url = vatar.url;
  }
}

다른 방식으로 작성할 수 있을 것이다. 비어 있는 값을 만날 때 연산이 정지하도록 작성해보자.

function getUser() {
  return new Just({
    getAvatar: function() {
      return Nothing; // 아바타 없음
    }
  });
}

var url = getUser()
            .bind(user => user.getAvatar())
            .bind(avatar => avatar.url);

if(url instanceof Just) {
  print('URL has value: ' + url.value);
} else {
  print('URL is empty');
}

List 모나드

List 모나드는 값의 목록에서 지연된 연산이 가능함을 나타낸다.

이 모나드의 unit 함수는 하나의 값을 받고 그 값을 yield하는 generator를 반환한다. bind 함수는 transform 함수를 목록의 모든 요소에 적용하고 그 모든 요소를 yield 한다.

function* unit(value) {
  yield value;
}

function* bind(list, transform) {
  for (var item of list) {
    yield* transform(item);
  }
}

배열과 generator는 이터레이션이 가능하며 그 반복에서 bind 함수가 동작하게 된다. 다음 예제는 지연을 통해 각각 요소의 합을 만드는 목록을 어떻게 작성하는지 보여준다.

var result = bind([0, 1, 2], function (element) {
  return bind([0, 1, 2], function* (element2) {
    yield element + element2;
  });
});

for (var item of result) {
  print(item);
}

다음 글은 다른 어플리케이션에서 JavaScript의 generator를 어떻게 활용하는지 보여준다.

Continuation 모나드

Continuation 모나드는 비동기 일감에서 사용한다. ES6에서는 다행히 직접 구현할 필요가 없다. Promise 객체가 이 모나드의 구현이기 때문이다.

  1. Promise.resolve(value) 값을 감싸고 pormise를 반환. (unit 함수의 역할)
  2. Promise.prototype.then(onFullfill: value => Promise) 함수를 인자로 받아 값을 다른 promise로 전달하고 promise를 반환. (bind 함수의 역할)

다음 코드에서는 Unit 함수로 Promise.resolve(value)를 활용했고, Bind 함수로 Promise.prototype.then을 활용했다.

var result = Promise.resolve(5).then(function(value) {
  return Promise.resolve(6).then(function(value2) {
      return value + value2;
  });
});

result.then(function(value) {
    print(value);
});

Promise는 기본적인 continuation 모나드에 여러가지 확장을 제공한다. 만약 then이 promise 객체가 아닌 간단한 값을 반환하면 이 값을 Promise 처리가 완료된 값과 같이 감싸 모나드 내에서 사용할 수 있다.

두번째 차이점은 에러 전파에 대해 거짓말을 한다는 점이다. Continuation 모나드는 연산 사이에서 하나의 값만 전달할 수 있다. 반면 Promise는 구별되는 두 값을 전달하는데 하나는 성공 값이고 다른 하나는 에러를 위해 사용한다. (Either 모나드와 유사하다.) 에러는 then 메소드의 두번째 콜백으로 포착할 수 있으며 또는 이를 위해 제공되는 특별한 메소드 .catch를 사용할 수 있다.

Promise 사용과 관련된 기사는 다음과 같다:

Do 표기법

Haskell은 모나드화 된 코드를 작업하는데 도움을 주기 위해 편리 문법(syntax sugar)인 do 표기법을 제공하고 있다. do 키워드와 함께 시작된 구획은 bind 함수를 호출하는 것으로 번역이 된다.

ES6 generator는 do 표기법을 JavaScript에서 간단하고 동기적으로 보이는 코드로 작성할 수 있게 만든다.

전 예제에서는 maybe 모나드가 다음과 같이 직접 bind를 호출했었다.

var result = new Just(5).bind(value =>
                 new Just(6).bind(value2 =>
                     new Just(value + value2)));

다음은 같은 코드지만 generator를 활용했다. 각각의 호출은 yield로 모나드에서 값을 받는다.

var result = doM(function*() {
  var value = yield new Just(5);
  var value2 = yield new Just(6);
  return new Just(value + value2);
});

이 작은 순서를 generator로 감싸고 그 뒤에 bind를 값과 함께 호출해 yield로 넘겨준다.

function doM(gen) {
  function step(value) {
    var result = gen.next(value);
    if (result.done) {
      return result.value;
    }
    return result.value.bind(step);
  }
  return step();
}

이 방식은 다른 Continuation 모나드와 같은 다른 모나드에서도 사용할 수 있다.

Promise.prototype.bind = Promise.prototype.then;

var result = doM(function*() {
  var value = yield Promise.resolve(5);
  var value2 = yield Promise.resolve(11);
  return value + value2;
}());

result.then(print);

다른 모나드와 같은 방식으로 동작하도록 thenbind로 별칭을 붙였다.

promise에서 generator를 사용하는 예는 Easy asynchrony with ES6를 참고하자.

연결된 호출 Chained calls

다른 방식으로 모나드화 된 코드를 쉽게 만드는 방법은 Proxy를 활용하는 것이다.

다음 함수는 모나드 인스턴스를 감싸 proxy 객체를 반환한다. 이 객체는 값이 있는지 없는지 확인되지 않은 프로퍼티라도 안전하게 접근할 수 있게 만들고 모나드 내에 있는 값을 함수에서 활용할 수 있게 돕는다.

function wrap(target, unit) {
  target = unit(target);
  function fix(object, property) {
    var value = object[property];
    if (typeof value === 'function') {
      return value.bind(object);
    }
    return value;
  }
  function continueWith(transform) {
    return wrap(target.bind(transform), unit);
  }
  return new Proxy(function() {}, {
    get: function(_, property) {
      if(property in target) {
        return fix(target, property);
      }
      return continueWith(value => fix(value, property));
    },
    apply: function(_, thisArg, args) {
      return continueWith(value => value.apply(thisArg, args));
    }
  });
}

이 래퍼는 빈 객체를 참조할 가능성이 있는 경우에 안전하게 접근하는 방법을 제공한다. 이 방식은 실존적 연산자(?.) 구현 방식과 동일하다.

function getUser() {
  return new Just({
    getAvatar: function() {
      return Nothing; // 아바타 없음
    }
  });
}

var unit = value => {
  // 값이 있다면 Maybe 모나드를 반환
  if (value === Nothing || value instanceof Just) {
    return value;
  }
  // 없다면 Just를 감싸서 반환
  return new Just(value);
}

var user wrap(getUser(), unit);

print(user.getAvatar().url);

아바타는 존재하지 않지만 url을 호출하는 것은 여전히 가능하며 빈 값을 반환 받을 수 있다.

동일한 래퍼를 continuation 모나드에서 일반적인 함수를 실행할 때에도 활용할 수 있다. 다음 코드는 특정 아바타를 가지고 있는 친구가 몇명이나 있는지 반환한다. 예제는 보이기엔 모든 데이터를 메모리에 올려두고 사용하는 것 같지만 실제로는 비동기적을 데이터를 가져온다.

Promise.prototype.bind = Promise.prototype.then;

function User(avatarUrl) {
  this.avatarUrl = avatarUrl;
  this.getFriends = function() {
    return Promise.resolve([
      new User('url1'),
      new User('url2'),
      new User('url11'),
    ]);
  }
}

var user = wrap(new User('url'), Prommise.resolve);

var avatarUrls = user.getFriends().map(u => u.avatarUrl);

var length = avatarUrls.filter(url => url.contains('1')).length;

length.then(print);

여기서 모든 프로퍼티의 접근과 함수의 호출은 간단하게 값을 반환하는 것이 아니라 모나드 안으로 진입해 Promise를 실행해 결과를 얻게 된다.

ES6의 Proxies에 대한 자세한 내용은 Array slices를 참고하자.


원본 포스트 https://curiosity-driven.org/monads-in-javascript (CC BY 3.0)

JavaScript에서 커링 currying 함수 작성하기

함수형 프로그래밍에서 자주 쓰이는 curry 함수를 JavaScript에서 구현하는 방법 번역

2015년 7월 21일

요즘 함수형 프로그래밍에 대한 관심이 많아져 여러가지 글을 찾아 읽고 있다. JavaScript에서도 충분히 활용 가능한데다 JS의 내부를 더 깊게 생각해볼 수 있고 다른 각도로 문제를 사고해보는데 도움이 되는 것 같아 한동안은 이와 관련된 포스트를 번역하려고 한다.

커링(currying)은 함수형 프로그래밍 기법 중 하나로 함수를 재사용하는데 유용하게 쓰일 수 있는 기법이다. 커링이 어떤 기법인지, 어떤 방식으로 JavaScript에서 구현되고 사용할 수 있는지에 대한 글이 있어 번역했다. 특히 이 포스트는 함수를 작성하고 실행하는 과정을 하나씩 살펴볼 수 있어 좋았다.

원본은 Kevin Ennis의 Currying in JavaScript에서 확인할 수 있다.


나는 최근 함수형 프로그래밍에 대해 생각을 많이 하게 되었다. 그러던 중 curry 함수를 작성하는 과정을 공유하면 재미있을 것 같다는 생각이 들었다.

처음 듣는 사람을 위해 간단히 설명하면, 커링은 함수 하나가 n개의 인자를 받는 과정을 n개의 함수로 각각의 인자를 받도록 하는 것이다. 부분적으로 적용된 함수를 체인으로 계속 생성해 결과적으로 값을 처리하도록 하는 것이 그 본질이다.

어떤 의미인지 다음 코드를 보자:

function volume( l, w, h ) {
  return l * w * h;
}

var curried = curry( volume );

curried( 1 )( 2 )( 3 ); // 6

면책 조항

이 포스트는 기본적으로 클로저와 Function#apply()와 같은 고차함수에 익숙한 것을 가정하고 작성했다. 이런 개념에 익숙하지 않다면 더 읽기 전에 다시 복습하자.

curry 함수 작성하기

앞서 코드에서 볼 수 있듯 curry는 인자로 함수를 기대하기 때문에 다음과 같이 작성한다.

function curry( fn ) {

}

다음으로 얼마나 많은 인자가 함수에서 필요로 할지 알아야 한다. (인자의 갯수 arity 라고 부른다.) 인자의 갯수를 알기 전까지 몇 번이나 새로운 함수를 반환하고, 어느 순간에 함수 대신 값을 반환하게 될지 알 수가 없다.

함수에서 몇개의 인자를 기대하는지 length 프로퍼티를 통해 확인할 수 있다.

function curry( fn ) {
  var arity = fn.length;
}

이제 여기서부터 약간 복잡해진다.

기본적으로는, 매번 curry된 함수를 호출할 때마다 새로운 인자를 배열에 넣어 클로저 내에 저장해야 한다. 그 배열에 있는 인자의 수는 원래 함수에서 기대했던 인자의 수와 동일해야 하며, 그 이후 호출 가능해야 한다. 다를 때엔 새로운 함수로 반환한다.

이런 작업을 하기 위해 (1) 인자 목록을 가질 수 있는 클로저가 필요하고 (2) 전체 인자의 수를 확인할 수 있는 함수와 함께, 부분적으로 적용된 함수를 반환 또는 모든 인자가 적용된 원래의 함수에서 반환되는 값을 반환해야 한다.

여기서는 resolver라 불리는 함수를 즉시 실행한다.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {

  }());
}

이제 resolver 함수와 함께 해야 할 첫번째 일은 지금까지 입력 받은 모든 인자를 복사하는 것이다. Array#slice 메소드를 이용, arguments의 사본을 memory라는 변수에 저장한다.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {
    var memory = Array.prototype.slice.call( arguments );
  }());
}

다음으로 resolver가 함수를 반환하게 만들어야 한다. 함수 외부에서 curry된 함수를 호출하게 될 때 접근할 수 있게 되는 부분이다.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {
    var memory = Array.prototype.slice.call( arguments );
    return function() {

    };
  }());
}

이 내부 함수가 실제로 호출이 될 때마다 인자를 받아야 한다. 또한 이 추가되는 인자를 memory에 저장해야 한다. 그러므로 먼저 slice()를 호출해 memory의 복사본을 만들자.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {
    var memory = Array.prototype.slice.call( arguments );
    return function() {
      var local = memory.slice();
    };
  }());
}

이제 새로운 인자를 Array#push로 추가한다.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {
    var memory = Array.prototype.slice.call( arguments );
    return function() {
      var local = memory.slice();
      Array.prototype.push.apply( local, arguments );
    };
  }());
}

좋다. 이제까지 받은 모든 인자를 새로운 배열에 포함하고 있으며 부분적으로 적용된 함수를 연결(chain)하고 있다.

마지막으로 할 일은 지금까지 받은 인자의 갯수를 실제로 curry된 함수의 인자 수와 맞는지 비교해야 한다. 길이가 맞다면 원래의 함수를 호출하고 그렇지 않다면 resolver가 또 다른 함수를 반환해 인자 수에 맞게 더 입력 받아 memory에 저장할 수 있어야 한다.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {
    var memory = Array.prototype.slice.call( arguments );
    return function() {
      var local = memory.slice();
      Array.prototype.push.apply( local, arguments );
      next = local.length >= arity ? fn : resolver;
      return next.apply( null, local );
    };
  }());
}

지금까지 작성한 내용을 앞서 보여줬던 예제와 함께 순서대로 살펴보자.

function volume( l, w, h ) {
  return l * w * h;
}

var curried = curry( volume );

curriedvolume 함수를 앞서 작성한 curry 함수에 넣은 결과가 된다.

여기서 무슨 일이 일어났는지 다시 살펴보면:

  1. volume의 인자 수 즉, 3을 arity에 저장했다.
  2. resolver를 인자 없이 바로 실행했고 그 결과 memory 배열은 비어 있다.
  3. resolver는 익명 함수를 반환했다.

여기까지 이해가 된다면 curry된 함수를 호출하고 길이를 넣어보자.

function volume( l, w, h ) {
  return l * w * h;
}

var curried = curry( volume );
var length = curried( 2 );

여기서 진행된 내용을 살펴보면 다음과 같다:

  1. 여기서 실제로 호출한 것은 resolver에 의해 반환된 익명 함수다.
  2. memory(아직은 비어 있음)를 local에 복사한다.
  3. 인자 (2)를 local 배열에 추가한다.
  4. local의 길이가 volume의 인자 갯수보다 적으므로, 지금까지의 인자 목록과 함께 resolver를 다시 호출한다. 새로운 memory 배열과 함께 새로 생성된 클로저는 첫번째 인자로 2를 포함한다.
  5. 마지막으로, resolver는 클로저 바깥에서 새로운 memory 배열에 접근할 수 있도록 새로운 함수를 반환한다.

이 과정으로 내부에 있던 익명 함수를 다시 반환한다. 하지만 이번에는 memory 배열은 비어 있지 않다. 앞서 입력한, 첫번째 인자인 (인자 2)가 내부에 있다.

앞서 만든 length 함수를 다시 호출한다.

function volume( l, w, h ) {
  return l * w * h;
}

var curried = curry( volume );
var length = curried( 2 );
var lengthAndWidth = length( 3 );
  1. 여기서 호출한 것은 resolver에 의해 반환된 익명 함수다.
  2. resolver는 앞에서 입력한 인자를 포함하고 있다. 즉 배열 2 를 복사해 local에 저장한다.
  3. 새로운 인자인 3local 배열에 저장한다.
  4. 아직도 local의 길이가 volume의 인자 갯수보다 적으므로, 지금까지의 인자 목록과 함께 resolver를 다시 호출한다. 그리고 이전과 동일하게 새로운 함수를 반환한다.

이제 lengthAndWidth 함수를 호출해 값을 얻을 차례다.

function volume( l, w, h ) {
  return l * w * h;
}

var curried = curry( volume );
var length = curried( 2 );
var lengthAndWidth = length( 3 );

console.log( lengthAndWidth( 4 ) ); // 24

여기서의 순서는 이전과 약간 다르다.

  1. 다시 여기서 호출한 함수는 resolver에서 반환된 익명 함수다.
  2. resolver는 앞에서 입력한 인자를 포함한다. 배열 [ 2, 3 ]를 복사해 local에 저장한다.
  3. 새로운 인자인 4local 배열에 저장한다.
  4. 이제 local의 길이가 volume의 인자 갯수와 동일하게 3을 반환한다. 그래서 새로운 함수를 반환하는 대신 지금까지 입력 받아서 저장해둔 모든 인자와 함께 volume 함수를 호출해 결과를 반환 받는다. 그 결과로 24 라는 값을 받게 된다.

정리

아직 이 커링 기법을 필수적으로 적용해야만 하는 경우를 명확하게 찾지는 못했다. 하지만 이런 방식으로 함수를 작성하는 것은 함수형 프로그래밍에 대한 이해를 향상할 수 있는 좋은 방법이고 클로저와 1급 클래스 함수와 같은 개념을 강화하는데 도움을 준다.

현재 Project Decibel에서 구인중이다. 보스턴 지역에서 이런 JavaScript 일을 하고 싶다면 이메일을 부탁한다.

그리고 이 포스트가 좋다면 내 트위터를 구독하라. 다음 한 달 중 하루는 글을 쓰기 위해 노력할 예정이다.


새로운 개념을 배워가는 과정에서 JavaScript의 새 면모를 배우게 되는 것 같아 요즘 재미있게 읽게 되는 글이 많아지고 있다. 지금 회사에서 JS를 front-end에서 제한적으로 사용하고 있는 수준이라서 아쉽다는 생각이 들 때도 많지만 이런 포스트를 통해 매일 퍼즐을 풀어가는 기분이라 아직도 배워야 할 부분이 많구나 생각하게 된다.

벌써 2015년도 반절이 지났다. 여전히 어느 것 하나 깊게 알고 있는 것이 없는 기분이라 아쉬운데 남은 한 해는 겉 알고 있는 부분을 깊이있게 접근할 수 있는 끈기를 챙길 수 있었으면 좋겠다.