[AngularJS] $scope과 controller-as 문법
* Scope 객체
AngularJS의 $scope은 뷰(View)와 컨트롤러(Controller)를 연결하는 객체이다.
HTML 페이지(뷰)는 자기자신이 포함된 컨텍스트의 $scope객체를 통해 컨트롤러에서 관리하는 데이터와의 양방향 동기화가 가능하며 컨트롤러가 제공하는 메서드도 호출할 수 있게 된다.
- 출처: https://github.com/Capgemini/ngTraining/wiki/AngularJS-Templates-101
그림에서 보는것과 같이, Scope은 비지니스 로직과 사용자 인터페이스간 연결을 담당하며 Controller는 Scope에 속성과 메서드를 추가하여 뷰에 노출시킨다.
scope 객체를 통해 컨트롤러와 뷰가 상호작용하는 간단한 코드를 보자.
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script>
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
$scope.name = "Park";
$scope.changeName = function(){
$scope.name = "Kim";
}
});
</script>
<body>
<div ng-app="myApp" ng-controller="myCtrl">
<h1 ng-click="changeName();">{{name}}</h1>
</div>
</body>
</html>
컨트롤러에서 $scope객체에 name속성을 추가하고 값을 할당하였다. 또한 changeName이라는 메서드를 추가하고 name값을 변경하는 코드를 작성했다.
그리고 HTML 페이지에서는 {{name}} 표현식을 사용하여 scope에 정의된 name값을 출력하고 값이 클릭되면 changeName() 함수를 호출하도록 하였다.
이렇듯 컨트롤러에서 $scope 객체를 통해 속성과 함수를 노출하면 뷰에서는 이와 상호작용 할 수 있게 되는 것이다.
* controller-as 문법
AngularJS 1.3부터 추가된 것으로, 뷰에서 컨트롤러를 선언하는 방식에 대한 문법이다.
기존까지는 ng-controller="controllerName" 이라고 선언하는 반면, 이 문법을 사용하면
ng-controller="controllerName as ctlName"과 같이 선언하는 방식이다. as 뒤에 오는 것은 일종의 별칭으로 뷰에서 ctlName이라는 키워드를 통해 컨트롤러에 접근할 수 있게 되는 것이다.
다음 코드는 앞선 예제에서 $scope 객체를 사용하는 대신, controller-as 문법으로 교체한 것이다.
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script>
var app = angular.module('myApp', []);
app.controller('myCtrl', function() {
var main = this;
main.name = "Park";
main.changeName = function(){
main.name = "Kim";
}
});
</script>
<body>
<div ng-app="myApp" ng-controller="myCtrl as main">
<h1 ng-click="main.changeName();">{{main.name}}</h1>
</div>
</body>
</html>
controller-as 문법의 사용으로 더 이상 컨트롤러 생성자 함수로 $scope 객체를 주입하지 않아도 된다. 그리고 HTML페이지(뷰)에서는 main이라는 이름으로 컨트롤러에 접근할 수 있게 되었다.
내부적으로는 controller-as 문법으로 정의한 컨트롤러의 이름은 여전히 $scope 객체 내부에 해당 일므의 변수로 추가된다.
그리고 참고로, 컨트롤러 코드에서 var main = this 하고 한 것은 필수 사항은 아니다. 단지 자바스크립트이 함수 컨텍스트 상의 this의 의미 모호성을 제거하기 위해 main이라는 임의의 이름으로 this(자기자신)를 미리 할당한 것이다. 이때 이왕이면 뷰에서 사용하는 이름과 똑 같이 하면 가독성이 좋아질 것이다.
사실 AngularJS 팀이 $scope 대신 controller-as 문법을 사용하기를 권장하고 이 문법을 추가한 것은 두 가지 이유 때문이다.
1. 뷰가 참조하는 컨트롤러의 모호성 제거
- $scope 객체를 통해 추가시킨 속성이나 메서드를 뷰에서 접근할 경우, 단지 속성명(메서드명)만 명시하면 된다. 이는 여러 컨트롤러가 복합적으로 사용되는 환경일 경우, 마크업에서 바인딩에 사용한 속성이 정확히 어디에 정의된 것인지에 대한 모호함을 유발시키며 이는 곧 코드의 가독성 및 유지보수성을 저해시킨다.
2. 묵시적 scope 상속의 나쁜 습관 방어
- 모든 scope 객체는 자신의 직계부모로부터 $rootScope에 이르기까지 계층 구조상 모든 부모 객체의 프로토타입을 상속받는다. 뷰에서 scope의 속성이나 메서를 참조할 경우 자신이 속한 컨텍스트의scope에 해당 속성(메서드)가 존재하지 않을 경우, scope 프로토타입 체인을 거슬러 올라가며 부모 객체를 계속 탐색해 나간다. 이런 동작은 프로토타입 기반의 상속 매커니즘을 가진 자바스크립트의 매커니즘과도 일치하며, 어떤 경우에는 편한 기능이기도 하다. 다만 게으른 개발자라면 부모 scope에 존재하는 속성이나 메서드를 그대로 사용하려고 할 것이며, 여러 곳에서 응용프로그램 전체에 걸쳐 공유되는 데이터에 접근하여 마구잡이로 변경하면 예측하지 못하는 문제들이 발생한다.
controller-as 문법은 더 나은 코딩을 위한 권고사항일 뿐이다. 우리는 여전히 $scope를 사용할 수 있다.
* Scope Hierarchies(Scope의
- 모든 AngularJS 응용프로그램은 하나의 root scope를 가지며, 이 root scope를 기준으로 컨트롤러나 디렉티브 내의 scope가 DOM 구조의 계층구조와 유사하게 child scope로 계층화 된다.
(단, DOM 계층구조가 모든 scope 계층구조를 결정하는 것은 아님에 주의하자)
즉 하나의 root scope와 하나 이상의 chind scope 형태의 계층구조로 이뤄지며, 이런 구조는 뷰가 scope 객체를 참조할 경우 자동으로 scope 프로토타입 체인을 거슬러 올라가며 탐색하도록 동작한다.
다음의 코드는 Scope의 계층구조 구성을 보여주며, 하위 계층에서는 상위계층의 속성에 접근할 수 있다는 것을 보여주고 있다.
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script>
var app = angular.module('myApp', [])
app.controller('ctrl_A', function($scope, $rootScope) {
$scope.name = 'name-A';
$scope.nameA= 'name-A-only';
$rootScope.masterName = 'master';
});
app.controller('ctrl_B', function($scope) {
$scope.name = 'name-B'
$scope.nameB = 'name-B-only'
});
</script>
<body>
<div ng-app="myApp">
<div ng-controller="ctrl_A">
<h1>Controller A</h1>
{{name}}, {{nameA}} , {{nameB}} {{masterName}}
<div ng-controller="ctrl_B">
<h1>Controller B</h1>
{{name}}, {{nameA}} , {{nameB}}, {{masterName}}
</div>
</div>
</div>
</body>
</html>
결과는 다음과 같다.
B컨트롤러는 A컨트롤러 하위계층에 속하며, B컨트롤러에 정의되지 않은 nameA속성과 rootScope의 masterName 속성에 접근할 수 있다. 반면 A컨트롤러는 B컨트롤러에 정의된 nameB 속성에 접근하지 못한다. 이로써, scope는 계층구조로 구성되며(묵시적으로 상속된다) 하위 scope는 자신에게 정의되지 않은 속성(메서드)를 찾기 위해 상위계층으로 거슬러 올라가며 자동으로 탐색한다는 것을 알 수 있다.
이번엔, 앞의 코드를 controller-as 문법을 사용하여 수정해 보자
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script>
var app = angular.module('myApp', [])
app.controller('ctrl_A', function($rootScope) {
var ctrlA = this;
ctrlA.name = 'name-A';
ctrlA.nameA= 'name-A-only';
$rootScope.masterName = 'master';
});
app.controller('ctrl_B', function() {
var ctrlB = this;
ctrlB.name = 'name-B'
ctrlB.nameB = 'name-B-only'
});
</script>
<body>
<div ng-app="myApp">
<div ng-controller="ctrl_A as ctrlA">
<h1>Controller A</h1>
{{ctrlA.name}}, {{ctrlA.nameA}} , {{ctrlB.nameB}}, {{masterName}}
<div ng-controller="ctrl_B as ctrlB">
<h1>Controller B</h1>
{{ctrlB.name}}, {{ctrlA.nameA}} , {{ctrlB.nameB}}, {{masterName}}
</div>
</div>
</div>
</body>
</html>
코드의 결과는 앞의 $scope 객체를 사용한 것과 동일하다. 하지만 뷰에서 참조하는 속성(메서드)이 정확히 어떤 컨트롤러에 정의된 것인지는 더욱 분명해 졌다.
* 다이제스트 주기(Digest cycle)
- AngularJS는 모델과 뷰의 내용을 자동으로 동기화 시킨다. 그 중심에 scope가 있다. scope는 MVVM 아키텍처 패턴에서 ViewModel에 해당하는 핵심 객체로 이해해도 무방하다.
AngularJS는 scope에 저장된 값이 변경되면 자동으로 뷰도 갱신시킨다. 이것은 AngularJS를 강력하게 만든 양방향 동기화이다. 그런데 이와같은 자동 동기화는 어떤 메커니즘으로 구현될 것일까?
AngularJS는 다이제스트 주기라는 순환적 매커니즘 하에 모델과 뷰를 자동으로 동기화 시킨다.
이 동작은 더티체크(dirty check)라는 개념 위에 구현된 것이며, 이것은 단순히 angular.equals 메서드를 이용하여 속성값이 이전 값과 현재 값을 비교하여 동기화 여부를 결정하는 것이다.
다음 그림은 AngularJS 공식 사이트에 게제된 다이제스트 주기를 표현한 그림이다.
아래 설명은 AngularJS in action의 설명을 요약(조금 수정)한 것이다.
AngularJS는 컴파일 과정을 거치면서 $scope 객체에 정의된 모든 속성에 대한 감시(watch) 표현식을 생성한다. 감시 표현식을 통해 $scope객체의 속성이 변경된 것을 알게 되면 리스너 함수가 호출된다.
간혹 AngularJS가 속성값이 변경되었다는 사실을 알아내지 못하는 경우도 있다. 이런 경우 $apply 객체를 통해 다이제스트 주기를 직접 실행할 수 있다. 대부분 API 호출이나 서드 파트 라이브러리가 수행한 작업을 AngualrJS에게 알리고자 할 때 이 기법을 활용한다.
AngularJS의 자동 바인딩 메커니즘의 개략적인 설명이며, 보다 자세한 이해를 위해서 다음의 블로그에 정리된 글을 읽어 보기를 권장한다.
=> http://www.nextree.co.kr/p8890/
설명이 아주 쉽고 자세히 잘 되어 있으며, 특히 실무에서 자주 맞주칠 수 있는 3rd Party 라이브러리 사용시 자동 바인딩을 위한 $scope.$apply() 함수 사용부분을 빼먹지 않고 봐두기를 바란다.
AngularJS는 기본적으로 위에서 설명한 메커니즘 하에, scope 속성이 변경되면 뷰에 자동으로 변경을 반영시킨다. 하지만 AngularJS의 관리 밖의 영역에서 발생한 이벤트는 인식하지 못하기 때문에 scope 속성값의 변화를 감지하지 못하게 된다. 따라서 $scope.$apply() 함수를 이용해 수동으로 다이제스트 주기를 시작하도록 할 수 있다.