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

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

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

이 글 공유하기

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

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

주제별 목록

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

November 12, 2015

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

이 글은 Todd Motto의 글 Killing it with Angular Directives; Structure and MVVM 를 번역한 글이다. Angular에서 디렉티브를 어떤 방식으로 사용해야 하는지 예제를 통해 설명하고 있다. Angu…

November 08, 2015

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

Johnpapa의 Do You Like Your Angular Controllers with or without Sugar? 를 번역한 글이다. 원본 포스트는 CC BY 2.5 라이센스로 작성되어 있다. 그냥 읽을 때는 괜찮게 느껴졌는데 옮기고…