PhoneGap

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

Advertisements

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

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

페이스북에서 아래와 같은 소식이 잠시 화제가 되었다.

image

한 장의 스크린샷과 함께, 안드로이드SDK/크롬SDK/Git/SQL를 지원할 예정이라는 내용이었다. 잘못 알려진 부분이 있어서 좀 더 자세히 다뤄보고자 한다.

일단 마이크로소프트는 “비주얼 스튜디오에서 안드로이드SDK, 크롬SDK, Git, SQL 지원 예정”이라는 발표를 한 적이 없다. 대신 최근에 Apache Cordova와 함께 하이브리드 앱 개발을 지원하겠다고 발표하였다. 현재 이 기능은 프리뷰 버전으로 제공되고 있다.

하이브리드앱은 앱 내에서 웹 컨텐츠를 처리하기 위해서 간단한 로컬 웹서버/웹뷰를 필요로 한다. 또한, 웹 컨텐츠에서 디바이스 기능을 호출할 수 있는 API와 각 플랫폼 별로 앱 패키징해주는 기능이 필요하다. 이러한 기능을 하는 것이 Apache Cordova이다. 원래는 PhoneGap이라는 이름으로 유명하지만, Adobe에서 이를 오픈소스화하여 코드 베이스를 Apache로 넘겨서 Apache Cordova가 되었다고 한다. 두 프로젝트는 현재 이름만 다른 패키지 배포판이다. (참고)

그렇다면, VS에서 하이브리드 앱을 지원하기 위한 기술 요소들을 한번 생각해보자. 먼저, Android나 iOS 개발도구가 필요하다. 각 앱을 빌드하고 에뮬레이터(Android의 경우)를 띄워서 앱을 실행할 수 있어야 하기 때문이다. Android 앱을 빌드하려면 Java도 필요하고, Webkit 기반의 웹뷰를 에뮬레이팅하기 위해 Chrome이 필요할 것으로 보인다. SQLite는 WebSQL를 대신해서 로컬 스토리지 용도로 쓰일 수 있다. 그리고, iOS 앱을 배포하기 위해서 iTunes가 필요할 것으로 보인다.

실제로, Multi-Device Hybrid Apps과 함께 제공되는 문서에 보면 이러한 내용이 명시되어 있다.

The installer will then ask permission to download certain dependencies. These are mostly open source software pre-requisites required by individual platforms or Apache Cordova to build and run your applications. Any dependencies that already exist on your system will not be re-installed (as long as the required version is present).

하이브리드 앱을 빌드하기 위해 필요한, 개별 플랫폼이나 Apache Cordova에 필요한 오픈소스 구성요소들을 설치한다고 되어있다. 좀 더 자세하고 정확한 설명은 다음과 같다.

Third Party Dependencies 

  • Joyent Node.js – Enables Visual Studio to integrate with the Apache Cordova Command Line Interface (CLI) and Apache Ripple™ Emulator
  • Git CLI – Required only if you need to manually add git URIs for plugins
  • Google Chrome – Required to run the Apache Ripple emulator for iOS and Android
  • Apache Ant 1.8.0+ – Required as a dependency for the Android build process
  • Oracle Java JDK 7 – Required as a dependency for the Android build process
  • Android SDK – Required as a dependency for the Android build process and Ripple
  • SQLLite for Windows Runtime – required to add SQL connectivity to Windows apps (for the WebSQL Polyfill plugin)
  • Apple iTunes – Required for deploying an app to an iOS device connected to your Windows PC

앞서 페이스북의 소식에 지원 예정이라고 되어 있었던, GitSQLite의 경우는 이전 Visual Studio 2012에서 부터 사용할 수 있었다.

이왕 살펴보는 겸, VS에서 하이브리드 앱을 어떻게 만드는지 좀 더 살펴보자.

image

VS 2013 Update 2부터는 TypeScript가 정식으로 지원되어, 위 화면과 같이 하이브리드 앱을 만들 때에도 JavaScript와 TypeScript 프로젝트 템플릿이 제공된다.

프로젝트 구성은 다음과 같다.(TypeScript의 경우)

image

여기서 살펴볼 만한 부분은 res, typeings, merges 정도이다.

res는 플랫폼 고유 리소스들이 들어 있는 폴더이다. 서명, 아이콘, 스플래시 스크린 등이 들어간다. 그리고, merges는 CSS 파일과 같은 플랫폼 고유의 코드를 추가하는데 사용한다.

기본 문서에는 JS로 되어 있으나, 호기심에 TypeScript(이하 TS)로 해보았다. JS 프로젝트와의 차이는, scripts 폴더에 index.js 대신 index.ts 파일이 있으며, 추가로 typings 폴더가 있다는 점이다. JS 프로젝트에는 typings 폴더가 없다. typings 폴더 밑에는 Cordova를 구성하는 각 클래스 별로 Type이 선언되어 있는 *.d.ts 파일이 있다.

image

다른 JavaScript 라이브러리의 TypeScript에서 사용하기 위해서 사용하는 파일들이다. Cordova의 API를 사용할 때 Type 체크나 IntelliSense 등에 사용된다. 자세한 내용은 이 글의 Ambient Declaration 부분이나 TypeScriptLang.org의 내용을 참고하자.

일단 비어있는 템플릿 앱을 실행해 보았다.

image

실행하는 옵션에 Android 에뮬레이터와 디바이스가 보인다. 그런데 실행하자 다음과 같은 에러가 나온다.

image

해당 에러를 검색해보면 스택오버플로우에 답변이 있다. 환경 변수와 PATH 설정이 잘 못 되어 있어서 그런 듯 하다. 가이드 대로 환경 변수와 PATH 설정을 변경하고 재시작하면, Ripple 에뮬레이터에서 실행하는데에는 더 이상 문제가 발생하지 않는다.

흥미로운 점은, Ripple이라는 하이브리드 앱의 에뮬레이터이다.

image

하이브리드 앱의 에뮬레이터가 Chrome 브라우저에서 실행되고 있다. 앞서 설치 과정에서 Chrome을 왜 설치하는지 알 수 있었다.

마지막으로 Device/Emulator에서 앱을 실행해보려고 했으나 위와 유사한 또 다른 에러가 발생했다. 내 경우에는 Android ADK 가 2곳에 설치가 되어 있었는데 예전에 설치했던 ADK의 버전이 낮아서 발생하는 문제인 것 같았다. Android SDK Manager에서 API 레벨을 19이상으로 업데이트해서 문제를 해결하고 다음과 같이 디바이스에서의 실행에 성공하였다.

_978

여기까지 Multi-Device Hybrid Apps 공식 가이드 문서를 참고로 하여 기본적인 하이브리드 앱 개발환경을 살펴보았다. 사실 개인적으로는 WinJS 라이브러리를 타 플랫폼에서 어떻게 사용할 수 있는지가 궁금한데, 이 부분은 다음 편에서 WinJS 앱 샘플을 통해 살펴보도록 하겠다.

PhoneGap으로 Windows 8 앱 개발하기

PhoneGap for Windows 8이 아주 오래 전(몇 개월 전)에 나왔다는 소식은 알고 있었지만, 실용성이 있을까 의문이 있어서 사용해 보지 않았다.

하지만 JavaScript(이하 JS)로 윈8앱을 개발을 하는 경우에 Windows API나 WinJS 라이브러리에 너무 의존해서 다른 플랫폼으로 포팅하는 것이 어려운 약점을 보완할 수 있을까 싶어서 한번 테스트해보기로 했다.

마침 딸래미를 위해서 간단한 앱을 만들어 주고 싶은 게 있어서 PhoneGap을 적용해 봤다.

1. PhoneGap for Windows 8 설치

일단, PhoneGap for Windows 8은 Cordova 웹서버를 설치해야 하는 일부 플랫폼과 다르게 cordova.js 파일 하나만 추가해주면 된다!(얼마나 쉬운지는 여기를 참고)

Windows 8 JS App의 프리젠테이션 레벨이 이미 최신 IE나 마찬가지이기 때문에 별도의 웹뷰나 웹서버 없이도 HTML5를 그대로 사용할 수 있다.

2. PhoneGap API 사용하기

그 다음에 할 일은 Windows RT나 WinJS의 API 대신 대응하는 PhoneGap API를 사용하는 것이다.

내가 잘 몰라서 그런지도 모르겠지만 PhoneGap API는 생각보다 복잡하지 않고 쉽게 되어 있다. Windows API도 굉장히 쉽다고 생각했는데 그보다 더 간결한 정도이다.

예를 들어, 윈8에서 메시지 팝업을 띄우려면 다음과 같이 한다.

new Windows.UI.Popups.MessageDialog("Hello World").showAsync();

똑같은 것을 PhoneGap API로 하면 다음과 같다.

navigator.notifications.alert("Hello World");

이렇게 동작하기 위해서 내부적으로 cordova.js 파일에서 navigator.notifications.alert() 함수를 Windows.UI.Popups.MessageDialog로 구현해 놓았다. 다른 플랫폼에서도 같은 함수가 똑같이 동작하도록 PhoneGap이 중간에서 처리해준다. 하지만, 여러 플랫폼에서 동작해야 하기 때문에 각 플랫폼에서 제공하는 고유의 컨트롤이나 API등은 많이 누락되어 있는 듯 한다. 일반적으로 플랫폼에서 제공되는 기능들이 PhoneGap API로 제공되고 전체 API 목록은 여기에서 볼 수 있다.

3. PhoneGap을 이용한 윈8앱을 스토어에 등록하기

PhoneGap API에서 Capture와 Notifications 정도를 이용하고, 나머지 필요한 기능은 HTML5 API의 video,  IndexedDB 등을 이용해서 간단한 윈8 앱을 만들어 봤다. 아무래도 PhoneGap for Windows 8은 나온지 얼마 되지 않았기 때문에 exception 처리나 Windows RT를 활용할 수 있는 정도에 제약이 있다. 따라서 cordova.js 파일을 직접 수정해서 필요한 부분을 개선 및 발전시켜서 쓰거나, Windows 에서만 실행하는 구문을 추가해서 처리해 주는 것이 필요했다.

PhoneGap을 이용해서 스토어에 등록 테스트를 한 결과 앱은 문제 없이 등록이 되었고, 현재 테스트를 위해서 캐나다 스토어에만 등록되어 있는 상태이다. 지역 설정을 캐나다로 변경해야 스토어에서 검색 및 다운로드가 가능하다.

앱 테스터 / 서울, 18개월

앱 테스터 / 여, 18개월

4. PhoneGap for WindowsPhone으로 윈폰8용으로 만들어 보기

기존 윈8 코드 그대로 윈폰8용으로 포팅을 시도해봤다. 윈8이 cordova.js만 추가하면 됐다면, 윈폰8은 웹뷰를 사용하기 때문에 전용 템플릿을 만들어서 사용해야 한다. 시작방법 참고

그리고 윈8과 윈폰8의 앱 컨텐츠 코드를 공유하기 위해서는 VS 프로젝트의 파일 링크 기능을 이용해서 W8과 WP8 프로젝트가 공통의 index.html/js/css 파일을 사용하도록 프로젝트를 구성한다. VS에서 파일을 선택하고 Alt키를 누른 상태에서 드래그하면 파일에 대한 링크를 만들 수 있다.

WP8 프로젝트에서 www 폴더 밑에 index.html, index.js 파일에 바로가기 표시가 있는 것을 확인할 수 있다.

WP8 프로젝트에서 www 폴더 밑에 index.html, index.js 파일아이콘에 바로가기 표시가 있는 것을 확인할 수 있다.

일단 CSS나 이미지 요소 등 수정 없이 그대로 윈폰8에서 실행해봤다.

css를 적용 안해서 html가 맨몸으로 나옴.

css를 적용 안해서 html가 맨몸으로 나옴.

버튼들을 눌러봤는데 제대로 동작을 안 했다. 아마 WinJS 등을 빼지 않아서 자바스크립트 오류가 발생한 것 같다. 이렇게 해놓고 모바일 용으로 앱을 포팅하면 어떨까 잠시 생각해봤는데 폰에서 구현하기에 적절하지 않은 부분이 있어서 이 정도로 마무리하고 더 이상 하는 것을 일단 잠정 보류하기로 했다. 나중에 윈8 앱이 잘 되고 야외에서 앱을 사용해야 하는 경우가 생기면 한번 좀 더 진행해 볼까 싶다. 윈폰 뿐 아니라 iOS나 안드로이드 용으로도…

결론적으로, PhoneGap으로 윈8과 윈폰8앱 모두 만들 수 있고 생각보다 실용적이라는 것을 확인했다.

앞으로 개발할 윈8 JS앱은 무조건 PhoneGap으로 할까 싶기도… 다만, 윈8의 다양한 컨트롤을 쓰려면 윈8 전용 코드가 들어가야 하고, 그렇다고 플랫폼 중립적인 UI 프레임워크를 쓰자니 적절한 것도 없다.