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가 대표적이며 많은 기능이 프로바이더로 구현되어 있다.

  • 팩토리를 작성하는 방법을 설명하는 글을 보면 노출식 모듈 패턴을 활용하는 경우가 종종 있어서 왜? 부분이 추가된 것 같다. 이 패턴은 일부 구현(메소드, 변수)에 대해 외부에서 접근할 수 있는지 없는지 명시적으로 지정할 수 있다는 특징이 있는데 그 특징으로 외부에서 접근할 수 없는 코드에 대해서는 값을 변경할 방법이 없다. 그런 특징 때문에 가이드에서는 호스트 객체로 반환할 것을 권장하고 있다. 
  • 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와 같이, 일종의 스태틱 클래스에서는 잘 어울리는 접근 방식이다. 패턴은 상황에 맞게 적용해야 한다.

    저번에 한참 HHKB를 구입하고 싶어서 구입창을 몇번이고 열었다 닫았다 했는데 이미 레오폴드서 구입한 키보드가 있었다. 회사에서 사용했는데 아무래도 MS 키보드 레이아웃이라서 자주 안쓰게 되서 집에 가져와서 먼지를 배양하고 있었다. 이 키보드도 맵핑만 바꾸면 나름 HHKB 분위기로 사용할 수 있다는 얘기를 듣고 솔깃해서 Karabiner를 받아서 키맵을 설정해서 사용하기 시작했다. 그러고서 계속 혼용해서 쓰다가 어느 순간부터 레오폴드 키보드를 메인으로 사용하게 되었다. (드디어!)

    Karabiner

    엄청나게 강력한 키맵을 제공하는데 오픈소스로 개발되고 있다. 제공되는 설정 목록을 보면 대부분 시나리오에 맞는 키맵 설정이 존재한다. 정말 방대한데다 직접 커스텀해서 만드는 것도 가능한데 옵션이 너무 많아서 한참 찾다가 찾지 못한 부분은 직접 확장을 만들었다. 아마 내장된 확장이나 누군가 만든, 더 좋은 확장도 분명 있을게 분명한데 찾아서 적용해서 확인할 시간 많은 분은 찾아보는게 좋겠다. (찾으면 알려주세요..)

    내 확장은 gist에 올려놨다.

    • 애플 키보드는 키맵 적용 안함
    • 애플 마우스/트랙패드는 키맵 적용 안함 (필요한진 몰라도)
    • 좌측 Ctrl을 Alt/Option으로
    • 좌측 Alt를 Cmd로
    • 우측 Ctrl을 Fn으로
    • F1~12를 애플 키보드 기본 기능으로

    나름 해피해킹스러운 맵도 넣었다.

    • Fn + {1~=} 조합을 F1~12로 (이건 내장된게 의도랑 다르게 동작해서 커스텀으로 추가)
    • Fn + ;[‘/ 조합을 방향키로

    화면 끄는 키가 없어서 이 키도 추가했다. Eject로 모니터만 끈다거나 슬립모드로 간다거나 하는 단축키를 쓸 수 있다.

    • F13(Print Screen)을 Eject로

    HHKB의 꽃인 CapsLock 위치 키에 맵핑 하려면 Seil이 필요하다. 정말 HHKB 스타일로 만들려면 필수적이겠지만 기본 동작을 변경해야 하는 부분이 있어서 설치하진 않았다.

    일반 키보드로 HHKB를 체험(?)해보고 싶다면, 또는 필요한 키를 변경하고 싶다면 강력하게 추천하고 싶은 앱이다.

    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--;
      }
    });
    

    저스틴님 댁에 살 때는 엄청나게 큼지막한 커피 머신이 있어서 커피 생각이 나면 내려서 먹을 수 있었지만 이사 온 이후로는 커피를 집에서 마실 일이 없었다. 그 핑계에 카페인 섭취량이 너무 많은 것 같아 조절해야겠다는 생각을 했는데 사실 매일 회사서 사 마시다 보니 차라리 집에 장비를 꾸려두고 적당히 조절하는 게 어떨까 하는 말도 안 되는 자기합리화 과정을 거치게 되었다.

    그래서 두 달 전 프렌치프레스(French press)를 구입했다. 뒷정리가 조금 번거롭긴 하지만 아침에 일어나자마자 커피 마시고 정신 차리는 게 하루를 밀도 있게 시작할 수 있는 좋은 방법이란 사실을 알고서 계속 마시고 있다. 카페인 걱정하던 나는 어디로 갔는지 잘 모르겠지만… 프렌치프레스를 구입하기 전에도 모카팟(Moka Pot)을 구입할지 말지 고민을 많이 하다가 프렌치 프레스로 구입했었다. 근데 여자친구가 2주년 선물로 모카팟을 선물해줘서 결국 둘 다 갖게 되었다. (선물 고마워요! ?)

    프렌치프레스는 필터 안 갈아도 되는 드립 커피 느낌이지만 모카팟은 일단 에스프레소 추출기라는 점에서 큰 차이가 있다. 압력밥솥과 같은 원리로 추출하는데 시각적으로도 신기하고 독특하다. 에스프레소를 좋아한다면 모카팟 하나 장만해 커피 내려 마시는(원리로는 올려 마시는?) 재미를 느껴보면 좋을 것 같다.

    모카팟은 원래 Bialetti에서 모카 익스프레스라는 이름으로 출시한 제품이 원조인데 이 제품의 원리를 사용한 모든 제품군을 모카팟으로 부르는 것 같다. 내가 선물 받은 팟도 이 원조 제품인데 1933년에 출시했더라. 역시 좋은 제품은 오랫동안 간다.

    The maling room coffee

    먼저 커피콩을 준비해서 블랜더에 갈아준다. 동네 카페에서 구입한 콩인데 처음엔 너무 시큼하단 생각을 했는데 먹다보니 적응 되었다.

    커피를 블렌더에 갈아준다

    블렌더에 갈리는 커피콩

    적당한 양을 넣고 갈아준다. 너무 살짝 거칠게 갈아야 한다고 모카팟 설명서에 써져 있어서 적당히 간다. 너무 곱게 갈면 압력이 너무 강해져 위험해서 그런게 아닌가 싶다.

    모카팟 해부

    모카팟은 곤충처럼 세 부분으로 나뉜다. 밑에 물을 넣으면 깔대기를 통과해 윗 공간에 커피가 모이는 방식이다. 단순하면서도 만듬새 있어서 평생 쓸 수 있을 것 같다.

    물을 콸콸 넣어준다

    하단 공간에 물을 먼저 넣어준다. 물을 넣을 때 안에 압력을 조절하는 구멍 같은게 있는데 그 구멍을 막지 않는 선까지 물을 넣어준다. 압력밥솥에서 일정 압력을 유지해주기 위해 달려있는 뚜껑 꼬다리와 같은 역할을 한다.

    깔대기를 하단부에 장착

    커피를 넣어준다

    깔대기를 놓고 앞서 갈았던 커피를 적당하게 넣어준다. 내 모카팟은 6컵을 한 번에 만들 수 있는데 4컵 정도 만들 분량만 넣는다. 한 잔 따라서 마시고 나머지는 보틀에 넣어 사무실에 갖고 가서 물타서 마시고 있다. 이렇게 커피 덕질을 시작하는 느낌.

    조립

    커피를 다 넣었으면 나머지를 잘 조립해준다. 꽉 닫아야 (정말인지 모르겠지만) 상단부가 날아갈 일 없고 압력이 빠지지 않는다고 한다.

    가스렌지에 올린다

    가스렌지에 올리고 불을 켠다. 중불로 하라고 하는데 약한 쪽에 올려서 최대로 트는게 더 간편해서 그러고 있다.

    커피가 나오는 모습

    적당한 때가 되면 치치 소리가 나면서 커피가 나온다고 한다. 나는 소리가 안나서 (아니면 그렇게 귀가 민감하지 않아서) 그냥 보고 있다가 한 60% 정도 커피가 올라오면 불을 끈다. 압력이 강하기 때문에 열어보다가 커피가 뿜어져 나올 수 있으니 조심해야 한다.

    따라 마시기

    불을 끄고 따라서 마시면 된다.

    커피 완성

    같은 콩이라도 프렌치프레스에 내려서 마시는 것과 에스프레소로 마시는 것은 생각보다 맛이 많이 달랐다. 믹스 타서 마시는 것에 비하면 과정도 길고 복잡한 기분이 들지만 이렇게 직접 해보니 또 새롭다. 일상적인 부분에서 새로운 점을 찾는다는 점은 즐거운 일이다. 오랜만에 직접 글을 써서 그런지 끝맺음을 어떻게 해야하나 막막하다. 여러분 커피 많이 드세요. 헤헤.

    이 글은 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);
    

    이 글은 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!

    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와 개발하는데 더 편리하게 한다고 생각한다. 그러니 둘 중 어느 것을 선택하는가는 온전히 당신의 몫이다.

    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/$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');
      });
    });
    

    이 글은 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. 느리다.

    색상을 바꿔요

    눈에 편한 색상을 골라보세요 :)

    Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.