/ 자바스크립트(JavaScript)

Vue.js 스터디 정리 Part.01

이 글은 Vue.js 가이드를 중심으로, 쉽고 빠르게 배우는 Vue.js 2 프로그래밍, Vue.js 퀵 스타트, Vue.js 이 정도는 알아야지를 참고하였습니다.

Vue.js를 처음 접하시는 분들은 Vue.js 가이드를 중심으로 진행하시길 권장해 드리고 싶네요. 그럼에도 불구하고 꼭 책을 구매해야 된다고 생각하시는 분들의 경우 쉽고 빠르게 배우는 Vue.js 2 프로그래밍를 추천드립니다(요즘에 Vue.js에 관한 책이 많이 출간되고 있지만, 참고한 3권의 서적을 제외하곤 별도로 읽어본 적이 없기에 개인적으로 읽어본 책 중에서 추천하였습니다).

Vue.js란

Vue는 UI(user interface)를 만들기 위한 자바스크립트(JavaScript) 프레임워크(framework)입니다. Vue는 뷰(view)에 초점을 맞추어 다른 라이브러리나 기존 프로젝트와의 통합이 매우 쉽고 다양한 도구와 많은 라이브러리를 지원하고 있기 때문에 단일 페이지 응용프로그램(single page application 혹은 SPA)을 만드는데 있어 부족함이 없습니다.

왜 Vue.js를 선택했는가?

개인적으로 react가 아닌 Vue.js를 선택한 이유는 1) 기존에 작게나마 사용하던 jQuery를 빠르게 대체할 수 있을꺼라 판단했으며, 2) 사용함에 있어서 react 보다 Vue.js가 학습 비용이 낮을 것으로 예상했기 때문이다. 만약, 실제 업무에서 사용한다면 react를 추천드리고 싶습니다.

이 글의 목표

이번 Part.01의 가장 큰 목표는 기존의 jQuery를 대체하여 사용할 수 있는 기능을 중심으로 정리하고 간단한 '장바구니' 기능을 Vue.js를 사용해서 구현해 보는 것입니다. 따라서 v-model, v-bind,v-show,v-if,v-for 등과 같은 디렉티브(directive)를 가볍게 학습하고, 이벤트를 처리하는 방법, Vue 인스턴스의 내부 구조와 생성 방법 마지막으로 Style(a.k.a CSS)을 적용하는 것이 중요한 학습 내용이었습니다. 최종적으로 '장바구니' 기능을 구현하도록 하겠습니다.

Vue.js 설치하기

이 글에선 vue-cli등과 같은 npm기반의 개발환경을 구성할 필요가 없습니다. 모든 예제는 CDN을 사용하여 Vue.js를 사용하면 되고, CSS 프레임워크는 Bootstrap을 사용합니다.

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>

선언적 렌더링(Declarative Rendering)

Vue.js의 핵심은 간단한 템플릿 구문(template syntax)을 사용해 선언적(declaratively)으로 DOM에 데이터를 렌더링(render)하는 것입니다.

<!DOCTYPE html>
<html>

<head>
  <title>선언적 렌더링(Declarative Rendering)</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
</head>

<body>
  <div id="app">
    {{ message }}
  </div>
  <script type="text/javascript">
    var model = {
      message: "Hello, World! Vue.js"
    };

    var app = new Vue({
      el: '#app',
      data: model
    })
  </script>
</body>

</html>
  • app 객체는 Vue.js의 인스턴스(instance)이며, model은 Vue 인스턴스에서 사용할 데이터 객체입니다. 데이터 객체인 model을 인스턴스의 내부의 data에 연결하여, 해당 데이터를 Vue 인스턴스와 연결합니다.

  • Vue 인스턴스는 HTML 요소를 렌더링하기 위해서 #app 이라는 id를 기반으로 HTML 요소(element)를 참조합니다. Vue 인스턴스에서 데이터(이 예제에선 model)가 변경되면 HTML 요소에 반영됩니다. 즉, 데이터가 변경되면 해당 요소를 다시 렌더링됩니다.

  • {{...}}와 같이 이중-중괄호 형태로 템플릿 표현식을 나타낸 부분을 Mustache 구문(Mustache syntax 또는 double curly braces)이라 부르며 이런 형태의 구문을 보간법(interpolation)이라 합니다. Mustache 구문은 해당 데이터 객체의 속성값으로 대체됩니다. 또한 데이터 객체의 속성이 변경될 때 마다 갱신되어 화면이 렌더링됩니다.

v-html과 v-text

  • Mustache 구문은 HTML이 아니라 일반 텍스트다. 실제 HTML을 출력하고 싶다면 v-html 디렉티브를 사용해야 합니다.

  • v-html의 경우 웹사이트에서 임의의 HTML을 동적으로 렌더링하려면 XSS 취약점으로 쉽게 이어질 수 있으므로 매우 위험합니다. 신뢰할 수 있는 콘텐츠에서만 사용하고, 일반 사용자가 제공한 콘텐츠에선 사용하지 않기를 권장하고 있습니다.

<!DOCTYPE html>
<html>

<head>
  <title>Welcome to Vue</title>
  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
  <script src="https://unpkg.com/vue"></script>
</head>

<body>
  <div id="simple">
    <h2>{{message1}}</h2>
    <span v-html="message2"></span>
  </div>
  <script type="text/javascript">
    var model = {
      message1: "첫 번째 Vue.js 입니다.",
      message2: "<h2> 첫 번째 Vue.js 입니다. </h2>"
    };

    var simple = new Vue({
      el: '#simple',
      data: model
    })
  </script>
</body>

</html>

v-bind

  • Mustache 구문은 HTML 속성에서 사용할 수 없습니다. HTML 속성의 경우 v-bind 디렉티브를 사용해야 되며, v-bind를 매번 사용하는게 번거롭기 때문에, :class, :id 등으로 축약할 수 있습니다.
<!DOCTYPE html>
<html>

<head>
  <title>Welcome to Vue</title>
  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
  <script src="https://unpkg.com/vue"></script>
</head>

<body>
  <div id="simple">
    <h2 v-bind:class="headerProperty">{{message1}}</h2>
    <h2 :class="headerProperty">{{message1}}</h2>
  </div>
  <script type="text/javascript">
    var model = {
      message1: "첫 번째 Vue.js 입니다.",
      headerProperty: "header"
    };

    var simple = new Vue({
      el: '#simple',
      data: model
    })
  </script>
</body>

</html>

v-model

  • v-model은 양방향 디렉티브로 view의 변경 사항을 model로, model의 변경 사항을 view로 전달합니다.
<html>

<head>
    <title>Welcome to Vue</title>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    <script src="https://unpkg.com/vue"></script>
</head>

<body>
    <div id="checkBox">
        <input type="checkbox" value="1" v-model="numbers">1,
        <input type="checkbox" value="2" v-model="numbers">2,
        <input type="checkbox" value="3" v-model="numbers">3
    </div>

    <hr>

    <div id="showNumber">
        선택한 숫자들 :
        <span v-html="numbers"></span>
    </div>

    <script type="text/javascript">
        var model = {
            numbers: []
        };

        var simple1 = new Vue({
            el: '#checkBox',
            data: model
        });

        var sample2 = new Vue({
            el: '#showNumber',
            data: model
        });
    </script>
</body>

</html>

v-show와 v-if(v-else, v-else-if)

  • v-show는 기본적으로 v-if와 기능이 같지만 결정적으로 차이는 HTML 요소를 렌더링 여부라 할 수 있습니다. v-show는 HTML 요소를 렌더링한 후 display 속성으로 화면에 보여줄지를 결정합니다. 반면 v-if 는 조건에 부합하지 않으면 렌더링 자체를 하지 않습니다. 따라서 화면이 자주 변경된다면 v-show를 사용하는게 좋다.
<!DOCTYPE html>
<html>

<head>
  <title>Welcome to Vue</title>
  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
  <script src="https://unpkg.com/vue"></script>
</head>

<body>
  <div id="account">
    금액 :
    <input type="text" v-model="amount">
    <h2 v-if="amount < 0">금액은 0 이하를 입력하실 수 없습니다.</h2>
    <h2 v-show="amount < 0">금액은 0 이하를 입력하실 수 없습니다.</h2>
  </div>

  <script>
    var simple1 = new Vue({
      el: '#account',
      data: {
        amount: 0
      }
    })
  </script>
</body>

</html>

v-for

  • v-for 디렉티브를 사용하여 배열을 기반으로 리스트를 처리 할 수 있습니다. v-for 디렉티브는 item in items 형태의 문법이 필요하며, items는 원본 데이터 배열이고 item은 반복자입니다.
<!DOCTYPE html>
<html>

<head>
  <script src="https://unpkg.com/vue"></script>
</head>

<body>
  <div id="example">
    <p>국가명 :
      <input type="text" v-model="countryname" placeholder="국가명">
    </p>
    <table id="list">
      <thead>
        <tr>
          <th>번호</th>
          <th>국가명</th>
          <th>수도</th>
          <th>지역</th>
        </tr>
      </thead>
      <tbody id="contacts">
        <tr v-for="c in countries">
          <td>{{c.no}}</td>
          <td>{{c.name}}</td>
          <td>{{c.capital}}</td>
          <td>{{c.region}}</td>
        </tr>
      </tbody>
    </table>
  </div>
  <script type="text/javascript">
    let model = {
      countryname: "",
      countries: [{
          no: 1,
          name: "미국",
          capital: "워싱턴DC",
          region: "america"
        },
        {
          no: 2,
          name: "일본",
          capital: "도쿄",
          region: "asia"
        },
        {
          no: 3,
          name: "영국",
          capital: "런던",
          region: "europe"
        },
        {
          no: 4,
          name: "중국",
          capital: "베이징",
          region: "asia"
        },
        {
          no: 5,
          name: "태국",
          capital: "방콕",
          region: "asia"
        },
        {
          no: 6,
          name: "한국",
          capital: "서울",
          region: "asia"
        },
      ]
    }

    new Vue({
      el: "#example",
      data: model
    })
  </script>
</body>

</html>
  • v-for를 사용하면 value, key, index를 개별적으로 처리할 수 있습니다.
<option v-for="(v, k) in lists" :value="key">{{val}}</>
<option v-for="(v, k, i) in lists">

computed

  • computed는 속성을 동적으로 처리할 수 있다. 템플릿 내에서 사용하는 표현식은 매우 편리하지만 단순한 연산에만 사용해야 합니다. 너무 많은 로직을 템플릿에 넣으면 유지보수가 어려워 질 수 있습니다.

  • 계산된 속성 대신 메소드와 같은 함수를 정의할 수 있습니다. 최종 결과에 대해 두 가지 접근 방식은 서로 동일하지만 차이점은 계산된 속성(computed)은 종속성에 따라 캐시된다는 것입니다. computed은 종속성 중 일부가 변경된 경우에만 다시 계산됩니다. 이것은 message가 변경되지 않는 한, 계산된 속성인 reversedMessage에 대한 다중 접근은 함수를 다시 수행할 필요 없이 이전에 계산된 결과를 즉시 반환한다는 것을 의미합니다.

<html>

<head>
    <script src="https://unpkg.com/vue"></script>
</head>
<div id="example">
    <p>국가명 :
        <input type="text" v-model="countryname" placeholder="국가명">
    </p>
    <table id="list">
        <thead>
            <tr>
                <th>번호</th>
                <th>국가명</th>
                <th>수도</th>
                <th>지역</th>
            </tr>
        </thead>
        <tbody id="contacts">
            <tr v-for="c in filtered">
                <td>{{c.no}}</td>
                <td>{{c.name}}</td>
                <td>{{c.capital}}</td>
                <td>{{c.region}}</td>
                <td>{{fullName}}
            </tr>
        </tbody>
    </table>
</div>
<script type="text/javascript">

    let model = {
        countryname: "",
        countries: [
            { no: 1, name: "미국", capital: "워싱턴DC", region: "america" },
            { no: 2, name: "일본", capital: "도쿄", region: "asia" },
            { no: 3, name: "영국", capital: "런던", region: "europe" },
            { no: 4, name: "중국", capital: "베이징", region: "asia" },
            { no: 5, name: "태국", capital: "방콕", region: "asia" },
            { no: 6, name: "한국", capital: "서울", region: "asia" },
        ]
    }

    new Vue({
        el: "#example",
        data: model,
        computed: {
            filtered: function () {
                let cname = this.countryname.trim();
                return this.countries.filter((item) => {
                    if (item.name.includes(cname)) {
                        return true;
                    };
                })
            }
        }
    })

</script>
</body>

</html>

인스턴스

data

  • data 옵션의 경우 주어진 모든 속성들은 Vue 인스턴스 내부에 직접 이용되지 않고 Vue 인스턴스와 data 옵션에 주어진 객체 사이에 proxy를 두어 처리합니다.

  • data 옵션은 Vue 인스턴스가 관찰하는 데이터 객체를 의미하므로 변경 사항은 즉시 감지됩니다.

var model = {
  name: "홍길동"
}
var vm = new Vue({
  el: '#test',
  data: model
})
  • 프록시를 사용하기 때문에 아래는 모두 name이라는 같은 객체를 참조하고 있으며, $data가 Vue 인스턴스에서 사용하는 프록시 입니다.
vm.name
model.name
vm.$data.name

el

  • el 옵션은 Vue 인스턴스에 연결할 HTML DOM 요소를 지정합니다. 또한 vue 인스턴스가 html 요소와 연결되면 도중에 연결된 요소를 변경할 수 없으며, 여러 요소와 연결할 수 없습니다.

computed

  • computed 옵션에서 지정한 값은 함수였지만 Vue 인스턴스는 프록시 처리하여 속성과 같이 취급할 수 있습니다.
new Vue({
  el: "#example",
  data: model,
  computed: {
    filtered: function () {
      let cname = this.countryname.trim();
      return this.countries.filter((item)=> {
        if(item.name.includes(cname)){
            return true;
        };
      })
    }
  }
})
vm.filtered
vm.$options.computed.filtered

methods

  • Vue 인스턴스에서 사용할 메서드를 등록하는 옵션입니다. 즉 계산을 하는 함수가 있다고 했을 때 computed 의 경우 캐싱된(최초) 값을 바로 리턴하지만, methods 의 경우 호출될 때마다 계산해서 값을 반환합니다. 일반적인 '함수'와 동일합니다.
methods: {
  print : function (n) {
      console.log(n);
      return true;
  }
}

watch

  • Vue.js 에서 하나의 데이터를 기반으로 다른 데이터를 변경할 필요가 있을 때 흔히 사용합니다. 주로 긴 처리 시간이 필요한 비동기 처리에 적합하다는 특징이 있습니다.
watch: {
  // x라는 속성이 변경되면 function(v)가 호출된다.
  x: function (v) {
      console.log('## x 변경');
      var result = Number(v) + Number(this.y);
      if (isNaN(result)) this.sum = 0;
      else this.sum = result;
  },
  y: function (v) {
      console.log('## y 변경');
      this.y = v;
      var result = Number(this.x) + Number(v);
      if (isNaN(result)) this.sum = 0;
      else this.sum = result;
  }
}

v-cloak

  • 새로고침이나 값이 변경될 때 아주 잠깐 {{...}} 의 표현식이 나타나는 것을 알 수 있습니다. 이 부분이 상당히 눈에 거슬리므로 보이지 않게 처리를 해주어야 하는데 이것이 바로 v-cloak 입니다.
<style>
    [v-cloak] { display: none;}
</style>

Event

v-on(v-on:click, @click)

<html>

<head>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
    crossorigin="anonymous">
</head>

<body>
  <div id="example" class="container">
    <p>
      <input type="text" v-model="amount" class="form-control" />
    </p>
    <p>
      <button id="deposit" v-on:click="deposit($event)" class="btn btn-primary">예금</button>
      <button id="withdraw" v-on:click="withdraw" class="btn btn-primary">인출</button>
    </p>
    <h3>계좌 잔고 : {{ balance }}</h3>
  </div>

  <script type="text/javascript">
    new Vue({
      el: "#example",
      data: {
        amount: 0,
        balance: 0,
      },
      methods: {
        deposit: function (e) {
          var amt = parseInt(this.amount);
          if (amt <= 0) {
            alert("0보다 큰 값을 예금해야 합니다");
          } else {
            this.balance += amt;
          }
        },
        withdraw: function (e) {
          var amt = parseInt(this.amount);
          if (amt <= 0) {
            alert("0보다 큰 값을 인출할 수 있습니다");
          } else if (amt > this.balance) {
            alert("잔고보다 많은 금액을 인출할 수 없습니다");
          } else {
            this.balance -= amt;
          }
        }
      }
    })
  </script>
</body>

</html>

default event

  • 기본 이벤트는 개발자가 이벤트를 연결하지 않아도 실행되는 기능입니다(a, form, contextMenu 등). 하지만 이러한 이벤트는 가끔 원하지 않는 이벤트를 강제로 발생하게 되는데, 이것을 막는 방법이 e.preventDefault()입니다. Vue 에서는 쉽게 이것을 사용할 수 있게 해줍니다.
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>04-04</title>
  <style>
    html,
    body {
      margin: 0;
      padding: 0;
    }

    #example {
      height: 98vh;
      min-height: 100%;
      padding: 5px
    }
  </style>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
</head>

<body>
  <div id="example" v-on:contextmenu.prevent="ctxStop">
    <a href="https://facebook.com" @click="confirmGOOGLE">구글</a>
  </div>
  <script type="text/javascript">
    var vm = new Vue({
      el: "#example",
      methods: {
        ctxStop: function (e) {},
        confirmGOOGLE: function (e) {
          if (!confirm("구글로 이동할까요?")) {
            e.preventDefault();
          }
        }
      }
    })
  </script>
</body>

</html>

stopPropagation

  • JavaScript의 이벤트 전파는 3단계를 거치는데, 1단계는 capturing으로 문서 내의 요소에서 이벤트가 발생했을 때 HTML 문서 밖에서부터 이벤트를 발생시킨 HTML 요소까지 포착해 들어가는 이벤트 포착 단계입니다. 2단계는 raising 이벤트를 발생시킨 요소에 다다르면 요소의 이벤트에 연결된 함수를 직접 호출시키는 이벤트 발생합니다. 3단계는 bubbling 이벤트가 발생한 요소로부터 상위 요소로 거슬러 올라가면서 동일한 이벤트를 호출시키는 버블링 단계입니다.

  • 일반적으로 2단계 raising와 3단계 bubbling에서 연결된 이벤트 함수가 호출됩니다. 아래 예제는 inner 에 해당하는 박스를 클릭했음에도 불구하고 버블링을 통해 OUTER 이벤트도 같이 실행되는 것을 알 수 있습니다. 이런 이벤트 전파를 막는 방법은 e.stopPropagation(), @click.stop이 있습니다.

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <style>
        #outer {
            width: 200px;
            height: 200px;
            border: solid 2px black;
            background-color: aqua;
            position: absolute;
            top: 100px;
            left: 50px;
            padding: 10px 10px 10px 10px;
        }

        #inner {
            width: 100px;
            height: 100px;
            border: solid 2px black;
            background-color: yellow;
        }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
</head>

<body>
    <div id="example">
        <div id="outer" @click="outerClick">
            <!--<div id="inner" @click="innerClick"></div>-->
            <div id="inner" @click.stop="innerClick"></div>
        </div>
    </div>
    <script type="text/javascript">
        var vm = new Vue({
            el: "#example",
            methods: {
                outerClick: function (e) {
                    console.log("### OUTER CLICK")
                    console.log("Event Phase : ", e.eventPhase);
                    console.log("Current Target : ", e.currentTarget);
                    console.log("Target : ", e.target);
                    // e.stopPropagation();
                },
                innerClick: function (e) {
                    console.log("### INNER CLICK")
                    console.log("Event Phase : ", e.eventPhase);
                    console.log("Current Target : ", e.currentTarget);
                    console.log("Target : ", e.target);
                    // e.stopPropagation();
                }
            }
        })
    </script>
</body>

</html>

keyup

  • 키보드 입력은 v-on:keyup.13와 같이 키보드 입력을 사용할 수 있습니다.
<input type="text" v-model="name" v-on:keyup.13="search" placeholder="두글자 이상을 입력하세요" />

mouseup.left

  • 마우스 입력은 mouseup.left와 같이 mouse 접두어를 사용하여 입력을 사용할 수 있습니다.
<div id="example" v-on:contextmenu.prevent="ctxStop" @mouseup.left="leftMouse" @mouseup.right="rightMouse">

Style

  • HTML은 대소문자를 구별하지 않기 때문에 -을 사용한 케밥 표시법을 사용합니다.

  • style 태그에 작성된 순서대로 CSS 스타일이 적용되므로 같은 클래스라면 제일 마지막에 작성된 css 를 따르게 됩니다.

mouseOver

  • v-bind:style="style1"와 같이 v-bind를 사용해서 스타일을 적용합니다.
<!DOCTYPE html>

<html>

<head>
  <meta charset="utf-8">
  <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
</head>

<body>
  <div id="example">
    <button id="a" v-bind:style="style1" @mouseover.stop="overEvent" @mouseout.stop="outEvent">테스트</button>
  </div>
  <script type="text/javascript">
    var vm = new Vue({
      el: "#example",
      data: {
        style1: {
          backgroundColor: "aqua",
          border: 'solid 1px gray',
          with: '100px',
          textAlign: 'center'
        }
      },
      methods: {
        overEvent: function (e) {
          this.style1.backgroundColor = "purple";
          this.style1.color = "yellow";
        },
        outEvent: function (e) {
          this.style1.backgroundColor = "aqua";
          this.style1.color = "black";
        }
      }
    })
  </script>
</body>

</html>

class Style

  • 인라인 스타일이 아닌 CSS의 클래스를 적용하려면, v-bind:class 혹은 :class를 사용하여 아래와 같은 형태로 작성할 수 있습니다.
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
</head>

<body>
  <div id="example">
    <button id="a" v-bind:style="style1" @mouseover.stop="overEvent" @mouseout.stop="outEvent">테스트</button>
  </div>
  <script type="text/javascript">
    var vm = new Vue({
      el: "#example",
      data: {
        style1: {
          backgroundColor: "aqua",
          border: 'solid 1px gray',
          with: '100px',
          textAlign: 'center'
        }
      },
      methods: {
        overEvent: function (e) {
          this.style1.backgroundColor = "purple";
          this.style1.color = "yellow";
        },
        outEvent: function (e) {
          this.style1.backgroundColor = "aqua";
          this.style1.color = "black";
        }
      }
    })
  </script>
</body>

</html>

computed style 예제

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>05-07</title>
  <style>
    .score {
      border: solid 1px black;
    }

    .warning {
      background-color: orange;
      color: purple;
    }

    .warnimage {
      width: 18px;
      height: 18px;
      top: 5px;
      position: relative;
    }
  </style>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
</head>

<body>
  <div id="example">
    <div>
      <p>1부터 100까지만 입력가능합니다.</p>
      <div>
        점수 :
        <input type="text" class="score" v-model.number="score" :class="info" />
        <img src="http://via.placeholder.com/5x5" class="warnimage" v-show="info.warning" />
      </div>
    </div>
  </div>
  <script type="text/javascript">
    var vm = new Vue({
      el: "#example",
      data: {
        score: 0
      },
      computed: {
        info: function () {
          if (this.score >= 1 && this.score <= 100)
            return {
              warning: false
            };
          else
            return {
              warning: true
            };
        }
      }
    })
  </script>
</body>

</html>

예제 - TodoList App

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <style>
    * {
      box-sizing: border-box;
    }

    ul {
      margin: 0;
      padding: 0;
    }

    ul li {
      cursor: pointer;
      position: relative;
      padding: 8px 8px 8px 40px;
      background: #eee;
      font-size: 14px;
      transition: 0.2s;
      -webkit-user-select: none;
      -moz-user-select: none;
      -ms-user-select: none;
      user-select: none;
    }

    ul li:hover {
      background: #ddd;
    }

    ul li.checked {
      background: #BBB;
      color: #fff;
      text-decoration: line-through;
    }

    ul li.checked::before {
      content: '';
      position: absolute;
      border-color: #fff;
      border-style: solid;
      border-width: 0px 1px 1px 0px;
      top: 10px;
      left: 16px;
      transform: rotate(45deg);
      height: 8px;
      width: 8px;
    }

    .close {
      position: absolute;
      right: 0;
      top: 0;
      padding: 12px 16px 12px 16px
    }

    .close:hover {
      background-color: #f44336;
      color: white;
    }

    .header {
      background-color: purple;
      padding: 30px 30px;
      color: yellow;
      text-align: center;
    }

    .header:after {
      content: "";
      display: table;
      clear: both;
    }

    .input {
      border: none;
      width: 75%;
      height: 35px;
      padding: 10px;
      float: left;
      font-size: 16px;
    }

    .addbutton {
      padding: 10px;
      width: 25%;
      height: 35px;
      background: #d9d9d9;
      color: #555;
      float: left;
      text-align: center;
      font-size: 13px;
      cursor: pointer;
      transition: 0.3s;
    }

    .addbutton:hover {
      background-color: #bbb;
    }

    .completed {
      text-decoration: none;
    }
  </style>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
</head>

<body>
  <div id="todolistapp">
    <div id="header" class="header">
      <h2>Todo List App</h2>
      <input class="input" type="text" id="task" v-model.trim="todo" placeholder="입력 후 엔터!" @keyup.enter="addTodo">
      <span class="addbutton" @click="addTodo">추 가</span>
    </div>
    <ul id="todolist">
      <li v-for="(a, index) in todolist" :class="checked(a.done)" @click="doneToggle(index)">
        <span>{{ a.todo }}</span>
        <span v-if="a.done"> (완료)</span>
        <span class="close" @click.stop="deleteTodo(index)">&#x00D7;</span>
      </li>
    </ul>
  </div>
  <script type="text/javascript">
    var vm = new Vue({
      el: "#todolistapp",
      data: {
        todo: "",
        todolist: [{
            todo: "영화보기",
            done: false
          },
          {
            todo: "주말 산책",
            done: true
          },
          {
            todo: "ES6 학습",
            done: false
          },
          {
            todo: "잠실 야구장",
            done: false
          },
        ]
      },
      methods: {
        checked: function (done) {
          if (done) return {
            checked: true
          };
          else return {
            checked: false
          };
        },
        addTodo: function (e) {
          if (this.todo !== "") {
            this.todolist.push({
              todo: this.todo,
              done: false
            });
            this.todo = "";
          }
        },
        deleteTodo: function (index) {
          this.todolist.splice(index, 1);
        },
        doneToggle: function (index) {
          this.todolist[index].done = !this.todolist[index].done;
        }
      }
    })
  </script>
</body>

</html>

최종 예제 진행

최종예제