Angular의 Controller As 문법 살펴보기

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');
  });
});
김용균

안녕하세요, 김용균입니다. 문제를 해결하기 위해 작고 단단한 코드를 작성하는 일을 합니다. 웹의 자유로운 접근성을 좋아합니다. 프로그래밍 언어, 소프트웨어 아키텍처, 커뮤니티에 관심이 많습니다.

이 글 공유하기

이 글이 유익했다면 주변에도 알려주세요!

페이스북으로 공유하기트위터로 공유하기링크드인으로 공유하기Email 보내기

주제별 목록

같은 주제의 다른 글을 읽어보고 싶다면 아래 링크를 확인하세요.

November 08, 2015

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

이 글은 Todd Motto의 글 A better way to $scope, angular.extend, no more “vm = this” 를 번역한 글이다. Angular에서 Controller As 문법을 사용하면 와 같이 this의 스코프…

November 03, 2015

논문 읽기에 도전하기

최근 논문 읽기와 관련한 포스트를 연달아 번역했다. 모두 papers we love 에서 언급된 포스트인데 논문을 어떻게 읽기 시작하면 좋을지 고민하는 사람에게 좋은 지침이 될 것 같아 번역해 공유했다. 내가 논문을 심각하게 읽어본 적이 있나 생각…