tag: 개발 이야기

AngularJS의 서비스와 팩토리

같은 듯 다른 서비스와 팩토리 구분해서 사용하기. Factory: I'm your father. Service: Noooo!!

2015년 11월 17일

AngularJS의 서비스 Services는 여러 코드에서 반복적으로 사용되는 코드를 분리할 때 사용하는 기능으로, 해당 서비스가 필요한 곳에 의존성을 주입해 활용할 수 있다. 서비스는 다음과 같은 특성이 있다.

  • 지연 초기화(Lazily instantiated): 의존성으로 주입하기 전까지는 초기화가 되지 않음.
  • 싱글턴(Singletons): 각각의 컴포넌트에서 하나의 인스턴스를 싱글턴으로 참조함.

AngularJS에서 서비스(Service)와 팩토리(factory)는 서로 상당한 유사성을 갖고 있기 때문에 쉽게 혼동할 수 있다. 특히 JavaScript의 유연한 타입으로 인해 라이브러리의 의도와는 다르게 그냥 동작하는 경우가 많다. 이 두 가지의 차이는 코드에서 확인할 수 있다. Angular의 코드를 보면 service는 factory를 사용해서 구현하고 있다.

  function service(name, constructor) {
    return factory(name, ['$injector', function($injector) {
      return $injector.instantiate(constructor);
    }]);
  }

위 코드를 보면 $injector.instaniate()에 생성자를 넣어 반환하는데 이 함수에서 Object.create()로 해당 생성자를 인스턴스화 한다. 이렇게 얻은 인스턴스를 factory에 넣어 나머지는 factory와 동일하게 처리하는 것을 확인할 수 있다. 그래서 라이브러리의 실제 의도와는 다른 구현도 문제 없이 구동될 수 있는 것이다.

Todd Motto의 AngularJS 스타일 가이드 중 Service and Factory을 살펴보면 이 구현의 차이를 다음과 같이 정리한다.

서비스와 팩토리

Angular의 모든 서비스는 싱글턴 패턴이다. .service()메소드와 .factory() 메소드의 차이는 객체를 생성하는 방법에서 차이가 있다.

서비스: 생성자 함수와 같이 동작하고 new 키워드를 사용해 인스턴스를 초기화 한다. 서비스는 퍼블릭 메소드와 변수를 위해 사용한다.

function SomeService () {
  this.someMethod = function () {
    // ...
  };
}
angular
  .module('app')
  .service('SomeService', SomeService);

팩토리: 비지니스 로직 또는 모듈 제공자로 사용한다. 객체나 클로저를 반환한다.

객체 참조에서 연결 및 갱신을 처리하는 방법으로 인해 노출식 모듈 패턴(Revealing module pattern) 대신 호스트 객체 형태로 반환한다.

function AnotherService () {
  var AnotherService = {};
  AnotherService.someValue = '';
  AnotherService.someMethod = function () {
    // ...
  };
  return AnotherService;
}
angular
  .module('app')
  .factory('AnotherService', AnotherService);

왜?: 노출식 모듈 패턴을 사용하면 초기값을 변경할 수 없는 경우가 있기 때문이다. 1


서비스와 팩토리에서 가장 두드러진 차이점을 꼽는다면, 서비스에서는 초기화 과정이 존재하기 때문에 자연스럽게 prototype 상속이 가능하다. 그래서 일반적으로 상속이 필요한 데이터 핸들링이나 모델링 등의 경우에는 서비스를 활용하고, helper나 정적 메소드와 같이 활용되는 경우는 팩토리로 구현을 많이 하는 것 같다.

물론 앞서 살펴본 것과 같이 둘은 아주 유연한 관계이기 때문에 서비스에서 일반 호스트 객체를 반환하면 팩토리와 다를 것이 없게 된다. 그래서 각각의 특징에 맞게 구현하기 위해 가이드라인을 준수하는게 바람직하다. 가이드라인을 따르지 않는다면 적어도 프로젝트 내에서 일정한 프로토콜을 준수할 수 있도록 합의가 필요하다.

서비스와 팩토리처럼 구현의 제한성이 있는 것이 싫다면 강력한 기능을 제공하는 프로바이더(Provider)를 사용할 수 있다. (factory는 provider를 쓴다.) AngularJS에서 흔히 사용하는 $http가 대표적이며 많은 기능이 프로바이더로 구현되어 있다.

  • 팩토리를 작성하는 방법을 설명하는 글을 보면 노출식 모듈 패턴을 활용하는 경우가 종종 있어서 왜? 부분이 추가된 것 같다. 이 패턴은 일부 구현(메소드, 변수)에 대해 외부에서 접근할 수 있는지 없는지 명시적으로 지정할 수 있다는 특징이 있는데 그 특징으로 외부에서 접근할 수 없는 코드에 대해서는 값을 변경할 방법이 없다. 그런 특징 때문에 가이드에서는 호스트 객체로 반환할 것을 권장하고 있다. 
  • 노출식 모듈 패턴 Revealing Module Pattern

    JavaScript 디자인 패턴, 명시적으로 노출될 메소드를 지정하는 디자인.

    2015년 11월 17일

    Carl Danley의 글 The Revealing Module Pattern을 요약 번역한 글이다. Todd의 Angular 스타일 가이드를 읽는 중 factory를 노출식 모듈 패턴으로 작성하라는 얘기가 있어서 찾아봤다.


    노출식 모듈 패턴 Revealing Module Pattern

    이 패턴은 모듈 패턴과 같은 개념으로 public과 private 메소드에 초점을 둔 패턴. 모듈 패턴과 달리 명시적으로 노출하고 싶은 부분만 정해서 노출하는 방식. 일반적으로 객체 리터럴({...}) 형태로 반환한다.

    장점

    • 개발자에게 깔끔한 접근 방법을 제공
    • private 데이터 제공
    • 전역 변수를 덜 더럽힘
    • 클로저를 통해 함수와 변수를 지역화
    • 스크립트 문법이 더 일관성 있음
    • 명시적으로 public 메소드와 변수를 제공해 명시성을 높임

    단점

    • private 메소드 접근할 방법이 없음 (이런 메소드에 대한 테스트의 어려움을 이야기하기도 하지만 함수 무결성을 고려할 때 공개된 메소드만 테스트 하는게 맞음. 관련 없지만 기록용으로.)
    • private 메소드에 대해 함수 확장하는데 어려움이 있음
    • private 메소드를 참조하는 public 메소드를 수정하기 어려움

    예제

    var myModule = (function(window, undefined) {
      function myMethod() {
        console.log('myMethod');
      }
    
      function myOtherMethod() {
        console.log('myOtherMethod');
      }
    
      return {
        someMethod: myMethod,
        someOtherMethod: myOtherMethod
      };
    })(window);
    
    myModule.myMethod(); // Uncaught TypeError: myModule.myMethod is not a function
    myModule.myOtherMethod(); // Uncaught TypeError: myModule.myOtherMethod is not a function
    myModule.someMethod(); // console.log('myMethod');
    myModule.someOtherMethod(); // console.log('myOtherMethod');

    Host 객체(JS에 내장된 객체가 아닌 사용자가 직접 정의한 객체)로 반환하는 형태는 관리하기 까다롭고 상속과 같은 방법으로 확장하기 어려워서 개인적으로 선호하지 않는 편이다. 하지만 Angular의 factory와 같이, 일종의 스태틱 클래스에서는 잘 어울리는 접근 방식이다. 패턴은 상황에 맞게 적용해야 한다.

    Angular 1.5의 새 기능, .component() 알아보기

    2015년 11월 15일

    Todd Motto의 글 Exploring the Angular 1.5 .component() method를 번역한 글이다. 아직 1.5 beta 1이라서 아직 한참 출시 전이긴 하지만 이 글에서 확인할 수 있는 변화는 크게 달라질 것 같지 않다. 이 글 후반부에서 Component() 메소드 구현을 붙여놓은 부분이 있는데 하위 호환성을 지키면서 상위 기능을 개발하는 방식이 어떤가 생각하고 읽으면 더 재미있게 느껴지는 것 같다.


    Angular 1.5의 새 기능, .component() 알아보기

    Angular 1.5에서는 component()라는 헬퍼 메소드를 소개하고 있다. directive() 메소드를 사용한 정의에 비해 훨씬 간단하게 일반적인 기본 동작과 모범적인 예를 활용할 수 있게 지원한다. .component()를 사용하는 것으로 Angular 2의 스타일을 사용해 작성할 수 있으며 차후 버전에도 적합하다.

    .directive()메소드와 .component() 메소드에서 사용하는 문법을 비교해보고 component()가 제공하는 멋진 추상을 살펴보자.

    노트: Angular 1.5는 여전히 beta므로 이 버전의 출시를 눈여겨 보자.

    .directive() 에서 .component() 로

    이 문법 변경은 아주 간단하다:

    // before
    module.directive(name, fn);
    
    // after
    module.component(name, options);

    name 인자는 컴포넌트로 정의하고 싶은 이름이며, options 인자에는 함수를 넣었던 1.4 그 이하 버전에 문법과 달리 이 컴포넌트에 대한 객체 형태의 정의가 들어간다.

    간단한 counter 컴포넌트를 미리 만들었는데 Angular 1.4.x에서 사용한 문법을 .component() 메소드를 활용해 v1.5.0에 맞게 리팩토링 하는 과정을 보여주려고 한다.

    .directive('counter', function counter() {
      return {
        scope: {},
        bindToController: {
          count: '='
        },
        controller: function () {
          function increment() {
            this.count++;
          }
          function decrement() {
            this.count--;
          }
          this.increment = increment;
          this.decrement = decrement;
        },
        controllerAs: 'counter',
        template: [
          '<div class="todo">',
            '<input type="text" ng-model="counter.count">',
            '<button type="button" ng-click="counter.decrement();">-</button>',
            '<button type="button" ng-click="counter.increment();">+</button>',
          '</div>'
        ].join('')
      };
    });

    1.4.x 디렉티브의 라이브 코드는 여기서 확인할 수 있다.

    Angular 1.4 버전에서 만든 이 버전을 변경하며 그 변화를 살펴보기로 한다.

    함수를 객체로, 메소드 이름의 변화

    function 인자를 Object로 변경하는 것을 먼저 해보자. 그리고서 이름을 .directive()에서 .component()로 변경한다:

    // before
    .directive('counter', function counter() {
      return {
        
      };
    });
    
    // after
    .component('counter', {
        
    });

    간단하고 좋다. .directive()에서 return {};가 필수적이었던 반면 .component()의 객체 사용은 훨씬 단순하게 보인다.

    "scope"와 "bindToController"를 "bindings"로 변경

    .directive() 메소드에서 scope 프로퍼티는 $scope를 고립할지 혹은 상속할지에 대한 정의를 위해 활용했는데 대부분의 경우 기본적으로 모든 스코프가 고립된 형태로 정의를 했다. 그래서 매번 디렉티브를 만들 때마다 과도하게 스코프를 매번 정의해야 하는 불편함이 있었다. bindToController가 소개된 후, 프로퍼티를 스코프에 넘기는지, 또는 컨트롤러에 바로 연결하는지를 명시적으로 선언할 수 있게 되었다.

    .component()bindings 프로퍼티는 이런 반복적인 기초 작업을 제거했다. 컴포넌트가 고립된 스코프를 갖는다는 가정을 기본적으로 하고서, 간단하게 컴포넌트에 내려주고 싶은 데이터만 정의해주면 된다.

    // before
    .directive('counter', function counter() {
      return {
        scope: {},
        bindToController: {
          count: '='
        }
      };
    });
    
    // after
    .component('counter', {
      bindings: {
        count: '='
      }
    });

    Controller와 controllerAs의 변경

    controller의 정의는 변경된 바가 없지만 좀 더 똑똑해졌다.

    컴포넌트에 컨트롤러를 그 자리에서 선언할 때는 이렇게 작성했을 것이다:

    // 1.4
    {
      ...
      controller: function () {}
      ...
    }

    컨트롤러를 다른 곳에서 정의했을 때는 이렇게 했을 것이다:

    // 1.4
    {
      ...
      controller: 'SomeCtrl'
      ...
    }

    controllerAs를 선언하고 싶을 때는, 새로운 프로퍼티를 추가해서 인스턴스의 별칭을 지정해야 했다.

    // 1.4
    {
      ...
      controller: 'SomeCtrl',
      controllerAs: 'something'
      ...
    }

    이 과정으로 template 내에서 컨트롤러의 인스턴스를 활용할 때 something.prop을 사용하는 것이 가능해졌다.

    이제 .component()으로 변경되면서 controllerAs 프로퍼티를 센스있게 추측해서 자동으로 생성한다. 다음 코드에서 볼 수 있는 것처럼 사용 가능성이 있는 다음 세가지 이름을 자동으로 배정해준다:

    // inside angular.js
    controllerAs: identifierForController(options.controller) || options.controllerAs || name,

    identifierForController 함수가 컨트롤러의 이름을 추측하는 방법은 다음과 같다:

    // inside angular.js
    var CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/;
    function identifierForController(controller, ident) {
      if (ident && isString(ident)) return ident;
      if (isString(controller)) {
        var match = CNTRL_REG.exec(controller);
        if (match) return match[3];
      }
    }

    이 함수로 .component()에서 다음과 같은 문법을 사용할 수 있게 된다:

    // 1.5
    {
      ...
      controller: 'SomeCtrl as something'
      ...
    }

    이 기능이 controllerAs 프로퍼티를 추가하지 않게 만든다... 하지만...

    controllerAs 프로퍼티를 하위 호환성을 위해, 또는 디렉티브/컴포넌트를 작성하는 스타일을 유지하기 위해 계속 사용할 수도 있다.

    세번째 선택지로는 그렇게 좋은 방법은 아니지만 controllerAs를 완벽하게 다 지워버리고 Angular가 자동으로 해당 컴포넌트의 이름을 별칭으로 사용하게 하는 방법을 사용 할 수 있다. 예를 들면:

    .component('test', {
      controller: function () {
        this.testing = 123;
      }
    });

    여기서 controllerAs의 정의는 자동으로 test가 된다. 그래서 template에서 test.testing을 사용하면 123 값으로 반환할 것이다.

    이 방법으로 controller를 추가하고 앞서 작성한 디렉티브를 컴포넌트로 변경하며 controllerAs 프로퍼티를 제거할 수 있다:

    // before
    .directive('counter', function counter() {
      return {
        scope: {},
        bindToController: {
          count: '='
        },
        controller: function () {
          function increment() {
            this.count++;
          }
          function decrement() {
            this.count--;
          }
          this.increment = increment;
          this.decrement = decrement;
        },
        controllerAs: 'counter'
      };
    });
    
    // after
    .component('counter', {
      bindings: {
        count: '='
      },
      controller: function () {
        function increment() {
          this.count++;
        }
        function decrement() {
          this.count--;
        }
        this.increment = increment;
        this.decrement = decrement;
      }
    });

    변경된 방법으로 정의하고 사용하는 것이 훨씬 간단하다.

    템플릿

    template에도 적어둘 만한, 세밀한 변화가 있다. template 프로퍼티를 추가하고 어떤지 확인하자.

    .component('counter', {
      bindings: {
        count: '='
      },
      controller: function () {
        function increment() {
          this.count++;
        }
        function decrement() {
          this.count--;
        }
        this.increment = increment;
        this.decrement = decrement;
      },
      template: [
        '<div class="todo">',
          '<input type="text" ng-model="counter.count">',
          '<button type="button" ng-click="counter.decrement();">-</button>',
          '<button type="button" ng-click="counter.increment();">+</button>',
        '</div>'
      ].join('')
    });

    template 프로퍼티는 이제 함수로 정의해서 $element$attrs를 주입하는 형태로 사용할 수 있다. 만약 template 프로퍼티에 함수를 넣으면 컴파일 할 수 있는 HTML 문자열을 반환해야 한다.

    {
      ...
      template: function ($element, $attrs) {
        // access to $element and $attrs
        return [
          '<div class="todo">',
            '<input type="text" ng-model="counter.count">',
            '<button type="button" ng-click="counter.decrement();">-</button>',
            '<button type="button" ng-click="counter.increment();">+</button>',
          '</div>'
        ].join('')
      }
      ...
    }

    동작하는 예제를 확인하자. Angular 버전 v1.5.0-build.4376+sha.aff74ec 예제다:

    여기까지 디렉티브를 컴포넌트로 변경하는 과정이었다. 여기서 끝내기 전에 살펴봐야 하는 변경점이 몇가지 더 있다.

    끼워넣기가 가정되어 있음 Assumed transclusion

    컴포넌트는 기본적으로 끼워넣기(Transclusion)를 가정하고 있는데, 다음 Angular 코드에 의해 설정된다:

    // angular.js
    {
      ...
      transclude: options.transclude === undefined ? true : options.transclude
      ...
    }

    이 기능을 끄고 싶다면 transclude: false로 정의하면 된다.

    고립된 스코프 끄기

    컴포넌트는 스코프 고립이 기본값이다. .component()에서 이 설정을 바꾸고 싶다면 간단하게 프로퍼티로 정의하면 된다:

    .component('counter', {
      isolate: false
    });

    다음 Angular의 삼항 연산자에 따라서 자동으로 scope에 빈 객체를 넣게 된다. .directive()에서 상속하는 방식대로 사용하고 싶다면 고립된 스코프를 끄면 된다. 그러면 scope: true로 동작한다. 내부 코드는 다음과 같다:

    {
      ...
      scope: options.isolate === false ? true : {}
      ...
    }

    비교를 위한 소스코드

    이 글 내내 Angular 소스 코드 스니핏을 교차 레퍼런스로 활용했다. 코드를 보고 싶다면 여기에서 확인하거나 아래 코드를 확인해보면 좋겠다. 정말 좋은 추상화 구현이다:

    component: function(name, options) {
      function factory($injector) {
        function makeInjectable(fn) {
          if (angular.isFunction(fn)) {
            return function(tElement, tAttrs) {
              return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs});
            };
          } else {
            return fn;
          }
        }
    
        var template = (!options.template && !options.templateUrl ? '' : options.template);
        return {
          controller: options.controller || function() {},
          controllerAs: identifierForController(options.controller) || options.controllerAs || name,
          template: makeInjectable(template),
          templateUrl: makeInjectable(options.templateUrl),
          transclude: options.transclude === undefined ? true : options.transclude,
          scope: options.isolate === false ? true : {},
          bindToController: options.bindings || {},
          restrict: options.restrict || 'E'
        };
      }
    
      if (options.$canActivate) {
        factory.$canActivate = options.$canActivate;
      }
      if (options.$routeConfig) {
        factory.$routeConfig = options.$routeConfig;
      }
      factory.$inject = ['$injector'];
    
      return moduleInstance.directive(name, factory);
    }

    다시 말하지만 Angular 1.5는 아직 릴리즈되지 않았다. 그래서 이 글에서 사용한 API는 아마 조금은 달라질 수 있다.

    Angular 2 로 업그레이드하기

    .component()를 사용해서 작성하는 스타일은 추후 Angular 2 로 옮기는데 도움이 된다. Angular 2의 문법에서 컴포넌트는 ECMAScript 5와 새로운 템플릿 문법을 활용하고 있긴 하지만 말이다:

    var Counter = ng
    .Component({
      selector: 'counter',
      template: [
        '<div class="todo">',
          '<input type="text" [(ng-model)]="count">',
          '<button type="button" (click)="decrement();">-</button>',
          '<button type="button" (click)="increment();">+</button>',
        '</div>'
      ].join('')
    })
    .Class({
      constructor: function () {
        this.count = 0;
      },
      increment: function () {
        this.count++;
      },
      decrement: function () {
        this.count--;
      }
    });

    Angular 디렉티브 때려잡기: 구조와 MVVM

    2015년 11월 12일

    이 글은 Todd Motto의 글 Killing it with Angular Directives; Structure and MVVM를 번역한 글이다.

    Angular에서 디렉티브를 어떤 방식으로 사용해야 하는지 예제를 통해 설명하고 있다. Angular에서 각각 코드 사이의 관계를 분리하는 방식은 Angular만의 방식이 아닌 MVVM 패턴을 활용하고 있다. 디렉티브의 구조를 어떤 방식으로 작성하는지, linkcontroller, 그리고 외부에서 서비스를 주입하는 것으로 각각 로직을 어떻게 분리하는지 이해하는데 도움되는 글이다.


    Angular 디렉티브 때려잡기: 구조와 MVVM

    이 포스트에서는 Angular 1.x에서 어떻게 디렉티브(Directives)를 작성하는가에 대한 원칙을 설명하려고 한다. 디렉티브를 어떻게, 왜, 어디에서 사용해야 하는지에 대한 혼란이 많다. 하지만 이 개념을 한번만 이해하고 구분하면 아주 간단한 기능이다. 이 포스트는 중첩된 디렉티브 또는 부모 스코프에서 흐르는 데이터 흐름과 같은 내용을 다루지 않는다. 대신 디렉티브를 만들고 구조화하고, 관계를 분리하는데 가장 이상적인 방법과 함께, controllerlink 프로퍼티를 어떻게 올바르게 사용하는가에 대해 다루려고 한다.

    기초적인 디렉티브, 구조, 그리고 Angular에서 사용하기 가장 좋은 방식으로 구조화하는 방법에 대해 다룬다. 디렉티브 작성에 대한 접근 방식을 보여주기 위해 모조 "파일 업로드" 디렉티브를 만들어보자.

    노트, 이 코드는 실제로 동작하지 않으며, 효과적으로 디렉티브를 구조화 하는 방법에 대해 설명하려는 의도로 작성했다.

    구조 Structure

    AngularJS 스타일 가이드를 따라 자연스럽게 기초적인 디렉티브 정의를 작성하고 Angular의 .directive() 메소드에 넣는다:

    function fileUpload () {
      
      return {};
    
    }
    angular
      .module('app')
      .directive('fileUpload', fileUpload);
    
    적당한 위치에 정의를 작성했으니 파일 업로드 컴포넌트를 위한 기본적인 프로퍼티를 추가한다:
    
    function fileUpload () {
      
      return {
        restrict: 'E',
        scope: {},
        template: [
          '<div>',
          '</div>'
        ].join(''),
        controllerAs: 'vm',
        controller: function () {},
        link: function () {}
      };
    
    }
    angular
      .module('app')
      .directive('fileUpload', fileUpload);

    이 코드가 내가 기본적으로 필요로 하는 모든 요소가 포함된 "디렉티브 보일러플레이트"로, 이 코드를 기초로 동작하는 디렉티브를 만든다.

    컨트롤러 Controller (presentational layer)

    컨트롤러를 controller: fn처럼 객체에 바인딩하는 대신 (link에도 동일), controller 프로퍼티를 fileUpload 함수 정의 내에서 바인딩을 먼저 한 후, 객체에 연결해 반환받는 형태로 작성한다. 이 방식으로 작성하면 함수를 객체에 직접 작성하는 방식보다 함수를 정의하는 공간이 있어서 연관된 함수를 더 쉽게 찾고 이해할 수 있고, 함수에 주석을 작성하는데 더 나은 구조가 된다. 이 형태는 "엄격해 보이는 API"에 묶여 있는 느낌보다 평범한 JavaScript처럼 표현된다.

    함수를 맨 위에 작성하고 주석 몇 개를 작성한다.

    /**
     * @name fileUpload
     * @desc <file-upload> Directive
     */
    function fileUpload () {
    
      /**
       * @name fileUploadCtrl
       * @desc File Upload Controller
       * @type {Function}
       */
      function fileUploadCtrl() {
    
      }
    
      /**
       * @name link
       * @desc File Upload Link
       * @type {Function}
       */
      function link() {
    
      }
    
      return {
        restrict: 'E',
        scope: {},
        template: [
          '<div>',
          '</div>'
        ].join(''),
        controllerAs: 'vm',
        controller: fileUploadCtrl
        link: link
      };
    
    }
    angular
      .module('app')
      .directive('fileUpload', fileUpload);

    멋진가? 당연하다. controllerAs: 'vm'에서 볼 수 있듯 컨트롤러를 vm이란 별칭으로 지정했다. (뷰모델 ViewModel을 뜻한다.) 이렇게 컨트롤러를 뷰모델로 다루는 것은 "프리젠테이션 모델 Presentation Model" 디자인 패턴에 해당한다. 이 문법에 익숙하지 않다면 ControllerAs(번역)에 대해 먼저 읽어보자. $scope를 필수적으로 주입하는 방식 대신 컨트롤러 자체를 $scopevm 별칭에 바인딩하는 방식, 즉 $scope.vm을 생성하게 된다. $scope 대신 this 키워드를 사용하는 것으로 이 뷰-모델을 마치 컨트롤러 "클래스"인 것처럼 작성할 수 있다.

    /**
     * @name fileUploadCtrl
     * @desc File Upload Controller
     * @type {Function}
     */
    function fileUploadCtrl() {
      this.files = [];
      this.uploadFiles = function () {
    
      };
    }

    this를 사용하는 것이 $scope 쓰는 것보다 훨씬 낫게 보인다. $scope$on 이벤트나 $watch를 사용하는 경우에나 필요하다. 이런 방식으로 컨트롤러 클래스인 "뷰모델"을 조금 다르게 작성할 수 있다.

    기본적인 함수 작성은 다 끝났다. 하지만 나는 "exports" 스타일을 더 선호하고, 모든 함수와 변수가 바인딩되고, 어떤 적절한 주석이든 작성할 수 있는 형태가 좋다. 이 모든 것을 염두해서 작성하면 다음과 같다:

    /**
     * @name fileUpload
     * @desc <file-upload> Directive
     */
    function fileUpload () {
    
      /**
       * @name fileUploadCtrl
       * @desc File Upload Controller
       * @type {Function}
       */
      function fileUploadCtrl() {
    
        /**
         * @name files
         * @desc Contains all files passed in by the user
         * @type {Array}
         */
        var files = [];
    
        /**
         * @name uploadFiles
         * @desc Uploads our files
         * @type {Array}
         */
        function uploadFiles() {
    
        }
    
        // exports
        this.files = files;
        this.uploadFiles = uploadFiles;
    
      }
    
      /**
       * @name link
       * @desc File Upload Link
       * @type {Function}
       */
      function link() {
    
      }
    
      return {
        restrict: 'E',
        scope: {},
        template: [
          '<div>',
          '</div>'
        ].join(''),
        controllerAs: 'vm',
        controller: fileUploadCtrl
        link: link
      };
    
    }
    angular
      .module('app')
      .directive('fileUpload', fileUpload);

    템플릿 융화 Template integration

    다음 순서로 <input type=file> 같이 파일 업로드를 위한 엘리먼트를 작성하고 모델에 연결해야 한다. 앞서 작성한 디렉티브에 ng-model 어트리뷰트와 값을 추가하자. 또한 ng-change와 함께 "업로드" 버튼을 추가하자. (그렇다. form이라면 ng-submit을 사용해야 하겠지만 간단하게 작성하기로 한다.)

    // ...
    template: [
      '<div>',
        '<input type="file" ng-model="vm.files">',
        '<button type="button" ng-change="vm.uploadFiles(vm.files);">',
      '</div>'
    ].join('')
    // ...

    이 컨트롤러에 어떻게 업로드를 다뤄야 하는지에 대한 주석을 작성하자. UploadService을 추가했고 (좋은 이름이다) 이 서비스의 의존성이 컨트롤러에 주입될 수 있도록 fileUploadCtrl에 매개변수로 추가했다.

    /**
     * @name fileUploadCtrl
     * @desc File Upload Controller
     * @type {Function}
     */
    function fileUploadCtrl(UploadService) {
    
      /**
       * @name files
       * @desc Contains all files passed in by the user
       * @type {Array}
       */
      var files = [];
    
      /**
       * @name uploadFiles
       * @desc Uploads our files
       * @type {Array}
       */
      function uploadFiles(files) {
        // hand off our files to a Service
        UploadService
        .uploadFiles(files)
        .then(function (response) {
          // success, we could get our file Object back
          // and render it in the View for the user
          // maybe some ng-repeat with a list of files inside
        }, function (reason) {
          // error stuff if not handled globally
        })
      }
    
      // exports
      this.files = files;
      this.uploadFiles = uploadFiles;
    
    }

    잠깐, 별로 많이 변경되지 않았다. 왜지? 왜 그런지 이유를 보자.

    서비스 Services (business logic layer)

    백엔드와 연결해서 파일을 업로드 하는 작업과 같이 API와 소통하는 무엇이든 절대, 절대 컨트롤러에 작성하지 않는다. 왜냐고? 관계를 분리하는 것이다. 물론 컨트롤러에 작성할 수도 있다. 하지만 컨트롤러를 뷰모델비지니스로직어쩌고가 아닌 뷰모델처럼 사용한다면 우리 삶을 너무나도 힘들게 만든다.

    여기서 서비스를 위한 모조 코드를 작성하진 않을 것이지만 왜 추상화된 비지니스 로직을 컨트롤러에 넘겨야 하는가에 대해서 이해하는 것은 아주 중요하다. 디렉티브의 구조와 의존성을 관리에 용이하고 확장 가능하게 구축하기 위해서는 처음부터 고려를 해야한다.

    서비스는 컨트롤러(뷰모델)가 사용자에게 데이터를 표현할 수 있도록 필요한 모델 데이터를 복제해서 제공할 수 있어야 한다.

    디렉티브는 표현 로직 레이어(컨트롤러)나 비지니스 로직 레이어(서비스)가 다루지 못하는 환상적인 통로를 제공하는데 그건 바로 DOM 문서 객체 모델(Document Object Model)이다. 종종 DOM이 필요한데 Angular는 우리를 위해 준비를 해두었다.

    여기서 작성한 파일 업로드 디렉티브는 흑마법 같은 드래그 드랍 없이는 완성되지 않은 것이나 마찬가지니 dragover, drop 같은 DOM 이벤트를 활용하자. 먼저 <div class="drop-zone">을 디렉티브에 추가하고 이 영역을 "드래그 드랍" 영역으로 제공한다.

    // ...
    template: [
      '<div>',
        '<input type="file" ng-model="vm.files">',
        '<button type="button" ng-change="vm.uploadFiles(vm.files);">',
        '<div class="drop-zone">Drop your files here!</div>',
      '</div>'
    ].join('')
    // ...

    이제 디렉티브와 묶어야 한다. link 함수는 여기서 유용하다. 이 함수에 $scope, $element, $attrs를 주입한다. (미안하지만 달러 표시로 프리픽스를 붙이는 것을 좋아한다. iAttrs을 보면 눈물이 앞을 가린다.)

    이제 .drop-zone엘리먼트에 특별한 이벤트 리스너를 연결해야 한다. link 함수를 최대한 가볍게 만든다는 점을 명심하자. 여기서 $scope 인자는 정말 드물게 사용하는데 여러분도 그래야 한다.

    엘리먼트에 이벤트 리스너를 추가한다:

    /**
     * @name link
     * @desc File Upload Link
     * @type {Function}
     */
    function link($scope, $element, $attrs) {
      var drop = $element.find('.drop-zone')[0];
      drop.addEventListener('dragenter', function(e) {
        // "dragenter"에 무언가 동작
      }, false);
      drop.addEventListener("dragleave", function(e) {
        // "dragleave"에 무언가 동작
      }, false);
      drop.addEventListener("dragover", function(e) {
        // "dragover"에 무언가 동작
      }, false);
      drop.addEventListener('drop', function(e) {
        // "drop"에 무언가 동작
      }, false);
    }

    다시 말하지만 나는 깔끔하게 보이는 것을 좋아하니까 주석과 추상성을 좀 더 다듬었다. dragenter, dragleave, dragover는 이 데모에서 필요 없으니 지운다.:

    /**
     * @name link
     * @desc File Upload Link
     * @type {Function}
     */
    function link($scope, $element, $attrs) {
    
      /**
       * @name drop
       * @desc Drop zone element
       * @type {Element}
       */
      var drop = $element.find('.drop-zone')[0];
    
      /**
       * @name onDrop
       * @desc Callback on "drop" event
       * @type {Function}
       * @param {Event} e Event passed in to grab files from
       */
      function onDrop(e) {
        
      }
      
      // events
      drop.addEventListener('drop', onDrop, false);
    
    }

    이벤트 리스너를 설정했고 e.dataTransfer.files에서 파일을 집어 업로드 API로 넘겨줄 수 있다. 하지만 같은 함수를 컨트롤러에 있는 uploadFiles 메소드를 사용하고 싶다.

    디렉티브 안으로 컨트롤러를 넘겨줄 수 있는데, $ctrl이라는 짧고 귀요미인 별칭을 사용해서 디렉티브에서 컨트롤러에 접근할 수 있도록 만든다. (역주. link가 호출될 때 4번째 인자로 컨트롤러가 제공됨.)

    /**
     * @name link
     * @desc File Upload Link
     * @type {Function}
     */
    function link($scope, $element, $attrs, $ctrl) {
    
      /**
       * @name drop
       * @desc Drop zone element
       * @type {Element}
       */
      var drop = $element.find('.drop-zone')[0];
    
      /**
       * @name onDrop
       * @desc Callback on "drop" event
       * @type {Function}
       * @param {Event} e Event passed in to grab files from
       */
      function onDrop(e) {
        if (e.dataTransfer && e.dataTransfer.files) {
          $ctrl.uploadFiles(e.dataTransfer.files);
        }
      }
      
      // events
      drop.addEventListener('drop', onDrop, false);
    
    }

    대박! 컨트롤러의 uploadFiles 메소드를 사용해서 API로 파일을 넘기는데 코드를 다시 사용했다! 이런 방식으로 표현 로직에서의 변경을 그대로 반영할 수 있게 되었다. 앞서 언급한 것처럼 업로드된 파일을 사용자에게 보여줄 때에도 컨트롤러에서 모든 코드를 다시 사용하고 활용할 수 있을 것이다.

    하지만 이건 아직 동작하지 않는다... 마법의 코드 $scope.$apply()를 잊었다:

    /**
     * @name onDrop
     * @desc Callback on "drop" event
     * @type {Function}
     * @param {Event} e Event passed in to grab files from
     */
    function onDrop(e) {
      if (e.dataTransfer && e.dataTransfer.files) {
        $ctrl.uploadFiles(e.dataTransfer.files);
        // force a $digest cycle
        $scope.$apply();
      }
    }

    파일이 업로드 된 후, $digest 사이클을 실행하도록 $scope.apply()를 추가한다. 파일을 업로드한 과정을 거치고 데이터가 변경된 후에 어플리케이션 또한 변경되도록 한다. 이 과정이 필요한 이유는 Angular 생태계 외부에서 존재하는 drop 이벤트 리스너를 활용했기 때문이다. 외부에 있어서 그 이벤트가 동작하는지, 무슨 일이 어떻게 일어났는지 알려야 할 필요가 있는 것이다.

    이제 모든 것이 갖춰졌다:

    /**
     * @name fileUpload
     * @desc <file-upload> Directive
     */
    function fileUpload () {
    
      /**
       * @name fileUploadCtrl
       * @desc File Upload Controller
       * @type {Function}
       */
      function fileUploadCtrl(UploadService) {
    
        /**
         * @name files
         * @desc Contains all files passed in by the user
         * @type {Array}
         */
        var files = [];
    
        /**
         * @name uploadFiles
         * @desc Uploads our files
         * @type {Array}
         */
        function uploadFiles(files) {
          // hand off our files to a Service
          UploadService
          .uploadFiles(files)
          .then(function (response) {
            // success, we could get our file Object back
            // and render it in the View for the user
            // maybe some ng-repeat with a list of files inside
          }, function (reason) {
            // error stuff if not handled globally
          })
        }
    
        // exports
        this.files = files;
        this.uploadFiles = uploadFiles;
    
      }
    
      /**
       * @name link
       * @desc File Upload Link
       * @type {Function}
       */
      function link($scope, $element, $attrs, $ctrl) {
    
        /**
         * @name drop
         * @desc Drop zone element
         * @type {Element}
         */
        var drop = $element.find('.drop-zone')[0];
    
        /**
         * @name onDrop
         * @desc Callback on "drop" event
         * @type {Function}
         * @param {Event} e Event passed in to grab files from
         */
        function onDrop(e) {
          if (e.dataTransfer && e.dataTransfer.files) {
            $ctrl.uploadFiles(e.dataTransfer.files);
            // force a $digest cycle
            $scope.$apply();
          }
        }
        
        // events
        drop.addEventListener('drop', onDrop, false);
    
      }
    
      return {
        restrict: 'E',
        scope: {},
        template: [
          '<div>',
            '<input type="file" ng-model="vm.files">',
            '<button type="button" ng-change="vm.uploadFiles(vm.files);">',
            '<div class="drop-zone">Drop your files here!</div>',
          '</div>'
        ].join(''),
        controllerAs: 'vm',
        controller: fileUploadCtrl
        link: link
      };
    
    }
    angular
      .module('app')
      .directive('fileUpload', fileUpload);

    정리, MVVM (Model-View-ViewModel)

    이 접근은 컨트롤러를 뷰 모델로 사용하는 방식이며 link 함수를 DOM 조작에 활용함과 동시에 컨트롤러에 간단한 일을 전달하는 역할을 하도록 처리하는 역할을 한다. 이 접근은 함수 내부에 객체를 제공하는 등의 방법으로 중첩된 여러 계층의 코드를 작성하는 것과 같이 복잡한 방법을 사용하지 않고, 마치 코드 자체가 별도로 구성된 것 같이, 함수를 분리하고 다시 할당하는 방식으로 서로 의존적인 관계를 분리하는 데 더 적합하다.

    의견이나 개선점은 GitHub 이슈로 남겨주기 바란다. Enjoy!

    $scope은 이제 그만, Angular에서 bindToController 활용하기

    2015년 11월 12일

    이 글은 Todd Motto의 글 No $scope soup, bindToController in AngularJS를 번역한 글이다.

    Angular에서 controllerAs 문법을 사용한다면 자연스럽게 따라오는 디렉티브 프로퍼티인 bindToController에 관한 글이다. 기존 $scope와 어떤 방식이 다른지, 어떻게 작성하는 것이 좋은지 확인할 수 있다.


    $scope은 이제 그만, Angular에서 bindToController 활용하기

    소프트웨어 공학에서 네임스페이스, 코드 일관성, 적절한 디자인 패턴은 정말 중요한 문제다. Angular는 프론트엔드 엔지니어로 직면할 수 있는 수많은 문제를 정말 잘 해결했다.

    디렉티브의 프로퍼티인 bindToController을 어떻게 사용하는지 설명하는 것으로 DOM-컨트롤러 네임스페이스를 정리하고, 코드의 일관성을 유지하는 방법과 함께 컨트롤러 객체를 생성하고 데이터를 다른 곳에서 사용하는데 더 편리한 디자인 패턴을 만드는 과정을 설명하려 한다.

    그 전에 해야 할 일

    bindToControllercontrollerAs 문법과 함께 사용해야 한다. 이 문법은 컨트롤러를 클래스 같은 객체로 다룰 수 있게 하는데 생성자처럼 초기화하는 과정에서 그 초기화를 통해 네임스페이스를 통제할 수 있게 된다. 다음 예를 살펴보자:

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

    controllerAs 문법이 없었던 예전에는 컨트롤러에 대해 고유한 네임스페이스가 제공되지 않았고 JavaScript 객체 프로퍼티가 붕 뜬 상태처럼 존재해 DOM 주변을 맴돌았는데 그로 인해 컨트롤러 내에서 코드 일관성을 유지하기가 힘들었다. 게다가 $parent로 인한 상속 문제도 지속적으로 발생했다. 이런 문제를 이 글에서 모두 해결하려고 하는데, 앞서 작성한 포스트(번역)에서도 그 문제를 자세히 확인할 수 있다.

    문제점

    컨트롤러를 controllerAs 문법으로 작성할 때 나타날 만한 문제는 컴포넌트를 클래스 같은 객체로 작성해야 하는 점, 그리고 상속된 데이터에 접근하기 위해 ("독립된 스코프"에서) $scope를 주입해야 하는 경우다. 간단한 예제로 시작하면:

    // controller
    function FooDirCtrl() {
    
      this.bar = {};
      this.doSomething = function doSomething(arg) {
        this.bar.foobar = arg;
      }.bind(this);
    
    }
    
    // directive
    function fooDirective() {
      return {
        restrict: 'E',
        scope: {},
        controller: 'FooDirCtrl',
        controllerAs: 'vm',
        template: [
            // vm.name doesn't exist just yet!
            '<div><input ng-model="vm.name"></div>'
        ].join('')
      };
    }
    
    angular
      .module('app')
      .directive('fooDirective', fooDirective)
      .controller('FooDirCtrl', FooDirCtrl);

    이제 "상속된" 스코프가 필요하다. 그래서 고립된 스코프인 scope: {}에 필요한 참조를 추가한다:

    function fooDirective() {
      return {
        ...
        scope: {
          name: '='
        },
        ...
      };
    }

    여기까지면 됐다. 이제 $scope를 주입해야 한다. 새로 작성한 클래스 같은 객체에 $scope 객체를 주입하게 되면 더 나은 디자인 원칙을 적용하는데 더 어려운 상황에 놓인다. 하지만 여기서는 주입해야먄 한다.

    더 지저분하게 만들어보자:

    // controller
    function FooDirCtrl($scope) {
    
      this.bar = {};
      this.doSomething = function doSomething(arg) {
        this.bar.foobar = arg;
        $scope.name = arg.prop; // reference the isolate property
      }.bind(this);
    
    }

    여기서 보면, 클래스 같은 객체 패턴을 사용해서 애써 새로운 디렉티브를 만들었는데 그 흥분을 $scope가 망쳐버렸다.

    그 뿐만 아니라 앞서 작성한 가사 템플릿을 다시 보면 vm. 접두어를 만들었는데도 네임스페이스 없는 변수가 또 다시 떠돌게 된다:

    <div>
      {% raw %}{{ name }}{% endraw %}
      <input type="text" ng-model="vm.username">
    </div>

    해결책

    해결책에 앞서, Angular가 클래스 같은 객체 패턴을 시도한 것에 대해 부정적인 반응이 많았다. 디자인에 대해 알고 전력으로 만들었지만 모든게 완벽할 순 없다. 2버전에서 다시 쓴다고 해도 모든 상황에 완벽해질 수 없다. 이 포스트는 Angular의 나쁜 $scope 습관을 버리기 위한, 위대한 해결책을 다루고 있고, 더 나은 JavaScript 디자인에 가깝도록 작성하는데 최선을 다하고 있다.

    bindToController 프로퍼티를 입력한다. 문서에서는 bindToController의 값을 true로 활성화하면 상속된 프로퍼티가 $scope 객체가 아닌 컨트롤러로 연결된다.

    function fooDirective() {
      return {
        ...
        scope: {
          name: '='
        },
        bindToController: true,
        ...
      };
    }

    이 코드로 앞서 작성한 코드를 리팩토링할 수 있게 되었다. $scope를 제거하자:

    // controller
    function FooDirCtrl() {
    
      this.bar = {};
      this.doSomething = function doSomething(arg) {
        this.bar.foobar = arg;
        this.name = arg.prop; // reference the isolate property using `this`
      }.bind(this);
    
    }

    Angular 문서는 bindToController: true 대신 객체를 사용하는 것을 제안하지 않지만, Angular 소스 코드에서 이런 코드를 확인할 수 있다:

    if (isObject(directive.bindToController)) {
      bindings.bindToController = parseIsolateBindings(directive.bindToController, directiveName, true);
    }

    bindToController에 객체가 온다면 앞서 본 형태의 상속과 달리 독립적인 바인딩을 사용하게 된다. 즉 scope: { name: '='} 예제를 더 명시적으로 컨트롤러에 바인딩하는 것으로 표현할 수 있다는 뜻이다. (내가 선호하는 문법이다.):

    function fooDirective() {
      return {
        ...
        scope: {},
        bindToController: {
          name: '='
        },
        ...
      };
    }

    (역주. scope에 선언한 객체는 $scope에 바인딩되고, bindToController에 선언한 객체는 this에 바인딩 된다. bindToControllertrue로 값을 넣으면 scope에 선언한 객체가 scope 대신 bindToController에 선언한 객체처럼 처리된다. 즉, $scope와 this를 구분해서 써야 하는 상황이라면, 위와 같이 별도로 선언하는 방법이 필요하겠다.)

    이제 JavaScript 해결책을 확인했다. 이 변화가 템플릿에 어떤 영향이 있는지 확인하자.

    이전에, name$scope에 상속했을 때와 달리 컨트롤러 내에서 동일한 네임스페이스를 사용할 수 있다. 다시 기뻐하자! 이 방법으로 모든 코드가 일관적이고 좋은 가독성을 지니게 되었다. 마지막으로 vm. 접두어를 name 프로퍼티 앞에 적어 템플릿도 일관적이게 변경하자.

    <div>
      {% raw %}{{ vm.name }}{% endraw %}
      <input type="text" ng-model="vm.username">
    </div>

    라이브 리펙토링 예제

    실제로 동작해볼 수 있는 예제를 jsFiddle에 올렸다. 이 예제로 리펙토링 과정을 시연한다. (이 변화는 최근 Angular 1.2에서 1.4로 변경한 우리 팀에게 특히 좋았다.)

    노트: 각 예제는 부모 컨트롤러에서 디렉티브로 양방향 고립 바인딩을 사용했고 입력창에 값을 변경해 부모에 반영되는지 확인할 수 있다.

    첫 예제는, $scope 객체를 넘긴다. 템플릿과 컨트롤러 로직에서 $scopethis가 복잡한 상태로 그대로 두었다. 라이브 예제 1

    angular
        .module('app', []);
    
    // main.js
    function MainCtrl() {
        this.name = 'Todd Motto';
    }
    
    angular
        .module('app')
        .controller('MainCtrl', MainCtrl);
    
    // foo.js
    function FooDirCtrl() {
    
    }
    
    function fooDirective() {
        
        function link($scope) {
            
        }
        
        return {
            restrict: 'E',
            scope: {
                name: '='
            },
            controller: 'FooDirCtrl',
            controllerAs: 'vm',
            template: [
                '<div><input ng-model="name"></div>'
            ].join(''),
            link: link
        };
    }
    
    angular
        .module('app')
        .directive('fooDirective', fooDirective)
        .controller('FooDirCtrl', FooDirCtrl);

    두번째 예제는 $scopebindToController: true와 함께 리팩토링했다. 템플릿의 네임스페이스 문제도 this 객체 밑에 컨트롤러 로직의 일관성을 유지하는 것으로 해결했다. 라이브 예제 2

    angular
        .module('app', []);
    
    // main.js
    function MainCtrl() {
        this.name = 'Todd Motto';
    }
    
    angular
        .module('app')
        .controller('MainCtrl', MainCtrl);
    
    // foo.js
    function FooDirCtrl() {
    
    }
    
    function fooDirective() {
        
        function link($scope) {
            
        }
        
        return {
            restrict: 'E',
            scope: {
                name: '='
            },
            controller: 'FooDirCtrl',
            controllerAs: 'vm',
            bindToController: true,
            template: [
                '<div><input ng-model="vm.name"></div>'
            ].join(''),
            link: link
        };
    }
    
    angular
        .module('app')
        .directive('fooDirective', fooDirective)
        .controller('FooDirCtrl', FooDirCtrl);

    선호하는 세번째 예제로, bindToController: true를 객체로 사용하고, scope: {}로 프로퍼티를 변경하는 것으로 더 명확하게 작성했다. 두번째 예제와 결과적으로 같지만, 함께 작업하는 개발자를 위해 더 명확하게 작성하는 방법이다. 라이브 예제 3

    angular
        .module('app', []);
    
    // main.js
    function MainCtrl() {
        this.name = 'Todd Motto';
    }
    
    angular
        .module('app')
        .controller('MainCtrl', MainCtrl);
    
    // foo.js
    function FooDirCtrl() {
    
    }
    
    function fooDirective() {
        
        function link($scope) {
            
        }
        
        return {
            restrict: 'E',
            scope: {},
            controller: 'FooDirCtrl',
            controllerAs: 'vm',
            bindToController: {
                name: '='
            },
            template: [
                '<div><input ng-model="vm.name"></div>'
            ].join(''),
            link: link
        };
    }
    
    angular
        .module('app')
        .directive('fooDirective', fooDirective)
        .controller('FooDirCtrl', FooDirCtrl);

    $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. 느리다.