IT_Programming/JavaScript

[펌] Knockout으로 양방향 데이터 바인딩

JJun ™ 2013. 7. 3. 04:01

 


출처: http://funnygangstar.tistory.com/m/post/view/id/149 


 

 

Knockout으로 양방향 데이터 바인딩 

 

다이내믹한 UI를 가진 웹 페이지를 만드는데 있어서 절실히 필요한 기능중에 하나는 서버로부터

전달 받은 데이터와 UI를 나타내는 HTML 간에 데이터가 양방향으로 유지(Synchronization)되도록 하는 것이다.

 

리스트-1 index.js에서 data 오브젝트의 message 값인 hello index.html에서 텍스트박스의 value 어트리뷰트에 자동으로 바인딩하고, 역으로 만일 사용자가 브라우저를 통해 텍스트박스의 value 값을 ‘world’로 수정하면 동시에 자바스크립트의 message 값이 자동으로 ‘world’

업데이트 되는 것을 양방향 데이터 바인딩이라 칭한다.

 

<리스트-1, 양방향 데이터 바인딩>

 

 <index.html>

 <input type=”text” value=”” />

 


 

 

 <index.js>

 var data = {

     message: ‘hello’

 }

 

 

물론 이러한 일련의 작업들은 순수 자바스크립트나 jQuery만을 사용해도 가능할 수 있지만

꽤 많은 자바스크립트 코드가 동원되어야 한다. 이러한 양방향 데이터 바인딩이 필요할 때마다

그러한 코드를 개발하는 것보다는 데이터 바인딩을 위한 전문적인 프레임워크를 사용하길 추천한다.

 

재 이러한 양방향 데이터 바인딩을 지원하는 프레임워크로는 대표적으로 Knockout, AngularJS, EmberJS 등이 있다. AngularJS EmberJS는 데이터 바인딩 외에도 라우팅, 유효성 검증,

RESTful 서비스와의 연동 등 다양한 기능을 자체적으로 지원하는 Full JavaScript Framework

성격이 강한 반면에 Knockout은 오직 데이터 바인딩 기능에만 집중한다.

 

 

그래서인지는 몰라도 Knockout이 데이터 바인딩에 있어서는 가장 직관적이면서도 풍부한 기능을 제공하고 있다고 보는데 개인마다 의견이 다를 수 있으니 나머지 프레임워크도 꼭 둘러보길 권한다. 본 샘플 소스에서는 Knockout을 사용한다.

 

리스트-2는 샘플 소스에서 블로그 포스트의 상세 내용을 보여주는 HTML과 자바스크립트를 일부

발췌했다. 간단히 설명하면, Knockout data-bind라는 커스텀 어트리뷰트를 사용해서 필요한

자바스크립트의 데이터를 바인딩 한다. detail.html 맨 첫 줄을 보면 data-bind=”with: post”라는

코드를 볼 수 있는데 이는 detail.js 파일의 post 프로퍼티를 바인딩 하겠다는 의미이다.

 

detail.js 파일을 보면 post 프로퍼티가 있고 getPost 함수 내부에서 Ajax 호출을 통해 서버로부터

전달받은 데이터를 post 프로퍼티에 바인딩한다. post 프로퍼티는 일반적인 자바스크립트 객체나 함수가 아닌 ko.observable() 이라는 함수를 할당했는데, 이는 Knockout에 정의되어있는 함수로써 HTML과 자바스크립트 사이에서 값의 변화를 감지해 양방향 데이터 바인딩을 가능하게 해준다.

 

detail.html 파일에서 세 번째 줄의 data-bind=”text: title” post title 프로퍼티를

h3 태그의 text 값으로 바인딩 하겠다는 의미이다. 그런데, 여기서 한가지 중요한 점이 있다.

 

 

<리스트-2, detail.html detail.js 일부 발췌>

 

 /* detail.html 중에서 일부 발췌 */

 <section id="section-post-detail" class="view" data-bind="with: post">

    <div class="page-header">

        <h3 data-bind="text: title"></h3>

    </div>

 

    <div class="content" data-bind="text: content"></div>

    <div class="time-created">

        <i class="icon-time"></i> Posted at <b data-bind="text: yyyymmdd"></b>

    </div>

    <!-- 이하 코드 생략 -->

 </section>

 

 /* detail.js 에서 일부 발췌 */

 define

 (

     ['jquery', 'knockout', 'knockout.mapping', 'data/data', 'models/models'],

     function ($, ko, mapping, data, models) 

      {

            var post = ko.observable(),

            mappingOption =  {

                create: function (options) {

                    return new models.Post(options.data);

                }

            },

 

            getPost = function (param)

              {

                if  (!param.id) {

                    return;

                      }

                              

                $.when(data.deferredRequest('postDetail', { id: param.id }))

                    .done(function (result) {

                        post(mapping.fromJS(result, mappingOption));

                        if  ($.isFunction(param))

                            param(post());

                    })

                    .fail (function (data, status) {

                        console.log('error: ' + status);

                    }

                );

 

            };

 

         return {

             post     : post,

             getPost  : getPost

         };

     }

 );

 

 

detail.js getPost 함수를 보면 Ajax 서비스를 호출해서 서버의 Post 모델 객체를 JSON 형태로 받아 post 프로퍼티에 바로 바인딩하는데 이 때 mappingOption을 통해서 자바스크립트단의 models/post.js 모델 타입을 사용한다. 그런데 리스트-3 Post 모델(models/post.js)을 들여다보면 title이나 content 같은 프로퍼티가 전혀 선언되어 있지 않다. title, content는 어디서 온 속성들일까?

 

<리스트-3, models/post.js 일부 발췌>

 

 define(['knockout', 'knockout.mapping', 'moment', './comment'],

    function (ko, mapping, moment, Comment) 

     {

            var Post = function (data)

              {

                     var self = this;

                     mapping.fromJS(data, {}, self);

                     self.yyyymmdd = ko.computed(function () {

                                  return moment(self.dateCreated()).format('YYYY-MM-DD');

                     });

           };

 

           return Post;

    }

 

 );

 

 

사실 title content는 서버측의 Post.cs 클래스에 이미 선언되어 있고 models/post.js 파일의 코드 중간에 있는 mapping.fromJS() 함수를 통해 런타임에 ko.observable() 타입의 title content

프로퍼티를 생성해 내는 것이다.

 

, 클라이언트(자바스크립트)에 모델을 꼭 선언하지 않더라도 Knockout은 데이터를 뷰(HTML)에 바인딩 할 수 있는 것인데, 이는 실제 개발시에 적지않은 개발 생산성을 가져다 준다.

왜냐하면, 서버에서 이미 정의된 모델을 클라이언트에서 일일이 다시 정의하지 않아도 되기 때문이다.

, 서버에서 넘어온 프로퍼티 외에 추가적인 프로퍼티를 바인딩 해야 할 필요가 있다면

그때는 models/post.js 처럼 모델을 생성해주어야 한다.

 

하지만 이때도 추가적으로 필요한 프로퍼티만 선언해주면 된다.

예를 들어, detail.html 코드 하단을 보면 data-bind=”text: yyyymmdd”를 사용하고 있고

이는 models/post.js에 추가적으로 선언되어 있는 프로퍼티이다.

 

yyyymmdd ko.observable()이 아닌 ko.computed() 함수가 할당되어 있는데,

이는 Knockout에서 제공하는 또 다른 함수로써 해당 객체 내의 다른 프로퍼티로부터 값을

참조해야 할 때 사용한다. 단순히 참조만 하는게 아니라 참조한 값이 변경되었을 때

자동으로 ko.computed() 가 할당된 변수도 업데이트 할 수 있다.

 

ko.computed() 함수는 잘만 사용하면 매우 유용하게 쓰일 수 있다.

샘플 사이트를 보면 상단 메뉴에 블로그 포스트를 검색하는 기능이 있다.

블로그 글의 제목을 기준으로 검색하는데 사용자가 검색어 입력란에서 검색어를 입력하자마자

바로 검색 결과를 페이지 왼편의 글 목록 영역에서 필터링하여 보여준다.

 

이 부분이 ko.computed() Knockout subscribe 기능을 사용해서 구현되었는데,

리스트-4를 보자.

 

<리스트-4, 검색 필터링 구현>

 

 -> vms/top.js

 define

 (

    ['knockout', './post/detail', 'nls/nls', 'amplify', 'infra/config'],

    function (ko, detail, resources, amplify, config)

    {

        var searchText    = ko.observable('') ;

        return

        {

            searchText    : searchText

        };

    }

 );

 

-> views/top.html

<div class="navbar-search">

    <input data-bind="value: searchText, valueUpdate: 'afterkeydown'" type="text" class="search-query" placeholder="Search">

</div>

 

-> vms/left.js

 define

 (

    [… 생략 …],

    function () 

    {

        var searchText = ko.observable(''),         

        posts = ko.observableArray([]),

        filteredPosts  = ko.computed(function () 

        {

                return _.filter(posts(), function (post) {

                    return post.title().toLocaleLowerCase().indexOf(searchText().toLocaleLowerCase()) > -1;

                });

        });

       // top.js searchText 프로퍼티 값의 변화를 전달받는다.

        if (top)

        {

            top.searchText.subscribe(function (newValue) {

                searchText(newValue);

            });

        };

 

        return

        {

            posts : posts,

            filteredPosts: filteredPosts

        };

 

    });

 

 -> views/left.html

 <div>

    <ul id="nav-items" class="nav nav-list" data-bind="foreach: filteredPosts">

        <li>

            <a data-bind="attr: {href: url}">

                <label data-bind="text: shortTitle"></label>

                <div class="date-created" >

                    <i class="icon-time"></i><label data-bind="text: yyyymmdd"></label>

                </div>

            </a>

        </li>

    </ul>

 </div>

 

 

top.html의 검색 필드에 바인딩 되어 있는 top.js searchText 프로퍼티가 observable 타입이므로

left.js에서 ko.subscribe()를 통해서 값의 변화를 전달 받는다.

 

left.js는 변화된 값을 left.js에 선언된 searchText 프로퍼티에 저장하고 computed 타입으로

선언된 filteredPosts도 역시 observable 타입이므로 left.html의 블로그 글 목록이 사용자의

검색 입력어에 따라 최종 필터링된다.

 

left.js에 코드가 생략되었지만 실제로 서버로부터 글목록을 전달받아 할당받는 프로퍼티는 filteredPosts가 아니고 posts 이다. 하지만 computed 기능과 양방향 데이터 바인딩 기능 덕분에

원래 전달받은 posts 는 그대로 유지하면서 필터링된 글 목록만 filteredPosts에 저장하여

사용자에게 보여줄수 있게 되었다.

 

사실 이러한 간단해 보이는 기능도 양방향 데이터 기능이나 subscribe 기능을 제공하는

라이브러리를 사용하지 않으면 꽤 복잡한, 많은 양의 코드가 동원되어야 할 것이다.

 

Knockout은 또한 Knockout의 기본 바인딩 기능들 (예를 들면, click, event, if, visible 등등) 외에도 개발자가 필요한 바인딩을 만들수 있게끔 해주는데 이를 "커스텀 바인딩"이라고 부른다.