Hybrid App

하이브리드 앱 개발 on Visual Studio #4

WinJS 앱을 살펴보도록 하자.

index.html

index.html에는 WinJS 컨트롤을 사용하고 있다.

image

data-win-control과 data-win-bind는 WinJS에서 컨트롤을 사용할 때 사용하는 확장 속성들이다. data-win-control은 컨트롤로 동작하게 할 때 사용하며, data-win-bind는 바인딩 기능을 이용해서 특정 변수 값과 요소의 속성을 연결시켜 준다.

처음에 div 태그에 WinJS.Binding.Template이 data-win-control에 지정되었는데, 이 부분이 추후에 템플릿으로 사용됨을 알 수 있다. 또한, input 태그에는 data-win-bind에 value: text가 적용되어서, 해당 컨텍스트(바인딩에서 사용하는 데이터 오브젝트)의 text 값을 value 속성에 바인딩 시키고 있다.

image

이 부분은 실제 앱의 UI 부분이다. 할일 내용을 넣는 부분에는 onchange 이벤트에 Xplat.datasource.addToDo(event.target) 함수가 핸들러로 지정되어 있다.

또한, main 섹션의 div 요소는 WinJS.UI.ListView로 선언되었다. data-win-options 속성을 통해 ListView 생성에 필요한 설정들을 전달한다. 여기에는 itemDataSource(리스트 데이터), itemTemplate(앞서 정의한 각 항목을 담는 템플릿 코드), layout(GridLayout과 ListLayout 중에 선택) 등이 포함되어야 한다.

index.ts

그럼 계속 index.ts 파일을 살펴보자. TypeScript를 사용하고 있기 때문에 JS에서 몇 가지 확장 키워드들이 사용되고 있다.

image

module은 코드를 모듈화 하는데 사용되는 키워드이며, 재사용성을 높여주고 네임스페이스를 만든다. export 명령을 통해 해당 네임스페이스에서 사용 가능한 클래스와 변수 등을 정의할 수 있다.

여기서는 Xplat.app 변수를 XplatApplication 형으로 선언하고, Xplat.XplatApplication 클래스를 정의하고 있다. TypeScript는 JS와 달리 클래스 정의를 위해 class 키워드와 멤버 관련 문법을 제공한다.

image

계속해서 Xplat.ApplicationEvents를 정의하고, Codova로 받는 이벤트를 처리할 핸들러 들을 포함하고 있다. 앞서 만든 app에는 XplatApplication의 인스턴스의 참조를 저장한다.

그리고, app.initialize()를 실행하고 initialize()의 this.bindEvents()를 통해 Codova의 deviceready에 onDeviceReady와 여러 가지 앱 이벤트들을 등록하여 초기화한다. dataSource.refresh()하여 데이터를 새로 가져오고, WinJS.UI.processAll()을 실행하여 현재 페이지의 WinJS 컨트롤들의 바인딩이 처리되도록 한다.

todo.ts

dataSource는 todo.ts 파일에 정의가 되어 있다.

image

index.ts와는 같은 Xplat 네임스페이스를 공유하고 있다. dataSource는 ToDo 클래스 인스턴스 참조를 저장하기 위한 변수로 선언되어 있으며, 아래 부분에 ToDo 클래스를 정의하고 있다.

ToDo 클래스는 this._items에 WinJS.Binding.List 인스턴스를 생성하여 참조하고 있고, 앞서 index.ts에서 호출된 dataSource.refresh()도 여기 정의되어 있는 것을 확인할 수 있는데, Storage.getAllToDoItems()를 비동기 호출하고 결과가 왔을 때 this._items에 각 아이템을 this._createTodoBinding(items[i])로 처리하여 추가(push)하는 것을 볼 수 있다.

(XPlat.)Storage 모듈은 services.ts 파일에 정의되어 있는데, 앞선 AngularJS나 BackboneJS 샘플처럼 LocalStorage나 AzureStorage에 대한 구현과 Bing Maps API를 사용하는 부분의 코드가 들어있다.

비동기 호출 이후, 결과 데이터가 왔을 때 처리하는 _createToDoBinding() 코드를 잠깐 살펴보자.

image

WinJS.Binding.as()를 이용해서 바인딩할 수 있는 bindingItem 오브젝트를 만들고, changeText/toggleDone/remove 등의 변수에 람다식 문법(() =>{})을 이용하여 bindingItem 오브젝트과 함께 실행할 메서드들을 묶어서 이벤트 핸들러 함수를 만들고 이를 WinJS.UI.eventHandler()를 통해 html에서 사용할 수 있게 한다.

image

index.html의 템플릿 코드를 보면 위와 같이 onchange: changeText나 onclick: remove, onmousedown: toggleDone 핸들러가 지정되어 있는 것을 볼 수 있다. 각 bindingItem이 이 템플릿의 context가 되기 때문에 changeText는 bindingItem.changeText를 가르킨다.

services.ts

마지막으로 services.ts를 간단히 살펴보면 다음과 같은 클래스 구현을 볼 수 있다.

image 

TypeScript의 interface/implements 키워드를 이용하여 IStorageImplementation 인터페이스와 LocalStorage, WindowsAzureStorage의 클래스 구현을 만들고, storageImplementation 변수에 둘 중에 하나의 참조를 넣은 후, getAllTodoItems() 등의 함수에서 이를 사용하여 데이터에 대한 처리를 구현한다.

여기까지 간단히 WinJS와 TypeScript로 구현된 Codova 샘플을 살펴보았다.

AngularJS, BackboneJS, WinJS 등 3가지 하이브리드 앱 샘플을 살펴보면서 느낀 각각의 장단점을 간단히 정리하자면,

  • AngularJS – 바인딩과 html 확장이 편리하며 코드가 간결하다. 처음에 여러 가지 개념 및 스코프 동작 등의 개념을 이해하기 어렵고 JS 코드 가독성이 다소 떨어진다.
  • BackboneJS – 한 페이지에서 여러 화면을 가지는 애플리케이션 구현이 간단하다. HTML을 확장하지 않지만, 그로 인해서 JS에서 처리해야 하는 코드가 많아지고 MVC의 구분이 명확하지 않아서 코드가 지저분해 질 수 있다.
  • WinJS – 템플릿과 바인딩을 활용하여 코드를 간결하게 작성할 수 있고 앱 구현에 유용한 컨트롤이나 네비게이션 기능 등이 제공된다. 바인딩이나 UI 컨트롤이 동작하는 방식을 이해하기 어렵다.

TypeScript에 대해서는 모듈 기능이 따로 제공되지 않는 WinJS와 함께 사용하면 코드의 가독성을 상당히 높일 수 있어서 유용하고, AngularJS 등과도 함께 쓰면 좋겠지만 이미 모듈 기능을 제공하고 있으므로 함께 사용하는 방법을 좀 더 고민해 볼 필요가 있을 듯 하다.

하이브리드 앱 개발 on Visual Studio #3

AngularJS 샘플에 이어서 BackboneJS 샘플을 살펴보고자 한다. 우선 샘플을 다운로드하여, 빌드를 해봤다.

  • AngularJS 샘플과 마찬가지로 framework JS파일들을 추가해 주어야 하고,
  • Bing Map API 키를 geolocation.js 파일에 넣고,
  • Azure API 키 및 Mobile Service URL을 services/mobile services/todolist-xplat/service.js 파일에 넣는다.

image

제대로 빌드했다면, 위와 같이 AngularJS 샘플에서 작성했던 Azure 상의 ToDo 항목들이 보인다.

BackboneJS의 기초

이번에는 BackboneJS의 기초를 먼저 살펴보고 샘플을 분석해 보는 것이 좋을 것 같다. 일단 튜토리얼은 여기에서 볼 수 있다.

아래 그림은 초보자용 영상에 나오는 샘플 앱의 구조인데, index.html은 Backbone Router를 통해 /, /edit, /new 등의 페이지 처리를 구현하고, Backbone의 View와 Model을 통해 REST API로 서버와 통신을 하게 된다. 영상에서는 아래 설계를 처음부터 하나씩 구현하여 간단한 사용자 관리 클라이언트를 만드는 과정을 보여주는데 다소 긴 영상이긴 하지만 꽤 볼만하다.

image

image 

위 코드는 튜토리얼에서 작성하는 코드의 일부인데, 살펴보면 Backbone. 으로 시작하는, Collection, View, Router 등의 클래스를 상속하여 사용자 정의 클래스들을 만들고 이를 통해 Router와 View, Model을 구현한다.

Router를 이용하여 페이지 네비게이션을 구현하는 부분이 편리해 보인다. router를 이용해 특정 페이지 요청이 있으면 해당 페이지에 연결된 View의 render() 메서드를 실행하여 화면을 그린다.

Backbone.View를 상속한 UserList의 경우는 Underscore의 _.template 함수로 Users Collection에서 가져온 users.models을 HTML 템플릿을 이용해 화면에 그려내고 있다.

BackboneJS 샘플 살펴보기

image

그럼 다시 샘플로 돌어가서 index.html 파일을 살펴보자. BackboneJS 샘플은 ToDo 목록의 templating을 위해서 underscore.js를 사용하고 있다.

    <!-- Refer to underscore.js for more information on its templating language -->
    <script type="text/html" id="todo-template">
        <div class="templateWrapper">
            <div class="templateContainer">
                <input class="templateTitle" type="text" value="<%- title %>" />
                <h3 class="templateAddress"><%- location %></h3>
                <button class="templateLeft templateToggle"></button>
                <button class="templateLeft templateRemove"></button>
            </div>
            <div class="templateBorder"></div>
        </div>
    </script>

underscore는 이름처럼 _.template(템플릿 문자열) 이런 식으로 사용한다. template 함수 관련해서는 여기를 참고하자. <%- title %> <%- location %> 등이 변수에서 값으로 치환되어 HTML로 생성된다.

index.html에서 template외에 나머지 부분에서 별다를 것은 없다.underscore는 HTML의 내장 오브젝트를 확장하지 않으면서 사용할 수 있는 여러 가지 함수형 프로그래밍 헬퍼들을 제공한다. 그런 면에서 ng-로 시작하는 속성이나 directive로 HTML를 적극 확장하는 AngularJS와는 다른 접근인 셈이다.

코드 살펴보기

index.js 파일을 살펴 보면 다음과 같다.

var app = app || {};

$(function () {
    "use strict";

    document.addEventListener('deviceready', onDeviceReady.bind(this), false);

    function onDeviceReady() {
        // Handle the Cordova pause and resume events
        document.addEventListener('pause', onPause.bind(this), false);
        document.addEventListener('resume', onResume.bind(this), false);
        // Initialize storage and the app's base view.
        app.initializeStorage();
        new app.BaseView();
    };

    function onPause() {
        // TODO: This application has been suspended. Save application state here.
    };

    function onResume() {
        // TODO: This application has been reactivated. Restore application state here.
    };
});

AngularJS 샘플과 달리, Codova에서 제공하는 앱에 대한 이벤트(deviceready, pause, resume)를 사용하고 있다. (AngularJS는 대신 초기화를 위해 html이나 body에 ng-app 속성을 지정하였다.) app.initializeStorage()는 storage.js 파일에, app.BaseView는 baseView.js 파일에 정의되어 있다.

storage.js 파일은 localStorage와 azureStorage 두 가지를 구현하고 mobileServiceClient의 사용 가능 여부에 따라 적절한 storage 구현을 app.Storage에 전달하는 역할을 한다.

baseView.js 파일은 Backbone.View를 상속하여 초기 화면을 구현하는 부분이다.

    app.BaseView = Backbone.View.extend({
        // This is the DOM element for the base view
        el: '#todoapp',

        // Hook up event handler to DOM keypress event
        events: {
            'keypress #new-todo': 'createNewTodo',
        },

        initialize: function () {
            // Retrieve DOM elements
            this.$input = this.$('#new-todo');
            this.$todoList = this.$('#todo-list');

            // Hook up some event handlers to the data source
            this.listenTo(app.todoCollection, 'add', this.addTodo);
            this.listenTo(app.todoCollection, 'reset', this.createNewTodosFromCollection);
            this.listenTo(app.todoCollection, 'all', this.render);

            // Retrieve existing data from storage
            app.Storage.getData();
        },

        // Create a new view from a todo and append it to our list.
        addTodo: function (todo) {
            var view = new app.TodoView({ model: todo });
            this.$todoList.append(view.render().el);
        },

el 속성에 #todoapp을 지정하여, index.html 파일에 “todoapp”이라는 id를 가진 <section> 태그를 baseView의 요소로 사용한다. 마우스 엔터키 이벤트를 keypress 이벤트로 가져와서 createNewTodo를 실행하고, app.todoCollection 데이터 소스의 add, reset, all 이벤트에 각각 핸들러를 추가하여 데이터에 변경사항이 있을 때 이를 처리하는 부분이 구현되어 있다. addTodo에는 app.TodoView를 생성하여 #todo-list 요소에 추가하고 있다.

todoView.js를 보면 다음과 같이 정의되어 있다.

app.TodoView = Backbone.View.extend({
        // This is the tag we wrap around our template
        tagName: 'div',

        // HTML item template
        template: _.template($('#todo-template').html()),

        // Hook up event handlers to mousedown and change events
        events: {
            'mousedown .templateToggle': 'toggleCompleted',
            'mousedown .templateRemove': 'deleteTodo',
            'change .templateTitle': 'updateTodo',
        },

        // Hook up event handlers to our model
        initialize: function () {
            this.listenTo(this.model, 'change', this.render);
            this.listenTo(this.model, 'todoDescriptionChanged', this.render);
            this.listenTo(this.model, 'destroy', this.remove);
        },

        // "Refresh" the todo in response to a change in the model
        render: function () {
            this.$el.html(this.getTemplateData());
            this.$input = this.$('.templateTitle');
            this.$toggle = this.$('.templateToggle');
            this.updateCrossOut();

            return this;
        },

        // We supply the model (as json) to instantiate our template
        getTemplateData: function () {
            return this.template(this.model.toJSON());
        },

template로 #todo-template를 사용하고 있고, 이건 앞서 index.html에서 확인하였다. render() 메서드를 baseView에서 호출하였으므로 살펴보면, getTemplateData()의 this.template(this.modeul.toJSON())을 통해서 todo 모델의 데이터가 적용된 HTML 템플릿을 가져와서 적용하고 리턴한다. $input, $toggle은 HTML 템플릿의 요소에 코드로 변경을 하기 위해 생성하는 참조이다. 리턴한 결과는 baseView에서 받아서 append한다.

todoCollection.js는 단순하다.

var app = app || {};

(function () {
    'use strict';

    var TodoCollection = Backbone.Collection.extend({
        model: app.TodoModel,
    });

    app.todoCollection = new TodoCollection;
})();

Backbone.Collection을 상속하고, model로 app.TodoModel을 지정한다.

app.TodoMobel은 todoModel.js 파일에 정의되어 있다.

    app.TodoModel = Backbone.Model.extend({

        defaults: {
            title: '',
            done: false,
            // This placeholder text is displayed while the app is querying the device's
            // location and supplying it to the restful service to obtain a street address.
            location: 'Getting your location...'
        },

        toggleCompleted: function () {
            this.save({
                done: !this.get('done')
            });
        },

        // We don't want to sync. We have a local/zumo storage implementation
        sync: function () { return false; }
    });

defaults에 title, done, location 등의 속성이 있고 toggleCompleted() 경우는 todoView에서 Todo 항목의 토글 버튼을 클릭했을 때 호출한다.

Todo를 추가하고 수정하는 등의 처리는 app.Storage를 통해서 하는데, 이러한 호출은 baseView(추가)와 todoView(수정, 삭제)에 있다. 아래 코드는 baseView의 createNewTodo 메서드이다.

createNewTodo: function (e) {
            if (e.which === ENTER_KEY && this.$input.val().trim()) {
                var todo = app.todoCollection.create({
                    title: this.$input.val().trim(),
                    done: false
                });

                // This callback will take data from the RESTful service, retrieve the street address,
                // and set it in the todo, then save it.
                var onSuccess = function (data) {
                    todo.save({
                        location: data.resourceSets[0].resources[0].address.formattedAddress
                    })

                    app.Storage.saveTodo(todo);
                };

                var onError = function (error, position) {
                    todo.save({
                        location: position.coords.latitude + "," + position.coords.longitude
                    })

                    app.Storage.saveTodo(todo);
                };

                // Retrieve the location data
                app.Geolocation.getLocation(onSuccess, onError).then(app.Geolocation.getAddressFromLocation);

                // Clear the input
                this.$input.val('');
            }
        }

살펴보면 app.todoCollection.create()를 이용해서 새로운 Todo 데이터를 만들고, 이를 app.Storage.saveTodo(todo)를 호출하여 서버 또는 로컬에 저장한다.

정리

여기까지 대략적인 BackboneJS 샘플의 구조와 코드를 살펴보았다.

  • Codova의 deviceready 이벤트를 받아서 app.Storage와 app.baseView를 초기화한다.
  • app.baseView는 Backbone.View를 상속받아서 구현하며, index.html의 <section id=”todoapp”>을 View로 사용한다.
  • app.baseView는 다시 app.todoView를 이용하여 각 todo 항목에 대한 UI 및 수정/삭제 등의 기능을 처리한다. 이 때 TodoView는 index.html의 todo-list-template을 가져와서 underscore.js의 template() 함수로 각각의 Todo 항목의 HTML을 생성하고 app.baseView에 리턴하여 append한다.
  • TodoModel과 TodoCollection은 각각 Todo 항목과 리스트를 가지고 있는데, BaseView와 TodoView는 사용자가 항목을 생성/수정/삭제할 때 TodoCollection에 반영하고, 이를 app.storage에 적용한다.
  • TodoCollection에 변경사항이 있을 때 이벤트는 다시 BaseView와 TodoView로 전달되어 화면을 업데이트한다.

BackboneJS는 처음 앱 구조를 봤을 때에 생각한 것에 비하여 이해하는데 어렵지 않았다. AngularJS처럼 이해해야 할 개념이 많지 않고 View/Collection을 꼭 다른 파일로 해야 하는 것이 아니라서 생각보다 쉽게 접근할 수 있었다. 다만, View가 UI와 데이터를 처리하는 부분까지 모두 포함하고 있는 부분이나 View가 여럿 있을 때 코드가 복잡해 질 수 있는 부분은 신경이 많이 쓰일 것 같다.

다음은 마지막으로 WinJS 샘플을 살펴보도록 하겠다.

하이브리드 앱 개발 on Visual Studio #2

이번에는 하이브리드 앱 샘플을 살펴보고자 한다. 샘플은 아래와 같이 3종류로 제공이 된다.

Angular나 Backbone은 자주 거론되는 프레임워크라 이번 기회에 간단히 살펴보았다.

AngularJS의 경우는 HTML의 선언적 코드 기능을 확장해서 (WinJS와 유사하게) ng-로 시작하는 속성들을 사용하여 JS 코드를 줄일 수 있고, 템플릿/리피터, MVC, 바인딩 등을 지원한다. 자세한 내용은 튜토리얼 참고.

HTML is great for declaring static documents, but it falters when we try to use it for declaring dynamic views in web-applications. AngularJS lets you extend HTML vocabulary for your application. The resulting environment is extraordinarily expressive, readable, and quick to develop.

BackboneJS는 MVC로 구조화된 웹앱을 만드는데 적합한 프레임워크이다. 이벤트, 모델, 콜렉션, 바인딩 등을 지원한다.

Backbone.js gives structure to web applications by providing models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing API over a RESTful JSON interface.

두 가지가 비슷한 기능을 하지만 AngularJS 쪽은 편의성과 간결함에, BackboneJS는 웹앱을 구조화하는데 초점을 맞춘 느낌이다.

WinJS는 원래 HTML/CSS/JS 코드로 Windows 앱을 만들 때 필요한 유틸리티나 컨트롤, 바인딩 등의 기능을 사용할 수 있도록 제공되는 라이브러리인데, 최근 오픈소스로 Android, iOS, XBOX 등에서도 사용할 수 있게 하겠다는 발표를 했다. AngularJS처럼 HTML 확장 속성들을 제공하여 컨트롤을 사용할 수 있고 바인딩을 사용할 수 있다.

앱 구조 비교

그러면 이 3가지 프레임워크를 이용해서 만든 하이브리드 앱 샘플들을 살펴보자. 비교를 위해서 각 솔루션을 열고 파일 구조를 살펴보았다.

image 

images, merges, res 폴더에는 차이가 없고, css의 경우는 WinJS에서만 기본 제공되는 2가지 테마 파일이 추가되어 있다.

scripts 폴더를 보면 frameworks에 각 프레임워크 별로 필요한 파일들이 들어 있다. BackboneJS의 경우는 jQuery를 포함하고 있는데, IE에서 DOM Manpulation 이슈 때문에 jQuery나 Zepto, JBone 등을 써야 한다고 한다.(참고)

AngularJS 샘플은 controllers.js, directives.js, service.js(guidGenerator, localStroage,  azureStorage, storage, maps, codova 관련 코드)등으로 구성되어 있다.

BackboneJS 샘플은 명시적으로 모델과 뷰 파일(-Model.js, –View.js) 및 storage.js(Local Storage와 Azure Mobile Service 사용 코드), baseView.js, geolocation.js, services 폴더 및 js 파일(Azure 모바일 서비스 초기화) 등으로 구성된다.

WinJS 샘플은 특이하게 TypeScript를 사용하고 있는데, 기본 TypeScript 하이브리드 앱 프로젝트에 포함되는 typings 폴더가 있고, mobileservices.d.ts, winjs.d.ts(이상 형 선언 파일), services.ts(Storage, Azure Mobile Service, Maps, guid 등 코드), todo.ts(ToDo 리스트 구현) 등이 있다.

파일 구조만 보면 Angular.js가 가장 깔끔해 보이지만, WinJS 프로젝트에서 TypeScript의 선언 파일들을 제외하면 index.ts + services.ts + todo.ts로 좀 더 간결하다. BackboneJS의 경우는 MVC가 명시적으로 나눠져 있고 구조화된 느낌이라 다소 복잡해 보인다.

빌드하기

프로젝트를 빌드/실행하려면 몇 가지 작업을 해야 한다. 샘플에 포함된 Readme.txt을 읽어보면 다음과 같다.

  1. Bing Maps API key 생성 및 코드 수정
  2. Microsoft Azure API key 생성 및 코드 수정, 아래와 같은 schema로 DB에 테이블 생성(선택사항, 없을 경우 LocalStorage에 저장)
    • (id:string, __createdAt:date, __updatedAt:date, __version:timestamp, text:string, done:boolean,

      address:string) 이 테이블은 각 Mobile Service의 Get Started 화면에서 원 크릭으로 생성할 수 있다.

  3. 실행하면 Powershell script가 실행되면서 솔루션에서 빠져있는 의존 라이브러리 파일들을 다운로드 받음(위 솔루션 탐색기 화면에서 Missing 표시 있는 것들. 자동 다운로드는 잘 안 됐는데 직접 다운로드해서 넣었다.)

먼저, Bing Maps API key는 여기서 만들 수 있고, Azure는 여기서 가입 및 Mobile Service 생성을 할 수 있다. Mobile Service 생성은 튜토리얼 참고.

image

Microsoft Azure Mobile Service에서 TodoItem 테이블 생성 화면

AngularJS 앱

AngularJS 앱부터 천천히 살펴보도록 한다. AngularJS에 필요한 framework 폴더의 JS파일들은 여기서 다운로드 할 수 있다. 앞서 API Key와 Mobile Service URL을 services.js 파일에 넣고 JS파일을 추가한 다음 실행해 보았다. 다음은 Ripple 에뮬레이터에서 실행 화면이다.

image 

Azure Mobile 서비스와 연동하여 잘 실행되는 것을 확인할 수 있다. Azure 포털에서 TodoItem DB 데이터를 확인해 보면 아래와 같이 앱에서 실행한 내용이 적용되어 있는 것을 확인할 수 있다.image

AngularJS 앱의 services.js 파일에 Azure 사용 부분은 다음과 같이 되어 있다.

(function () {
    'use strict';

    angular.module("xPlat.services")
...
        .factory("azureStorage", ["$q", "$resource", "$rootScope", "guidGenerator", function ($q, $resource, $rootScope, guidGenerator) {
            var azureMobileServicesInstallationId = guidGenerator();
...
	    var azureStorage = {
                getAll: function () {
                    return toDoItem.query();
                },

                create: function (text, address) {
                    var item = new toDoItem({
                        text: text,
                        address: address,
                        done: false
                    });

                    return item.$save();
                },

                update: function (item) {
                    return item.$update();
                },

                del: function (item) {
                    return item.$delete();
                },
            };

            Object.defineProperty(azureStorage, "isAvailable", {
                enumerable: false,
                get : function(){ return azureMobileServicesKey && azureMobileServicesAddress; },
            });

            return azureStorage;
        }])
...

AngularJS는 .module()로 기능들을 모듈화하는 것을 권장하고 있다. 모듈은 몇 가지 메서드를 갖는데 factory()는 Factory 패턴을 사용하여 지정한 service 클래스(여기서는 azureStorage)의 인스턴스를 생성해주는 역할을 한다.(module과 factory에 대한 설명은 여기, angular.module 레퍼런스는 여기 참고) 여기서는 getAll, create, update, del 메서드를 가진 azureStorage 인스턴스를 생성해서 리턴한다. localStorage도 동일하게 factory()로 구현이 되어 있고, 아래와 같이 storage 팩토리에서 Azure와 localStorage 중에 선택하여 리턴하는데 사용된다. 

.factory("storage", ["$injector", function($injector) {
            var azureService = $injector.get('azureStorage');
            return azureService.isAvailable ? azureService : $injector.get('localStorage');
        }])

원래 factory() 메소드는 factory(name, providerFunction) 이렇게 구성되는데, providerFunction을 [“$injector”, function($injector) {…}] 와 같이 전달하면 해당 scope의 변수들을 함수 레퍼런스와 함께 인수로 전달한다. azureStorage 또는 localStorage의 인스턴스를 가져오기 위해 $injector.get(name)을 사용하고 있다. 이렇게 리턴한 인스턴스는 controller.js 파일에서 사용된다. controller.js 파일은 index.html 파일을 살펴보면서 다시 보자.

AngularJS는 코드는 간결하지만 모듈이나 팩토리 같은 개념들을 이해하지 않고 접근하기에는 다소 어려운 듯 하다. 기본 개념들은 여기를 참고하면 좋을 듯.

코드 살펴보기

AngularJS는 HTML을 확장해서 사용하므로 HTML 코드도 같이 살펴볼 필요가 있어 보인다. 다음은 index.html의 body 태그 부분이다.

<body ng-app="xPlat">
    <section id="todoapp" ng-controller="ToDoCtrl">
        <header id="header">
            <div id="headerBand"></div>
            <input id="new-todo" placeholder="What needs to be done?" ng-text-change="addToDo()" ng-model="newToDoText" autofocus>
        </header>
        <section id="main">
            <div id="todo-list">
                <div class="templateWrapper" ng-repeat="toDoItem in todos">
                    <div class="templateContainer">
                        <input class="templateTitle" ng-class="{crossedOut: toDoItem.done}" type="text" ng-text-change="changeToDoText(toDoItem)" ng-model="toDoItem.text" />
                        <h3 class="templateAddress">{{toDoItem.address}}</h3>
                        <button class="templateLeft templateToggle" ng-class="{'checked': toDoItem.done, 'unchecked': !toDoItem.done}" ng-mousedown="toggleToDoDone(toDoItem)"></button>
                        <button class="templateLeft templateRemove" ng-click="removeToDo(toDoItem)"></button>
                    </div>
                    <div class="templateBorder"></div>
                </div>
            </div>
        </section>
    </section>

    <script src="scripts/index.js"></script>
    <script src="scripts/services.js"></script>
    <script src="scripts/controllers.js"></script>
    <script src="scripts/directives.js"></script>
</body>

body의 ng-app 값이 xPlat으로 설정되어 있다. ng-app은 html이나 body 태그에서 AngularJS 앱이라는 것을 나타내는 지시어이고, xPlat는 index.js에서 다음과 같이 생성하고 있다.

(function() {
    'use strict';
    angular.module('xPlat', ['xPlat.services', 'xPlat.controllers', 'xPlat.directives']);
    angular.module('xPlat.directives', []);
    angular.module('xPlat.controllers', []);
    angular.module('xPlat.services', ['ngResource']);
})();

‘xPlat’ 뒤의 […] 부분은 dependency를 나타낸다. directives, controllers, services 등의 하위 모듈들을 불러온다. index.html에서 <section> 요소에 속성으로 지정한 ng-controller=”ToDoCtrl”은 Todo 리스트로 동작하게 하기 위해 ToDoCtrl이라는 컨트롤러를 지정하고 있다. 이는 controller.js 파일에 xPlat.controllers 모듈로써 구현되어 있다. 앞서 살펴보았던 storage가 여기에서 사용된다. ToDoCtrl의 providerFunction의 인자로 전달하는 maps와 storage는 services.js 파일에 xPlat.services의 각 모듈로 구현되어 있다.

    angular.module("xPlat.controllers")
        .controller('ToDoCtrl', ['$scope', 'maps', 'storage', function ($scope, maps, storage) {
            var refresh = function() {
                $scope.todos = storage.getAll();
            }

            var getAddress = function() {
                return maps.getCurrentPosition()
                    .then(maps.getAddressFromPosition, function(error) { return error.message; });
            }

            $scope.addToDo = function () {
                var text = $scope.newToDoText;
                if (!text) {
                    return;
                };

                $scope.newToDoText = '';
                getAddress().then(
                    function (address) { return storage.create(text, address); },
                    function (errorMessage) { return storage.create(text, errorMessage); })
                .then(function (todo) {
                    $scope.todos.push(todo);
                });
            }
...

index.html의 <input> 태그에 보면 ng-text-change=”addTodo()”와 ng-model=”newTodoText”가 있는데, 각각 위 코드 상의 ToDoCtrol 컨트롤러에 정의 되어 있는 $scope.addTodo()와 $scope.newTodoText를 가르킨다. ($scope에 대한 내용은 여기 참고). ng-model은 <input> 태그와 addToDo()에서 호출하는 $scope.netToDoText와 바인딩을 시키는데 사용하고, ng-text-change는 개발자가 지정한 directive로 directives.js 파일에 아래와 같이 설정되어 있다.

angular.module('xPlat.directives')
        .directive('ngTextChange', function() {
            return {
                restrict: 'A',
                replace: 'ngModel',
                link: function(scope, element, attr) {
                    element.on('change', function() {
                        scope.$apply(function() {
                            scope.$eval(attr.ngTextChange);
                        });
                    });
                }
            };
        });

“ng-text-change”라는 속성(A)이 있을 시에 할당된 함수를 해당 요소의 ‘change’ 이벤트에 핸들러로 지정하도록 되어 있다. AngularJS의 자세한 directive 용법은 여기를 참고하자.

굳이 change를 쓰지 않고 ng-text-change를 정의한 이유는 다음에 나오는 리스트에서 각 항목의 제목을 수정했을 때에도 똑같이 사용하기 때문인 것으로 보인다. index.html에 다음과 같은 부분이 있다.

<div id="todo-list">
                <div class="templateWrapper" ng-repeat="toDoItem in todos">
                    <div class="templateContainer">
                        <input class="templateTitle" ng-class="{crossedOut: toDoItem.done}" type="text" ng-text-change="changeToDoText(toDoItem)" ng-model="toDoItem.text" />
                        <h3 class="templateAddress">{{toDoItem.address}}</h3>
                        <button class="templateLeft templateToggle" ng-class="{'checked': toDoItem.done, 'unchecked': !toDoItem.done}" ng-mousedown="toggleToDoDone(toDoItem)"></button>
                        <button class="templateLeft templateRemove" ng-click="removeToDo(toDoItem)"></button>
                    </div>
                    <div class="templateBorder"></div>
                </div>
            </div>

todos에 있는 내용을 리스트 형태로 보여주는 template이다. 첫 번째 <input> 요소에 ng-text-change=”changeToDoText(toDoItem)” 이 있는 것을 확인할 수 있다. todos는 controller.js 파일에 refresh() 함수에서 storage.getAll()을 통해 불러오고 있다.

정리

정리를 해보면,

  • index.html에서 ng-app으로 AngularJS앱으로 실행하고 index.js에서 각 모듈을 초기화한다.
  • directives.js에서 index.html의 <input> 태그에 ng-text-change 속성을 설정하고, 값이 변경될 때 controller.js의 addToDo()를 실행한다.
  • addToDo()는 services.js의 storage 팩토리를 통해서 localStorage 또는 azureStorage를 가져오고, storage.create()로 해당 ToDo 아이템을 저장한다.
  • 현재 저장된 ToDo 리스트는 controller.js에서 refresh()->storage.getAll()로 가져오며, AngularJS의 template으로 뿌려준다.

 

여기까지 AngularJS 샘플을 간단히 살펴보았다. BackboneJS나 WinJS 등도 함께 살펴보고자 했는데, 내용이 워낙 많아서 하나의 포스트에서 모두 다루기는 어려워 보인다. 다음 글에서 Backbone.js와 WinJS를 살펴보도록 하겠다.