Angular.js를 사용하는 ionic 기반의 웹앱을 구성하는 프로젝트를 다루기 위해서는 프레임워크를 어느정도 이해하여야 코딩에 손을 댈 수 있다. 자바스트립트와 html 문법은 잘 안다는 가정하에 이 프레임워크를 이해하여야 한다. 아직까지 Angular.js가 정말로 유용한 프레임워크인지는 의문이 간다. Angular.js version 2가 나오면서 더욱 더 복잡해졌기 때문이다. 하지만 일단 version 1 에 대한 프로젝트를 커버하기 위해 기본적인 사항을 알아본다.
Angular.js는 구글이 만든 MV*(Model - View - Whatever) 자바스크립트 프레임워크로 모델과 뷰가 분리되어 동적인 웹 화면을 구성하기에 적합한 구조를 가지고 있다.
AngularJS의 기본구조를 나타낸 그림으로 AngularJS가 어떻게 로딩되고 시작되는지를 나타내주는 그림으로 순서는 다음과 같다.
<body ng-app>
<span>Insert your name:</span>
<input type="text" ng-model="user.name" />
<h3>Echo: {{user.name}}</h3>
</body>
See the Pen NdYzMX by youngdeok (@medusakiller) on CodePen.
동작방식은 ng-model
디렉티브로 input
이 양방향 바인딩(two-way binding) 되어 데이터를 표현한다. 그러면 user.name
는 어디에 저장되는지가 궁금할텐데 user.name
은 $scope
에 저장되어 있다. input
에 입력할 때마다 스코프의 user.name
객체가 갱신되고 Angular.js의 {{ ... }} 인터폴레이션으로 모델을 출력할 수 있다. 그래서 HTML에서 user.name
의 값을 볼 수 있는 것이다. input
에 입력할 때 user.name
이 스코프에 저장되고 인터폴레이션으로 HTML에서 그 값을 보게 된다.
템플릿에서 $scope
에 user.name
을 설정하면 컨트롤러에서도 user.name
에 접근할 수 있다.
var app = angular.module('app', []);
app.controller('MainCtrl', function($scope) {
$scope.message = 'World';
});
<body ng-app="app" ng-controller="MainCtrl">
Hello, {{ message }}
</body>
See the Pen angular.js ex02 by youngdeok (@medusakiller) on CodePen.
app
에 컨트롤러를 생성하여 코드내에서 message
에 대한 내용을 받는다.
body태그에 포함된 ng-app="app" ng-controller="MainCtrl"
는 directive 라고 한다. ng-app은 body 요소가 Anuglar 어플리케이션에 포함되어 있다고 알려준다. 즉, body 요소내의 모든 것을 Angular 어플리케이션이 관리하도록 한다. ng-controller는 요소의 스코프를 MainCtrl이라는 컨트롤러에 할당하겠다는 것이다.
// app.js
var app = angular.module('app', []);
app.controller('MainCtrl', function($scope) {
$scope.greet = function() {
$scope.message = "Hello, " + $scope.user.name;
}
});
<body ng-app="app" ng-controller="MainCtrl">
What's your name?:
<input type="text" ng-model="user.name" />
<button ng-click="greet()">Click here!</button>
<h3>{{ message }}</h3>
</body>
See the Pen Angular.js ex03 by youngdeok (@medusakiller) on CodePen.
컨트롤러를 보면 $scope
에 함수를 연결하는 방법을 알 수 있다. $scope
에 연결한 함수에서 인풋에 입력된 값인 $scope.user.name
의 문자열을 이어붙혀서 $scope
에 message
로 추가한다.
그리고 HTML에서 버튼을 생성하고 ng-click
디렉티브를 사용했다. 간단히 얘기하자면 ng-click
디렉티브는 해당 요소를 클릭할 수 있게 만들어서 클릴할 때마다 할당한 함수를 실행하는데 이 예제에서는 greet()
를 실행한다.
<body>
<div id="chart"></div>
</body>
이 코드에서 id가 어떤 역할을 하는지 살펴보려면 구현된 자바스크립트 파일중에 하나를 봐야 한다.
// charts.js
$('#chart').pieChart({ ... });
이 부분을 찾아내면 위의 HTML이 파이차트를 담는 곳이라는 것을 알 수 있다. 여기서 문제점은 페이지에 사용한 모든 자바스크립트 파일을 보지 않으면 페이지가 무엇을 하는지 정확히 알 수 없다는 것이다.
이제 Angular 어플리케이션의 코드를 보자.
<body>
<pie-chart width="400" height="400" data="data"></pie-chart>
</body>
파이차트를 추가한다는 간다한 사실뿐만 아니라 크기가 어느정도 이고 어떤 데이터가 할당되었는지를 알 수 있다.
이미 ng-app
, ng-controller
, ng-click
, ng-model
과 같은 ng 접두사를 사용하는 다수의 내장 디렉티브가 존재한다.
<button ng-click="show = !show">Show</button>
<div ng-show="show">
I am only visible when show is true.
</div>
show
는 표현식이 true
일때만(예제에서는 바인딩한 값) 해당 요소를 보여준다. 이 예제에서 ng-click
을 사용한 방법을 주의깊게 보길 바란다. 이 예제에서는 컨트롤러에 함수를 생성할 필요가 없으므로(컨트롤러 자체도 필요없다!) 디렉티브의 인자를 표현식으로 작성해서 show
의 값을 토글했다. show
는 undefined
로 시작되고 첫 클릭시 true
가 된다. ng-show
의 반대인 ng-hide
도 있다.
배열목록을 출력해 보자.
var app = angular.module('app', []);
app.controller('MainCtrl', function($scope) {
$scope.developers = [
{
name: "Youngdeok", country: "Korea"
},
{
name: "Dave", country: "Canada"
},
{
name: "Wesley", country: "USA"
},
{
name: "Krzysztof", country: "Poland"
}
];
});
<body ng-app="app" ng-controller="MainCtrl">
<ul>
<li ng-repeat="person in developers">
{{person.name}} from {{person.country}}
</li>
</ul>
</body>
See the Pen angularjs ex04 by youngdeok (@medusakiller) on CodePen.
HTML에서 ng-repeat
디렉티브를 사용했다. ng-repeat
는 컬렉션의 아이템마다 새로운 템플릿을 생성할 것이다. 예제에서는 4개의 아이템이 있으므로 ng-repeat
가 이 코드 부분을 4번 생성할 것이다.
각 반복된 <li>는 자신만의 스코프를 가지며 person은 developers의 요소에 접근한다.
이처럼 각 디렉티브는 역할이 있으며 자신만의 디렉티브도 만들어낼 수 있다.
<body ng-app="app" ng-controller="MainCtrl">
What's your name?:
<input type="text" ng-model="user.name" />
<button ng-click="greet()">Click here!</button>
<h3>{{ message }}</h3>
</body>
이 코드에서 페이지가 로딩 되었을 때 input에 포커스를 주고 싶다면 다음과 같은 디렉티브를 만들어낼 수 있다.
// focus.js
app.directive('focus', function() {
return {
link: function(scope, element, attrs) {
element[0].focus();
}
};
});
디렉티브는 객체를 반환해야 하고 이 반환 객체에 몇가지 속성을 정의할 수 있지만 이 예제에서는 속성을 사용하지 않았다. 디렉티브는 link
함수를 반환할 수 있는데 이 link
함수 안에 템플릿 로직의 대부분을 작성하고 여기서 DOM 리스너를 등록하거나 DOM을 갱신하는 등의 작업을 할 수 있다.
이 link
함수는 3개의 파라미터를 받는다.(실제로는 4개지만 이는 약간 고급에 해당한다.) 3개의 파라미터는 scope
와 디렉티브를 사용한 element
와 element
의 속성인 attr
이다. 여기서 HTML 요소에 click
이벤트나 mouseenter
이벤트를 바인드할 수 있다.
이 예제에서는 첫번째 요소(인풋)을 가져와서 focus
함수를 호출했다. element
가 어떻게 동작하는지 궁금하면 공식문서 Element API를 참고해라.
이제 남은 작업을 이 디렉티브를 사용하는 것 뿐이다. 포커스를 주고자 하는 요소에 이 디렉티브 이름을 추가하면 된다.
<body ng-app="app" ng-controller="MainCtrl">
What's your name?:
<input type="text" focus ng-model="user.name" />
<button ng-click="greet()">Click here!</button>
<h3>{{ message }}</h3>
</body>
그러면 HTML을 렌더링하는 디렉티브는 어떻게 작성해야 할까? 다음 예제를 보자.
// hello.js
app.directive('hello', function() {
return {
restrict: "E",
replace: true,
template: "<div>Hello readers, thank you for coming</div>"
}
});
true
로 설정하면 해당 요소는 새로운 템플릿으로 대체될 것이다.따라서 hello 디렉티브는 다음과 같이 사용 가능하다.
<hello></hello>
이 코드는 template의 코드인 <div>Hello readers, thank you for coming</div>
같이 변경된다. html을 별도의 파일로 관리하려면 templateUrl에 파일을 지정할 수도 있다. 이때 로드할 html의 상대위치를 정의한다.
<body>
<div ng-controller="Ctrl">
<my-example></my-example>
</div>
</body>
// app.js
angular.module('app', [])
.controller('Ctrl', function($scope) {
$scope.person = {
name: 'Youngdeok',
address: 'Incheon'
};
})
.directive('myExample', function() {
return {
templateUrl: 'my-example.html'
};
});
<!-- my-example.html -->
<span> Name: {{person.name}} </br> Address: {{person.address}} </span>
디렉티브를 사용한 HTML의 태그에 template 또는 templateUrl에 포함된 태그 내용을 추가할지 교체할지 설정한다. true로 설정할 경우 HTML의 디렉티브를 사용한 태그를 template 또는 templateUrl에 작성된 내용으로 교체된다.
// app.js
angular.module('app', [])
.controller('Ctrl', function($scope) {
$scope.person = {
name: 'Youngdeok',
address: 'Incheon'
};
})
.directive('myExample', function() {
return {
restrict: 'E',
template: '<div>Hello AngularJS</div>',
replace: true
};
});
그 밖에 transclude
를 사용하면 원본 내용을 포함하는 형태로 교체할 수 있다. 원본은 template나 templateUrl에 <span ng-transclude>
를 사용한 곳에 들어가게 된다.
기본적인 표현식을 파이프를 사용해 필터를 적용할 수 있다. 예를 들어 ng-repeat
에서 개발자 목록을 출력을 이름으로 정렬 한다면,
<ul>
<li ng-repeat="person in developers | orderBy:'name'">
{{ person.name }} from {{ person.country }}
</li>
</ul>
와 같이 orderBy를 사용한다. 만일 -name을 사용하면 역순으로 정렬될 것이다.
이정도로도 유용하다고 생각할 것이지만 좀 더 괜찮은 예제를 보자. 4명의 개발자가 아니라 300명의 개발자가 있고 이를 (name, country등으로) 필터링하기를 원한다고 해보자. 이를 위해서 컬렉터를 필터링하는 방법을 정하고 필터링되지 않은 컬렉션을 필터링한 컬렉션으로 교체하려고 할텐데 더 간단한 방법이 있다.
<body ng-app="app" ng-controller="MainCtrl">
Search: <input ng-model="search" type="text" />
<ul>
<li ng-repeat="person in developers | filter:search">
{{ person.name }} from {{ person.country }}
</li>
</ul>
</body>
input에 할당된 search는 filter에 전달되어 필요한 정보에 따라 필터링 한다. 좀더 정밀하게 name을 골라내려면,
<body ng-app="app" ng-controller="MainCtrl">
Search: <input ng-model="search.name" type="text" />
<ul>
<li ng-repeat="person in developers | filter:search">
{{ person.name }} from {{ person.country }}
</li>
</ul>
</body>
search.name을 사용해 바인딩하여 이름을 전달 하는 것이다.
자신만의 필터를 직접 생성하는 방법을 알아본다. 첫글자를 대문자를 필터를 어떻게 작성하는지 보자.
app.filter('capitalize', function() {
return function(input, param) {
return input.substring(0,1).toUpperCase()+input.substring(1);
}
});
이 예제에서는 인풋의 첫글자를 대문자로 바꿨다. 이제 이 필터를 사용해 보자.
<span>{{ "this is some text" | capitalize }}</span>
서비스는 어플리케이션의 어떤 기능을 제공하는 싱글톤 클래스다. 어플리케이션 로직을 컨트롤러에 분산시키는 대신 다른 서비스에 로직을 둘 수 있다.
Angular에는 HTTP 요청을 관리하는 $http
나 Promise에 대한 $q
와 같은 많은 내장 서비스가 있다. 간단한 서비스를 생성해 이해해 보자.
서비스를 사용하는 가장 일반적인 경우는 컨트롤러간에 정보를 공유해야하는 경우이다. 모든 컨트롤러는 자신만의 스코프를 가지므로 다른 컨트롤러의 스코프를 바인딩할 수 없다. 이 문제를 해결하기 위해 서비스를 사용하면 한 곳에서 데이터를 가지고 있고 필요한 어디서나 이 데이터를 사용하도록 할 수 있다. 우선 문제의 상황을 보기 위해 서비스가 없는 다음 예제를 살펴보자.
<div ng-controller="MainCtrl">
MainCtrl:
<input type="text" ng-model="user.name">
</div>
<div ng-controller="SecondCtrl">
SecondCtrl:
<input type="text" ng-model="user.name">
</div>
// controllers.js
app.controller('MainCtrl', function($scope) {
});
app.controller('SecondCtrl', function($scope) {
});
위 처럼 두개의 분리된 컨트롤러에서 user.name을 다루려고 할때 안쪽에서 이름을 쓰면 다른쪽에서 똑같이 반영되도록 하고 싶을때 이코드는 제대로 동작하지 않는다. 왜냐면 스코프가 분리되어 있기 때문이다.
이 문제를 해결하기 위해 두 컨트롤러에서 사용할 수 있도록 사용자 이름을 가진 서비스를 생성할 것이다.
// user_information.js
app.factory('UserInformation', function() {
var user = {
name: "Angular.js"
};
return user;
});
서비스를 생성하기 위해 app
모듈의 factory
함수를 사용했다. 서비스를 생성하는 좀더 고급적인 방법도 있다.(service
와 provider
함수를 사용하지만 이는 다른 글에서 설명하겠다.) 서비스를 생성하는 여러가지 방법이 있지만 이 예제에서는 미리 정의해 놓은 이름으로 private 사용자 객체를 생성해서 반환했다. 이 서비스는 컨트롤러에서 다음과 같이 사용한다.
// controllers.js
app.controller('MainCtrl', function($scope, UserInformation) {
$scope.user = UserInformation;
});
app.controller('SecondCtrl', function($scope, UserInformation) {
$scope.user = UserInformation;
});
이 예제는 다음과 같이 된다.
이 예제는 의도대로 잘 동작한다. MainCtrl
와 SecondCtrl
양쪽의 $scope.user
는 UserInformation
를 사용하고 서비스가 싱글톤이기 때문에 한 컨트롤러에서 UserInformation
의 값을 바꾸면 다른 쪽에서도 바뀐다.
여기서 UserInformation
파라미터가 어디서 왔는지 궁금할 것이다. Angular는 서비스를 필요로 하는 곳에 서비스를 주입하는 의존성 주입을 사용한다. 의존성 주입이 동작하는 방식을 설명하는 것은 이 글의 주제를 벗어나지만 간단히 말하자면 서비스를 생성하면 어느 컨트롤러나 디렉티브, 다른 서비스에도 이 서비스를 주입할 수 있다. 주입하는 방법은 그냥 파라미터에 서비스의 이름을 전달하면 된다. 아마 이 의존성 주입이 $scope
파라미터를 사용한 것과 같은 것인지 궁금할텐데 $scope
는 다른 컨트롤러에 주입되기는 하지만 실제로 서비스는 아닌 예외사항 중 하나이다.
"어떤 것을 완전히 알려거든 그것을 다른 이에게 가르쳐라."
- Tryon Edwards -