ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [AngularJS] 4. 템플릿 시스템과 데이터 바인딩 - 1 - AngularJS 강좌
    Web/AngularJS 2016. 1. 29. 15:29

    템플릿 시스템과 데이터 바인딩






    구조적이고 재사용하기 좋은 웹 어플리케이션을 개발하려면 화면과 데이터의 분리가 필수적입니다. 나아가 데이터와 화면 사이의 싱크도 필요합니다. 기존 JS 기반의 웹 어플리케이션 대부분이 데이터와 화면 사이가 끈끈하게 이어져 있고 데이터와 화면 사이의 싱크를 위해서 반복적인 코드를 사용했습니다.


    그렇게 하다보니 스파게티 코드(Spaghetti Code)라 하여 화면 처리 코드와 데이터 처리 코드가 서로 엉켜있어 유지보수하기 어렵고 재사용 불가능한 어플리케이션을 만들게 됩니다.


    하지만 AngularJS는 화면과 데이터의 분리를 용이하게 하는 템플릿 시스템데이터와 화면 사이를 싱크할 수 있게 하는 데이터 바인딩을 제공함으로써 앞선 문제점을 말끔하게 해결해 줍니다. 




    1. 템플릿의 이해

     브라우저에서 사용자가 보는 화면을 그리려면 HTML 문서를 작성해야 합니다. 작성한 HTML 문서를 웹 서버에 두면 사용자의 웹 브라우저가 URL을 받아 해당 웹 서버로 요청을 보내고, 웹 서버는 응답으로서 요청한 HTML 문서를 보내줍니다. 웹 어플리케이션도 마찬가지로 HTML 문서를 작성해야 합니다.


     하지만 한 번 작성하면 오랫동안 바뀌지 않는 웹 페이지와는 다르게 브라우저의 요청이 있을 때마다 주어진 데이터에 따라 내용이 바뀌어야 합니다. 그래서 지금까지 이러한 일을 웹 템플릿 시스템을 이용해서 해결했고, 주로 서버 측에서 하곤 했습니다.


     친숙한 서버 측 동적 페이지 예로 JSP(JavaServer Pages) 가 있습니다. 다음은 JSP의 템플릿 코드입니다. ${...}으로 JSP에서 자바의 데이터에 접근할 수 있습니다.



    1
    2
    3
    4
    5
    6
    <html>
        <head>...</head>
        <body>
            <h1>Hello ${name}</h1>
        </body>
    </html>
    cs




     최근에는 서버에서 데이터와 템플릿을 조합해 요청마다 전체 페이지를 브라우저로 보내주기보다는 JSON 포맷의 데이터나 HTML 페이지의 일부 조각만 서버가 보내주고 클라이언트에서 이를 동적으로 처리하는 AJAX 기술을 많이 사용해 왔습니다. AJAX를 이용하면 사용자의 입력에 즉각적으로 반응하여 화면을 동적으로 처리할 수 있기 때문에 UI/UX적으로 뛰어난 웹 어플리케이션을 개발할 수 있기 때문입니다.


     다음은 jQuery를 이용해서 구현한 간단한 예제입니다.




     * usingjquery.html


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <!DOCTYPE HTML>
    <html>
        <head>
            <meta charset="UTF-8"/>
            <title>jquery dom handling</title>
            <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>
            <script>
                $(function() {
                    var name="Yeonsu";
                    var favorite = ["Watermelon""Orange""Strawberry"];
                    
                    $("#greeting").text("hello " + name);
                    for (var i = favorite.length - 1; i >= 0; i--) {
                        $("#favorite").append("<li><a href='#" + favorite[i] + "'>" + favorite[i] + "</a></li>");
                    }
                });
            </script>
        </head>
        <body>
            <h1 id="greeting"></h1>
            <h2>좋아하는 과일</h2>
            <ul id="favorite"></ul>
        </body>
    </html>
    cs




     








     위 코드를 보면 name과 favorite이라는 변수가 있고, jQuery를 이용해 이러한 데이터 기반의 DOM을 생성하고 추가했습니다. 하지만 위와 같은 방식으로 어플리케이션을 개발하면서 유지보수와 확장성에 대한 문제점이 나오곤 했습니다. 위 코드를 보면 HTML의 구조를 자바스크립트가 잘 알고 있습니다. 가령 favorite이라는 id를 가진 <ul> 태그가 있어야 하며 <ul> 태그에 들어갈 <li> 태그도 자바스크립트에서 직접 정의하고 있습니다. 어플리케이션의 크기가 커질수록 자바스크립트에서 HTML 태그를 작성하는 코드가 늘고 DOM을 처리하는 복잡도가 증가할 것 입니다.


     이제 이 정도로 템플릿을 언급하고 본격적으로 AngularJS의 템플릿을 적용해보도록 하겠습니다. 




    2. AngularJS 템플릿

     AngularJS에서는 템플릿이 HTML 그 자체입니다. AngularJS는 DOM 자체를 템플릿으로 사용합니다. 이 DOM은 HTML, CSS 그리고 AngularJS가 제공하는 특정한 요소나 속성인 지시자가 포함됩니다. 쉽게 이해할 수 있게 앞에서 작성한 예제를 AngularJS로 작성해봤습니다.



     * angulartem.html


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!DOCTYPE HTML>
    <html ng-app>
        <head>
            <meta charset="UTF-8"/>
            <title>angularJS Template</title>
            <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.min.js"></script>
        </head>
        <body ng-init="person = {name: 'yeonsu', favorite : ['수박', '귤', '사과']}">
            <h1> hello {{person.name}}</h1>
            <h2> 좋아하는 과일</h2>
            <ul ng-repeat='fruit in person.favorite'>
                <li><a href="#{{fruit}}">{{fruit}}</a></li>
            </ul>
        </body>
    </html>
    cs







     위 코드를 보면 자바스크립트 코드를 단 한 줄도 작성하지 않음을 알 수 있습니다. 그리고 ng로 시작하는 속성과 {{}}을 제외하고는 모두 HTML 코드입니다. 


     AngularJS는 DOM 자체 템플릿이므로 일일이 자바스크립트로 render와 같은 함수를 호출할 필요도 없고 그 결과를 특정 DOM에 삽입할 필요도 없습니다.


     다음은 템플릿 작성시 사용되는 AngularJS의 기능입니다.



     - 지시자: 기본 HTML을 확장하거나 새로 추가한 요소나 속성. ng-repeat, ng-app 등이 지시자에 해당. 또한 사용자가 만든 재사용할 수 있는 위젯과 같은 지시자도 해당

     - 마크업: 기본 HTML 마크업과 이중 중괄호 - {{ 표현식 }}

     - 필터: 사용자에게 보여주는 데이터의 형식을 필터로 처리

     - 폼 컨트롤: 입력 상자의 유효성 검사를 위해 AngularJS가 폼 태그를 확장. 폼 태그를 사용하게 되면 자동으로 폼 컨트롤을 사용.





     2-1. 이중 중괄호와 AngularJS 표현식

      템플릿에서 이중 중괄호('{{}}')를 사용해 특정 위치에 표현할 데이터를 지정했습니다. 앞의 예제에서 다음과 같은 코드를 보았습니다.



    1
    <h1>hello {{person.name}}</h1>
    cs




      위 코드를 해석하면 "hello라는 문자열 옆에 person 객체의 name 속성을 구해서 h1의 크기로 화면에 출력"이라고 해석할 수 있습니다. 그렇습니다. 이중 중괄호 내에 AngularJS 표현식을 작성 할 수 있습니다. 


      AngularJS 표현식은 자바스크립트 표현식과 비슷합니다. 아래 목록은 AngularJS 표현식과 자바스크립트 표현식의 차이점입니다.

      

      - 객체의 접근: 기본적으로 자바스크립트는 모든 객체를 최상위 객체인 window 객체안에서 찾습니다. 이와 반대로 AngularJS는 표현식에 사용된 객체를 scope 안에서 찾습니다. 가령 {{ user }} 라고 작성했다면 user 객체에 접근하는데 window.user가 아닌 scope.user로 scope 객체에 접근합니다. 


      - undefined와 null 무시: 자바스크립트 표현식으로 objectA.propertyA라고 작성했다고 가정합니다. 만약 objectA가  선언되지 않으면 objectA는 undefined일 것입니다. Undefined에서 propertyA에 접근하려 했으니 자바스크립트에서는 오류를 발생합니다. 하지만 AngularJS에서는 오류를 발생하지 않고 무시합니다. 즉 템플릿 {{objectA.propertyA}}라고 작성하여 접근 시에 propertyA가 선언되어 있지 않더라도 아무런 오류를 발생하지 않습니다. 


      - 제어문 작성이 안 됨: if문과 같은 조건절이나 for문과 같은 반복문 그리고 throw문은 작성할 수 없습니다.


      - 필터: AngularJS의 필터를 표현식에서 사용할 수 있습니다. 필터는 파이프(|)를 이용해서 표현식에서 사용할 수 있습니다. 가령 money가 있고 2000이라는 값이 할당되었을 때, 이때 2000을 2,000원 혹은 $2,000 으로 표현하고 싶을 때 우리는 currency 필터를 사용할 수 있습니다. 템플릿에서 {{ money | currency }} 와 같은 방식으로 작성하면 됩니다. AngularJS에서는 몇 가지 필터를 미리 만들어서 제공하고 있습니다. 


     

      아래는 위의 몇 가지 표현식을 실행해보는 예제입니다.


     * angularexp.html


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <!DOCTYPE HTML>
    <html ng-app>
        <head>
            <meta charset="UTF-8"/>
            <title>angularJS template</title>
            <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.min.js"></script>
        </head>
        <body ng-init="person = {name: 'yeonsu', favorite: ['사과', '귤', '감']}">
            <h1>hello {{person.name}}</h1>
            <p> 좋아하는 과일의 갯수: {{numberOfFavorite = person.favorite.length}}</p>
            <p>과일 갯수 * $100 = {{numberOfFavorite * 100 | currency}}</p>
            <p>yeonsu가 맞습니까? {{person.name == 'yeonsu'}}</p>
            <p>좋아하는 과일 수가 4개보다 많습니까? {{numberOfFavorite > 4 }}</p>
            <p>scope에 없는 객체 접근 하면? {{car.type.name}}</p>
        </body>
    </html>
    cs











    3. 데이터 바인딩의 이해

     자바스크립트 웹 앱의 복잡도가 증가하면서 브라우저의 메모리에 있는 여러 개의 자바스크립트 객체와 화면에 있는 데이터를 일치시키기가 매우 어려워졌습니다. 이러한 어려운 작업을 쉽게 해주는 해결책이 있다면 그건 데이터 바인딩일 것입니다. 


     데이터 바인딩이란 두 데이터 혹은 정보의 소스를 모두 일치시키는 기법입니다. 즉 화면에 보이는 데이터와 브라우저 메모리에 있는 데이터를 일치시키는 기법입니다. 많은 자바스크립트 프레임워크가 이러한 데이터 바인딩 기술을 제공하고 있습니다. 하지만 대다수의 자바스크립트 프레임워크가 단방향 데이터 바인딩을 지원하는 반면 AngularJS는 양방향 데이터 바인딩을 제공합니다. 다음은 단방향 데이터 바인딩과 양방향 데이터 바인딩의 차이점을 설명한 그림입니다.










     단방향 데이터 바인딩은 데이터와 템플릿을 결합하여 화면을 생성합니다. 반면 양방향 데이터 바인딩은 데이터의 변화를 감지해 템플릿과 결합하여 화면을 갱신하고 화면에서의 입력에 따라 데이터를 갱신합니다. 즉, 데이터와 화면 사이의 데이터가 계속해서 일치되는 것입니다.


     다음은 단방향 데이터 바인딩 예제입니다.



     * mustache.html


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
     <!doctype html>
    <html>
        <head>
            <meta charset="UTF-8"/>
            <title>one way data binding</title>
            <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.1/mustache.js"></script>
            <script>
                $(function() {
                    // 
                    var menuList = [
                        {itemId: 1, itemName: '베이글', itemPrice: 3000, itemCount: 0},
                        {itemId: 2, itemName: '소이 라떼', itemPrice: 4000, itemCount: 0},
                        {itemId: 3, itemName: '카라멜 마끼아또', itemPrice: 4500, itemCount: 0}
                    ];
                    
                    // 
                    var menuListTpl = $("#menuListTpl").html();
                    var invoiceTpl = $("#invoiceTpl").html();
                    
                    // 
                    var menuListHtml = Mustache.render(menuListTpl, menuList);
                    var invoiceHtml = Mustache.render(invoiceTpl, {totalPrice: 0});
                    
                    //
                    var invoiceEl = $("#invoice").html(invoiceHtml);
                    
                    $("#menu-list").html(menuListHtml);
                    
                    // 
                    $("#addContract").click(function() {
                        var totalPrice = 0;
                        for (var i = menuList.length - 1; i >= 0; i--) {
                            $itemEl = $("#item-id-" + menuList[i].itemId);
                            var price = menuList[i].itemPrice;
                            var count = $itemEl.find(".item-count").val();
                            
                            totalPrice = totalPrice + (price * Number(count));
                        };
                        
                        //
                        invoiceEl.html(Mustache.render(invoiceTpl, {totalPrice: totalPrice}));
                    });
                });
            </script>
        </head>
        <body>
            <div>
                <h1>메뉴판</h1>
                <h2>메뉴 목록</h2>
                <div id="menu-list">
                </div>
                <button id="addContract">구매</button>
                <h2>구입 가격</h2>
                <div id="invoice">
                </div>
            </div>
            
            <script type="text/template" id="menuListTpl">
                <table>
                    <thead>
                        <tr><th>메뉴</th><th>가격</th><th>갯수</th></tr>
                    </thead>
                    <tbody>
                        {{#.}}
                        <tr id="item-id-{{itemId}}">
                            <td class="item-name">
                                {{itemName}}
                            </td>
                            <td class="item-price">
                                {{itemPrice}}
                            </td>
                            <td>
                                <input class="item-count" type="text" value="{{itemCount}}">
                            </td>
                        </tr>
                        {{/.}}
                    </tbody>
                </table>
            </script>
            <script type="text/template" id="invoiceTpl">
                가격: {{totalPrice}}
            </script>
        </body>
    </html>
    cs






     다음은 위 예제를 양방향 데이터 바인딩으로 변경해 보겠습니다.



     * angularway.html


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    <!DOCTYPE HTML>
    <html ng-app="way">
        <head>
            <meta charset="UTF-8"/>
            <title>two way data binding</title>
            <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.min.js"></script>
            <script>
                var app = angular.module("way", []);
                app.controller("mainCtrl"function($scope) {
                    var menuList = [
                        {itemId: 1, itemName: '베이글', itemPrice: 3000, itemCount: 0},
                        {itemId: 2, itemName: '소이 라떼', itemPrice: 4000, itemCount: 0},
                        {itemId: 3, itemName: '카라멜 마끼아또', itemPrice: 4500, itemCount: 0}
                    ];
     
                    $scope.menuList = menuList;
                    $scope.totalPrice = 0;
                    
                    $scope.buy = function(){
                        $scope.totalPrice = 0;
                        
                        angular.forEach($scope.menuList, function(menu, idx) {
                            $scope.totalPrice = $scope.totalPrice + (menu.itemPrice * Number(menu.itemCount));
                        });
                    };
                });
            </script>
        </head>
        <body ng-controller="mainCtrl">
            <div>
                <h1>메뉴판</h1>
                <h2>메뉴 목록</h2>
                <table>
                    <thead>
                        <tr>
                            <th>메뉴</th>
                            <th>가격</th>
                            <th>갯수</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr ng-repeat="menu in menuList">
                            <td>{{menu.itemName}}</td>
                            <td>{{menu.itemPrice}}</td>
                            <td><input type="text" ng-model="menu.itemCount"></td>
                        </tr>
                    </tbody>
                </table>
                <button ng-click="buy()">구매</button>
                <h2>구입 가격</h2>
                <div>
                    가격: {{totalPrice}}
                </div>
            </div>
        </body>
    </html>
    cs











      이전 코드와 비교해 보면 훨씬 더 간결하고 읽기 쉬워졌습니다. 더욱 중요한 건 버튼 클릭 이벤트를 처리하는 코드와 화면 갱신하는 자바스크립트 코드가 빠졌다는 것입니다. 






    4. 반복적인 데이터 표현을 위한 템플릿(반복 지시자)

     웹 앱에서 특정한 형식이 있는 데이터를 반복해서 표현하는 일은 가장 흔한 데이터 표현일 것입니다. 가령 구매 목록이나 연락처 목록들이 그러합니다. 반복적으로 표현할 데이터는 주로 배열에 들어 있는데 AngularJS에서는 반복적인 데이터 표현을 위해서 ng-repeat 지시자를 제공합니다. 다음은 ng-repeat의 사용법입니다.


     - <any ng-repeat="변수명 in 표현식">

      변수명은 주어진 배열의 요소를 반복문 내부에서 참조할 때 사용됩니다. 표현식은 $scope 내의 배열과 같은 순환할 대상을 가리킵니다.


      - <any ng-repeat="(key 변수명, value 변수명) in 표현식">

      자바스크립트 객체같은 데이터를 순환할 때 사용합니다. key 변수명은 반복문 내부에서 객체의 key를 참조할 변수명이고 value 변수명은 마찬가지로 참조하는 value의 변수명입니다.


      - <any ng-repeat="변수명 in 표현식 track by 표현식" >

      배열 요소와 생성되는 DOM 요소를 연결할 때 사용하는 고유한 값을 지정할 수 있습니다. Track by를 별도로 작성하지 않으면 AngularJS는 동일하지 않은 값에 $$hashKey 속성을 추가하여 DOM 요소와 연결할 때 사용합니다. 그리고 var items =[1,1]; 과 같은 동일한 값을 ng-repeat으로 표현하려고 하면 item in item track by $index로 $index에 의해 DOM 요소와 연결되게 해야 합니다. Track by를 이용해 고유한 속성값을 지정하면 무의미한 DOM 랜더링을 막을 수 있습니다. Track by를 지정하지 않으면 매번 다른 $$hashKey를 생성하므로 그 때마다 매번 DOM을 변경하기 때문입니다.




      ng-repeat을 적용한 HTML 요소는 배열 요소의 개수만큼 HTML 요소를 생성합니다. 그리고 해당 HTML 요소에는 별도의 scope 영역이 생성되는데 해당 scope 영역에서만 사용할 수 있는 특별한 속성을 제공합니다.



      다음은 고객 목록과 친구목록을 보여주는 예제로 ng-repeat의 각 사용법을 보도록 하겠습니다.



     * repeat.html


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <!doctype html>
    <html ng-app>
        <head>
            <meta charset="UTF-8"/>
            <title>ng-repeat exam</title>
            <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.min.js"></script>
        </head>
        <body>
            <div ng-init="customerList = [{name:'Jack', age:31}, {name:'Jolie', age:25}]">
                고객 목록
                <ul>
                    <li ng-repeat="customer in customerList">
                        [{{$index + 1}}] 고객 이름: {{customer.name}}, 고객 나이: {{customer.age}}
                    </li>
                </ul>
            </div>
            <div ng-init="friendList = {name:'Paul', age: 22, hobby:'Game'}">
                내 친구 소개
                <ul>
                    <li ng-repeat="(attr, value) in friendList">
                        <p>{{attr}} : {{value}} </p>
                    </li>
                </ul>
            </div>
        </body>
    </html>
    cs


     








    * 이 포스팅은 '시작하세요! AngularJS 프로그래밍'을 바탕으로 작성하였습니다.

    댓글

Designed by Tistory.