UI Laboratory

UI 개발을 위한 레퍼런스

INDEX

AngularJS가 제공하는 주요 기능


1장. AngularJS 부트스트랩


AngularJS는 단순한 HTML 페이지를 AngularJS 웹 애플리케이션으로 동작하기 위한 프로세스가 존재한다. 이를 AngularJS 부트스트랩이라고 할 수 있다. AngularJS 부트스트랩은 크게 ng-app 지시자를 이용한 부트스트랩과 angular.bootstrap 메서드를 이용한 부트스트랩으로 나눌 수 있다.

ng-app 지시자를 이용한 부트스트랩.

웹 페이지에 AngularJS를 적용하기 위해서는 특정 태그에 ng-app 속성을 추가해야만 AngularJS 웹 애플리케이션이 된다.

			<!doctype html>
			<html ng-app lang="ko-KR">
				<head>
					<meta charset="utf-8">
					TODO App Demo
					
				</head>
				<body ng-init="name='AngluarJS'">
					

Hello {{name}}

</body> </html>

ng-app은 AngularJS 웹 애플리케이션의 범위를 제한한다. ng-app 지시자를 추가한 노드가 루트 노드가 되며 하위 노드들은 AngularJS의 기능을 사용할 수 있게 된다. 한가지 주의할 사항은 ng-app 지시자는 해당 페이지에서 한번만 사용해야 한다는 점이다. 그래서 대부분 <html> 태그나 <body> 태그에서 ng-app 지시자를 사용한다.

			<div ng-app>
				<h1>{{1+2}}</h1>
			</div>
			<div>
				{{1+2}}
			</div>
		

첫번째 <div>태그는 ng-app 지시자를 추가했기 때문에 첫 번째 {{1+2}}가 계산되어 브라우저에 3이 출력된다. 하지만 아래에 있는 &div>는 ng-app 지시자가 추가된 태그의 하위 노드가 아니므로 계산되지 않은 {{1+2}} 자체가 출력된다.

IE7을 지원하려면 id="ng-app"을 이용해야 한다. 사용자 정의 속성인 data를 이용해 data-ng-app으로 작성할 수도 있다.

ng-app 지시자를 추가함으로써 AngularJS가 웹 애플리케이션을 부트스트랩하게 된다. 부트스트랩 과정은 다음과 같다. 최초에 AngularJS는 AngularJS 스크립트가 샐행되고 DOMContentLoaded 이벤트가 발생하여 document.readyState가 'complete' 상태가 되면 HTML 페이지를 읽으면서 ng-app 속성을 찾는다. 그리고 ng-app 속성을 발견하면 다음과 같은 부트스트랩 절차가 이뤄진다.

  1. 1. ng-app에 값으로 주어진 모듈을 로드한다.
  2. 2. 애플리케이션에 유일한 injector를 생성한다.
  3. 3. ng-app 지시자가 적용된 정적 DOM을 루트로 하여 컴파일한다. 컴파일 시 $rootScope를 전달하고 앞에서 얘기했듯이 해당 정적 DOM에 AngularJS가 적용되어 동적 DOM이 만들어지면 이를 브라우저가 렌더링하여 우리가 보는 뷰가 만들어진다.

자바스크립트 API를 이용한 부트스트랩

AngularJS는 수동으로 AngularJS를 부트스트랩할 수 있는 API를 제공한다. 수동으로 특정 DOM을 부트스트랩하는 API는 다음과 같다.

			angular.bootstrap(DOM 객체)	
		

예를 들어, 다음과 같이 두개의 <div> 영역이 있고 각 <div> 태그의 아이디가 app1, app2라고 하자.

			
{{1+2}}
{{1+2}}

여기에 다음과 같이 자바스크립트를 추가하면 아이디가 app1인 <div>영역에 AngularJS가 정상적으로 부트스트랩되어 브라우저에 3이 출력될 것이다.

			<script>
				angular.element(document).ready(function(){
					angular.bootstrap(document.getElementById('app1'));
				});
			</script>
		

수동으로 부트스트랩하면 자동으로 부트스트랩할 때와 달리 하나의 페이지에 여러 개의 DOM을 부트스트랩할 수 있다. 다음 코드와 같이 아이디가 app2인 <div> 태그 또한 선택하여 부트스트랩할 수 있는 것이다.

			<script>
				angular.element(document).ready(function(){
					angular.bootstrap(document.getElementById('app1'));
					angular.bootstrap(document.getElementById('app2'));
				});
			</script>
		

수동 부트스트랩은 위와 같이 한 페이지에서 서로 다른 AngularJS 웹 애플리케이션을 만들고자 할 때 사용하거나 DOMContentLoaded 이벤트가 발생한 시점이 아닌 별도의 시점에서 AngularJS를 부트스트랩할 때 사용할 수 있다.

2장. 템플릿 시스템과 데이터 바인딩


AngularJS는 화면과 데이터의 분리를 용이하게 하는 템플릿 시스템과 데이터와 화면 사이를 싱크할 수 있게 하는 데이터 바인딩을 제공함으로써 구조적이로 재사용하기 좋은 웹 애플리케이션을 개발할 수 있다.

AngularJS의 템플릿

AngularJS도 클라이언트 측의 템플릿 시스템을 제공한다. 하지만 AngularJS에서는 템플릿이 HTML 그 자체다. 이 DOM에는 HTML, CSS, 그리고 AngularJS에서 제공하는 특정한 요소나 속성인 지시자가 포함된다.

다음은 AngularJS로 작성한 템플릿의 예이다. [예제보기]

<!doctype html>
<html ng-app lang="ko-KR">
	<head>
		<meta charset="utf-8">
		<title>
		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
		<script src="../bower_components/angular/angular.js">
	<head>
	<body ng-init="person={name:'영희', favorite:['사과','딸기','포도']}">
		

hello {{person.name}}

<h2>좋아하는 과일 </body> </html>

위 코드를 보면 우선 자바스크립트 코드를 단 한줄도 작성하지 않았다. 그리고 ng로 시작하는 속성과 {{}}를 제외하고는 모두 순수한 HTML 코드다.

다음은 템플릿 작성시 사용되는 AngularJS의 기능이다.

| 이중 중괄호와 AngularJS 표현식

			

hello {{person.name}}

템플릿에서 이중 중괄호 {{}}을 사용해 특정 위치에 표현할 데이터를 지정할 수 있으며 이중 중괄호에는 AngularJS 표현식을 작성할 수 있다. AngularJS의 표현식은 자바스크립트의 표현식과 대부분 비슷하다. 다음 목록은 AngularJS 표현식과 자바스크립트 표현식과의 차이점이다.

다음은 여러 방식으로 표현식을 사용한 예제 코드다. [예제보기]

<!doctype html>
<html ng-app lang="ko-KR">
	<head>
		<meta charset="utf-8">
		<title>
		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
		<script src="../bower_components/angular/angular.js">
	<head>
	<body ng-init="person={name:'영희', favorite:['사과','딸기','포도']}">
		

hello {{person.name}}

좋아하는 과일의 갯수 : {{numberOfFavorite=person.favorite.length}}

과일 갯수 * $100 = {{numberOfFavorite*100|currency}}

재도가 맞나요? {{person.name=='재도'}}

좋아하는 과일 수가 4개보다 많나요? {{numberOfFavorite>4}}

scope에 없는 객체 접근하면? {{car.type.name}}

</body> </html>

AngularJS는 이중 중괄호와 표현식을 이용해 템플릿에서 데이터를 표현했다. 하지만 이 표현식을 꼭 이중 중괄호에서만 사용할 수 있는 건 아니다. 위에서 ng-init을 보았듯이 특정 속성의 값으로도 표현식을 사용할 수 있다. 어떤 속성의 값에 표현식을 사용할 수 있는지는 개발자 사이트인 docs.angularjs.org의 API 섹션에서 확인할 수 있다.

데이터 바인딩의 이해

데이터 바인딩이란 두 데이터 혹은 정보의 소스를 모두 일치시키는 기법이다. 즉 화면에 보이는 데이터와 브라우저 메모리에 있는 데이터를 일치시키는 기법이다. 대다수의 자바스크립트 프레임워크가 단방향 데이터 바인딩을 지원하는 반면 AngularJS는 양방향 데이터 바인딩을 제공한다.

단방향 데이터 바인딩은 데이터와 템플릿을 결합하여 화면을 생성한다. 반면 양방향 데이터 바인딩은 데이터의 변화를 감지해 템플릿과 결합하여 화면을 갱신하고 화면에서의 입력에 따라 데이터를 갱신한다. 즉, 데이터와 화면 사이의 데이터가 계속해서 일치하게 되는 것이다.

다음은 AngularJS의 양방향 데이터 바인딩을 보여주는 코드이다. [예제보기]

<!doctype html>
<html ng-app lang="ko-KR">
	<head>
		<meta charset="utf-8">
		<title>
		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
		<script src="../bower_components/angular/angular.js">
		<script>
			function mainCtrl($scope){
				var menuList = [
					{itemId : 1, itemName : '샌드위치', itemPrice : 2000, itemCount : 0},
					{itemId : 2, itemName : '아메리카노', itemPrice : 1000, itemCount : 0},
					{itemId : 3, itemName : '카푸치노', itemPrice : 1500, itemCount : 0},
				];

				$scope.menuList = menuList;
				$scope.totalPrice = 0;

				$scope.buy = function(){
					$scope.totalPrice = 0;

					angular.forEach($scope.menuList, function(menu, idx){
						$scope.totalPrice = $scope.totalPrice + (menu.itemPrice * Number(menu.itemCount));
					});
				};
			}
		</script>
	<head
	<body ng-controller="mainCtrl">
		

메뉴판

<h2>메뉴 목록
메뉴판가격갯수
{{menu.itemName}} {{menu.itemPrice}}
<h2>구입 가격
가격 : {{totalPrice}}
</body> </html>

AngularJS의 양방향 데이터 바이딩은 웹 애플리케이션의 복잡도가 증가하면 증가할수록 빛을 발한다. 수많은 코드의 양을 줄여줄 뿐만 아니라 유지보수나 코드를 관리하기 매우 쉽게 해준다. 양방향 데이터 바인딩은 AngularJS의 주요기능이자 핵심이다.

반복적인 데이터 표현을 위한 템플릿(반복 지시자)

반복적으로 표현해야 할 데이터는 주로 배열에 들어 있는데 AngularJS에서는 반복적인 데이터 표현을 위한 ng-repeat 지시자를 제공한다.

ng-repeat의 사용법
  • <any ng-repeat="변수명 in 표현식">

    변수명은 주어진 배열의 요소를 반복문 내부에서 참조할 때 사용된다. 표현식은 $scope 내의 배열과 같은 순환할 대상을 가리킨다.

  • <any ng-repeat="(key 변수명, value 변수명) in 표현식">

    자바스크립트 객체같은 데이터를 순활할 때 사용한다. key 변수명은 반복문 내부에서 객체의 key를 참조할 변수명이고 value 변수명은 마찬가지로 참조하는 value의 변수명이다.

  • <any ng-repeat="변수명 in 표현식 track by 표현식">

    배열 요소와 생성되는 DOM 요소를 연결할 때 사용하는 고유한 값을 지정할 수 있다.

ng-repeat을 적용한 HTML 요소는 배열 요소의 개수만큼 HTML 요소를 생성한다. 그리고 해당 HTML 요소에는 별도의 scope 영역이 생성되는데 해당 scope 영역에서만 사용할 수 있는 특별한 속성을 제공한다.

다음은 고객 목록과 친구 목록을 보여주는 예제이다. [예제보기]

<!doctype html>
<html ng-app lang="ko-KR">
	<head>
		<meta charset="utf-8">
		<title>
		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
		<script src="../bower_components/angular/angular.js">
	<head>
	<body>
		
고객 목록
  • [{{$index+1}}] 고객 이름 : {{customer.name}}, 고객 나이 : {{customer.age}}
내 친구 소개
  • {{attr}} : {{value}}

</body> </html>

조건적인 데이터 표현을 위한 템플릿(조건 지시자)

AngularJS의 템플릿에서도 자바스크립트의 switch와 if문과 비슷한 기능의 지시자를 제공한다. ng-switchng-if가 그렇다. 비슷한 지시자로 ng-showng-hide 지시자가 있다. 이 두 지시자는 조건적으로 적용된 DOM의 CSS 속성 중 display 속성을 제어하는 지시자다.

ng-switch의 사용법
						<ANY ng-switch="표현식">
							<ANY ng-switch-when="조건 일치 값1">...
							<ANY ng-switch-when="조건 일치 값2">...
							...
							<ANY ng-switch-default>...
						<ANY>
					

다음은 ng-switch 지시자를 사용하는 예제이다. 상단에 있는 텍스트에 따라 아래에 있는 색을 선택하여 바꾸는 예제다. [예제보기]

<!doctype html>
<html ng-app lang="ko-KR">
	<head>
		<meta charset="utf-8">
		<title>
		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
		<script src="../bower_components/angular/angular.js">
		<style type="text/css">
			.box {width:100px; height:100px;}
			.red {background-color:red;}
			.green {background-color:green;}
			.blue {background-color:blue;}
			.black {background-color:black;}
		</style>
	<head>
	<body>
		
빨간색
녹색
파란색
</body> </html>
ng-if 지시자의 사용법
						<ANY ng-if="표현식">
							...
						<ANY>
					

ng-if는 표현식 결과값의 참/거짓 여부에 따라 해당 요소를 없애거나 생성하는 지시자다. 참고로 요소 자체가 없어지거나 생성될 때 scope 또한 없어지고 생성된다.

다음은 약관 동의에 따라 다음으로 진행할 수 있게 하는 버튼이 보이는 예제이다. [예제보기]

<!doctype html>
<html ng-app lang="ko-KR">
	<head>
		<meta charset="utf-8">
		<title>
		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
		<script src="../bower_components/angular/angular.js">
	<head>
	<body>
		약관에 동의 : <input type="checkbox" ng-model="checked" ng-init="checked=false" />
동의하면 다음으로 진행됩니다. <button ng-if="checked">다음</button> </body> </html>
ng-show와 ng-hide 지시자의 사용법
						<ANY ng-show="표현식">
							...
						<ANY>
						<ANY ng-hide="표현식">
							...
						<ANY>
					

ng-show와 ng-hide 지시자는 ng-if 지시자와 마찬가지로 표현식 결과값(참/거짓)에 따라 해당 요소를 화면에 보여주거나 숨긴다. ng-show 지시자는 참일때 요소를 보이게 하고 거짓일때 숨기며, ng-hide 지시자는 반대로 참일때 숨기고 거짓일 때 보이게 한다. [예제보기]

<!doctype html>
<html ng-app lang="ko-KR">
	<head>
		<meta charset="utf-8">
		<title>
		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
		<script src="../bower_components/angular/angular.js">
	<head>
	<body>
		동의 여부 : <input type="checkbox" ng-model="checked" />
다음으로 진행합니다.
다음으로 진행할 수 없습니다.
</body> </html>

비즈니스 로직 처리를 위한 템플릿(컨트롤러 지시자)

데이터를 가공하거나 서버에 데이터를 저장하고 서버로부터 데이터를 불러오는 등의 애플리케이션 로직에 해당하는 코드는 자바스크립트로 작성해야 한다. 그래서 템플릿의 특정 부분을 처리하는 자바스크립트 함수 이름을 템플릿에 명시하게 한다. 이러한 함수를 컨트롤러 함수라고 하고 ng-controller 지시자를 이용해 템플릿이 컨트롤러 함수를 참조할수 있게 해준다.

템플릿에서 컨트롤러 함수를 사용하려면 ng-controller 지시자를 특정 요소의 속성으로 작성하면 된다. 그러면 해당 요소와 자식 요소를 모두 포함하여 컨트롤할 대상 함수를 지정하게 되고 대상 컨트롤러 함수에서 애플리케이션의 로직을 구현할 수 있다.

ng-controller의 사용법
						<ANY ng-controller="표현식">
							...
						<ANY>
					

여기서 표현식은 전역적으로 접근할 수 있는 자바스크립트 함수의 이름이거나 모듈로 등록된 컨트롤러 함수 이름이거나 현재 scope에서 접근할 수 있는 함수의 이름이다.

다음은 고객 목록 중 18세 미만인 고객을 별도로 보여주는 예제이다. [예제보기]

<!doctype html>
<html ng-app lang="ko-KR">
	<head>
		<meta charset="utf-8">
		<title>
		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
		<script src="../bower_components/angular/angular.js">
		<script>
			function customerCtrl($scope){
				var customerList = [{name:'영희', age:10}, {name:'순희', age:28}];
				var youngCusterList = [];
				angular.forEach(customerList, function(value, key){
					if(value.age<18){
						youngCusterList.push(value);
					}
				});

				$scope.customerList = customerList;
				$scope.youngCusterList = youngCusterList;
			}
		</script>
	<head>
	<body ng-controller="customerCtrl">
		
고객 목록
  • [{{$index+1}}] 고객 이름 : {{customer.name}}, 고객 나이 : {{customer.age}}
18세 미만 고객
  • [{{$index+1}}] 고객 이름 : {{youngCuster.name}}, 고객 나이 : {{youngCuster.age}}
</body> </html>

예제를 보면 해당 고객 목록을 인자로 받는 $scope에 대입하고 있다. 이렇게 $scope에 속성으로 대입해야 컨트롤러 함수와 연결된 템플릿의 요소에서 표현식을 이용해 접근할 수 있다. ng-controller 지시자로 컨트롤러 함수와 해당 지시자를 사용한 템플릿의 요소가 연결되어 컨트롤러 함수가 연결된 요소의 비즈니스 로직을 처리한다.

폼과 유효성 검사를 위한 템플릿(폼/입력 지시자)

사용자와 서버가 서로 데이터를 주고받기 위해 HTML 에서는 <form>요소를 제공한다. 사용자가 입력한 값이 유효한 값으로 채워졌는지 검사하는 것을 폼 양식 유효성 검증이라 한다. AngularJS는 유효성 검증을 손쉽게 처리할 수 있게 폼 양식 유효성 검증과 관련된 지시자를 제공한다.

| <input> 타입 사용법

텍스트 타입 사용법
						<input type="text" ng-model="문자열" name="문자열" ng-required="문자열" ng-minlength="숫자" ng-maxlength="숫자" ng-pattern="문자열" ng-change="문자열">
					
  • ng-model : 바인딩 대상이 되는 모델
  • name : 폼에서 사용하는 이름
  • ng-required : 필수 입력 여부
  • ng-minlength : 입력박스에서 입력되는 값의 최소 글자수
  • ng-maxlength : 입력박스에서 입력되는 값의 최대 글자수
  • ng-pattern : 입력된 값과 비교될 정규표현식이며 /정규표현식/과 같은 값이 요구된다.
  • ng-change : 사용자의 입력이 발생할 때 실행될 표현식

다음은 이름과 힌드폰 번호를 입력하는 <input> 요소의 유효성을 검증하여 그 결과를 보여주는 예제이다. [예제보기]

<!doctype html>
<html ng-app lang="ko-KR">
	<head>
		<meta charset="utf-8">
		<title>
		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
		<script src="../bower_components/angular/angular.js">
	<head>
	<body>
		
이름 : 필수입력
핸드폰 번호 : 000-0000-0000

사용자 정보 : {{name}}/{{tel}}

smapleForm.name.$valid = {{sampleForm.name.$valid}}

smapleForm.name.$error = {{sampleForm.name.$error}}

smapleForm.tel.$valid = {{sampleForm.name.$valid}}

smapleForm.tel.$error= {{sampleForm.name.$error}}

smapleForm.$valid = {{sampleForm.$valid}}

smapleForm.$error.required= {{!!sampleForm.$error.required}}

</body> </html>

| FormController와 NgModelController

AngularJS는 표준 HTML 태그 또한 AngularJS 지시자로 만들 수 있다. 즉, <form>태그도 지시자로 간주하고 확장할 수 있다. 그래서 AngularJS는 폼의 상태를 관리하기 위해서 FormController를 만들었다. AngularJS 기반의 웹 애플리케이션에서 각 <form>은 FormController의 인스턴스이고 <form>의 name 속성에 준 값을 이용해 $scope에서 접근할 수 있다. 가령 다음과 같이 <form> 태그를 템플릿에서 사용했다고 가정하자.

			
...

그러면 폼에 sampleForm이라는 이름이 주어졌기 때문에 $scope.sampleForm으로 접근할 수 있다. <form>은 FormController의 인스턴스이므로 객체다. 다음은 이 객체의 속성과 메서드이다.

 구분

 속성/메서드 명

 내용

 속성

 $pristine

사용자의 입력이 없었으면 true다 

 $dirty 

사용자의 입력이 있었으면 true다 

 $valid

<form>에 있는 컨트롤 요소(input)가 모두 유효성 검증을 통과하면 true다 

 $invalid

<form>에 있는 컨트롤 요소(input)가 모두 유효성 검증을 통과하면 false다 

 $error

유효성 검증의 이름(required, email, minlength..)을 키로 하고 각 컨트롤 요소의 name을 값으로 가진 객체다 (예) { "required" : false, "maxlength" : false } 

 메서드

 $addControl()

컨트롤 요소를 추가한다 

 $removeControl()

컨트롤 요소를 삭제한다 

 $setDirty()

$dirty 값을 바꾼다. 즉, 강제로 폼이 수정된 상태를 변경한다 

 $setValidity()

<form> 요소의 유효성 상태를 바꾼다 

 $setPristine()

<form> 요소의 $pristine을 false로 변경한다 

앞에서 본 FormController가 <form> 요소의 유효성 상태나 사용자의 입력상태를 관리한다면 <form> 요소에 있는 컨트롤 요소 즉 <input>, <select>, <textarea> 요소 개개의 유효성 상태나 사용자 입력상태는 NgModelController가 관리한다. 컨트롤 요소는 모두 이 NgModelController의 인스턴스로 제어가 된다. 또한 컨트롤 요소의 name 속성의 값을 이용해 해당 인스턴스에 접근할 수 있다. 다음은 NgModelController의 속성과 메서드이다.

 구분

 속성/메서드 명

 내용

속성 

 $viewValue

화면에서 보이는 값이다 

 $modelValue

컨트롤 요소가 바인딩된 모델 값이다 

 $parsers

함수들의 배열이다. 각 함수는 순서대로 DOM으로부터 값을 읽을 때마다 호출된다. 즉 $viewValue의 값이 바뀔 때 호출된다. 호출된 함수가 반환한 값은 다음 함수로 전달되고 최종적으로 $modelValue에 값이 전달된다 

 $formatters

 함수의 배열이다. 각 함수는 순서대로 바인딩된 데이터($modelValue)가 바뀔 때마다 호출된다. 호출된 함수가 반환한 값은 다음 함수로 전달되고 최종적으로 $viewValue에 전달된다

 $viewChangeListeners

 화면 요소의 값이 변경될 때마다 호출되는 함수의 배열이다. 해당 함수들은 어떠한 인자도 없이 호출되고 반환된 값은 무시한다

 $error

 유효성 검증의 이름(required, email, minlength..)을 키로 하고 각 컨트롤 요소의 name을 값으로 가진 객체다 (예) { "required" : false, "maxlength" : false } 

 $pristine

 사용자의 입력이 없었으면 true다 

 $dirty

 사용자의 입력이 있었으면 true다 

 $valid

<form>에 있는 컨트롤 요소(input)가 모두 유효성 검증을 통과하면 true다  

 $invalid

<form>에 있는 컨트롤 요소(input)가 모두 유효성 검증을 통과하면 false다  

 메서드

$render() 

화면이 업데이트될 때마다 호출된다. 지시자를 만들어 NgModelController으 $render에 함수를 대입해 놓으면 화면이 업데이트될 때마다 호출된다

$setValidity(validationErrorKey, isValid)

유효성 상태를 설정하고 컨트롤 요소의 유효성 상태가 변경될 때 FormController에게 알려준다 

$isEmpty()

입력 요소의 값이 빈 값인지 확인한다. 여기서 빈 값이란 undefined, '', null 또는 NaN이 해당된다. 그리고 해당 메서드를 오버라이드하면 빈 값에 대한 정의를 다시 할 수 있다 

$setPristine()

 요소의 $pristine을 false로 변경한다 

$setViewValue()

화면에 값을 추가한다 

| 체크박스 타입 사용법

체크박스 타입 사용법
						<input type="checkbox" ng-model="문자열" name="문자열" ng-true-value="문자열" ng-false-value="문자열" ng-required="true/false" ng-change="문자열">
					
  • ng-model : 바인딩 대상이 되는 모델명
  • name : 폼에서 사용하는 이름
  • ng-true-value: 체크박스를 체크했을 때 바인딩된 모델에 대입할 값(기본값은 true)
  • ng-false-value: 체크박스의 체크를 해제했을 때 바인딩된 모델에 대입할 값(기본값은 false)
  • ng-required: 필수 입력 여부
  • ng-change : 사용자의 입력이 발생할 때 실행될 표현식

다음은 단순히 체크박스와 바인딩된 값을 화면에 보여주는 예제이다. [예제보기]

<!doctype html>
<html ng-app lang="ko-KR">
	<head>
		<meta charset="utf-8">
		<title>
		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
		<script src="../bower_components/angular/angular.js">
	<head>
	<body>
		
선택 1 :
선택 2 :

선택1의 바인딩된 값 : {{value1}}

선택2의 바인딩된 값 : {{value2}}

</body> </html>

AngularJS는 텍스트 타입과 체크박스 타입 외에도 숫자 타입, URL 타입, 이메일 타입, 라디오 타입의 <input> 요소를 제공한다. 숫자 타입은 min, max 속성으로 최대값과 최소값을 정의할 수 있고 라디오 타입은 value 속성으로 해당 라디오 버튼을 클릭했을 때 바인딩된 모델에 대입할 값을 정의할 수 있다.

| <select> 요소 사용법

선택박스 요소 사용법
						<select ng-model="문자열" name="문자열" ng-options="별도의 표현식" ng-required="true/false">
					
  • ng-model : 바인딩 대상이 되는 모델명
  • name : 폼에서 사용하는 이름
  • ng-required : 필수 입력 여부
  • ng-options : 옵션을 나타내기 위한 별도의 표현식

    배열 데이터일 때

    • label for value in array
    • select as label for value in array
    • label group by group for value in array
    • select as label group by group for value in array

    객체 데이터일 때

    • label for (key, vlaue) in object
    • select as label for (key, value) in object
    • label group by group for (key, value) in object
    • select as label group by group for (key, value) in object

AngularJS에서 제공하는 ng-options 지시자는 ng-repeat 처럼 반복적인 데이터를 위한 별도의 표현식을 제공한다. for-in이 기본 구조이고 as 혹은 group by 를 같이 사용한다. 다음은 ng-options 지시자에 사용하는 표현식을 구성하는 요소에 관한 설명이다.

다음은 항공 예약할 때 자주 보는 출발, 경유, 도착 국가를 콤보박스에서 선택하는 예제이다. [예제보기]

<!doctype html>
<html ng-app lang="ko-KR">
	<head>
		<meta charset="utf-8">
		<title>
		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
		<script src="../bower_components/angular/angular.js">
	<head>
	<body>
		
출발 국가 :
경유 국가 :
도착 국가(대륙 그룹별) :

출발 국가 : {{depCountry.name}}

경유 국가 : {{viaCountry}}

도착 국가 : {{arrCountry.name}}

출발 국가와 도착 국가는 필히 선택해주세요.
</body> </html>

| CSS 클래스로 유효성 검증 결과 표현하기

AngularJS는 컨트롤 요소의 유효성 검증 결과를 요소의 CSS 클래스로 알아서 추가해 준다. 가령 특정 텍스트 타입의 입력 요소가 필수 입력 요소인데 해당 입력 요소에 값이 주어지지 않으면 AngularJS는 ng-invalid CSS 클래스를 해당 입력 요소에 추가하다. 그런데 값이 입력되면 ng-invalid CSS 클래스는 없어지고 ng-valid CSS 클래스가 추가된다. 이처럼 ng-required나 ng-pattern과 같은 유효성 검사를 위한 속성이 <input> 요소와 같은 컨트롤 요소에 사용되고 유효성 검증에 성공하면 ng-valid CSS 클래스가 실패하면 ng-invalid CSS 클래스가 자동으로 추가되는 것이다. 그뿐만 아니라 해당 입력 요소에 사용자 입력이 없으면 ng-pristine CSS 클래스가 추가되고 사용자 입력이 발생하면 ng-dirty CSS 클래스가 추가된다.

다음은 이름과 핸드폰 번호 입력에 의한 유효성 검증 결과에 따라 ng-valid, ng-invalid, ng-pristine, ng-dirty와 같은 CSS 클래스가 동적으로 추가되는 예제이다. [예제보기]

<!doctype html>
<html ng-app lang="ko-KR">
	<head>
		<meta charset="utf-8">
		<title>
		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
		<script src="../bower_components/angular/angular.js">
		<style type="text/css">
			.ng-invalid {border:3px solid red;}
			.ng-valid {border:3px solid green;}
			.ng-pristin {border-style:solid;}
			.ng-dirty {border-style:dashed;}
		</style>
	<head>
	<body>
		
이름 : 핸드폰 번호 :
</body> </html>

이벤트 처리를 위한 템플릿(이벤트 처리 지시자)

AngularJS는 HTML 요소에서 발생하는 이벤트에 대한 처리를 자바스크립트를 사용하지 않고 할수 있게 해준다. 가령 특정 요소를 클릭하면 발생하는 클릭 이벤트를 ng-click 지시자를 이용해 이벤트를 처리할 수 있다.

			<div class="btn" ng-click="clickCount++">
		

사용자 클릭 이벤트 외에도 마우스오버, 마우스엔터, 키프레스 등 다양한 이벤트를 지원한다. 다음은 AngularJS에서 제공하는 이벤트 관련 지시자와 설명이다.

 이벤트명

지시자명 

설명 

 click

ng-click 

html 요소를 클릭했을 때 표현식이 계산된다 

 dbclick

ng-dbclick 

html 요소를 더블 클릭했을 때 표현식이 계산된다 

 keydown

ng-keydown 

키보드의 키를 누를 때 표현식이 계산된다. $event 객체를 이용해 keyCode 값 등을 가지고 올 수 있다 

 keypress

ng-keypress 

키보드의 키를 눌려 실제 문자가 입력됐을 때 표현식이 계산된다. $event 객체를 이용해 keyCode 값 등을 가지고 올 수 있다  

 keyup

ng-keyup 

키를 뗄 때 표현식이 계산된다. $event 객체를 이용해 keyCode 값 등을 가지고 올 수 있다  

 mousedown

ng-mousedown 

마우스 버튼을 누를 때 표현식이 계산된다 

 mouseenter

ng-mouseenter 

마우스 포인터가 개체 안에 들어올 때 표현식이 계산된다 

 mouseleave

ng-mouseleave 

마우스 포인터가 개체 밖으로 나갈 때 표현식이 계산된다 

 mousemove

ng-mousemove 

마우스 포인터가 개체 위에서 움직일 때 표현식이 계산된다 

 mouseover

ng-mouseover 

마우스 포인터가 개체 위로 들어올 때 표현식이 계산된다 

 mouseup

ng-mouseup 

마우스 포인터가 개체 위에 있는 동안 마우스 버튼을 뗄 때 표현식이 계산된다 

 change

ng-change 

<input>, <textarea> 요소에서 사용할 수 있다. 값이 변경될 때 표현식이 계산된다 

 blur

ng-blur 

<input>, <select>, <textarea>, <a> 요소에서 사용할 수 있다. 개체가 포커스를 잃을 때 표현식이 계산된다 

ng-change 지시자와 ng-blur 지시자를 제외한 다른 이벤트 지시자는 모든 요소에서 사용할 수 있다.

이벤트 지시자의 사용법
						<ANY ng-이벤트명="표현식">
							...
						</ANY>
					

이벤트가 발생하면 해당 표현식이 계산된다. 다음은 이벤트 지시자를 사용하여 다양한 이벤트 처리를 보여주는 예제이다.[예제보기]

<!doctype html>
<html ng-app lang="ko-KR">
	<head>
		<meta charset="utf-8">
		<title>
		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
		<script src="../bower_components/angular/angular.js">
		<script>
			function mainCtrl($scope){
				$scope.message = "";
				$scope.eventCnt = 0;
				$scope.handleEvt = function(message){
					$scope.message = message;
					$scope.eventCnt++;
				}
			}
		</script>
	<head>
	<body ng-controller="mainCtrl">
		
Click
Mousedown
Mouseenter
Mousemove
change : keydown :

{{message}} {{eventCnt}}

</body> </html>

예제를 보면 네 개의 <div> 요소가 있고 두개의 <input> 요소가 있다. 네개의 <div> 요소는 ng-click, ng-mousedown, ng-mouseenter, ng-mousemove 지시자를 사용해 각 이벤트가 발생했을 때 "handleEvt('mesg')" 표현식을 해석해 컨트롤러의 handleEvt 메서드를 호출한다. 그리고 두개의 <input>요소는 ng-change, ng-keydown 지시자를 이용해 값이 변경되거나 키보드의 키가 눌리면 handleEvt 메서드를 호출한다. 이렇게 AngularJS는 특정요소에 대한 이벤트 처리 함수를 제이쿼리와 다르게 자바스크립트에서 등록하지 않고 템플릿에서 작성한다.

CSS 클래스/스타일을 동적으로 처리하기 위한 템플릿 (클래스 지시자/스타일 지시자)

웹 애플리케이션을 개발하다 보면 특정 상황에서 CSS 클래스를 동적으로 처리해야 할 때가 있다. AngularJS 에서는 ng-class 지시자를 이용해 템플릿에서 CSS 클래스를 동적으로 처리할 수 있다.

ng-class의 사용법
						<ANY ng-class="표현식">
							...
						</ANY>

						또는

						<ANY class="ng-class:표현식">
							...
						</ANY>
					

ng-class는 태그의 속성 또는 class 속성의 값으로 ng-class:표현식과 같이 사용할 수 있다. 표현식의 결과값은 CSS 클래스 이름을 스페이스로 구분한 문자열이거나, CSS 클래스 이름들로 구성된 배열이거나, CSS 클래스 이름이 속성이름이고 true/false를 값으로 하는 객체여야 한다. 객체일 경우 특정 CSS 클래스 이름의 값이 true 일때에 해당 CSS 클래스가 요소에 적용된다.

다음은 과일 이름을 나열한 후 과일과 관련된 CSS를 추가하고 짝수 목록에 파란색을 나타내는 CSS 클래스를 추가하는 예제다. [예제보기]

<!doctype html>
<html ng-app lang="ko-KR">
	<head>
		<meta charset="utf-8">
		<title>
		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
		<script src="../bower_components/angular/angular.js">
		<style type="text/css">
			.apple {background-color:red;}
			.lemon {background-color:yellow;}
			.even {background-color:blue;}
		</style>
	<head>
	<body>
		
  • {{fruit}}
  • {{fruit}}
</body> </html>

만일 CSS 클래스가 미리 정의되어 있지 않고 HTML 요소의 스타일을 동적으로 변경하고 싶을 때는 어떻게 할 수 있을 까? AngularJS를 이용하면 템플릿에서 ng-style을 이용해 이를 해결할 수 있다.

ng-style의 사용법
						<ANY ng-style="표현식">
							...
						</ANY>
					

표현식의 결과는 객체여야 하고 해당 객체의 키가 CSS 스타일의 이름이 되며 객체의 값이 CSS 스타일 이름에 대한 값이 된다.

다음은 색 변경 버튼을 클릭하면 상단의 긴 박스의 배경색이 노란색으로 바뀌는 예제다. [예제보기]

<!doctype html>
<html ng-app lang="ko-KR">
	<head>
		<meta charset="utf-8">
		<title>
		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
		<script src="../bower_components/angular/angular.js">
		<script>
			function mainCtrl($scope){
				$scope.bgStyle = {backgroundColor: 'red'};
				$scope.changeColor = function(color){
					$scope.bgStyle.backgroundColor = color;
				}
			}
		</script>
	<head>
	<body>
		
{{bgStyle.backgroundColor}}
</body> </html>

3장. MVC - 모델, 뷰, 컨트롤러


모델

AngularJS에서는 사용자 정보, 도서 정보, 북마크 정보처럼 하나의 엔터티나 여러 개의 엔터티가 모델이 된다. 하지만 AngularJS는 모델을 정의하는데 있어서 다른 자바스크립트 프레임워크와 다른 점이 있다. ExtJS나 BackboneJS에서는 기본 모델 클래스가 있고 이를 개별 모델 클래스가 상속받는 구조인 반면 AngularJS에서는 별다른 상속없이 순수 자바스크립트 객체가 모델이 된다는 것이다. 하지만 중요한 점은 이러한 모델 객체는 AngularJS의 $scope 객체로부터 접근할 수 있어야 한다는 것이다.

모델은 컨트롤러에서 $scope 객체에 선언하거나 템플릿에서 선언할 수 있다. 다음 코드는 컨트롤러에서 모델을 선언한 코드다.

			function mainCtrl($scope){
				$scope.userId : "jeado";
				$scope.bookmark : {name: "구글", url: "www.google.com", image: "google.png"};
				$scope.bookmarkList : [{name: "구글", url: "www.google.com", image: "google.png"}, {name: "네이버", url: "www.naver.com", image: "naver.png"}]; 
			}
		

$scope의 속성명은 모델명을 나타내고 값은 모델이 된다. 여기서 모델은 단순한 문자열이 될 수도 있고 객체나 배열이 될 수도 있다. 즉, 모델은 평범한 자바스크립트 객체다. 모델은 자바스크립트에서 선언할 수도 있지만 HTML 템플릿에서도 선언할 수도 있다.

			

{{userId}}, {{bookmark}}

위 코드에서는 ng-init 지시자에서 표현식에 대입 연산자를 이용함으로써 직접 모델을 선언하였다. 하지만 HTML 템플릿에서 직접 모델을 선언하지 않았는데 간접적으로 모델이 만들어지기도 한다.

			
			
		

위 템플릿 코드를 보면 <input> 태그에서 ng-model 속성에 search 값을 대입했다. AngularJS에서는 ng-model 지시자를 사용하면 해당 모델이 $scope에 없을 경우 암묵적으로 $scope에 search 속성을 만들고 <input> 태그의 값을 search 속성의 값으로 대입한다. 즉 모델을 만들고 데이터를 연결하는 것이다. 또한 ng-repeat 지시자에서도 모델을 만들게 되는데 bookmarkList 배열 요소의 개수만큼 DOM을 생성하면서(앞의 코드에서는 <li> 요소) 해당 DOM과 연결된 $scope를 만든다. 그리고 해당 $scope에 모델을 추가한다.(앞의 코드에서는 bookmark 모델)

AngularJS에서 뷰는 문서객체모델이다. 브라우저에서 HTML 문서를 읽어서 DOM을 생성하는데 AngularJS에서는 이 DOM이 뷰가 되는 것이다. 템플릿과 뷰를 혼동할 수 있는데 AngularJS에서는 HTML문서가 템플릿이고 이 템플릿을 AngularJS가 읽어서 뷰를 생성한다. 뷰를 생성하는 과정은 다음과 같다.

  1. 1. HTML로 작성한 템플릿을 브라우저가 읽는다.
  2. 2. 브라우저는 문서 객체 모델을 생성한다.
  3. 3. <script src="angular.js">가 실행되어 AngularJS 소스가 실행된다.
  4. 4. DOM 생성시 DOM Content Loaded 이벤트가 발생하는데 AngularJS가 이때 생성된 정적 DOM을 읽고 뷰를 컴파일한다. 컴파일 시 확장 태그나 속성을 읽고 처리한 후 데이터를 바인딩한다.
  5. 5. 컴파일을 완료하면 동적 DOM, 즉 뷰가 생성된다.

컨트롤러

AngularJS에서 컨트롤러는 많은 일을 하지 않는다. 단 하나의 역할 즉, 애플리케이션의 로직을 담당한다. 다르게 설명하면 컨트롤러는 모델의 상태를 정의, 변경한다고 할 수 있다. 결국 $scope 객체에 데이터나 행위를 선언하는 것이다. 그리고 컨트롤러는 인자로 $scope를 전달받는 단순한 자바스크립트 함수다. 다음은 초기 모델의 상태를 정의하는 컨트롤러 함수다.

			function demoCtrl($scope){
				$scope.bookmarkList = [{name: "구글", url: "www.google.com", image: "google.png"}, {name: "네이버", url: "www.naver.com", image: "naver.png"}]; 
			}
		

이렇게 정의된 컨트롤러는 템플릿에서 ng-controller 지시자를 이용해 템플릿에서 사용할 수 있다.

			

위 처럼 모델의 초기상태를 정의할 수 있을 뿐만 아니라 몇 가지 행위를 추가할 수 있다.

			function demoCtrl($scope){
				$scope.addBookmark = function(name, url){
					$scope.bookmarkList.push({name:name, url:url});
				}
			}
		

하지만 컨트롤러 코드를 작성할 때 주의해야 할 점이 있다. 컨트롤러는 단 하나의 뷰에 해당하는 애플리케이션의 로직만을 담당해야 한다. 화면상의 로직이 아니라 애플리케이션 비즈니스 로직이다. 즉 다음 코드와 같이 DOM을 조작하는 행위와 같은 화면상의 로직은 사용하면 안된다.

			function demoCtrl($scope){
				$scope.addBookmark = function(name, url){
					$("ul#bookmarkList").add("
  • 이름 : "+name+", 주소 : "+url+"
  • "); } }

    AngularJS에서는 하나의 화면에 여러 컨트롤러를 작성할 수 있다. 하나의 화면은 사실 여러 뷰의 조합으로 이뤄질 수 있기 때문이다. 가령 검색 조건 뷰와 검색 결과 목록 뷰가 이루어진 북마크 조회 화면과 같이 말이다. 이렇게 여러 뷰가 만들어지면 하나의 컨트롤러를 하나의 뷰와 연결하는 것을 권장한다.

    AngularJS의 컨트롤러는 하나의 컨트롤러에 하나의 $scope만을 가지게 된다. searchCtrl 컨트롤러 함수와 bookmarkListCtrl 컨트롤러 함수 두개가 있을 경우 컨트롤러 함수당 별도의 $scope 객체가 생성된다. 그리고 AngularJS 애플리케이션 루트에 해당하는 $rootScope가 있다. 이렇게 하나의 화면에 여러 컨트롤러를 사용하면 컨트롤러별로 독립된 $scope가 생성된다. 각 독립된 $scope는 서로 참조할 수 없다. 그래서 컨트롤러 사이에 데이터를 공유해야 할 경우, 이를 해결하는 방법으로 서비스를 이용할 수 있다.

    $rootScope와 $scope

    $scope는 양방향 데이터 바인딩의 핵심이자 뷰와 컨트롤러를 이어주는 징검다리 역할을 한다. 사실 $scope는 그저 단순한 자바스크립트 객체에 불과하지만 연결된 DOM 요소에서 표현식이 계산되는 실행환경이며 뷰와 컨트롤러에서 사용되는 데이터와 기능이 살아 숨쉬는 공간이다.

    다음은 $scope의 특징을 보여준다.

    | $scope의 계층적 구조

    모든 AngularJS 애플리케이션은 하나의 $rootScope를 가진다. 이 $rootScope는 ng-app을 생성하며 ng-app이 선언된 DOM 요소가 최상위 노드가 되어 여러가지 $scope을 가지게 된다. 즉 DOM과 같은 계층적 구조에서 최상위 계층에 $rootScope가 존재하는 것이다. 또한 ng-controller나 ng-repeat과 같이 별도의 $scope를 생성하는 지시자는 각 지역변수 영역을 가지고 있다고 할 수 있다.

    다음은 ng-controller를 계층적으로 가지는 예제코드이다. [예제보기]

    <!doctype html>
    <html ng-app lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
    		<script src="../bower_components/angular/angular.js">
    		<script>
    			function parentCtrl($scope){
    				$scope.parent = {name:"parent Kim"};
    			}
    
    			function childCtrl($scope){
    				$scope.child = {name:"child Ko"};
    				$scope.changeParentName = function(){
    					$scope.parent.name = "another kim";
    				};
    			}
    		</script>
    	<head>
    	<body>
    		

    부모이름 : {{parent.name}}

    <h2>부모이름: {{parent.name}}</h2> <h3>자식이름: {{child.name}}</h3>
    </body> </html>

    위 코드를 보면 우선 <html> 태그에 ng-app을 사용해 $rootScope가 하나 만들어진다. 그리고 parentCtrl함수와 childCtrl함수를 해당 컨트롤러 이름으로 가지는 ng-controller로 작성된 <div> 태그를 연결하는 $scope가 있다. 그래서 총 세개의 $scope가 있다. childCtrl 컨트롤러와 연결된 <div> 태그를 보면 <h2>부모이름: {{parent.name}}</h2>라고 작성된 부분이 있는데 childCtrl 컨트롤러 함수에서는 parent라는 모델이 선언되지 않았지만 브라우저에서는 부모이름:parent Kim이라고 출력되며 또한 부모이름변경 버튼을 클릭하면 parentCtrl 컨트롤러의 parent 모델까지 값을 변경하는 것을 볼수 있다. 이는 부모 $scope로부터 프로토타입을 상속받기 때문이다. 즉, 자식 $scope에서 없는 모델 즉, 속성을 부모 $scope에서 찾는다.

    | Scope 타입

    지금까지 본 $scope 객체나 $rootScope 객체는 AngularJS 내부에서 정의하는 Scope 타입의 인스턴스다. 즉, 다음과 같이 별도의 생성자 함수가 AngularJS 내부에 정의되어 있다.

    			function Scope(){...}
    			Scope.prototype.$apply = function(){};
    			Scope.prototype.$digest = function(){};
    			Scope.prototype.$watch = function(){};
    			Scope.prototype.$new = function(){};
    		

    AngularJS는 초기 부트스트랩시 프레임워크 내부에서 $rootScope을 new Scope()과 같이 생성한 후 해당 $rootScope을 서비스로 제공한다. 그리고 ng-controller나 웹 애플리케이션에서는 다음과 같이 $rootScope을 이용해 자식 $scope 객체들을 만들수 있다.

    			var $scope = $rootScope.$new();
    		

    다음은 scope 타입의 프로토타입 메서드이다.

    위 함수를 사용 시점별로 묶으면 데이터 바이딩 처리시 $apply, $digest, $watch, $watchCollection을 사용하고 사용자 정의 이벤트처리 시 $broadcast, $emit, $on을 사용한다. $eval과 $evalAsync는 표현식을 $scope 객체의 컨텍스트에서 계산할때 사용하고 $new와 $destroy는 $scope의 생성과 파괴 처리시 사용한다. 컨트롤러에서 사용하는 $scope 객체는 scope 타입의 인스턴스이므로 프로토타입 상속에 의해 위 메서드를 사용할 수 있다.

    | $scope에서 사용자 정의 이벤트 처리

    사용자 정의 이벤트는 모두 $scope 객체를 통하여 처리되는데 $scope 객체에서 특정 이벤트를 발생시키면 이벤트를 발생한 $scope 객체의 자식이나 부모 $scope에서 해당 이벤트를 듣고 있다 처리를 한다.

    이벤트를 발생시키는 API는 $scope 객체의 $broadcast와 $emit 메서드가 있다. $broadcast는 자식 $scope에게 특정 이벤트의 이름으로 주어진 데이터와 함께 이벤트를 발생시킨다. 그리고 $emit은 반대로 부모 $scope에게 특정 이벤트의 이름으로 주어진 데이터와 함께 이벤트를 발생시킨다.

    $emit과 $broadcast로 발생되는 이벤트는 모두 $on 메서드를 이용해 특정 이벤트 이름에 해당하는 이벤트 리스너 함수를 등록할 수 있다. 이렇게 등록된 이벤트 리스너는 등록된 이벤트 이름으로 이벤트가 발생하게 되면 해당 이벤트 리스너 함수가 호출된다. 이벤트 리스너 함수의 첫번째 인자는 이벤트 객체이고 다음 인자는 $emit과 $broadcast로 이벤트 발생시 전달하는 데이터가 된다.

    다음은 하단에 메시지를 입력하면 중앙에 입력한 메시지가 보이고 상단에 공지사항을 입력하면 "[공지]"라는 접두사가 붙어 중앙에 메시지가 보이는 예제코드이다. [예제보기]

    <!doctype html>
    <html ng-app lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
    		<script src="../bower_components/angular/angular.js">
    		
    		
    	<head>
    	<body ng-controller="mainCtrl">
    		
    		
    		
    • {{msg}}
    </body> </html>

    위 예제는 맨 위에 mainCtrl 컨트롤러 함수와 그 아래로 chatMsgListCtrl 컨트롤러 함수와 chatMsgInputCtrl 컨트롤러 함수가 정의돼 있다. 그리고 chatMsgListCtrl과 chatMsgInputCtrl 컨트롤러 함수에 연결된 DOM이 모두 mainCtrl 컨트롤러 함수에 연결된 DOM의 자식이므로 두 컨트로러 함수의 $scope은 mainCtrl 컨트롤러 함수 $scope의 자식이 된다.

    mainCtrl 컨트롤러 함수의 broadcast 메서드는 "chat:noticeMsg" 이벤트 이름으로 사용자가 입력한 메시지 내용과 함께 이벤트를 발생시킨다($broadcast로). 그러면 chatMsgListCtrl 컨트롤러 함수의 $scope 객체는 mainCtrl 컨트롤러 함수의 부모 $scope에서 발생한 "chat:noticeMsg" 이벤트를 듣고 있다가 공지사항 내용을 "[공지]" 문자열과 결함하여 메시지 목록에 추가하고 이를 메시지 목록 화면에 반영되게 된다.

    하지만 하단의 chatMsgInputCtrl 컨트롤러 함수의 $scope 객체는 chatMsgListCtrl 컨트롤러 함수와 헝제관계다. 그래서 chatMsgInputCtrl 컨트롤러 함수에서 $broadcast 이벤트가 발생하면 chatMsgListCtrl 컨트롤러 함수의 $scope가 이벤트를 감지할 방법이 없다. 이럴 때는 chatMsgInputCtrl 컨트롤러가 $rootScope까지 이벤트를 전파하는 $emit 메서드를 이용해 "chat:newMsg" 이벤트를 전파하고 chatMsgListCtrl 컨트롤러는 $rootScope를 주입받아 $rootScope에 $on 메서드를 이용해 "chat:newMsg" 이벤트에 대한 처리를 할 수 있다.

    4장. 모듈


    모듈은 대체로 관련된 기능을 하나로 묶어 다른 코드와 결합도를 줄이고 재사용성을 높이기 위해 사용한다. AngularJS에서도 이러한 모듈을 만들 수 있는 기능을 제공하고 별도의 모듈 패턴을 구현할 필요가 없는 API를 제공한다. 다음은 모듈을 선언하는 코드다.

    angular.module("모듈이름", ["사용할 모듈", ...])

    angular.module 함수를 사용해 모듈을 만들면 모듈 인스턴스가 반환되는데 해당 모듈 인스턴스는 컨트롤러, 서비스, 지시자, 필터들을 담는다.

    모듈 인스턴스 메서드
    모듈 메서드 설명
    Module.config(configFunction) 모듈이 로딩될 때 호출되며 config 함수에 해당 익명 함수로 서비스를 설정할 수 있다.
    Module.constant(name, object) 모듈에서 사용되는 상수를 등록한다.
    Module.controller(name, constructor) 모듈에서 사용되는 컨트롤러를 등록한다.
    Module.directive(name, directiveFactory) 모듈에서 사용되는 지시자를 등록한다.
    Module.factory(name, providerFunction) 모듈에서 사용되는 서비스를 팩토리형태로 등록한다.
    Module.filter(name, filterFactory) 모듈에서 사용되는 필터를 등록한다.
    Module.provider(name, providerType) 모듈에서 사용되는 서비스를 제공하는 프로바이더를 등록한다.
    Module.run(initializationFn) 애플리케이션 초기화 함수를 등록한다. 모든 모듈의 등록을 완료했을 때 초기화 함수가 실행된다.
    Module.service(name, constructor) 모듈에서 사용되는 서비스를 등록한다.
    Module.value(name, object) 모듈에서 사용되는 객체를 등록한다.

    하나의 AngularJS 웹 애플리케이션은 하나의 모듈을 지정할 수 있다. 해당 모듈은 run 함수를 이용해 애플리케이션 시작에 대한 로직을 작성할 수 있다. 그리고 해당 애플리케이션에서 사용하는 컨트롤러, 서비스, 필터 그리고 지시자를 등록할 수 있다.

    모듈을 이용한 컨트롤러 등록

    모듈을 이용해 컨트롤러를 선언하고 해당 모듈을 사용하는 애플리케이션이 등록한 컨트롤러를 사용할 수 있다.

    다음 예제는 북마크를 관리하는 컨트롤러이다. [예제보기]

    <!doctype html>
    <html ng-app="ngBookmark" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
    		<script src="../bower_components/angular/angular.js">
    		
    	<head>
    	<body>
    		

    북마크 목록

    • {{bookmark.name}} : {{bookmark.url}}
    </body> </html>

    다른 모듈의 사용

    ng-app에 해당 애플리케이션이 사용하는 모듈명을 값으로 주었다. 배열로 준 것이 아니라 하나의 값이 들어가므로 하나의 애플리케이션에서는 하나의 모듈만 사용할 수 있는 것처럼 보인다. 그렇다면 재사용되는 코드의 단위를 좀 더 세세하게 쪼개어 여러 모듈로 만들고 싶을 때에는 어떻게 할 수 있을까? 모듈을 선언하는 자바스크립트 API를 보면 사용할 모듈의 이름을 배열로 전달할 수 있다.

    angular.module("ngBookmark", ["moduleA", "moduleB"])

    위와 같이 ngBookmark 모듈을 선언할 때 해당 모듈이 참조하는 moduleA와 moduleB를 선언하는 것이다. 사실 우리가 ng-repeat, ng-select와 같은 지시자와 $http, $log와 같은 서비스 모두 기본 모듈인 ng 모듈에서 제공하는 것들이다. 우리가 모듈을 선언하면 기본적으로 ng 모듈을 사용하게 된다. 하지만 기본 ng 모듈 외에도 AngularJS에서는 별도의 모듈을 제공하고 있다. ngMock, ngCookies, ngResource, ngSanitize가 그러하다. 다음은 ngCookies 모듈을 사용하는 예제이다.[예제보기]

    <!doctype html>
    <html ng-app="cookieDemo" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
    		<script src="../bower_components/angular/angular.js">
    		
    	<head>
    	<body ng-controller="mainCtrl">
    		

    Cookie 서비스 사용

    test 키로 저장된 값 : {{value}}


    </body> </html>

    5장. 지시자의 모든 것


    AngularJS에서는 기존 HTML에서 제공하지 않는 기능을 확장하는 방식을 지시자로서 제공한다. AngularJS에서는 해당 DOM과 연결된 하나의 함수를 만들고 이 함수가 DOM을 조작하여 새로운 기능을 추가하는 등의 행위를 할 수 있다. 그렇다 이 함수가 특정 DOM과 연결되는 지시자 함수다. 다시 말하자면 자시자 함수는 연결된 특정 DOM에 $scope를 연결하거나 연결된 DOM을 조작하여 특정 행위를 정의할 수 있다. 이런 지시자 함수를 이용해 HTML을 확장하는 것이다.

    예를 들어 아코디언 컴포넌트를 구현한다고 하자. AngularJS는 부트스트랩시 템플릿을 읽을 때 accordian 태그를 발견하면 해당 지시자 함수를 호출해 accordian 태그가 지시자에서 정의한 방식으로 DOM에 기능을 추가한다. 이 지시자 함수에서 이벤트 바인딩이나 데이터 바인딩을 처리하게 되는데 아코디언 컴포넌트에 대한 속성이나 이벤트 처리에 대한 인터페이스로 HTML 태그를 이용한다. <accordian data="accordionItem">이라고 작성하면 컨트롤러이 scope에 있는 accordianItem이 해당 컴포넌트의 데이터로 주입되는 것이다.

    HTML에서 지시자를 사용하는 방법

    지시자의 이름은 낙타표기법으로 작성한다. 이런 지시자의 이름을 HTML 즉, 템플릿에서는 영문자 중간에 대문자로 시작되는 부분에 :, -, 또는 _ 문자를 넣고 대문자를 소문자로 바꾸어 사용할 수 있다. 가령 myDirective라는 이름의 지시자가 있다면 이를 my-directive, my:directive, my_directive와 같은 형태로 HTML에서 사용한다. 정의된 지시자를 HTML 즉, 템플릿에서 호출하는 방법으로는 크게 네 가지 방법이 있다.

    1. 1. 요소의 속성을 이용한 호출

      <span my-directive="expression"></span>

    2. 2. 요소의 클래스를 이용한 호출

      <span class="my-directive:expression;"></span>

    3. 3. 요소 이름을 이용한 호출

      <my-directive"></my-directive>

    4. 4. 코멘트를 이용한 호출

      <!-- directive:my-directive expression -->

    | 웹 표준 준수 대비

    앞 코드와 같이 AngularJS의 지시자를 사용하면 웹 표준을 준수하지 않았다는 결과를 얻게 되는데 AngularJS에서는 이를 위한 별도의 방법을 제공한다. 바로 x- 또는 data-를 사용할 지시자 앞에 붙이는 것이다. 예를 들면 ng-click을 사용할 때 x-ng-click 또는 data-ng-click으로 사용한다. 이렇게 사용하면 웹 표준 준수에서 좋은 결과를 얻을 수 있다.

    | 오래된 인터넷 익스플로러 지원하기

    IE7이나 IE8과 같은 오래된 브라우저에서는 요소명으로 호출하려면 다음 코드와 같이 AngularJS가 템플릿을 컴파일하기 이전에 DOM이 생성되야 한다. IE8을 지원해야 하는 웹 애플리케이션이라면 다음과 같이 코드를 작성한다.

    			<head>
    				
    			</head>
    		

    AngularJS가 제공하는 지시자

    AngularJS는 HTML을 확장하는 여러 내장 지시자를 제공한다. 내장 지시자의 목록은 http://docs.angularjs.org/api/에서 API 문서를 보면 확인할 수 있다.

    사용자 정의 지시자

    AngularJS의 가장 큰 매력은 '지시자를 이용해 웹 UI 컴포넌트를 만들 수 있는 메커니즘을 제공한다'라고 말할 수 있다. 우리가 만드는 UI 컴포넌트는 양방향 데이터 바인딩을 제공할 수 있고 도메인 특화된 HTML 태그를 구성할 수도 있다.

    | 간단한 지시자 정의

    다음 예제는 <div hello name="angularJs"></div>으로 작성하면 화면에 name 속성의 값을 대상으로 인사말을 보여주는 간단한 지시자 예제다. 지시자를 만들려면 모듈 API의 directive 메서드를 사용해야 한다. [예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="../bower_components/angular/angular.js">
    		
    	<head>
    	<body>
    		<div hello name="angularJs"></div>	
    	</body>	
    </html>
    		

    위 예제코드에서 directive 메서드는 첫번째 인자로 지시자의 이름을 요구한다. 지시자 이름은 낙타표기법으로 작성해야 한다. 그 다음으로 지시자 설정 함수를 줄수 있는데 이 함수에서 다른 서비스의 주입을 받고 싶을 때는 서비스 이름으로 인자를 정의하면 된다. 다음 코드는 지시자 함수 부분에 $log 서비스를 주입받은 코드다.

    			directive('hello', function($log){
    				return function(scope, iElement, iAttrs, controller, transcludeFn){
    					$log.log("

    hello " + iAttrs.name + "

    ") iElement.html("

    hello " + iAttrs.name + "

    ") }; });

    directive 메서드에서 두번째 인자인 지시자 설정 함수는 함수나 객체를 반환해야 한다. 위 예제 코드에서는 함수를 반환했는데 이 함수는 링크 함수다. 링크 함수는 해당 지시자가 적용된 DOM에 연결된 함수를 의미한다. 이 연결 함수에서는 순서대로 scope 객체, 연결된 요소 객체, 속성 객체, 컨트롤러 객체가 인자로 주어진다. 그럼, 각 인자별로 살펴보자.

    directive 메서드에서 두번째 인자인 지시자 설정 함수가 함수를 반환하면 링크 함수를 반환하는 것이고 객체를 반환하면 설정 객체를 반환하는 것이다.

    | 지시자 설정 객체

    지시자 설정 함수에서 반환되는 객체를 지시자 설정 객체라 한다. 이 설정 객체로 AngularJS가 지시자를 만들게 된다. 다음 예제는 앞에 hello 지시자를 설정 객체를 이용해 다시 작성해 보았다. [예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
    		<script src="../bower_components/angular/angular.js">
    		
    	<head>
    	<body>
    		<div hello name="angularJs"></div>	
    	</body>	
    </html>
    		

    | 자체 템플릿을 가지는 지시자

    이번에는 링크 함수에서 DOM을 직접 생성하지 않고 template 설정을 이용해 생성하려는 DOM을 템플릿화 해보자. [예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="../bower_components/angular/angular.js">
    		
    	<head>
    	<body>
    		<div hello name="angularJs"></div>	
    	</body>	
    </html>
    		
    			//helloTpl.html
    
    			

    hello {{name}}

    templateUrl을 사용해 지시자가 사용하는 템플릿을 외부 파일로 바꾸면 지시자 정의 코드와 템플릿 코드를 깔끔하게 분리할 수 있다. 즉, 디자인 변경이나 템플릿 문서 구조의 변경과 자바스크립트 로직 사이를 분리할 수 있다. 그리고 표현식으로 scope의 데이터를 템플릿에서 표현할 수 있는 것 처럼 지시자의 템플릿에서도 이렇게 표현식을 가지고 scope의 데이터를 표현할 수 있다. 즉, 템플릿 코드가 지시자 정의 코드와 명확히 분리되고 데이터를 표현하기 위해 문서 구조를 정확히 알지 않아도 된다. 하지만 마지막으로 한가지 더 할일이 남아 있는데 scope에 데이터를 연결하는 일은 사실 지시자 컨트롤러에서도 할 수 있다. 또한 우리는 지시자 설정에서 지시자 컨트롤러를 정의할 수 있다는 것을 보았다. 그럼 위코드를 아래와 같이 변경할 수 있다.

    			angular.module('sampleApp', []).
    				directive('hello', function(){
    					return {
    						templateUrl: 'helloTpl.html',
    						restrict: 'AE',
    						controller: function($scope, $element, $attrs, $transclude){
    							$scope.name = $attrs.name;
    						}
    					};
    				});
    		

    링크 함수를 정의하지 않고 지시자에 컨트롤러를 정의한 것이다. 하지만 해당 예제 코드는 큰 문제점이 있다. 바로 scope 설정이 잘못된 것이다. 다음과 같이 하나의 같은 문서에서 hello 지시자를 작성해 보자.

    			

    생각한 것과는 다르게 모두 hello naver가 나오게 된다. 이유는 모두 같은 scope를 사용해서 마지막에 호출된 지시자가 이전의 scope.name의 값을 바꾸어 버렸기 때문이다.

    | scope 설정 완전 정복

    지시자를 만들 때 가장 중요한 부분이 바로 "scope"를 어떻게 설정할 것인가?" 다. scope를 잘못 설정하면 의도하지 않은 결과가 발생하므로 분명한 의도에 맞는 scope를 설정해야 한다.

    ∎ 별도의 새로운 scope를 만들지 않는 설정

    지시자 설정 객체에 scope 속성을 별도로 명시하지 않거나 scope:false로 하면 scope가 만들어 지지 않는다. 앞에서 다룬 hello 지시자가 이 경우에 해당하는데 scope를 별도로 만들지 않으므로 hello 지시자 템플릿에 사용한 표현식은 부모 scope를 이용해 계산된다. 그래서 지시자의 link 함수나 컨트롤러 함수에서 scope에 연결된 모델을 변경하면 다른 모든 지시자에 영향을 주게 된다.

    ∎ 부모 scope를 상속받는 scope 설정

    지시자 설정 객체에 scope 속성에 true 값을 주면 부모 scope를 상속받는 새로운 scope가 생성된다. 다음은 hello 지시자에 scope 설정을 true로 한 예제 코드다.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="../bower_components/angular/angular.js">
    		
    	<head>
    	<body ng-controller="demoCtrl">
    		<div hello name="google"></div>	
    		<div hello name="naver"></div>	
    		<div hello></div>	
    	</body>	
    </html>
    		

    위 코드에서는 hello 지시자 설정 객체의 scope 설정에 값을 true로 주었다. 그러면 템플릿에서 사용되는 <div hello name="google"></div>, <div hello name="naver"></div>, <div hello></div> 별로 scope가 생성되어 태그별 name의 값이 이전 지시자의 scope를 덮어씌우지 않는다. 마지막으로 hello 지시자가 적용된 <div hello></div>가 브라우저에서 "hello Ctrl에서 사용된 name 모델"로 그려졌다는 것이다. 이는 hello 지시자의 내부 scope가 부모 scope로부터 상속받으므로 demoCtrl 컨트롤러 함수의 $scope.name의 값을 참조하기 때문이다. 어쩌면 이건 우리가 원하는 결과가 아니다. 부모의 $scope에 영향을 받으므로 지시자를 만들 때 이를 신경쓰지 않으면 이와 같이 원치 않는 결과가 나올 수도 있다. 이때 우리는 독립 scope 설정을 생각해 볼수 있다.

    | @을 이용한 독립 scope 설정

    독립 scope를 설정하려면 지시자 설정 객체의 scope 속성에 객체 리터럴을 이용해 객체를 선언해 주면 된다. 그리고 해당 객체에서 부모 객체와의 연관성을 정의한다.

    scope : {}

    부모 객체와 연관된 지시자 내부 scope에서 사용할 scope 객체 속성명에 "@" 혹은 "@ 연결된 DOM 속성명"을 값으로 줄 수 있다.

    scope : {name : "@"} 혹은 scope : {name : "@to"}

    다음은 독립된 scope를 가지는 hello 지시자 예제 코드다. [예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="../bower_components/angular/angular.js">
    		
    	<head>
    	<body ng-controller="demoCtrl">
    		<div hello to="angularJs"></div>	
    		<div hello name="google"></div>	
    		<div hello name="naver"></div>	
    		<div hello></div>	
    	</body>	
    </html>
    		

    독립된 scope를 사용하므로 부모 scope의 name 속성을 상속받지 않는다. 또한 scope 설정에서 지시자 내부 scope의 name 속성에 "@to"을 주어 지시자가 적용된 <div> 태그의 to 속성에 대한 값이 지시자 내부 scope의 name 속성에 연결됐다. 그래서 이전에 컨트롤러에서 작성했던 if($attrs.name) $scope.name = $attrs.name; 코드는 필요없게 됐다. 여기서 한가지 더 응용할 수 있다. <div> 태그의 to 속성에 표현식을 사용해 보는 것이다. demoCtrl 컨트롤러 함수의 $scope에 helloList 객체를 설정하고 다음과 같이 템플릿을 작성할 수 있다. [예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="../bower_components/angular/angular.js">
    		
    	<head>
    	<body ng-controller="demoCtrl">
    		<div ng-repeat="helloSb in helloList" hello to="{{helloSb.name}}"></div>
    	</body>	
    </html>
    		

    "@"를 이용해 연결된 DOM의 속성값에 표현식을 사용하여 부모 scope의 값을 전달할 수 있는 것을 보았다.

    | &을 이용한 독립 scope 설정

    scope 설정에서 속성의 값으로 "&"나 "&연결된 DOM 속성명"을 주면 부모 scope이 환경에서 실행될 수 있ㄴ느 표현식에 대한 레퍼런스를 가지고 올 수 있다. 이번에는 대상자에게 메시지를 보내는 기능을 추가해 보자. 아래 예제는 인사 문구 옆에 버튼을 두고 버튼을 클릭하면 콘솔에 메시지를 보내는 기능이 추가된 hello 지시자 예제다. [예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="../bower_components/angular/angular.js">
    		
    	<head>
    	<body ng-controller="demoCtrl">
    		<div ng-repeat="helloSb in helloList" hello to="{{helloSb.name}}" send="sendMessage(helloSb.name)"></div>
    	</body>	
    </html>
    		
    			//helloTpl2.html
    
    			

    hello {{name}}

    템플릿을 보면 해당 버튼을 클릭하면 "send()" 표현식을 계산하게 된다. 그러면 $scope.send의 값에 ()연산을 하여 결국 함수를 호출하게 되는데 이 send는 지시자 설정 객체의 scope에서 "&"로 설정됐다. 그래서 호출할 함수의 레퍼런스는 이 지시자가 적용된 DOM의 send 속성 값을 함수의 내용으로 하는 함수의 레퍼런스가 된다. 즉, 가상의 function(){sendMessage(helloSb);}가 있고 이 함수의 레퍼런스를 지시자 내부의 send가 갖게 되는 것이다. 그리고 가상의 함수는 부모 scope의 컨텍스트에서 실행된다. 여기서 부모 scope는 ng-controller에서 만들어지는 scope가 아니라 ng-repeat으로 인해 만들어지는 scope다. 그래서 {helloSb: "..."}로 만들어진 scope를 컨텍스트로 하여 실행되어 console.log(toSb+"에게 메시지를 보낸다.");에 google, naver, angular 값이 인자로 전달되고 콘솔창에 보이게 된다.

    | =을 이용한 독립 scope 설정

    scope 설정에서 속성의 값으로 "="나 "=연결된 DOM 속성명"을 주면 부모 scope의 특정 속성과 지시자 내부 scope의 속성과 양방향 데이터 바인딩을 한다. 여기서 주의할 점이 있다. 다음 scope 설정을 보자.

    scope : {name : "=to"}

    이렇게 설정돼 있으면 부모 scope의 to 속성과 양방향 데이터 바인딩이 됐다고 생각하기 쉬운데 지금까지 scope 설정과 같이 to는 부모 scope의 속성명이 아니라 연결된 DOM의 속성명이다. 이 연결된 DOM의 to라는 속성명에 대한 값이 바로 부모 scope의 속성명이 된다. 다음 예제는 hello 지시자의 부모 scope 모델에 연결된 <input>을 변경하면 hello 지시지 내부 scope의 모델을 변경하고 또 반대로도 변경되는 hello 지시자 예제다.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="../bower_components/angular/angular.js">
    		
    	<head>
    	<body ng-controller="demoCtrl">
    		
    <div ng-repeat="helloSb in helloList" hello to="helloSb.name"></div> </body> </html>
    			//helloTpl3.html
    
    			

    hello {{name}}

    위 코드는 지시자 내부 scope의 name과 demoCtrl 컨트롤러의 helloList 배열의 각 요소 객체의 name이 모두 연결되어 각 속성의 값을 바꾸면 서로 갱신되는 것이다.

    참고

    ng-repeat 대상인 helloList를 ["google", "naver", "angular"]로 바꾸면 위의 예제가 정상적으로 작동하지 않는다. AngularJS에서는 ng-model은 될 수 있으면 "."을 이용해 즉, 객체를 이용해 바인딩해야 한다. 문자열과 같은 원시형 데이터를 ng-model에서 사용하면 양방향 데이터 바인딩이 이루어 지지 않는다.

    | ngTransclude와 translude 설정

    AngularJS에서 지시자를 이용하여 ng-click과 같이 특정 DOM에 이벤트를 연결하거나 ng-repeat처럼 DOM을 조작하는 일을 할수 있다. 하지만 hello 지시자처럼 재사용할 수 있는 기능이 있는 컴포넌트를 만들 수 있다. 트리 컴포넌트, 그래프 컴포넌트와 같은 UI 컴포넌트도 그러하다. 하지만 다른 컴포넌트를 포함하거나 다른 DOM을 담고 있는 컨테이너를 만들 때에도 사용할 수 있다. 위젯이나 패널이 그러하다고 볼 수 있다. 이렇게 위젯이나 패널을 <widget>, <panel>로 태그를 작성하고 widget, panel 지시자를 만들면 되는 것이다. 하지만 <panel> 태그의 내용을 지시자 템플릿에서 사용해야 한다. 이러한 경우를 위하여 transclude에 true 값을 주면 템플릿에서 ng-transclude라고 설정된 부분에 <panel> 태그의 내용이 복사된다. 다음은 간단한 패널 지시자를 만드는 예제 코드이다. [예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<style type="text/css">
    			.panel{margin:10px;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;border:1px solid black;}
    			.panel.info .panel-title{background-color:gray; color:white;}
    			.panel.alert .panel-title{background-color:red; color:white;}
    			.panel .panel-title {background-color:black; color:white; padding:10px;}
    			.panel .panel-content {padding:10px;}
    		</style>
    		<script src="../bower_components/angular/angular.js">
    		<script>
    			angular.module('sampleApp', []).
    				controller('demoCtrl', ['$scope', function($scope){
    					$scope.noticeList = [{
    						url : "notice/1",
    						text : "공지사항 첫번째 글입니다."
    					},{
    						url : "notice/2",
    						text : "공지사항 두번째 글입니다."
    					},{
    						url : "notice/3",
    						text : "공지사항 세번째 글입니다."
    					}];
    				}]).
    				directive('panel', function(){
    					return {
    						templateUrl: 'panelTmpl.html',
    						restrict: 'AE',
    						transclude: true,
    						scope: {
    							title: "@",
    							type: "@"
    						} 
    					};
    				});
    		</script>
    	<head>
    	<body ng-controller="demoCtrl">
    		
    			

    AngularJS는 자바스립트 웹 애플리케이션을 쉽게 개발하게 도와줍니다.

    </body> </html>
    			//panelTmpl.html
    
    			
    {{title}}

    위 코드를 보면 transclude 옵션을 true로 주었을 때 템플릿에서 ng-transclude를 사용한 것을 알 수 있다. 이렇게 transclude 옵션을 사용하면 <panel> 태그의 내용이 템플릿에서 <div ng-transclude></div> 안으로 잘라서 붙여진 것과 같이 된다. 그리고 <panel> 태그에 title과 type 옵션을 이용해 패널의 타이틀과 타이틀 바 색상을 설정하듯이 변경할 수 있게 했다. AngularJS의 transclude를 사용하면 이렇게 컨테이너와 같은 역할을 하는 UI 컴포넌트를 재사용하도록 만들 수 있다.

    6장. 의존 관계 주입과 서비스


    AngularJS에서의 서비스란?

    다음은 서비스의 다양한 역할을 설명하고 있다.

    이처럼 서비스는 각 컨트롤러 사이의 데이터를 공유하게 해주며 애플리케이션에서 다루는 객체의 싱글톤을 유지하게 해준다.

    AngularJS에서의 의존관계 주입

    하나의 객체가 다른 객체를 사용하는 순간 의존관계가 성립된다. 그러므로 어느 애플리케이션이든 여러 객체 사이에 의존관계가 필연적으로 성립될 수 밖에 없다. AngularJS 개발자 문서에서는 자바스크립트 상에서 객체들 사이의 의존관계가 크게 세가지 경우에 생성된다고 한다.

    1. 1. new 키워드를 통한 의존관계 성립
    2. 2. 전역변수 참조를 통한 의존관계 성립
    3. 3. 인자를 통하여 참조를 전달받아 의존 관계 성립

    1번과 2번은 의존관계가 강하게 연결됐다고 하고 3번은 느슨하게 연결됐다고 한다. 다음은 new 키워드로 만들어진 의존관계를 보여주는 예제 코드다.

    			function demoCtrl(){
    				var bookmark = new BookmarkResource(new Ajax(), new JsonParser());
    			}
    		

    위 데모 컨트롤러 함수는 BookmarkResource 함수를 사용하고 있다. 하지만 데모 컨트롤러 함수는 BookmarkResource 함수가 어떻게 bookmark 객체를 생성해야 하는지, BookmarkResource를 사용하기 위해 Ajax와 JsonParser 인자가 필요하다는 것까지 너무도 잘 알고 있어야 한다는 문제점이 있다. 가령 BookmarkResource가 Ajax 요청을 사용하지 않거나 JSON 형식의 데이터가 아니라 XML 형식으로 바뀐다면 BookmarkResource를 사용하는 모든 컨트롤러 소스를 다 수정해야 한다. 그럼, BookmarkResource가 어떻게 객체를 생성하는지 알지 못하도록 팩토리 함수를 전역 변수로 만들어 이러한 문제점을 없애보자.

    			var factory = {
    				getBookmarkResource : function(){return new BookmarkResource(factory.getAjax(), factory.getJsonParser());},
    				getAjax : function(){return new Ajax();},
    				getJsonParson : function(){return new JsonParser();}
    			}
    
    			function demoCtrl(){
    				var bookmark = new BookmarkResource(new Ajax(), new JsonParser());
    			}
    		

    이렇게 팩토리를 이용해 bookmark 객체를 얻게 되면 데모 컨트롤러는 BookmarkResource가 어떻게 생성되는지 몰라도 된다. 이는 나중에 Ajax를 사용하지 않거나 JSON 형식이 아닌 XML 형식으로 데이터 형식이 바뀌게 되어도 모든 컨트롤러 함수를 수정할 필요없이 팩토리 객체만 수정하면 된다. 그러나 테스트하기 어렵다는 문제점이 여전히 존재하고 아직 전역 객체와의 강한 결합은 존재한다. 그럼, 마지막으로 인자를 참조하여 만들어진 의존관계를 살펴보자.

    			function demoCtrl(BookmarkResource){
    				var bookmark = BookmarkResource.get();
    			}
    		

    위 코드는 데모 컨트롤러가 BookmarkResource를 인자로 전달받아 의존관계가 성립된 것을 볼 수 있다. 이렇게 인자를 통하여 BookmarkResource를 데모 컨트롤러가 주입을 받아 의존관계 주입이 되는 것이다. 의존관계 주입(DI)을 통하여 데모 컨트롤러와 BookmarkResource는 약한 결합을 갖게 된다. 데모 컨트롤러는 BookmarkResource가 어떻게 생성되는지 알 필요도 없고 단위 테스트에서도 얼마든지 필요한 테스트용 BookmarkResource를 주입받을 수 있는 것이다. 이처럼 AngularJS에서는 주입되는 대상을 서비스라 하여 BookmarkResource를 서비스로서 개발하고 이를 컨트롤러나 다른 서비스 혹은 지시자 등에 주입되는 방식인 DI를 이용해 컴포너트별 의존관계를 정의할 수 있다.

    | Module.factory를 이용한 Hello 서비스 만들기

    서비스를 만들려면 모듈 인스턴스가 필요하다. angular.module() 함수를 이용해 모듈을 만들면 모듈 인스턴스를 얻을 수 있다. 이 모듈 인스턴스는 서비스를 만들 수 있는 다양한 메서드를 제공한다. 다음은 주어진 이름에 인사하는 서비스 예제이다. [예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="../bower_components/angular/angular.js">
    		<script>
    			angular.module('sampleApp', []).
    				factory('hello', [function(){
    					var helloText = "님 안녕하세요.";
    
    					return {
    						say : function(name){
    							return name + helloText;
    						}
    					};
    				}]).
    				controller('mainCtrl', function($scope, hello){
    					$scope.hello = hello.say("철수"); 
    				});
    		</script>
    	<head>
    	<body ng-controller="mainCtrl">
    			

    {{hello}}

    </body> </html>

    angular.module() 함수를 이용해 sampleApp 모듈을 선언하고 해당 인스턴스의 factory() 메서드를 호출해 hello 서비스를 만들었다. factory 함수의 첫번째 인자로 서비스명을 주고 다음 인자로 서비스를 주입받을 때 반환할 객체를 설정하는 팩토리 함수를 준다. 이렇게 만든 hello 서비스는 컨트롤러에서 인자로 주입해 사용할 수 있다.

    사실 모듈 API의 factory 메서드는 $provide.factory 메서드와 같다. 모듈은 각 지시자, 서비스, 컨트롤러 등과 같은 컴포넌트를 담는 박스와 같다. $provide를 이용해 서비스를 정의하지만 모듈의 의미와 개발 편의성을 위하여 모듈 API를 이용해 $provide 기능들이 제공되는 것이다. 실제로 AngularJS에서 모든 서비스는 이 $provide를 이용해 정의하게 된다. 이렇게 정의된 서비스는 $injector로 얻어오고 각 서비스의 의존관계도 $injector에 의하여 생성되게 된다. 서비스를 특정함수에 주입하거나 등록된 서비스를 얻어오는 역할을 $injector가 하는 것이다. $injector는 서비스의 싱글톤을 유지하기 위해 내부적으로 캐시를 가지고 있어 서비스가 매번 새로운 객체를 생성하는 것을 방지한다.

    | $provide를 이용한 Provider 정의

    AngularJS에서 $provide를 이용해 주입할 수 있는 서비스를 제공해 주는 프로바이더를 정의할 수 있다. 이렇게 정의된 서비스 프로바이더가 특정 서비스를 제공해주는 것이다. 그래서 서비스를 단순하게 생각하면 $provide로 정의한 프로바이더가 생성하는 객체라고 할 수 있다. 이런 서비스를 만드는 방법은 크게 다섯가지 방법이 있다.

    ∎ Value로 정의하는 방법

    웹 애플리케이션을 개발하다 보면 애플리케이션 전역에서 사용하는 특별한 값이 필요한 경우가 종종 있다. 가령 애플리케이션 이름이나 컨텍스트 루트가 그러할 것이다. 다음 예제는 이러한 값을 반환하는 서비스이다. [예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="../bower_components/angular/angular.js">
    		<script>
    			angular.module('sampleApp', []).
    				value('AppNm', 'demo app').
    				controller('mainCtrl', function($scope, AppNm){
    					$scope.appNm = AppNm;
    				});
    		</script>
    	<head>
    	<body ng-controller="mainCtrl">
    			

    애플리케이션 이름 : {{appNm}}

    </body> </html>

    AppNm 서비스는 단순히 'demo app' 문자열 값을 제공해주는 서비스다.

    ∎ Factory로 정의하는 방법

    실제로 서비스를 만들 때는 주로 factory를 이용하여 만들게 된다. factory는 서비스를 생성하는 로직을 담고 있는 함수다. 기본적인 factory 정의는 다음과 같다.

    module.factory('서비스 이름', function([주입받을 서비스]){...});

    value와는 다르게 두번째 인자로 함수를 전달해 준다. 해당 함수는 팩토리 함수로서 인자로 주입받을 다른 서비스를 줄 수 있다. 다음은 주어진 이름과 이메일을 추가하고 하단에 목록을 보여주는 예제다. [예제보기]

    factory를 사용해 UserResource라는 서비스를 만들고 해당 서비스의 addUser로 사용자를 추가하고 selectUsers로 전체 사용자 목록을 가져오는 서비스를 구현한 코드이다.

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="../bower_components/angular/angular.js">
    		<script>
    			angular.module('sampleApp', []).
    				factory('AppNm', [function(){
    					return 'demo app';		
    				}]).
    				factory('UserResource', function(){
    					var userList = [];
    
    					return {
    						addUser : function(newUser){
    							userList.push(newUser);
    						},
    						updateUser : function(idx, updatedUser){
    							userList[idx] = updatedUser;
    						},
    						deleteUser : function(idx){
    							userList[idx] = undefined;
    						},
    						selectUsers : function(){
    							return userList;
    						}
    					}
    				}).
    				controller('mainCtrl', function($scope, AppNm, UserResource){
    					$scope.appNm = AppNm;
    
    					$scope.users = UserResource.selectUsers();
    
    					$scope.addNewUser = function(newUser){
    						UserResource.addUser({
    							name : newUser.name,
    							email : newUser.email
    						});
    					};
    				});
    		</script>
    	<head>
    	<body ng-controller="mainCtrl">
    		

    애플리케이션 이름 : {{appNm}}

    이름 : , 이메일 :
    • {{user.name}}, {{user.email}}
    </body> </html>

    UserResource 서비스는 addUser, updateUser, deleteUser, selectUsers 메서드를 가지는 객체를 반환하고 있다. 그리고 UserResource 팩토리는 내부에 userList 배열을 가지고 있는데 이는 모두 반환되는 객체의 메서드로만 접근이 가능하다.

    ∎ Service로 정의하는 방법

    자바스크립트 언어의 특성상 함수를 클래스로 사용하면서 상속과 같은 OOP 특성을 이용해 개발할 수 있다. Service 메서드는 이러한 생성자 함수를 객체화할 때 사용한다.

    일반적으로 자바스크립트에서는 생성자 함수에 new 키워드를 이용해 인스턴스를 얻게 된다. 이러한 new 키워드를 이용한 인스턴스화와 $provide.service 메서드와의 차이점은 매번 다른 새로운 객체를 반환하는 것이 아니라 싱글톤을 유지하여 같은 객체를 반환한다는 것이다. 다음 예제는 계산 기능을 하는 생성자 함수를 service 메서드를 이용해 서비스를 정의한 예제다.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="../bower_components/angular/angular.js">
    		<script>
    			function Calculator(){
    				this.lastValue = 0;
    
    				this.add = function(a, b){
    					var returnV = a+b;
    					this.lastValue = returnV;
    					return returnV;
    				};
    
    				this.minus = function(a, b){
    					var returnV = a-b;
    					this.lastValue = returnV;
    					return returnV;
    				};
    			}
    
    			angular.module('sampleApp', []).
    				factory('CalcByF', [function(){
    					return new Calculator();
    				}]).
    				service('CalcByS', Calculator).
    				controller('mainCtrl', function($scope, CalcByF, CalcByS){
    					$scope.val1 = CalcByF.add(10,3);
    					console.log(CalcByF.lastValue);
    					$scope.val2 = CalcByS.minus(20, 10);
    					console.log(CalcByS.lastValue);
    				});
    		</script>
    	<head>
    	<body ng-controller="mainCtrl">
    		

    10 + 3 = {{val1}}

    20 - 10 = {{val2}}

    </body> </html>

    Calculator 생성자 함수가 정의되어 있고 해당 생성자 함수는 두가지 방법을 이용해 AngularJS 서비스로 정의돼 있는데, 첫번째가 factory를 이용해 등록한 것이다.

    			factory('CalcByF', [function(){
    				return new Calculator();
    			}])
    		

    위와 같이 CalcByF 서비스는 factory로 등록되어 new 키워드를 이용해 Calculator 생성자 함수의 인스턴스 객체를 얻어오게 된다. new 키워드로 인스턴스 객체를 얻더라도 컨트롤러에서는 매번 같은 인스턴스 객체를 전달받아 싱글톤이라는 것을 유념하자. 그리고 다음으로 service 메서드를 이용해 AngularJS 서비스로 정의하는 부분이다.

    			service('CalcByS', Calculator).
    		

    service 메서드는 단지 생성자 함수 레퍼런스만 전달하면 된다. factory 메서드에 비해 간단하게 서비스를 선언할 수 있다.

    ∎ Provider로 정의하는 방법

    value, factory, service는 모두 provider 메서드를 사용해 의미상 별칭처럼 제공하는 메서드들이다. 실제로 서비스를 등록할 때 AngularJS는 $provide.provider만을 인지하게 된다. 다음 예제는 provider 메서드를 사용해 로깅 처리를 하는 서비스를 정의한다.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="../bower_components/angular/angular.js">
    		<script>
    			angular.module('sampleApp', []).
    				provider('Logger', [function(){
    					function Logger(msg){
    						if(checkNativeLogger) console.log(msg);
    					}
    
    					Logger.debug = function(msg){if(checkNativeLogger) console.debug(msg)};
    					Logger.info = function(msg){if(checkNativeLogger) console.info(msg)};
    
    					function checkNativeLogger(){
    						if(console) return true;
    						return false;
    					}
    
    					this.$get = [function(){
    						return Logger;
    					}];
    				}]).
    				controller('mainCtrl', function($scope, Logger){
    					Logger('console.log로 출력하는 로그 메시지');
    					Logger.debug('console.debug 출력하는 로그 메시지');
    				});
    		</script>
    	<head>
    	<body ng-controller="mainCtrl">
    	</body>	
    </html>
    		

    provider 함수는 this.$get = [function(){...}]을 포함하고 있으면 된다. 이 this.$get에 대입되는 부분이 나중에 $injector가 서비스를 주입 대상에 주입할 때 호출될 부분이다. this.$get에는 배열 또는 함수를 줄 수 있는데 배열은 다음과 같이 다른 서비스를 주입할 때 사용한다.

    			this.$get = ['$window', function(win){win.console.log("...");}];	
    		

    $provide.factory와 $provide.service 메서드 내부에서는 $provide.provider를 호출하고 그 안에서 $injector를 이용해 팩토리 함수를 호출하면서 의존관계를 맺고 있는 다른 서비스를 주입하고 또 생성자 함수를 인스턴스화하면서 필요한 서비스를 주입해주는 것이다

    ∎ 서비스 프로바이더 설정하기

    사용자 정의 서비스는 대부분 factory, service 메서드로 정의하는데 특별한 경우에는 provider 메서드를 이용해야만 한다. 바로 서비스를 주입하기 전에 별도의 설정을 해야 할 때가 그렇다. 이때 Module API의 config 함수를 사용할 수 있는데 config 함수를 사용하려면 서비스 프로바이더가 설정할 수 있게 만들어져 있어야 한다. 다음 예제는 Logger를 로깅 레벨을 설정할 수 있게 변경한 코드다.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="../bower_components/angular/angular.js">
    		<script>
    		  angular.module('sampleApp', []).
    			provider('Logger', [function () {
    			  var defaultLogLevel = 'log';
    
    			  function Logger(msg) {
    				if(checkNativeLogger) {
    				  if(defaultLogLevel === "debug"){
    					console.debug(msg);
    					return;
    				  }
    				  if(defaultLogLevel === "info"){
    					console.info(msg);
    					return;
    				  }
    				  console.log(msg);
    				}
    			  }
    
    			  Logger.debug = function(msg) { if(checkNativeLogger) console.debug(msg); };
    			  Logger.info = function(msg) { if(checkNativeLogger) console.info(msg); };
    
    			  function checkNativeLogger() {
    				if(console) return true
    				return false;
    			  }
    
    			  this.setDefaultLevel = function(level) {
    				switch(level){
    				  case 'debug': 
    					defaultLogLevel = 'debug';
    					break;
    				  case 'info':
    					defaultLogLevel = 'info';
    					break;
    				  default:
    					defaultLogLevel = 'log'
    				}
    			  };
    
    			  this.$get = [function() {
    				return Logger;
    			  }];
    			}]).
    			config(['LoggerProvider',function (LoggerProvider) {
    			  LoggerProvider.setDefaultLevel("debug");
    			}]).
    			controller('mainCtrl',function($scope, Logger) {
    			  Logger("console.log로 출력하는 로그메시지");
    			  Logger.debug("console.debug 출력하는 로그메시지");
    			});
    		</script>
    	<head>
    	<body ng-controller="mainCtrl">
    	</body>	
    </html>
    		

    Logger 서비스 프로바이더가 setDefaultLevel 메서드로 기본 로깅 레벨을 설정하는 것을 볼수 있다. setDefaultLevel 메서드는 this.setDefaultLevel로 정의되는데 여기서 this가 가리키는 것이 서비스 프로바이더 자체이다. 그리고 이 Logger 서비스 프로바이더는 Module.config 메서드에 주입되고 서비스 프로바이더 함수내의 this 객체에 연결된 속성이나 메서드를 호출할 수 있는 것이다. AngularJS에서는 관례로 특정 서비스를 정의하면 해당 서비스명 뒤에 Provider를 더하여 "서비스명Provider"로 서비스 프로바이더 이름을 정의한다. 그리고 이 이름으로 config에서 서비스 프로바이더를 주입받아 설정하게 된다.

    ∎ constant로 정의하는 방법

    constant로 정의하는 방법은 value로 정의하는 방법과 흡사하다. constant 메서드로 정의한 서비스는 config 함수에서도 주입할 수 있지만, value 메서드로 정의한 서비스는 config 함수에서는 주입할 수 없다. value, factory, service는 모두 provider 함수를 감싼 레퍼 함수에 불과하다. 그리고 이 provider 함수로 정의된 서비스 프로바이더는 뒤에 Provider라는 접미사가 붙어 이 이름을 통하여 config 함수에서 주입받을 수 있다. 그렇다면 value로 정의한 상수 값은 뒤에 Provider를 써서 config 함수에서 해당 서비스 프로바이더가 주입되는 것이지 value로 정의한 상수 값 자체가 주입되는 건 아니다. 하지만 constant 메서드로 정의한 상수 값은 config나 다른 서비스나 모두 같은 상수 값을 주입 받을 수 있다. 다음은 주어진 반지름으로 원을 그리고 해당 원의 넓이를 하단에 보여주는 예제다.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="../bower_components/angular/angular.js">
    		<script>
    		  angular.module('sampleApp', []).
    			constant('PI', 3.14159).
    			provider('Cal', [function () {
    			  var defaultRadius = 10;
    
    			  this.setDefaultRadius = function(radius) {
    				defaultRadius = radius;
    			  };  
    			
    			  this.$get = ['PI',function(PI) {
    				return {
    				  getCircleArea : function(radius) {
    					var r = radius || defaultRadius;
    					return r*r*PI;
    				  }
    				};
    			  }];
    			}]).
    			config(function (CalProvider, PI) {
    			  CalProvider.setDefaultRadius(5);
    			  console.log(PI);
    			}).
    			directive('circle', ['Cal','PI',function (Cal,PI) {
    			  return {
    				restrict: 'E',
    				template : '',
    				link: function (scope, iElement, iAttrs) {
    				  var context = iElement.find("canvas")[0].getContext('2d');
    				  var radius = 30;
    
    				  context.beginPath();
    				  context.arc(50, 50, radius, 0, 2 * PI, false);
    				  context.fillStyle = 'green';
    				  context.fill();
    				  context.lineWidth = 5;
    				  context.strokeStyle = '#003300';
    				  context.stroke();
    				  iElement.append("

    반지름 30px인 원의 넓이 : "+Cal.getCircleArea(radius)+"px

    ") } }; }]) </script> <head> <body ng-controller="mainCtrl"> </body> </html>

    위 코드를 보면 PI라는 원주율 값을 constant 메서드를 이용해 상수로 등록했다. 이 상수는 circle 지시자와 Cal 서비스에서 주입돼 사용된다. 물론 이 원주율을 value 메서드를 이용해 정의할 수도 있다. 하지만 constant 메서드로 정의했기 때문에 코드에서 보는 것과 같이 config 메서드를 사용할 때 다른 프로바이더와 같이 주입할 수 있다. 이 설정 함수에서 프로바이더를 설정할 때 원주율 PI와 같은 상수 값을 이용할 수 있는 것이다.

    Features / Recipe type Factory Service Value Constant Provider
    다른 서비스를 주입받을 수 있나? yes yes no no yes
    타입 기반의 주입이 가능한가? no yes yes* yes* no
    config 메서드에서 주입이 가능한가 no no no yes yes**
    함수/기본형 생성이 가능한가? yes no yes yes yes

    | $injector를 이용한 서비스 주입

    AngularJS의 $injector는 $provide를 통해 등록된 서비스 프로바이더를 이용해 서비스 인스턴스를 생성하는 역할을 한다. 그리고 $injector는 AngularJS 애플리케이션이 생성될 때 자동으로 생성되며 하나의 AngularJS 애플리케이션은 하나의 $injector만 가지게 된다. 사실 $injector는 내부 코드에서 많이 사용하지만 다음과 같은 방법으로 $injector를 직접 가지고 올 수 있다.

    var injector = angular.injector(['mySampleModule', 'ng']);

    $injector를 이용하면 특정 서비스 객체를 얻을 수 있다. 또는 특정 서비스가 해당 모듈에 정의돼 있는지 확인할 수도 있다. 다음은 $injector를 이용해 서비스 객체를 얻거나 확인하는 예제이다.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="../bower_components/angular/angular.js">
    		<script>
    			//AngularJS 내부
    			angular.module('sampleApp', []).
    				factory('Hello', [function(){
    					return {
    						helloTo : function(name){
    							console.log('hello ' + name);
    						}
    					};
    				}])
    			
    			//AngularJS 외부 
    			var injector = angular.injector(['ng', 'sampleApp']),
    				hasHello = injector.has('Hello'),
    				HelloSvc = null;
    			
    			if(hasHello){
    				HelloSvc = injector.get('Hello');
    				HelloSvc.helloTo("철수");
    			}
    		</script>
    	<head>
    	<body>
    	</body>	
    </html>
    		

    위 예제를 보면 sampleApp 모듈을 선언한 후 angular.injector(['ng', 'sampleApp'])를 이용해 $injector 객체를 가지고 왔다. 그리고 $injector의 has 메서드로 주어진 서비스가 ng 모듈과 sampleApp 모듈에 정의돼 있는지 알 수 있다. 예제 코드에서는 Hello라는 서비스가 정의돼 있는지 체크하여 정의돼 있다면 get 메서드를 이용해 Hello 서비스 객체를 얻어와 helloTo메서드를 사용해 콘솔에 "hello 철수"를 출력했다. 여기서 $injector.has 메서드가 주어진 메서드명에 해당하는 서비스의 등록 여부를 알려주고 $injector.get 메서드가 주어진 서비스명에 해당하는 서비스 객체를 반환한다는 것을 알 수 있다.

    $injector는 has와 get 메서드외에 instantiate, invoke, annotate 메서드를 제공한다. invoke 함수는 주어진 함수의 매개변수에 필요한 서비스를 주입하며 호출하고 instantiate는 주어진 생성자 함수를 객체화할 때 필요한 서비스를 주입하며 객체화한다. AngularJS 프레임워크가 호출하는 코드가 아닌 프레임워크 바깥 코드에서 $injector와 같은 프레임워크 기능들을 어떻게 활용하는지 예제를 통해 보자.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js">
    		<script src="../bower_components/angular/angular.js">
    		<script>
    			//AngularJS 내부
    			angular.module('sampleApp', []).
    				factory('Hello', [function(){
    					return {
    						helloTo : function(name){
    							console.log('hello ' + name);
    						}
    					};
    				}])
    			
    			//AngularJS 외부 
    			$(function(){
    				var injector = angular.injector(['ng', 'sampleApp']),
    					invokedReturnValue = null,
    					helloAppenderInstance1 = null,
    					helloAppenderInstance2 = null;
    
    				invokedReturnValue = injector.invoke(function(Hello){
    					var hello = Hello.helloTo('철수');
    					$('body').append(hello);
    					return hello;
    				});
    
    				function HelloAppender(Hello, $compile, $rootScope){
    					var helloEl = $('

    {{hello}}

    '); var scope = $rootScope.$new(); scope.hello = ""; $('body').append($compile(helloEl)(scope)); this.setName = function(name){ scope.hello = Hello.helloTo(name); scope.$digest(); }; } helloAppenderInstance1 = injector.instantiate(HelloAppender); helloAppenderInstance2 = injector.instantiate(HelloAppender); helloAppenderInstance1.setName("영희"); helloAppenderInstance2.setName("가영"); console.log("invokedReturnValue : ", invokedReturnValue); console.log("helloAppenderInstance1: ", helloAppenderInstance1); console.log("helloAppenderInstance2: ", helloAppenderInstance2); }); </script> <head> <body> </body> </html>

    | 의존관계 주입을 받을 수 있는 곳

    다음 목록은 AngularJS 내부에서 $injector를 이용해 주어진 함수들의 매개변수에 적절한 서비스가 주입되는 곳이다. 즉, AngularJS에서 DI가 가능한 함수라고 볼 수 있다.

    7장. 필터를 사용하고 만들어 보자.


    AngularJS에 필터는 크게 두가지 용도로 사용된다. 첫번째로 데이터에 대하여 보이는 모습을 바꾸는데 사용되고 두번째로 여러 데이터 중 조건에 맞는 데이터만 보여줄 때 사용된다. 즉, 포맷팅과 필터링하는데 사용된다고 볼 수 있다.

    AngularJS에서 제공하는 필터

    다음은 AngularJS에서 제공하는 필터와 그에 대한 간략한 설명이다.

    다음은 포맷팅 필터별로 필터 적용 전과 후를 보여주는 예제다. [예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js">
    		<script src="../bower_components/angular/angular.js">
    		<script>
    			angular.module('sampleApp', []).
    				controller('mainCtrl', ['$scope', function($scope){
    					$scope.cValue = 6300;
    					$scope.dNow = new Date();
    					$scope.jObj = {name:'순희'};
    					$scope.lString = 'Cindy Kim';
    					$scope.nValue = 10/3;
    				}]);
    		</script>
    	<head>
    	<body>
    		

    필터사용 예제

    <h2>currency 필터 사용

    원래 : {{cValue}}

    필터적용 : {{cValue | currency}}

    </body> </html>

    다음은 filter, limitTo, orderBy 필터를 이용해 구현한 코드이다. [예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js">
    		<script src="../bower_components/angular/angular.js">
    		<script>
    			angular.module('sampleApp', []).
    				controller('mainCtrl', ['$scope', function($scope){
    					$scope.userList = [
    						{userId:'jay', userName:'제이', userEmail:'jay@ng.com'},
    						{userId:'soon', userName:'순희', userEmail:'soon@ng.com'},
    						{userId:'cindy', userName:'가영', userEmail:'cindy@ng.com'},
    						{userId:'mino', userName:'민호', userEmail:'mino@ng.com'},
    						{userId:'teapong', userName:'태홍', userEmail:'teapong@ng.com'}
    					];
    				}]);
    		</script>
    	<head>
    	<body>
    		
    <h1>필터사용 예제</h1>
    사용자 이름:
    • {{user.userId}} | {{user.userName}} | {{user.userEmail}}

    <h2>limitTo 필터사용</h2>
    제한 갯수:
    • {{user.userId}} | {{user.userName}} | {{user.userEmail}}

    <h2>orderBy 필터사용</h2>
    정렬순서 : 아이디 이름
    역순 여부 :
    • {{user.userId}} | {{user.userName}} | {{user.userEmail}}

    </body> </html>

    위 예제는 모두 템플릿에서 필터를 사용했다. 하지만 템플릿이 아닌 컨트롤러나 서비스, 지시자에서도 필터를 사용할 수 있다. 필터를 템플릿 외에 자바스크립트 코드에서 사용하려면 $filter 서비스를 이용해야 한다. 다음 예제는 $filter 서비스를 컨트롤러에서 주입받아 date 필터와 filter 필터를 사용하는 코드다.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js">
    		<script src="../bower_components/angular/angular.js">
    		<script>
    			angular.module('sampleApp', []).
    				controller('mainCtrl', ['$scope', '$filter', function($scope, $filter){
    					var userList = [
    						{userId:'jay', userName:'제이', userEmail:'jay@ng.com'},
    						{userId:'soon', userName:'순희', userEmail:'soon@ng.com'},
    						{userId:'cindy', userName:'가영', userEmail:'cindy@ng.com'},
    						{userId:'mino', userName:'민호', userEmail:'mino@ng.com'},
    						{userId:'teapong', userName:'태홍', userEmail:'teapong@ng.com'}
    					];
    					$scope.value = new Date();
    					$scope.dataFormatedValue = $filter('date')($scope.value, 'yyyy-dd-mm');
    					$scope.userList = userList;
    					$scope.filter = function(filterObj){
    						$scope.userList = $filter('filter')(userList, filterObj);
    					}
    				}]);
    		</script>
    	<head>
    	<body>
    		

    필터사용 전 날짜 데이터 : {{value}}

    date 필터 사용 : {{dataFormatedValue}}


    사용자 이름 :
    • {{user.userId}} | {{user.userName}} | {{user.userEmail}}

    </body> </html>

    위 코드를 보면 mainCtrl 컨트롤러 함수에서 $filter 서비스를 주입받은 것을 볼 수 있다. 이 $filter 서비스에 사용하고자 하는 필터 이름을 인자로 주고 함수를 호출하면 해당 필터 함수를 가지고 올 수 있다. 해당 필터 함수에 각 필터 함수마다 필요로 하는 인자를 전달하여 포맷팅이나 필터링 처리를 한 결과 값을 얻을 수 있다. 예제 코드에서는 $filter('date')와 $filter('filter')로 date 필터 함수와 filter 필터 함수를 반환받았다. 그리고 date 필터에는 포맷팅하고자 하는 데이터와 날짜 포맷을 문자열로 전달하고 filter 필터 함수는 필터링하고자 하는 데이터와 필터링 조건을 전달하여 결과 값을 반환받았다.

    AngularJS는 $filter 서비스를 이용해 등록된 필터 함수를 얻을 수 있고 이렇게 얻은 필터 함수를 이용해 특정 데이터를 포맷팅하거나 필터링 처리할 수 있는 것이다.

    필터를 만들어 보자

    AngularJS는 원하는 필터를 개발할 수도 있다. 사용자 정의 필터를 만들러면 $filterProvider를 이용하면 된다. $filterProvider에서 register 메서드를 이용해 사용자 정의 필터를 등록할 수 있다. 다음 예제는 첫문자를 대문자로 바꾸어주는 필터를 정의하는 코드다.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js">
    		<script src="../bower_components/angular/angular.js">
    		<script>
    			angular.module('sampleApp', []).
    				config(function($filterProvider){
    					$filterProvider.register('capitalize', function(){
    						return function(text){
    							return text.charAt(0).toUpperCase() + text.slice(1);
    						};
    					});
    				}).
    				controller('mainCtrl', ['$scope', function($scope){
    					$scope.hello = "hello";
    				}]);
    		</script>
    	<head>
    	<body>
    		

    {{hello | capitalize}}

    </body> </html>

    문자열의 첫 문자만 대문자로 변경하는 간단한 필터를 만들었다. $filterProvider를 이용해 capitalize 필터를 등록했으면 템플릿에서 {{표현식 | capitalize}}로 사용할 수 있다.

    8장. 단일 페이지 웹애플리케이션의 이해.


    단일 페이지 웹 애플리케이션은 간단히 말해서 웹 애플리케이션을 사용하는 동안 웹 페이지를 리로드하지 않는 웹 애플리케이션을 의미한다.

    단일 페이지 웹 애플리케이션을 만들려면 반드시 고려해야 할 사항이 있다. 바로 딥 링킹이다. 딥 링킹을 지원하지 않으면 현재 사용자가 보는 화면의 리소스를 확인할 수도 없을 뿐 아니라 해당 화면을 북마크할 수 없고 원하는 화면을 URL로 접근할 수도 없기 때문에 사용자 경험을 크게 떨어뜨릴 수 있다.

    딥 링킹 Deep Linking

    웹에서 URL은 언제나 단 하나의 자원을 가리키고 있어야 한다. 가령 www.example.com/about은 example.com을 소개하는 페이지를 가리키고 www.example.com/contact는 example.com에 연락하기 위한 페이지라고 볼 수 있다. 앞에서 본 두 개의 URL이 딥 링크의 예제라고 볼 수 있다.

    하지만 단일 웹 애플리케이션에서는 XMLHttpRequest를 이용해 화면에 변경이 필요한 페이지 조각을 서버로부터 받아와서 브라우저 전체를 다시 읽지 않고 특정 영역의 DOM을 변경한다. 그렇다 보니 화면의 상태가 바뀌었음에도 URL이 변경되지 않고 브라우저의 히스토리가 관리되지 않는다.

    AngularJS는 딥 링킹 처리를 위한 서비스를 제공하고 있다. 바로 $location 서비스다. $location 서비스를 이용해 브라우저 URL의 변경을 감지하고, URL의 상태를 변경할 수 있다. $location 서비스는 HTML5의 History API를 지원하지 않는 옛날 버전의 브라우저를 지원해 주고 AngularJS 애플리케이션 라이프 사이클에 맞춰 작동한다.

    AngularJS는 $location 서비스를 기반으로 좀 더 편리하게 싱글 페이지 웹 애플리케이션의 딥 링킹 처리를 도와주는 $route 서비스를 제공한다.

    9장. $route 서비스를 이용한 라우터 구현


    자바스크립트에서 라우터는 브라우저의 현재 URL에 맞는 애플리케이션의 뷰와 컨트롤러를 연결해 주는 유틸리티를 의미한다. 단일 페이지 웹 애플리케이션은 별도의 처리가 없으면 딥 링킹 처리가 되지 않지만 라우터를 이용하면 이런 딥 링킹 문제를 해결할 수 있다.

    AngularJS는 이러한 라우터 기능을 $route 서비스로 제공해 $route 서비스를 이용해 여러 라우트를 정의하고 브라우저의 URL이 변경되면 정의된 라우트 경로에 맞는 템플릿과 컨트롤러를 호출하게 된다.

    ngRoute 모듈

    $route는 기본 ng 모듈로 제공되지 않고 ngRoute 모듈로 제공된다. 그래서 라우트 기능과 관련된 모든 서비스는 ngRoute 모듈로 접근할 수 있다. ngRoute 모듈을 적용하려면 다음 코드와 같이 별도의 angular-route.js 파일을 추가해야 한다.

    			<script src="angular.js">
    			<script src="angular-route.js">
    		

    ngRoute 모듈에서 제공하는 서비스와 지시자는 다음과 같다.

    | $routeProvider

    $routeProvider는 $route 서비스를 설정하는 서비스 프로바이더다. 이 서비스 프로바이더를 이용해 여러개의 라우터를 등록할 수 있다. 라우트를 정의할 때 라우트 경로와 이에 해당하는 컨트롤러와 템플릿을 설정한다. 다음은 간단한 라우팅 처리에 대한 예제 코드다.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title></title>
    		<style>
    			ul {padding:0;}
    			ul.menu li {padding:5px; border:1px solid #000; background:#000; display:inline;}
    			ul li a {text-decoration:none; color:white;}
    		</style>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
    		<script src="../bower_components/angular/angular.js"></script>
    		<script src="../js/angular/angular-route.js"></script>
    		<script>
    			angular.module('sampleApp', ['ngRoute']).
    				config(function($routeProvider){
    					$routeProvider
    						.when('/home', {templateUrl : 'template/home.tmpl.html'})
    						.when('/about', {tmeplateUrl : 'template/about.tmpl.html', controller:'aboutCtrl'})
    						.when('/contact', {templateUrl : 'template/contact.tmpl.html', controller:'contactCtrl'})
    						.otherwise({redirectTo : '/home'});
    				}).
    				controller('aboutCtrl', function($scope){
    					$scope.sales = 200000000;
    				}).
    				controller('contactCtrl', function($scope){
    					$scope.contactSubmit = function(contact){
    						alert(contact.name + " 에게 " + contact.contents+" 를 전달했습니다.");
    					};
    				});
    		</script>
    	<head>
    	<body>
    		
    		
    	</body>	
    </html>
    		

    각 메뉴를 클릭할 때마다 메뉴 아래에 있는 콘텐츠 부분이 동적으로 바뀌면서 브라우저의 URL이 바뀐다. 여기서 라우트의 경로와 매칭되는 URL은 페이지 URL 뒤에 #이 붙고 그 뒤로 라우트의 경로와 같다. HTML5 모드를 활성화하면 # 없이 처리할 수 있지만, IE8와 같은 오래된 브라우저를 고려해 #을 사용해야 한다.

    위 예제를 보면 모듈의 config 메서드를 이용해 $routeProvider를 주입받아 라우트를 설정하고 있다. 특정 경로에 대한 템플릿과 컨트롤러에 대한 설정은 $routeProvider의 when 메서드를 이용한다. 브라우저의 현재 URL이 변경되면 해당 URL과 일치하는 라우터가 활성화 되면서 AngularJS는 활성화된 라우터의 templateUrl로 비동기 요청을 하여 서버로부터 템플릿을 읽어온다. 그리고 <ng-view></ng-view> 부분에 템플릿이 삽입된다. 이 ngView 지시자가 현재 라우터와 연결된 템플릿이 삽입되는 부분을 나타낸다.

    ∎ $routeProvider.when(라우트 경로, 라우트 연결 설정 객체)

    $routeProvider는 말 그대로 라우트를 정의한다. 라우트 경로에 따른 라우트 연결 설정 정보를 등록할 수있다. 라우트 경로는 문자열로 작성하면 되고 route 연결 설정 객체는 라우트 연결 정보를 객체로 표현한 것인데 다음과 같은 속성 키를 가질 수 있다.

    ∎ $routeProvider.otherwise(라우트 연결 설정 객체)

    브라우저의 URL이 변경될 때의 URL이 등록된 라우트 경로와 일치하는 것이 얿을 때 활성화될 라우트 연결 정보를 설정한다. 단 하나의 매개변수로 라우트 연결 설정 객체를 받는다.

    | $route

    $route를 주입받으면 어떠한 정보를 읽어올 수 있고, 또 해당 서비스에서 어떤 이벤트를 발생하는지 다음 예제를 보자.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title></title>
    		<style>
    			ul {padding:0;}
    			ul.menu li {padding:5px; border:1px solid #000; background:#000; display:inline;}
    			ul li a {text-decoration:none; color:white;}
    		</style>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
    		<script src="../bower_components/angular/angular.js"></script>
    		<script src="../js/angular/angular-route.js"></script>
    		<script>
    			angular.module('sampleApp', ['ngRoute']).
    				config(function($routeProvider){
    					$routeProvider
    						.when('/home', {templateUrl : 'template/home.tmpl.html'})
    						.when('/about', {templateUrl : 'template/about.tmpl.html', controller:'aboutCtrl'})
    						.when('/contact', {templateUrl : 'template/contact.tmpl.html', controller:'contactCtrl'})
    						.otherwise({redirectTo : '/home'});
    				}).
    				controller('mainCtrl', function($scope, $route){
    					$scope.route= $route;
    					$scope.routes = $route.routes;
    					$scope.$on("$routeChangeSuccess", function(e, cRoute, pRoute){
    						console.log("현재 라우트 정보 : ", cRoute.loadedTemplateUrl);
    						if(pRoute) console.log("이전 라우트 정보 : ", pRoute.loadedTemplateUrl);
    					});
    					$scope.reload = function(){
    						$route.reload();
    					};
    				}).
    				controller('aboutCtrl', function($scope){
    					$scope.sales = 200000000;
    				}).
    				controller('contactCtrl', function($scope){
    					$scope.contactSubmit = function(contact){
    						alert(contact.name + " 에게 " + contact.contents+" 를 전달했습니다.");
    					};
    				});
    		</script>
    	<head>
    	<body ng-controller="mainCtrl">
    		
    		
    		
    <h2>라우트 정보</h2> <h3>현재 라우트 정보</h3> {{route.current}}
    <h4>등록된 라우트 정보</h4>
    • <h5>{{key}}</h5>

      {{value}}

    </body> </html>

    위 예제를 보면 mainCtrl 컨트롤러 함수에서 $route 서비스를 주입받는 것을 확인할 수 있다. 해당 $route 서비스를 이용해 현재 라우트 정보와 전체 등록된 라우트 목록을 확인할 수 있다. 또한 라우트에 발생하는 이벤트에 대한 리스너 함수를 등록할 수 있으며, current 속성을 이용해 현재 라우트 정보를 가져오고 routes 속성을 이용해 라우트 목록을 가져온다.

    $scope에 $routeChangeSuccess 이벤트에 대한 리스너 함수를 등록해 라우트가 성공적으로 변경될 때 이전 라우트 정보나 새로운 라우트 정보를 콘솔에 출력하고 있다. 내부적으로 라우팅되거나 라우트 정보가 바뀔 때 여러 이벤트를 $rootScope에서 브로드캐스트하고 있어 어느 $scope에서든 $on 메서드를 이용해 이벤트 리스너 함수를 등록할 수 있다. $route 서비스에서 발생하는 전체 이벤트 목록은 다음과 같다.

    | $routeParam

    $routeProvider.when을 이용해 라우트를 정의할 때 첫번째 인자로 라우트 경로를 URL 문자열로 작성한다고 했다. 사실 라우트 경로는 단순한 URL 문자열이 아니다. 해당 문자열은 다음을 포함할 수 있다.

    예를 들어, 다음과 같이 라우트 경로를 설정했다고 하자.

    			$routeProvider.when('/floor/:floorNum/room/:roomNum', {...})
    		

    그리고 브라우저의 접속 URL이 다음과 같다면

    http://hoteml.com/guestList.html#/floor/3/room/120?guest=soon

    $routeParam을 이용해 :floorNum과 :roomNum은 3과 120으로 그리고 ?로 시작하는 쿼리 문자열의 값들은 guest에 soon이라는 값으로 가지고 올 수 있다. 즉, 다음과 같이 $routeParam은 객체로 표현된다.

    $routeParam : {floorNum: 3, roomNum: 120, guest: "soon"}

    다음 간단한 예제로 $routeParam을 사용해 보자.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title></title>
    		<style>
    			ul {padding:0;}
    			ul.contact-list li {margin:2px; padding:2px; border:1px solid yellow; background:#000; color:white;}
    			ul.contact-list li:hover {background:yellow; color:black;}
    		</style>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
    		<script src="../bower_components/angular/angular.js"></script>
    		<script src="../js/angular/angular-route.js"></script>
    		<script>
    			angular.module('sampleApp', ['ngRoute']).
    				config(function($routeProvider){
    					$routeProvider
    						.when('/contacts', {templateUrl : 'template/contact-list.tmpl.html', controller:'contactListCtrl'})
    						.when('/contacts/:contactId', {templateUrl : 'template/contact-detail.tmpl.html', controller:'contactDetailCtrl'})
    						.otherwise({redirectTo : '/contacts'});
    				}).
    				factory('ContactSvc', [function(){
    					var cList = [
    						{id:'c001', name:'순희', email:'soon@ng.com', phone:'011-2222-3333'},
    						{id:'c002', name:'철수', email:'cheol@ng.com', phone:'011-2222-3333'}
    					];
    
    					return {
    						getList : function(){
    							return cList;
    						},
    						get : function(id){
    							var returnObj = {};
    
    							for(var i=0;i<cList.length;i++){
    								if(id===cList[i].id){
    									returnObj = cList[i];
    									break;
    								}
    							}
    							return returnObj;
    						}
    					};
    				}]).
    				controller('contactListCtrl', function($scope, ContactSvc, $location){
    					$scope.contactList = ContactSvc.getList();
    					$scope.viewDetail = function(id){
    						$location.path('/contacts/'+id);
    					};
    				}).
    				controller('contactDetailCtrl', function($scope, ContactSvc, $routeParams){
    					$scope.contact = ContactSvc.get($routeParams.contactId);
    				});
    		</script>
    	<head>
    	<body>
    		
    	</body>	
    </html>
    		

    위 예제는 연락처 정보를 목록으로 보여주고 해당 목록 아이템을 클릭하면 상세 연락처 정보를 보여주는 간단한 코드다. 라우트는 두 개의 경로가 설정돼 있다. '/contacts'와 /contacts/:contactId'가 그러한데 여기서 두번째 연락처 상세 정보에 대한 라우터 경로를 보면 세미콜론과 함께 :contactId로 설정했다. 이렇게 contactId라는 이름으로 그룹을 설정하면 $routeParam을 이용해 그 값을 가지고 올 수 있다.

    contactListCtrl 컨트롤러 함수에서 $scope.viewDetail 메서드가 있는데, 이는 목록 아이템을 클릭하면 호출되며, 여기에 매개변수로 연락처 아이디가 전달된다. 이렇게 전달받은 매개변수를 이용해 $location.path('/contacts/'+id)로 브라우저의 URL 경로를 변경하게 된다. 그러면 앞에서 설정한 '/contacts/:contactId' 라우트 경로가 매치되어 template/contact-detail.tmpl.html 파일이 읽어져 <ng-view></ng-view> 영역에 삽입되는 것이다.

    '/contacts/:contactId' 라우트 경로에 매치되면 contactDetailCtrl 컨트롤러 함수가 활성화되는데 :contactId에 매치되는 값을 $routeParams.contactId으로 가지고 올 수 있다. 이렇게 얻어진 연락처 아이디는 ContactSvc.get() 메서드에 매개변수로 전달해 해당 아이디에 해당하는 연락처 상세 정보를 반환받는다. 그리고 해당 값을 $scope.contact에 대입해 화면을 보여준다.

    10장. $http 서비스를 이용한 서버 통신


    AngularJS에서는 AJAX 처리를 위해 $http 서비스를 제공한다. $http 서비스는 서버와 비동기 통신을 하기 위해 네이티브 API인 XMLHttpRequest를 추상화하여 제이쿼리의 $.ajax처럼 손쉽게 비동기 HTTP 요청을 할 수 있게 해준다.

    $http 서비스

    $http 서비스는 HTTP 요청을 생성하기 위해 설정 객체 하나만 인자로 받는 함수다. 그 결과로 successerror 두 메서드를 가지는 promise 객체를 반환한다. 다음은 간단한 $http 요청에 대한 예제 코드다.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title></title>
    		<style>
    			.info {margin:0 auto; height:20px; background-color:aliceblue;}
    			.info button {float:right;}
    		</style>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
    		<script src="../bower_components/angular/angular.js"></script>
    		<script>
    			angular.module('sampleApp', []).
    				directive('infoBox', [function(){
    					return {
    						restrict : 'E',
    						scope : {infoMessage : '='},
    						template : '<p class="info">{{infoMessage}}<button ng-click="hide()">x</buton><p>',
    						link : function(scope, iElement, iAttrs){
    							scope.$watch("infoMessage", function(newData, beforeData){
    								if(newData === undefined || newData === ''){
    									scope.hide();
    								}else{
    									iElement.show({
    										duration : 3000,
    										complete : function(){
    											iElement.hide();
    										}
    									});
    								}
    							});
    							scope.hide = function(){
    								iElement.hide();
    								scope.infoMessage = undefined;
    							};
    						}
    					};
    				}]).
    				controller('mainCtrl', function($scope, $http){
    					$scope.user = {};
    					$scope.search = function(){
    						var reqPromise = $http({
    							method : 'GET',
    							url : 'json/sample.json'
    						});
    						reqPromise.success(function(data, status, headers, config){
    							$scope.user = data;
    						});
    						reqPromise.then(function(response){
    							$scope.msg = response.data.userId+"로딩 완료.";
    						}, function(response){
    							$scope.msg = "Error!";
    						});
    					};
    				});
    		</script>
    	<head>
    	<body ng-controller="mainCtrl">
    		
    		

    사용자 아이디 : {{user.userId}}
    사용자 이름 : {{user.username}}
    사용자 이메일 : {{user.userEmail}}

    </body> </html>

    조회 버튼을 클릭하면 컨트롤러의 search 메서드가 실행돼 $http 서비스를 호출한다. 그리고 서버 응답이 성공적으로 전달되면 사용자 정보를 화면에 보여주는데 상단의 정보탕(infobox 지시자)이 그 내용을 알려주고 사라진다. $http 서비스는 설정 객체에 method는 'GET', url은 'json/sample.json'으로 서버에 요청을 보냈다. 다음 목록은 설정 객체에 작성할 수 있는 설정 키와 설명이다.

    $http 서비스를 함수로 호출할 경우 promise가 반환되는데 이 promise는 기본 then 메서드와 특별한 두 메서드 successerror 를 포함하는 객체다. then 메서드는 success와 error시의 두 콜백 함수를 인자로 받고 success와 error는 하나의 콜백 함수를 인자로 받는다. Promise의 then 메서드에 사용되는 콜백 함수들은 응답 객체가 전달된다. 다음은 전달되는 객체의 속성값이다.

    하지만 success와 error 메서드의 인자로 주어지는 콜백 함수에서는 응답 객체가 전달되지 않고 해당 속성들이 나열되어 인자로 들어간다.

    단축 메서드 제공

    $http 서비스를 메서드를 제공하는 객체처럼 사용할 수도 있다. $http 서비스는 편의상 GET, POST, PUT, DELETE와 같은 HTTP 메서드별로 단축 메서드를 제공한다. 다음 메서드는 $http 서비스에서 사용할 수 있는 메서드들이다.

    메서드 명에서 알 수 있듯이 $http.get은 HTTP GET 요청을 보내는 것이고 $http.delete는 HTTP DELETE 요청을 보내는 것이다. 다음은 $http 서비스를 단축 메서드로 사용하는 예제 코드다.

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title></title>
    		<style>
    			.info {margin:0 auto; height:20px; background-color:aliceblue;}
    			.info button {float:right;}
    		</style>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
    		<script src="../bower_components/angular/angular.js"></script>
    		<script>
    			angular.module('sampleApp', []).
    				value('baseUrl', 'http://localhost:9000/').
    				controller('mainCtrl', function($scope, $http, baseUrl){
    					$scope.user = {};
    					$scope.editable = false;
    					$scope.search = function(searchObj){
    						var reqPromise = $http.get(baseUrl+'getUser', {
    							params : searchObj 
    						});
    						reqPromise.success(function(data, status, headers, config){
    							if(data.userId){
    								$scope.user = data;
    								$scope.isSearched = true;
    							}
    							$scope.error = undefined;
    						});
    						reqPromise.error(error);
    					};
    
    					$scope.insert = function(){
    						$scope.edit = "insert";
    						$scope.editable = true;
    						$scope.user = {};
    					};
    
    					$scope.update = function(){
    						$scope.edit = "update";
    						$scope.editable = true;
    					};
    
    					$scope.save = function(edit, user){
    						var reqPromise;
    						switch(edit){
    							case "insert" :
    								reqPromise = $http.post(baseUrl+'getUsers', user);
    								break;
    							case "update" :
    								reqPromise = $http.put(baseUrl+'updateUser', user);
    								break;
    							default :
    								reqPromise = {};
    								break;
    						}
    						reqPromise.success(function(data, status, headers, config){
    							reset();
    						});
    						reqPromise.error(error);
    					};
    
    					$scope.delete = function(user){
    						$http.delete(baseUrl+'deleteUser', {
    							params : user
    						}).success(function(){
    							reset();
    						});
    					};
    
    					$scope.cancel = function(){
    						reset();
    					};
    
    					function error(data, status, headers, config){
    						$scope.user = {};
    						$scope.error = "로드 실패";
    					}
    
    					function reset(){
    						$scope.user = {};
    						$scope.edit = undefined;
    						$scope.error = undefined;
    						$scope.editable = false;
    					}
    				});
    		</script>
    	<head>
    	<body ng-controller="mainCtrl">
    		
    사용자 아이디 :

    {{error}}


    사용자 아이디 :
    사용자 이름 :
    사용자 임일 :

    </body> </html>

    $httpProvider

    $http 서비스는 $httpProvider를 이용해 다양한 설정을 할 수 있다. 예를 들어, 요청에 따른 결과 데이터나 요청으로 보낼 데이터의 변환 함수를 등록한다거나 요청 전후 처리에 관한 인터셉터 함수를 등록한다거나 기본 HTTP 헤더 설정을 추가/변경할 수 있다.

    | 헤더 정보 변경

    $http 서비스는 기본 헤더 정보가 설정돼 있다. 그래서 $http 서비스로 HTTP 요청을 보내면 어떤 HTTP 메서드든 Accept:"application/json, text/plain, */*" 헤더 정보를 담아 보낸다. POST와 PUT 메서드인 경우에는 Content-Type:"application/json:charset=utf-8"를 같이 보내게 되어 있다. $httpProvider를 이용하면 기본 헤더 정보를 변경할 수 있고 새로운 헤더 정보를 추가할 수도 있다.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title></title>
    		<style>
    			.info {margin:0 auto; height:20px; background-color:aliceblue;}
    			.info button {float:right;}
    		</style>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
    		<script src="../bower_components/angular/angular.js"></script>
    		<script>
    			 angular.module('sampleApp', []).
    			  config(function ($httpProvider) {
    			   $httpProvider.defaults.headers.common['Accept'] = "application/xml";
    			   $httpProvider.defaults.headers.post['Content-Type'] = "application/xml";
    			  }).
    			  controller('mainCtrl',function($scope, $http) {
    			   $scope.header = $http.defaults.headers;
    			   $http({method: 'GET', url: 'xml/test.xml'}).
    				success(function (data) {
    				 console.log(data);
    				});
    			  });
    		</script>
    	<head>
    	<body ng-controller="mainCtrl">
    		 
    현재 $http 헤더정보

    {{header}}

    </body> </html>

    $httpProvider.defaults.headers의 common은 모든 HTTP 메서드에 공통으로 사용되는 헤더 정보를 설정할 수 있고 post는 HTTP POST 메서드로 요청을 보낼 때 사용되는 헤더 정보를 설정하게 된다.

    | 데이터 변환 함수 등록

    AngularJS는 기본적으로 요청/응답 시 데이터를 JSON으로 변환하는 변환 함수가 등록돼 있다. 그래서 위 예제처럼 $http 성공 콜백 함수에서 응답 결과를 객체로 받지 못하고 XML 문자열로 받는 것이다. 물론 데이터 변환 함수를 새로 추가하거나 기본 변환 함수를 덮어 씌울 수 도 있다.

    새로 추가되는 데이터 변환 함수는 각 배열의 순서에 따라 체인이 된다. 즉, 첫번째 변환 함수의 반환 값이 두번째 변환 함수에 주어지고 그 반환값이 다시 그 다음으로 이어지게 된다. 다음 예제는 XML 문자열을 읽어 객체로 변환하는 변환 함수를 추가한 코드이다.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title></title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
    		<script src="lib/xml2json.min.js"></script>
    		<script src="../bower_components/angular/angular.js"></script>
    		<script>
    			 angular.module('sampleApp', []).
    			  config(function ($httpProvider) {
    			   $httpProvider.defaults.headers.common['Accept'] = "application/xml";
    			   $httpProvider.defaults.headers.post['Content-Type'] = "application/xml";
    			   $httpProvider.defaults.transformResponse.push(function(data, getHeader){
    				if(getHeader("Content-Type") === "application/xml"){
    					var x2js = new X2JS(),
    						obj = x2js.xml_str2json(data);
    					return obj;
    				}else{
    					return data;
    				}
    			   });
    			  }).
    			  controller('mainCtrl',function($scope, $http) {
    			   $scope.header = $http.defaults.headers;
    			   $http({method: 'GET', url: 'xml/test.xml'}).
    				success(function (data) {
    					$scope.result = data;
    				});
    			  });
    		</script>
    	<head>
    	<body ng-controller="mainCtrl">
    			
    요청 결과값

    {{result}}

    </body> </html>

    위 코드를 보면 서비스 프로바이더 설정 부분에서 $httpProvider.defaults.transformResponse에 새로운 데이터 변환 함수를 추가한 것을 볼 수 있다. 새로 추가된 데이터 변환 함수는 XML 문자열을 자바스크립트 객체로 변경해주는 예제이며 라이브러리인 x2js를 이용했다.

    11장. RESTful 웹 서비스를 위한 $resource 서비스


    $resource 서비스는 RESTful 웹 서비스와 더욱 직관적이고 편리하게 대화하기 위해 AngularJS에서 제공하는 서비스다. $resource 서비스는 기본 ng 모듈이 아닌 ngResource 모듈로 배포되므로 해당 angular-resource.js 파일을 꼭 포함시켜야 한다.

    ngResource 모듈

    다음과 같이 별도의 angular-resource.js 파일을 추가해야 한다.

    			<script src="../js/angular/angular.js"></script>
    			<script src="../js/angular/angular-resource.js"></script>
    		

    ngResource 모듈은 별도의 지시자나 필터가 없고 오직 $resource 서비스만 제공한다.

    $resource 서비스

    $resource 서비스는 특정 RESTful 웹 서비스의 리소스를 의미하는 리소스 생성자 함수를 생성한다. 가령 사용자 리소스를 제공하는 RESTful 웹 서비스가 있다고 하면 다음과 같이 사용자 리소스 생성자 함수를 생성할 수 있다.

    			var UserResource = $resource('/user/:userId');	
    		

    즉, $resource 서비스는 리소스 생성자 함수를 생성하는 팩토리 함수다. 팩토리 함수의 필수 인자로 RESTful 웹 서비스의 URL을 전달해줘야 하는데 해당 URL은 ':' 접두사를 이용해 라우트 경로와 마찬가지로 URL의 특정 부분을 매개변수화 할 수 있다. 위 코드에서는 userId 이름의 매개변수가 만들어지게 된다.

    그리고 두번째 인자로 해당 매개변수의 기본 값을 설정할 수 있다. 객체로 주어지고 속성 이름이 매개변수 이름이 된다. 속성의 값이 함수이면 매개변수의 값이 필요로 하는 요청마다 해당 함수가 실행되어 함수의 반환 값이 매개변수에 전달되어 URL을 만들게 된다. 예를 들어, 다음과 같이 사용자 리소스 생성자 함수가 정의돼 있다고 하자.

    			var UserResource = $resource('/user/:userId', {userId : 'jay'});	
    		

    그러면 해당 사용자 리소스 생성자 함수로 조회 액션이 취해지면 매개변수의 기본 값 설정에 의하여 'user/jay'라는 URL이 만들어지게 된다.

    12장. $q 서비스를 이용한 비동기 처리


    자바스크립트는 태생부터 웹 브라우저에서 사용하도록 만들어진 단일 스레드를 기반으로 동작하는 언어다. 그래서 한 줄의 코드를 읽고 실행하는 일이 매우 오래 걸리면 다음 코드로 넘어가지 않게 된다. 그러므로 필연적으로 이벤트 기반으로 동작하여 이벤트가 발생하면 특정 콜백 함수가 실행하는 구조를 갖는다.

    자바스크립트 비동기 프로그래밍

    자바스크립트로 단일 페이지 웹 애플리케이션을 개발하다 보면 비동기 프로그래밍을 하지 않을 수 없다. 예를 들어, 다른 페이지로 이동하거나 서버로부터 데이터를 조회하는 XMLHttpRequest 요청들 그리고 버튼 클릭과 같은 사용자 액션은 언제 시작하고 끝날지 모른다. 그래서 한없이 기다릴 수가 없으므로 액션에 대한 처리나 Ajax 요청에 대한 처리는 다른 코드 구문을 해석하고 실행하다 나중에 실제 사용자가 클릭 이벤트나 응답 완료 이벤트가 왔을 때 콜백 처리를 한다.

    하지만 비동기 프로그래밍을 하다 보면 여러 이벤트를 순서대로 제어하고자 할 때가 있다. 다음 코드는 제이쿼리 1.4를 이용해 ajax 처리를 하는 코드다.

    			$.ajax({
    				success : function(){...},
    				failure : function(){...}
    			});
    		

    요청에 대한 성공이나 실패 콜백 함수를 요청 설정 객체를 넘길 때 같이 넘기도록 코드를 작성 했다. 하지만 제이쿼리 1.5 이후부터는 위 코드를 다음과 같은 방식으로 변경하게 됐다.

    			var promise = $.ajax();
    			promise.done(function(){...});
    			promise.fail(function(){...});
    			promise.always(function(){...});
    		

    위 코드를 보면 AJAX 요청을 하게 되면 promise라는 객체가 반환된다. 그리고 해당 객체의 done 메서드를 이용해 성공 처리를 한다. 제이쿼리 1.5 이전과 바뀐 점은 promise를 반환하는가의 여부다. 이 promise는 Common Js Promises/A에서 비동기 프로그래밍에 대한 처리를 위해 표준 스펙으로 정의한 내용이다. Promise를 반환한다는 건 언젠가 일어날 일에 대하여 성공 했을 때 done 메서드에 콜백 함수를 넣어주면 호출할 것이고 실패 했을 때 fail 메서드에 콜백 함수를 넣어주면 호출하고 성공이나 실패 상관없이 always 메서드에 콜백 함수를 넣어주면 성공/실패에 상관없이 호출한다는 의미다. Promisedeferrd를 이용해 생성할 수도 있다.

    $q 서비스

    AngularJS는 Common JS Promises/A 스펙에 대한 구현 API를 $q 서비스를 이용해 제공한다. $q 서비스는 AngularJS의 scope 모델의 데이터 바인딩에 대한 처리가 최적화돼 있다. $q 서비스는 기본 ng 모듈에 포함돼 있어 별도의 자바스크립트 파일을 포함할 필요가 없다.

    | Promise 객체

    Promise는 문자 그대로 약속을 표현하는 자바스크립트 객체다. 다음은 AngularJS에서 promise 객체가 포함하는 메서드 목록이다.

    실제 AngularJS에서는 $http, $timeout, $resource, $route 등 여러 서비스에서 promise 객체를 반환한다. 다음은 AngularJS의 promise API를 이용한 간단한 예제이다.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title></title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
    		<script src="../bower_components/angular/angular.js"></script>
    		<script>
    			angular.module('sampleApp', []).
    				controller('mainCtrl', ['$scope', '$timeout', function($scope, $timeout){
    					var threeSecPromise = $timeout(function(){
    							return $scope.answer;
    						}, 3000);
    
    					threeSecPromise.then(function(val){
    						if(val == 39){
    							$scope.result = "맞았어요.";
    						}else{
    							$scope.result = "틀렸어요.";	
    						}
    					}, function(){
    						$scope.result = "너무 어려웠나요?";
    					});
    
    					threeSecPromise.finally(function(){
    						$scope.info = "다시 시작하려면 refresh 해주세요.";
    					});
    
    					$scope.giveUp = function(){
    						$timeout.cancel(threeSecPromise);	
    					};
    				}]);
    		</script>
    	<head>
    	<body ng-controller="mainCtrl">
    		

    3초안에 답을 말하세요

    10-1+29+2-1 =

    {{result}}

    {{info}}

    </body> </html>

    $timeout 서비스는 setTimeout을 AngularJS가 추상화한 서비스로 기본적인 사용법은 같다. 하지만 $timeout 서비스는 promise 객체를 반환한다. 예제를 보면 3초가 지나면 자동으로 promise 객체가 반환되고 then 메서드의 성공 콜백 함수가 실행된다. 성공 콜백 함수에 인자로 주어지는 값이 $timeout에 주어지는 함수가 반환하는 값이 된다.

    $timeout은 $timeout.cancel(promise 객체)로 해당 약속을 취소할 수도 있다. 그러면 $timeout에 주어진 콜백 함수는 실행되지 않고 promise의 실패 콜백 함수가 실행된다. 예제에서는 사용자가 포기 버튼을 클릭하면 해당 3초 안에 답을 주는 약속을 취소하게 되고 약속을 포기했을 때 "너무 어려웠나요?" 라는 결과가 화면에 표시된다. 그리고 약속이 지켜지든 취소되든 "다시 시작하려면 refresh 해주세요."라는 메시지가 화면에 표시된다.

    Promise.then 메서드는 사실 새로운 promise를 반환한다. 즉, 해당 약속에 대한 처리를 정의하면 약속에 대한 처리가 정의된 새로운 약속이 만들어지는 것이다. 다음 코드를 보자.

    			newPromise = promise.then(function(result){
    				...
    				return {before:result, newProperty:"new"};
    			});
    
    			newPromise = promise.then(function(result){
    				//result -> {before:result, newProperty:"new"};
    			});
    		

    새로 만든 약속은 기존 약속에서 처리하여 반환한 결과와 연결된다. 이러한 연결은 얼마든디 더 만들어질 수 있다. 하지만 약속을 연결할 때 주의할 점이 있는데 기존 약속의 실패 콜백에서 반환된 값은 새로 만든 약속의 성공 콜백으로 연결된다. 만약 새로 만든 약속의 실패 콜백으로 이어지게 하려면 $q.reject를 이용해야 한다.

    | Deferred 객체

    promise를 가지고 지켜지거나 거절 혹은 취소될 때의 미래의 일을 정의했다. 하지만 누군가는 약속을 지키거나 거절해야 한다. 이러한 일을 하는 것이 deferred 객체다. 약속을 만든 사람이 약속을 거절하고 취소하듯이 deferred는 약속을 만들고 이 약속의 상태를 변경한다. AngularJS에서는 $q.defer()를 이용해 deferred 객체를 생성할 수 있다. (deferred 객체의 생성은 곧 promise의 생성이기도 하다.) 이렇게 생성한 deferred 객체는 resolve, reject, notify를 통하여 약속을 지키거나 거절/취소하거나 진행 상태를 알려주게 된다. 다음 목록은 deferred 객체의 메서드와 속성이다.

    다음은 $q.defer()를 사용해서 직접 deferred 객체를 생성하는 예제 코드이다.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title></title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
    		<script src="../bower_components/angular/angular.js"></script>
    		<script>
    			angular.module('sampleApp', []).
    				factory('Teacher', [function(){
    					function Teacher(name){
    						this.name = name;
    					}
    					Teacher.prototype.makeScore = function(data){
    						if(data>5){
    							return 100;
    						}else{
    							return 4;
    						}
    					}
    					Teacher.prototype.giveCandy = function(num, student){
    						return student.name+"(군/양) 사탕"+num+"개 받으세요.";
    					}
    					Teacher.prototype.hitHip = function(num, student, error){
    						return student.name+"(군/양) 엉덩이"+num+"대 맞으세요.", error;	
    					}
    					return Teacher;
    				}]).
    				factory('Student', ['$q', '$timeout', function($q, $timeout){
    					function Student(name){
    						this.name = name;
    					}
    					Student.prototype.doHomework = function(homework){
    						var deferred = $q.defer(),
    							time = (Math.random()*10 + homework.length)*500;
    
    						console.log('숙제하는데 걸리는 시간 :' + time);
    
    						$timeout(function(){
    							var homeworkResult = time/1000;
    							console.log('숙제 결과 :' + homeworkResult);
    							if(time<6000){
    								deferred.resolve(homeworkResult);
    							}else{
    								deferred.reject('핑계들...');
    							}
    						}, time);
    
    						return deferred.promise;
    					}
    					return Student;
    				}]).
    				controller('mainCtrl', function($scope, $timeout, Teacher, Student){
    					var jay = new Student('jay'),
    						cindy = new Teacher('cindy'),
    						promiseWithStudent = jay.doHomework('숙제내용...');
    
    					promiseWithStudent.then(function(data){
    						if(cindy.makeScore(data) === 100){
    							console.log(cindy.giveCandy(100, jay));
    						}else{
    							console.log(cindy.giveCandy(50, jay));
    						}
    					}, function(error){
    						console.log(cindy.hitHip(10000000, jay, error));
    					});
    				});
    		</script>
    	<head>
    	<body ng-controller="mainCtrl">
    	</body>	
    </html>
    		

    위 예제를 보면 두 개의 서비스가 등록돼 있다. 하나는 Student 서비스이고 다른 하나는 Teacher 서비스다. Teacher 서비스는 Teacher 생성자 함수를 반환하는데 Teacher의 역할은 점수를 채점하고(makeScore) 사탕을 주거나(giveCandy) 야단을 친다(hitHip). 이제 Student 서비스도 마찬가지로 Student 생성자 함수를 반환하는데 Student 서비스는 $q 서비스를 사용한다. Student 서비스의 주요 역할은 숙제를 하는 것인데 숙제는 주어진 숙제에 따라 어느 정도 시간이 걸릴지 모른다. 그 부분이 다음 코드다.

    			time = (Math.random()*10 + homework.length)*500;
    		

    그리고 해당 시간이 6초 이하이면 숙제를 다 하고 제출한다. 이때 deferred.resolve API를 호출해 숙제를 하겠다는 약속을 지킨다고 하는 것이다. 그러면서 숙제 결과를 인자로 전달한다. 6초를 초과하면 deferred.reject를 호출하며 숙제를 한다는 약속을 지키지 않는다. 그리고 마지막으로 deferred.promise를 반환하며 메서드 호출하는 쪽에 약속을 제공한다.

    다음으로 mainCtrl 컨트롤러 함수는 Student 서비스를 이용해 Student 객체를 만들고 doHomework 메서드를 호출해 숙제를 하겠다는 약속을 받았다

    			promiseWithStudent = jay.doHomework('숙제내용...');
    		

    그리고 해당 약속에 then 메서드를 이용해 숙제 결과를 Teacher 객체가 채점을 하여 그 결과를 콘솔에 출력하고 있다. 이렇게 $q.defer를 이용해 deferred 객체를 생성하고 resolve와 reject을 이용해 만든 약속을 지키거나 지키지 않는다. 그리고 deferred.promise로 해당 약속을 전달할 수 있다.

    Promise 병렬 제어

    $q 서비스는 미래에 지켜지거나 지켜지지 않을 여러 약속을 하나의 약속으로 처리할 수 있는 API도 제공한다. $q.all(약속들) 메서드를 이용해 약속을 반환하는 여러 비동기적인 일이 병렬적으로 행해지고 그 약속을 하나로 묶어 성공/실패 처리할 수 있게 해준다. [예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title></title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
    		<script src="../bower_components/angular/angular.js"></script>
    		<script>
    			angular.module('sampleApp', []).
    				controller('mainCtrl', function($scope, $q, $http){
    					$scope.userList = [];
    					var httpPromise1 = $http.get('json/sample.json');
    					var httpPromise2 = $http.get('json/sample2.json');
    
    					$q.all([httpPromise1, httpPromise2])
    						.then(function(resultArray){
    							angular.forEach(resultArray, function(value, key){
    								$scope.userList.push(value.data);
    							});
    						});
    				});
    		</script>
    	<head>
    	<body ng-controller="mainCtrl">
    		
    • {{user.userName}}, {{user.userEmail}}
    </body> </html>

    위 예제를 보면 두 HTTP 요청이 발생하고 해당 요청에 대한 약속이 만들어진다. $q.all([httpPromise1, httpPromise2]) 코드를 보면 알수 있듯이 이 두 약속을 $q.all 메서드를 이용해 새로운 약속을 만들 수 있다. 그리고 해당 약속이 지켜지면 성공 콜백에 해당 약속에 대한 결과를 인자로 받을 수 있게 된다.

    $http 서비스 인터셉터 등록시 promise 활용

    $httpProvider를 이용하면 HTTP 요청을 보내기 전에 특정 행위를 하는 인터셉터를 등록할 수 있다. 이 인터셉터들은 promise 객체를 반환해 해당 약속이 지켜지고 나면 $http 서비스는 HTTP 요청을 하게 된다.[예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title></title>
    		<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet" />
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
    		<script src="../bower_components/angular/angular.js"></script>
    		<script>
    			angular.module('sampleApp', []).
    		
    				config(function($httpProvider){
    					$httpProvider.interceptors.push(function($q, $timeout){
    						return {
    							'request' : function(config){
    								return $timeout(function(){
    									return config;
    								}, 3000)
    							},
    							'response' : function(response){
    								return response || $q.when(response);
    							}
    						};
    					});
    				}).
    				controller('mainCtrl', function($scope, $http){
    					$scope.send = function(){
    						$http({method: 'GET', url:'json/sample.json'}).
    							success(function(data){
    								$scope.result = data;
    							});
    					}
    				});
    		</script>
    	<head>
    	<body ng-controller="mainCtrl">
    		
    		

    {{result}}

    </body> </html>

    예제를 보면 $httpProvider.interceptors 배열에 익명함수를 추가한 것을 알수 있다. 이 익명 함수가 인터셉터 함수인데 인터셉터 함수는 이렇게 익명함수로 추가하거나 서비스로 등록한 다음 해당 서비스 이름을 추가해도 된다. 인터셉터 함수는 'request', 'response', 'requestError', 'responseError'를 속성으로 가지는 객체를 반환해야 한다. 해당 속성들은 함수를 값으로 가지게 되고 각 함수는 promise 객체나 전달받은 매개변수를 반환해야 한다. 예제에서는 요청 시 3초간 기다리는 인터셉터를 추가했고 $timeout 함수는 promise 객체를 반환하기에 해당 함수를 return 문에 작성했다. 그리고 $timeout 함수의 성공 콜백 함수에서 전달받은 config(요청 설정 객체)를 반환하고 있다. 이렇게 인터셉터를 등록하게 되면 요청 전이나 응답 전에 애플리케이션에서 공통으로 행하는 기능을 처리할 수 있다.

    라우팅 처리 시 promise 제어

    특정 약속이 지켜지면 라우팅 처리를 해야 하는 경우가 있다. $route 또한 라우팅 처리시 promise 객체에 의하여 라우팅 처리를 할 수 있다. [예제보기]

    <!doctype html>
    <html ng-app="sampleApp" lang="ko-KR">
    	<head>
    		<meta charset="utf-8">
    		<title></title>
    		 <style>
    		  ul { padding: 0;}
    		  ul.contact-list li{
    		   margin: 2px;
    		   padding: 2px;
    		   border: 1px solid yellow;
    		   background: black;
    		   color: white;
    		  }
    		  ul.contact-list li:hover{
    		   background-color: yellow;
    		   color: black;
    		  }
    		 </style>
    		<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
    		<script src="../bower_components/angular/angular.js"></script>
    		<script src="../js/angular/angular-route.js">
    		<script>
    			 angular.module('sampleApp', ['ngRoute']).
    			  config(function ($routeProvider) {
    			   $routeProvider
    				.when('/contacts', {templateUrl: 'template/contact-list.tmpl.html', controller: 'contactListCtrl'})
    				.when('/contacts/:contactId', {templateUrl: 'template/contact-detail.tmpl.html', controller: 'contactDetailCtrl',resolve: {
    				   contact : function ($q, $route, $timeout, ContactSvc) {
    					var deferred = $q.defer();
    					$timeout(function () {
    					 var result = ContactSvc.get($route.current.params.contactId);
    					 deferred.resolve(result);
    					}, 2000);
    				   return deferred.promise;
    				  }
    				 }})
    				.otherwise({redirectTo: '/contacts'});
    			  }).
    			  factory('ContactSvc', [function () {
    			   var cList = [
    				{id:'c001',name:'순희',email:'soon@ng.com',phone:'011-2222-3333'},
    				{id:'c002', name:'철수',email:'cheol@ng.com',phone:'011-1111-3333'}
    			   ];
    			  
    			   return {
    				getList : function() {
    				 return cList;
    				},
    				get : function (id) {
    				 var returnObj = {};
    				 for (var i = 0; i < cList.length; i++) {
    				  if(id===cList[i].id) {
    				   returnObj = cList[i];
    				   break;
    				  } 
    				 }
    				 return returnObj;
    				}
    			   };
    			  }]).
    			  controller('contactListCtrl',function($scope,ContactSvc,$location) {
    			   $scope.contactList = ContactSvc.getList();
    			   $scope.viewDetail = function(id) {
    				$location.path('/contacts/'+id);
    			   };
    			  }).
    			  controller('contactDetailCtrl',function($scope,contact) {
    			   $scope.contact = contact;
    			  });
    		</script>
    	<head>
    	<body>
    		 
    	</body>	
    </html>
    		

    위 예제는 '/contacts/:contactId' 라우트 경로를 설정할 때 resolve 설정을 추가했다. 라우트 설정 객체에서 resolve 설정에는 나중에 라우트 컨트롤러 함수에 주입될 객체를 설정하는 resolve 객체를 값으로 주게 된다. 예제에서는 contact 속성을 가지는 resolve 객체가 설정된 것을 볼 수 있다. 이 contact는 contactDetailCtrl 컨트롤 함수에서 contact 이름으로 주입되어 사용되는 것도 알 수 있다. 라우트 resolve 설정에서 contact의 값으로는 promise 객체를 반환하는 함수를 주는데 이 함수에서도 다른 서비스를 주입받을 수 있다. $timeout을 이용해 2초 후에 해당 약속을 지키며 ContactSvc의 get 메서드의 반환 값인 $route.current.params.contactId에 해당하는 연락처 정보를 전달한다. 이 연락처 정보는 contactDetailCtrl 컨트롤 함수에서 contact 매개변수로 전달받아 $scope.contact에 연결된다.

    라우트 설정에서 resolve를 사용하면 특정 서비스의 결과가 다 완료됐을 때 라우팅 처리를 하게 할 수 있다. 이는 라우팅 처리가 먼저 되고 라우팅되는 화면에 연결할 데이터를 늦게 가지고 오거나 하는 비동기 제어에 관한 문제를 말끔하게 해결해 준다.

    13장. 북마크 웹 애플리케이션 개발

    [예제보기]

    애플케이션의 전체 공통 영역을 index.html 파일로 두고 바뀌는 부분을 라우터에 연결될 템플릿 파일로 분리한다.

    ▼ index.html

    index.html에서 <html> 태그에 ng-app="ngBookmark"를 추가하고 두 템플릿 파일이 위치할 <div class="container"></div> 안에 <ng-view></ng-view> 지시자 를 추가.

    <!doctype html>
    
    
    
    <html class="no-js lt-ie9" ng-app="ngBookmark">
      <head>
        
        
        북마크 웹 애플리케이션
        
        
        
        
        
        <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
        
        
        
        <link rel="stylesheet" href="app/styles/main.css">
        
      </head>
    	 <body ng-controller="bookmarkMainCtrl">
    		
    
    		
    <script src="bower_components/jquery/dist/jquery.js"> <script src="bower_components/angular/angular.js"> <script src="bower_components/bootstrap/dist/js/bootstrap.js"> <script src="bower_components/angular-resource/angular-resource.js"> <script src="bower_components/angular-route/angular-route.js"> <script src="app/scripts/app.js"> <script src="app/scripts/bookmark/services/bookmarkSvc.js"> <script src="app/scripts/bookmark/controllers/bookmarkDetailCtrl.js"> <script src="app/scripts/bookmark/controllers/bookmarkAddCtrl.js"> <script src="app/scripts/bookmark/controllers/bookmarkListCtrl.js"> </body> </html>

    ▼ bookmark-list.tmpl.html

    			
    			
    			

    {{bookmark.bookmarkUrl}}

    ▼ bookmark-detail.tmpl.html

    			
    사이트 프리뷰

    ▼ bookmark-new.tmpl.html

    			

    ▼ app.js

    ngBookmark 모듈을 정의하고 라우트를 설정한다.

    			'use strict';
    
    			/**
    			 * @ngdoc overview
    			 * @name ngBookmark 
    			 * @description
    			 * # ngBookmark 
    			 *
    			 * Main module of the application.
    			 */
    			angular.module('ngBookmark', ['ngRoute', 'ngBookmark.bookmark']);
    
    			angular.module('ngBookmark.bookmark', ['ngBookmark.bookmark.controller', 'ngBookmark.bookmark.service']);
    			angular.module('ngBookmark.bookmark.controller', []);
    			angular.module('ngBookmark.bookmark.service', ['ngResource']);
    
    			angular.module('ngBookmark')
    				.config(['$routeProvider', function($routeProvider){
    					$routeProvider
    						.when('/bookmarks', {templateUrl:'/angularjs/bookmarkApp/app/scripts/bookmark/template/bookmark-list.tmpl.html', 
    							controller:'bookmarkListCtrl',
    							resolve : { //비동기 제어에 관한 문제를 해결하고자 resolve 속성 추가
    								bookmarks : function(Bookmark){
    									return Bookmark.query().$promise;
    								}
    							}
    						})
    						.when('/bookmarks/:bookmarkId', {templateUrl:'/angularjs/bookmarkApp/app/scripts/bookmark/template/bookmark-detail.tmpl.html', 
    							controller:'bookmarkDetailCtrl',
    							resolve : {
    								// bookmark 객체는 bookmarkId에 해당하는 북마크 상세정보를 담고 있으며 bookmarkDetailCtrl 컨트롤러 함수에 주입된다.
    								bookmark : function(Bookmark, $route){
    									//get 메서드를 이용해 현재 라우트 경로의 bookmarkId 매개변수 값을 조회 조건으로 전달
    									return Bookmark.get({
    										bookmarkId : $route.current.params.bookmarkId
    									}).$promise;
    								}
    							}
    						})
    						.when('/new-bookmark', {templateUrl:'/angularjs/bookmarkApp/app/scripts/bookmark/template/bookmark-new.tmpl.html',
    							controller:'bookmarkAddCtrl'
    						})
    						.otherwise({redirectTo: '/bookmarks'});
    				}])
    				.controller('bookmarkMainCtrl', ['$scope', function($scope){
    					$scope.bookmarkListViewType = 'grid';
    					$scope.searchInfo = {
    						bookmarkName : ''
    					};
    
    					$scope.toggleBookmarkListViewType = function(){
    						if($scope.bookmarkListViewType === 'grid'){
    							$scope.bookmarkListViewType = 'list';
    						}else{
    							$scope.bookmarkListViewType = 'grid';
    						}
    					};
    
    					$scope.search = function(searchInfo){
    						//bookmarkListCtrl 컨트롤러 함수에서 관리하고 있는 북마크 목록 정보에 접근하고자
    						//사용자정의 이벤트를 만들어 직접 참조가 아닌 이벤트 방식으로 데이터를 교환
    						//$broadcast는 'search:newSearchInfo' 이벤트를 모든 하위 $scope에게 발생시킨다.
    						$scope.$broadcast('search:newSearchInfo', searchInfo);
    					};
    				}]);
    		

    $resource 서비스를 이용해 리소스 생성자를 생성하고 리소스 생성자를 반환하는 서비스를 정의했다면 라우트 설정에 resolve 속성을 추가하여 비동기 제어에 관한 문제를 해결할 수 있다.

    				...
    				.when('/bookmarks', {templateUrl:'/angularjs/bookmarkApp/app/scripts/bookmark/template/bookmark-list.tmpl.html', 
    					controller:'bookmarkListCtrl',
    					resolve : { //비동기 제어에 관한 문제를 해결하고자 resolve 속성 추가
    						bookmarks : function(Bookmark){
    							return Bookmark.query().$promise;
    						}
    					}
    				})
    				...
    			

    Bookmark.query()를 호출해 $promise를 통해 promise 객체를 얻어 온다. 그리고 해당 약속을 반환하면 북마크 데이터를 가지고 오겠다는 약속이 다 지켜지고 나서 라우팅 처리를 하게 된다. 그리고 resolve 설정에서 정의한 bookmarks를 bookmarkListCtrl 컨트롤러에서 주입받을 수 있다. bookmarks는 Bookmark 서비스가 mongoLab으로부터 북마크 데이터 목록 요청에 대한 결과값이다. 컨트롤러에서 $scope에 속성을 추가하고 bookmarks를 대입해 템플릿에서 mongoLab으로부터 가져온 모델을 사용할 수 있게 한다.

    공통 영역에서 관리하는 search 메서드는 bookmarkMainCtrl에 추가한다. 그러나 데이터 목록은 bookmarkListCtrl 처럼 다른 컨트롤러 함수에서 관리하고 있어 이 목록 정보를 bookmarkMainCtrl에서 직접 접근하기는 어려워 보인다. 이때 사용자 정의 이벤트를 만들어 두 컨트롤러 사이의 직접 참조가 아닌 이벤트 방식으로 데이터를 교환하도록 구현할 수 있다.

    				.controller('bookmarkMainCtrl', ['$scope', function($scope){
    					...
    
    					$scope.searchInfo = {
    						bookmarkName : ''
    					};
    
    					...
    
    					$scope.search = function(searchInfo){
    						$scope.$broadcast('search:newSearchInfo', searchInfo);
    					};
    				}]);
    
    			

    $broadcast는 사용자정의 이벤트인 'search:newSearchInfo' 이벤트를 모든 하위 $scope에게 발생시킨다. 그리고 bookmarkListCtrl에서 'search:newSearchInfo' 이벤트가 발생했을 때 처리하는 부분을 추가할 수 있다.

    				.controller('bookmarkListCtrl', ['$scope','bookmarks','Bookmark','$location', function($scope, bookmarks, Bookmark, $location){
    					...
    
    					$scope.$on('search:newSearchInfo', function(e, searchInfo){
    						var searchQuery = '{"bookmarkName" : {"$regex": "^'+searchInfo.bookmarkName+'"}}';
    						$scope.bookmarkList = Bookmark.query({q: searchQuery});
    					});
    
    					...
    				}]);
    
    			

    'search:newSearchInfo' 이벤트가 발생하면 검색조건 문자열을 만든다. 해당 검색 조건 문자열은 MongoDB에서 검색에 필요한 문자열이다. 해당 문자열을 Bookmark 서비스의 query 메서드에 매개변수로 전달하여 호출하면 MongoLab에 북마크 이름으로 검색하는 RESTful 웹 서비스를 호출하여 해당 결과를 전달받을 수 있다.

    ▼ bookmarkListCtrl.js

    			angular.module('ngBookmark.bookmark.controller')
    				.controller('bookmarkListCtrl', ['$scope','bookmarks','Bookmark','$location', function($scope, bookmarks, Bookmark, $location){
    					//bookmarks는 라우트설정에 resolve 속성을 추가하여 promise객체를 반환하는 객체를 주입받음.
    					//bookmarks는 Bookmark 서비스가 MogoLab으로부터 북마크 데이터 목록 요청에 대한 결과값이다.
    					//bookmarkList 속성을 추가하고 bookmarks를 대입해 템플릿에서 bookmarkList 모델을 사용할 수 있게 한다.
    					$scope.bookmarkList = bookmarks;
    
    					//사용자정의 이벤트 'search:newSearchInfo' 이벤트가 발생했을 때 처리하는 로직
    					$scope.$on('search:newSearchInfo', function(e, searchInfo){
    						var searchQuery = '{"bookmarkName" : {"$regex": "^'+searchInfo.bookmarkName+'"}}';
    						$scope.bookmarkList = Bookmark.query({q: searchQuery});
    					});
    
    					$scope.newBookmark = function(){
    						$location.url("/new-bookmark");
    					};
    				}]);
    		

    ▼ bookmarkSvc.js

    			angular.module('ngBookmark.bookmark.service')
    				.value('mongolabApiKey', 'fwVdG10BuKHU3oHmrf7ROV7GjpOgpz_D')
    				.factory('Bookmark', ['$resource', 'mongolabApiKey', function($resource, mongolabApiKey){
    					//$resource 서비스를 이용해 북마크 리소스 생성자를 생성
    					var bookmarkResource = $resource('https://api.mongolab.com/api/1/databases/sample/collections/bookmarks/:bookmarkId?apiKey=:apiKey', {
    								apiKey : mongolabApiKey
    							},{
    								'update' : {
    									method : 'PUT'
    								}
    							});
    
    					return bookmarkResource;
    				}]);
    		

    $resource 서비스를 이용해 북마크 리소스 생성자를 생성하고 북마크 리소스 생성자를 반환하는 Bookmark 서비스를 정의했다.

    이 Bookmark 서비스를 바로 bookmarkListCtrl에 주입받아 query 메서드를 이용해 북마크 목록을 가지고 올 수도 있지만 북마크 목록 템플릿을 <ng-view> 영역에 삽입하고 나서 북마크 데이터가 로드되어 화면이 깜빡임과 같은 현상이 발생하는데 이를 방지하기 위해 북마크 목록 화면의 라우트 설정에 resolve 속성을 추가한다.

    ▼ bookmarkDetailCtrl.js

    			angular.module('ngBookmark.bookmark.controller')
    				.controller('bookmarkDetailCtrl', ['$scope','bookmark','$route','Bookmark','$location', function($scope, bookmark, $route, Bookmark, $location){
    					//템플릿에서 사용할 bookmark 모델을 선언하고 주입받은 bookmark 객체를 대입한다.
    					$scope.bookmark = bookmark;
    
    					$scope.edit = function(){
    						$scope.isEditing = true;
    					};
    
    					$scope.cancel = function(){
    						$route.reload();	
    					};
    
    					$scope.save = function(bookmark){
    						var updatePromise = Bookmark.update({
    							bookmarkId : bookmark["_id"].$oid
    						}, angular.extend({}, bookmark, {'_id':undefined})).$promise;
    
    						updatePromise.then(function(){
    							$route.reload();
    						});
    					};
    
    					$scope.delete = function(bookmark){
    						var deletePromise = Bookmark.delete({
    							bookmarkId : bookmark["_id"].$oid
    						}).$promise;
    
    						deletePromise.then(function(){
    							//$location 서비스의 url 메서드를 이용해 북마크가 정상적으로 삭제됐으면 '/bookmarks' 라우트 경로로 이동
    							$location.url("/bookmarks");
    						});
    					};
    				}]);
    		

    ▼ bookmarkAddCtrl.js

    			angular.module("ngBookmark.bookmark.controller")
    				.controller('bookmarkAddCtrl', ['$scope', 'Bookmark', '$location', function($scope, Bookmark, $location){
    					$scope.save = function(bookmark){
    						//새로운 bookmark 리소스 인스턴스를 생성
    						var newBookmark = new Bookmark();
    
    						//전달받은 bookmark 객체속성을 angular.extend 메서드로 상속받는다
    						angular.extend(newBookmark, bookmark);
    
    						//bookmark리소스 객체의 $save 메서드롤 호출해 RESTful API를 통하여 데이터를 서버에 저장
    						newBookmark.$save(function(d){
    							$location.url('/bookmarks');
    						});
    					};
    				}]);