안녕하세요,

'생각의 웹'입니다.


4주 차에 걸쳐 진행된 직접 IoT 만들기(Do IoT Yourself!) App In 세미나가 이번 주 금요일(2014년 10월 24일)을 마지막으로 대장정의 마무리를 내립니다.


짧지 않은 시간동안 평일 저녁과 주말을 포함해 여러 회에 걸쳐 진행된 강연이였기에 참석조차 쉽지 않으셨을 텐데 빠지지 않고 참석해 주신 분들께 감사드립니다.

강사 또한 참석자 분들의 배움의 열정에 감탄을 금치 못했으며 가르침을 통해 배움 못지 않은 많은 깨달음을 얻었습니다. 



이번 마지막 강연에서는 지난 3주 차 강연에서 만들어 본 RESTful Web API를 손쉽게 사용할 수 있도록 JavaScript API로 만들어 보고 이 API를 이용해서 현재 온도를 알려주는 온도계와 지금까지의 온도 변화를 보여주는 경향(trend) 그래프(graph)를 google chart API를 이용해 만들어 봅니다.



지난 번 강의 시, 제가 시간 관리를 제대로 하지 못해 다음 예정된 강의에 지장을 초래할 정도로 지연되었는데 

부디 이번 강의에서는 제 시간에 끝났으면 합니다.

(그런데 초안 슬라이드를 작성해 보니 2시간 내에 제대로 다루기에는 벅찬 느낌이 들기도 하네요. ^^;; )


마지막으로 다시 한번 매 강연마다 호평을 아끼지 않으시고 적극적으로 참여해주신 참석자 분들께 다시 한번 감사의 말씀 전합니다.

또한 강연 자리를 마련해 주시고 값 비싼 아두이노 호환 보드 세트를 원가에 못 미치는 가격에 참석자 분들께 제공해 주신 AppCenter 분들께도 감사드립니다.


제 소망은 초 연결 미래 사회를 향한 이 작은 발걸음들을 통해 새로운 통찰을 얻고 비즈니스에 적용하여 미래를 만들어 가는 분들이 등장하는 것입니다.

사물 인터넷의 시대, 여러분들의 아이디어를 직접 만들어 평가하고 공개하고 개선해서 멋진 서비스로 발전시켜 가시길 바랍니다!


P.S. 이 모든 자료는 크리에이티브 커먼스(Creative Commons) 3.0에 의거하여 출처를 밝히는 조건 하에서 자유롭게 사용하실 수 있습니다. 

P.S. 2. github에 공개한 DIoTY 코드에 대한 설치 가이드자주 나온 질문들을 링크와 같이 정리했습니다. 참고하시기 바랍니다.

 



안녕하세요, 생각의 웹입니다.


이번 포스팅은 요즘 잘 나가는 backbone.js를 이용해 객체 지향 Web App을 만든 사례를 소개하려 합니다.

backbone.js는 MVC (Model-View-Control) 모델을 기반으로 JavaScript 코드의 유지 보수성과 재사용성을 향상시킨 오픈소스 라이브러리입니다.

1. Prerequisites

backbone.js를 사용하기 위해서는 해당 라이브러리 코드를 사이트에서 다운로드 받아야 합니다. 또한, backbone.js은 underscore.js에 강한 종속성(dependency)가 있으니 함께 다운로드 받아야 합니다.

저는 코드를 다운로드 받아 js/libs 폴더에 두고 다음과 같이 index.html에 삽입했습니다. (이후 사용을 위해 가장 보편적인 DOM 관련 라이브러러인 jQuery 또한 참조하는 점도 주의깊게 봐 주시기 바랍니다. )

<script src="js/libs/underscore.js"></script> <script src="js/libs/backbone.js"></script> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>


2. 모델 생성하기

모델을 생성하기 위해 Backbone.Model.extend() 함수를 사용합니다.

이번 웹앱으로 HTML5 기능을 이용한 간단한 생일 카드를 만들기 위해 다음과 같은 코드를 작성했습니다.

    var Card = Backbone.Model.extend({
    defaults: {
        img: 'Happy-Birthday-Greetings.png',
        title: 'Happy birthday to you!',
        music: 'Kevin_Lax_-_Happy_Birthday.mp3'
    },
    initialize: function () {        
        console.log("Card object initialized.");
    },
    getImageUrl: function () {
        return 'img/' + this.get('img');
    },
    getMusicUrl: function () {
        return 'res/' + this.get('music');
    }
});

카드에 포함될 기본 데이터로 이미지, 음악, 제목 속성을 지정하고 이미지는 img 폴더에 음악은 res 폴더에 두도록 설계했습니다.

initialize 함수 (정확히는 함수형 속성)는 해당 모델이 생성되는 시점에 호출되며 여기에서 생성 시 필요한 초기화를 할 수 있습니다.


이제 이 모델을 통해 인스턴스를 하나 생성해 보도록 합시다.

    var birthdayCard = new Card({    
    to: 'My beloved Joyan',
    from: 'Your father',    
    message:'May happiness be with you all!'
});

이 카드 모델에 추가로 필요한 속성들(수신인, 발신인, 내용)을 추가해서 인스턴스를 만들었습니다.

이상 없이 코딩했다면 console 창에 "Card object initialized."가 찍히겠죠.

3. 뷰 생성하기

앞서 소개드린 바로는 아직 별다른 이득(benefits)이 하나 없습니다. 이제 이걸로 뷰(view) 객체를 만들어 어떤 이득을 얻을 수 있는지 확인해 보겠습니다.

<div id="myCard"></div>


    var CardView = Backbone.View.extend({
    el: '#myCard',
    audio: null,
    canvas: null,

    initialize: function () {

        this.render();
        /* 
        * retrieving any HTML element by JQuery returns array object.
        * Select first one of the retrieved array.  
        */
        this.canvas = $('#myCanvas')[0];
        this.audio = $('#myAudio')[0];

        console.log('Card view intialized.');
    },
    render: function () {
        // append canvas, audio tag
        this.$el.append('<canvas id="myCanvas" width="730" height="414"></canvas>');
        this.$el.append('<audio id="myAudio"></audio>');
        return this;
    } 
});

모델을 생성할때와 유사하게 Backbone.View.extend() 함수를 사용해서 뷰를 생성합니다.

멤버 속성 중 하나인 el 에 index.html에 추가한 div 태그를 참조하기 위한 DOM selector를 저장합니다. 그리고, 앞서 설명드린 바와 같이 초기화 과정 중 호출되는 initialize() 함수를 살펴보면 index.html에 선언한 myCard라는 아이디의 div 태그를 참조하도록 되어 있으며 생성 시, HTML5 canvas, audio를 myCard div 내에 삽입하고 이를 멤버 속성인 audio와 canvas에 저장하도록 되어 있습니다.

이제 이 뷰의 인스턴스를 생성해 봅시다.

    var birthdayCardView = new CardView({
    model: birthdayCard
 });

CardView의 생성자에 model이라는 속성으로 앞서 생성한 birthdayCard를 지정합니다.

정상적으로 코드가 수행하면 console 창에 'Card view intialized.'가 출력됩니다.


3. 뷰에서 모델 정보 참조하기

이제 뷰와 모델이 연결되었습니다. 이제는 모델에 저장된 정보를 가지고 뷰를 꾸며보도록 하겠습니다.


    var CardView = Backbone.View.extend({
    el: '#myCard',
    canvas: null,
    audio: null,

    initialize: function () {
        this.render();
        /* 
        * retrieving any HTML element by JQuery returns array object.
        * Select first one of the retrieved array.  
        */
        this.canvas = $('#myCanvas')[0];
        this.audio = $('#myAudio')[0];

        this.showImage(); /* add image to canvas */

        console.log('Card view intialized.');
    },
    render: function () {
        /* omitted to short */
    },
 
    showImage: function () {
        var view = this;
        var context = view.canvas.getContext('2d');
        var imageObj = new Image();
        // clear canvas
        context.clearRect(0, 0, view.canvas.width, view.canvas.height);
        imageObj.onload = function () {
            context.drawImage(imageObj, 5, 5, 730, 414);
        };
        imageObj.src = view.model.getImageUrl();
        console.log('An image is shown.');
        this.state = 'image';
    }  
});

이로써 초기화 과정 중 birthdayCard 인스턴스에 저장된 image 파일을 가져다 canvas에다 뿌렸습니다.

showImage() 함수의 imageObj.src = view.model.getImageUrl(); 코드와 같이 view.model로 모델의 객체에 접근할 수 있습니다.

4. 뷰에서 이벤트 처리하기

이번에는 마우스를 이미지에 올릴 경우, 모델 객체에 저장된 음악을 출력하고 이미지에서 마우스를 내릴 경우 멈추도록 이벤트 핸들링을 해 봅시다.

    var CardView = Backbone.View.extend({
    el: '#myCard',
    canvas: null,
    audio: null,

    events: {
        'mouseover': 'playMusic',
        'mouseout': 'pauseMusic'
    },
    initialize: function () {
        this.render();
        /* 
        * retrieving any HTML element by JQuery returns array object.
        * Select first one of the retrieved array.  
        */
        this.canvas = $('#myCanvas')[0];
        this.audio = $('#myAudio')[0];
        this.setMusic(); /* 음악 파일 경로를 설정 */
        this.showImage();
        console.log('Card view intialized.');
    },
    render: function () {
        // append canvas, audio tag
        this.$el.append('<canvas id="myCanvas" width="730" height="414"></canvas>');
        this.$el.append('<audio id="myAudio"></audio>');
        return this;
    },
    setMusic: function () {
        // Set a birthday song from model
        this.audio.src = this.model.getMusicUrl();
    },

    showImage: function () {
       /* omitted to short*/
    },
 
    playMusic: function () {
        this.audio.play();
        console.log('The birthday song is playing.');
    },
    pauseMusic: function () {
        this.audio.pause();
        console.log('The birthday song paused.');
    }
});

뷰 객체의 events 속성으로 'mouseover', 'mouseout' 이벤트에 대한 핸들러를 playMusic, pauseMusic 과 같이 지정함으로써, 사진에 마우스를 올렸을때 음악이 흘러나오도록 하는 이벤트를 처리할 수 있습니다.

추가로 마우스 클릭 시, 이미지 대신 하트를 그리고 다시 클릭 시 메세지를 출력해 봅시다.

    var CardView = Backbone.View.extend({
    el: '#myCard',
    canvas: null,
    audio: null,
    state: null, /* 클릭 시 상태 변이를 저장하는 속성 */
    events: {
        'click': 'onClick',
        'dblclick': 'showImage',
        'mouseover': 'playMusic',
        'mouseout': 'pauseMusic'
    },
    initialize: function () {
        this.render();
        /* 
        * retrieving any HTML element by JQuery returns array object.
        * Select first one of the retrieved array.  
        */
        this.canvas = $('#myCanvas')[0];
        this.audio = $('#myAudio')[0];
        this.setMusic();
        this.showImage();
        console.log('Card view intialized.');
    },
    render: function () {
        // append canvas, audio tag
        this.$el.append('<canvas id="myCanvas" width="730" height="414"></canvas>');
        this.$el.append('<audio id="myAudio"></audio>');
        return this;
    },
    setMusic: function () {
        // Set a birthday song from model
        this.audio.src = this.model.getMusicUrl();
    },
    onClick: function () {
        switch (this.state) {
            case 'image':
                this.showHeart();
                break;
            case 'heart':
                this.showMessage();
                break;
            case 'message':
                // ignore state change
                break;
        }
    },
    showImage: function () {
        var view = this;
        var context = view.canvas.getContext('2d');
        var imageObj = new Image();
        // clear canvas
        context.clearRect(0, 0, view.canvas.width, view.canvas.height);
        imageObj.onload = function () {
            context.drawImage(imageObj, 5, 5, 730, 414);
        };
        imageObj.src = view.model.getImageUrl();
        console.log('An image is shown.');
        this.state = 'image';
    },
    showHeart: function () {
        var view = this;
        var context = view.canvas.getContext('2d');
        // clear canvas
        context.clearRect(0, 0, view.canvas.width, view.canvas.height);
        context.strokeStyle = "#000000";
        context.strokeWeight = 3;
        context.shadowOffsetX = 4.0;
        context.shadowOffsetY = 4.0;
        context.lineWidth = 10.0;
        context.fillStyle = "#FF0000";
        var d = 150;
        var k = 100;
        context.moveTo(k, k + d / 4);
        context.quadraticCurveTo(k, k, k + d / 4, k);
        context.quadraticCurveTo(k + d / 2, k, k + d / 2, k + d / 4);
        context.quadraticCurveTo(k + d / 2, k, k + d * 3 / 4, k);
        context.quadraticCurveTo(k + d, k, k + d, k + d / 4);
        context.quadraticCurveTo(k + d, k + d / 2, k + d * 3 / 4, k + d * 3 / 4);
        context.lineTo(k + d / 2, k + d);
        context.lineTo(k + d / 4, k + d * 3 / 4);
        context.quadraticCurveTo(k, k + d / 2, k, k + d / 4);
        context.stroke();
        context.fill();
        console.log('A heart is shown.');
        this.state = 'heart';
    },
    showMessage: function () {
        var view = this;
        var context = this.canvas.getContext('2d');
        // Print 'To' message
        context.font = 'bold 20pt Segoe UI';
        context.fillText('To. ' + view.model.get('to'), 10, 50);
        // Print 'title' message
        context.font = 'italic 40pt Calibri';
        context.fillStyle = 'gray';
        context.fillText(view.model.get('title'), 10, 310);
        // Print 'message' message
        context.font = 'italic 20pt Calibri';
        context.fillText(this.model.get('message'), 10, 350);
        // Print 'from' message
        context.font = '10pt Segoe UI';
        context.fillStyle = 'black';
        context.fillText('Sincerely, \n' + view.model.get('from'), 10, 400);
        console.log('The birthday message is shown.');
        this.state = 'message';
    },
    playMusic: function () {
        this.audio.play();
        console.log('The birthday song is playing.');
    },
    pauseMusic: function () {
        this.audio.pause();
        console.log('The birthday song paused.');
    }
});

이로써 이번 샘플 앱을 모두 완성했습니다.

onClick 함수에서 click 이벤트가 발생할 때마다 state 속성을 image -> heart -> message로 변경시켜 이에 따라 알맞는 함수를 호출하고 있습니다. showHeart()의 경우, canvas에 벡터 방식으로 heart를 그리고 있고 showMessage() 함수에서는 모델에서 가져온 속성들 (to, title, message, from)을 출력하고 있습니다.


이상으로 간략하게 나마 backbone.js를 이용해 코딩하는 법을 소개했습니다.

서두에 말씀드린 것처럼 backbone.js를 사용하면 기존 코드보다 훨씬 가독성이 좋은 코드로 구현되고 역할 별로 책임(responsibility)이 분리되어 유지 보수성이 향상됩니다.

추가적으로 컬렉션, REST API 연동 등의 우수한 기능을 제공하기 때문에 이번 기회에 익혀 두시면 좋을 듯 합니다.


긴 글 끝까지 읽어 주셔서 감사합니다. 행복한 하루 되시길!

  1. 생각의 웹 WebofThink 2014.09.25 17:03 신고

    소스 코드를 아래 공유합니다: https://github.com/hyunghunny/HTML5BirthdayCard

+ Recent posts