하이브리드 앱 개발 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를 살펴보도록 하겠다.

댓글 남기기