Vue.js 살펴보기

첫인상

뷰js 문서를 읽어보면서 느낀것은 "쉬운데?"

앵귤러나 리엑트 문서와는 다르게 비교적 이해하기 쉬운 문서였다. 최신 유행하는 웹 프레임웍에 익숙해진 탓인지 비슷한 기술로 구현되어 있는 뷰js가 쉽게 느껴졌다. 무엇보다도 잘 번역된 한글 문서가 이 녀석을 더 친숙하게 해준것 같다.

twitter-said-about-vue
twitter-said-about-vue

새로운 프레임웍이 나올때 마다 토이 프로젝트에 적용해 보는 편이지만 앵귤러나 리액트 모두 가이드라인에서 그쳤다. 새로운 것을 경험하는 것은 반가운 일이지만 요즘은 너무 많이 쏟아져 나오는 그런 느낌. 그런면에서 뷰js는 문서를 읽고 이해하는데 하루면 충분하다.

뷰js에 대해 의심의 시선도 있는데 이유중 하나가 중국산이라는 것. 중국인 에반 유가 만들었다. 난 중국에 대한 선입견이 없을 뿐더러 아시아 출신이 만든 코드라 더 신선했다. 자바스크립트 풀스택 프레임웍인 미티어 개발자 출신인 점도 믿음이 간다. 그에 대한 인터뷰 내용은 여기서 확인할 수 있다.

아키텍쳐

뷰모델

MVVM 모델의 VM을 뷰js가 담당한다.

mvvm-of-vue
mvvm-of-vue

웹페이지는 돔과 자바스크립트의 연합으로 만들어지게 되는데 돔이 View 역할을 하고, 자바스크립트가 Model 역할을 한다. 뷰모델이 없는 아키텍처에서는 getElementById 같은 돔 API를 직접 이용해서 모델과 뷰를 연결해야 한다. 자바스크립트에서 컨트롤러 혹은 프리젠터 역할까지 하면서 코드양이 증가하는 단점이 생기기게 되는데 제이쿼리를 이용해 돔에 데이터 뿌려주는 코드들이 대부분 그랬다.

하지만 그것을 뷰모델이 대신 수행해 주는 것이 MVVM 모델이다. 뷰모델에 자바스크립트 객체와 돔을 연결해 주면 뷰모델은 이 둘간의 동기화를 자동으로 처리한다. 이것이 뷰js가 하는 역할이다.

가상돔

가상돔 기술은 리엑트가 렌더링 성능을 높이고자 사용하면서부터 유명해진 것 같다. 앵귤러에서는 못봤다. 뷰에서도 렌더링 성능을 위해 가상돔을 사용하는데 코어 라이브러리단에서 직접 구현했다고 한다.

vurtual-dom-with-matrix
vurtual-dom-with-matrix

돔 요소가 많아지면 자바스크립트로 돔을 핸들링하는 일이 무거워진다. 그래서 돔과 비슷한 구조로 자바스크립트를 만든다. 이것은 진짜 돔과는 달리 메모리에 올라가있는 것이기 때문에 비교적 매우 빠른 성능을 보인다. 뷰js가 가상 돔을 수정하면 진짜 돔을 수정하는 것보다 빠르다. 뷰는 버추얼 돔이 변경될 때마다 진짜 돔과 비교해서 차이를 찾는다. 그 결과 차이난 부분의 돔만 수정하는 동작을 한다.

좀더 재밌는 애니메이션 영상이다. React and the Virtual DOM - YouTube

컴포넌트

앵귤러를 하면서 디렉티브가 컴포넌트인줄 알았다. angular.module().directive('hello') 함수로 만들어 디렉티브를 만들수 있는데 이상하게도 이 녀석은 네 가지 방법으로 사용할 수 있다. 아직도 왜 이렇게 많은 방법이 필요한지 의문이다.

<hello>
  <div hello>
    <div class="hello">
      <!-- hello -->
    </div>
  </div></hello
>

세번째 클래스명으로 사용하는 방법은 css 클래스와 역할이 중복되고, 네번째 주석방법은 사용하는 사람이 있을까 의문이었다. 실제로는 첫번째 방법을 많이 사용했었고 모양이 컴포넌트와 비슷해서 이게 컴포넌트인줄 오해했다. 버전1.5에 와서야 component() 함수가 생기긴 했지만 귀찮아서 directive() 함수를 계속 사용했다.

뷰js는 철저하게 컴포넌트 기반의 개발을 유도한다. 모든 것을 컴포넌트로 표현하도록 했는데 이 부분이 리엑트와 가장 비슷한 점이다.

컴포넌트는 마크업과 동일한 모습으로 사용한다.

<hello />

컴포넌트에 데이터를 넣고 싶은 경우는 속성으로 값을 전달할 수 있다.

<hello name="chris" />

컴포넌트로부터 이벤트를 받아 후속 처리를 할 수도 있다.

<hello typed="onType()" />

뷰js는 단일 파일 컴포넌트를 추천한다.

vue-component-structure
vue-component-structure

확장자가 vue인 파일로 컴포넌트를 만들고 HTML, 자바스크립트, CSS 코드로 구성한다. 이 부분이 꾀 놀라웠다. CSS까지 컴포넌트로 들어가 있다니! 게다가 이것은 상위 CSS의 영향도 받지 않는다고 하는데 빌드 시점에 고유한 셀렉터 이름으로 대체하는 방식을 사용하기 때문이다.

vue-component-tree
vue-component-tree

레이아웃의 각 섹션이 트리 형태로 구성되듯이 이에 상응하는 컴포넌트를 트리형태로 구성하여 개발할 수 있는 구조다. 마음이 편안해지는 그림이다.

Vue 인스턴스

그럼 헬로월드 코드부터 살펴보자.

<div id="app">{% raw %}{{ msg }}{% endraw %}</div>

<script>
  var vm = new Vue({
    el: "#app",
    data: {
      msg: "Hello world",
    },
  })
</script>

Vue 생성자 함수로 뷰모델을 만드는데 이것이 Vue 인스턴스다. 이것은 자바스크립트 객체인데 내부적으로 연결될 템플릿이 필요하다. 그것을 el 키에 셀렉터 값으로 넣어주면 된다.

{
  el: "#app"
}

그리고 data 객체는 인스턴스의 상태를 저장하는 역할을 하는데 템플릿에 바인딩할 데이터들의 집합이다. msg 변수 값을 템플릿에 인터폴레이션 문법으로 바인딩하여 출력하였다.

{% raw %}{{ msg }}{% endraw %}

브라우져로 확인해 보면 메세지 변수값인 'Hello world' 문자열이 출력된다.

뷰 인스턴스에는 methods 객체도 있는데 이것은 인스턴스의 행동 즉 메써드의 집합니다. 이것은 주로 돔에 연결할 이벤트 핸들러로 사용된다. 버튼을 클릭하면 경고를 띄워주는 클릭 이벤트 핸들러를 아래와 같이 만들 수 있다.

<div id="app">
  <button v-on:click="onClick">클릭미!</button>
</div>

<script>
  var vm = new Vue({
    el: "#app",
    methods: {
      onClick: function () {
        alert("Hello world")
      },
    },
  })
</script>

데이터 바인딩과 이벤트 바인딩 부분이 앵귤러와 매우 유사하다. 에반 유는 앵귤러를 사용하면서 데이터 바인딩 방법에 대한 영향을 많이 받았다고 한다.

하지만 그는 여기에 한 가지 더 추가한 것이 있는데 computed 객체다. 이것은 methods와 비슷한 역할을 하지만 캐쉬를 가지고 있어서 함수 본문에서 사용하는 상태값에 변화가 없으면 함수를 수행하지 않고 저장한 캐쉬값을 바로 반환하기 때문에 비교적 빠르다.

<div id="app">{% raw %}{{ greeting }}{% endraw %}</div>

<script>
  var vm = new Vue({
    el: "#app",
    data: { name: "Chris" },
    computed: {
      greeting: function () {
        return "Hello " + this.name
      },
    },
  })
</script>

인스턴스는 생성 -> 마운트 -> 소멸의 라이프 사이클을 가지고 있다.

vue-lifecycle
vue-lifecycle

인스터스 이벤트 사이클을 이해할 필요가 있는데 각 시점마다 이벤트 훅을 사용할수 있기 때문이다. 내가 본 샘플코드에서는 객체 생성후 백엔드 리소스를 요청을 위해 create 훅에 ajax를 호출하고 있었다. 뷰모델과 돔을 연결할 데이터를 동적으로 백엔드에 가져오는 시점을 create에서 처리하는 것이다. DOMContentLoaded 이벤트와는 다른 시점이다.

var vm = new Vue({
  created: function () {
    fetchData()
  },
})

Vue 컴포넌트

웹 컴포넌트라는 용어를 자주 보는데 아직도 정확한 개념은 모르겠다. 재사용 가능한 컴포넌트 제작 기술의 집합을 말하는 것 이라고 한다.

  • 커스텀 태그
  • 템플릿
  • 돔과 스타일시트의 캡슐화

이러한 컴포넌트는 재사용할 수 있어 프로그램 개발에 효율성을 높일수 있다. 이미 앵귤러, 리엑트, 폴리머 등 유수 프레임웍에서는 웹 컴포넌트 제작 방법을 구현해놨다.

  • 앵귤러: angular.module().component('MyComponent', { ... })
  • 리엑트: class MyComponent extends React.Component { ... }
  • 폴리머: class MyElement extends HTMLElement { ... }

마찬가지로 뷰js도 컴포넌트를 만들수 있는 메서드를 제공한다.

Vue.componet()

Vue 인스턴스는 모든 컴포넌트의 상위 컴포넌트로서 자식 컴포넌트를 여러개 가질 수 있는데 이 component 함수로 만들어 자식으로 사용한다.

<div id="child">Hello {% raw %}{{ name }}{% endraw %}</div>

<div id="app"><Child></Child></div>

<script>
  Vue.component("Child", {
    el: "#child",
    data: function () {
      return {
        name: "Chris",
      }
    },
  })

  var vm = new Vue({
    el: "#app",
  })
</script>

Child 컴포넌트를 정의했는데 Vue 인스턴스를 만들 때와 다른점은 data 부분이다. 컴포넌트는 재사용이 가능하기 때문에 고유한 데이터를 가지고 있어야 한다. 그래서 매번 함수를 실행한 결과 새로운 객체를 반환하도록 했다. (Vue 인스턴스에서는 객체를 할당했다)

컴포넌트가 여러개 생기면서 이들간의 커뮤니케이션 방법도 필요하다. 트리형태로 구성했기 때문에 상위에 있는 것을 부모(Parent) 컴포넌트, 아래 있는것(중첩되어 있는)을 자녀(Child) 컴포넌트라고 부른다.

vue-component-communication
vue-component-communication

데이터는 부모 -> 자녀 방향으로만 전달할 수 있다. 이점은 리액트의 단방향 데이터 흐름과 같다. 그래서 뷰js는 자녀 컴포넌트의 프로퍼티를 이용해 데이터를 전달한다.

<Child name="Chris"></Child>

<script>
  Vue.component("Child", {
    el: "#child",
    props: ["name"],
  })
</script>

Child 컴포넌트를 사용하는 부모 컴포넌트 측에서는 Child의 name 속성에 "Chris" 문자열을 데이터로 전달했다. Child 컴포넌트는 이 값을 받기 위해 componet 함수 옵션 객체에 props 배열에 'name' 값을 추가하면된다. 필요한 만큼 배열에 추가하는 방식이다.

한편, 자녀 -> 부모 방향으로는 데이터를 전달이 불가능하다. 커뮤니케이션하는 방법이 없을까? 바로 이벤트를 전달하는 방식이다. 버튼 엘레멘트가 클릭 이벤트를 발생하고 이를 상위 엘레맨트의 누군가 수신하듯이 말이다. 뷰js에서는 자식이 이벤트를 발생(emit)하면 부모에서는 이 이벤트를 수신할수 있는 핸들러 함수와 연결해 주면 된다.

<Child v-on:typed="onTyped"></Child>

<script>
  Vue.component("NameForm", {
    el: "#child",
    methods: {
      onClick: function () {
        this.$emit("typed")
      },
    },
  })
</script>

Child 컴포넌트는 onClick 메서드에서 'typed' 이벤트를 발생시킨다. 그럼 부모쪽에서는 typed 이벤트에 연결된 onTyped 메서드를 실행할수 있다.

작은 규모의 어플리케이션에서는 이러한 방법으로 컴포넌트간의 상태를 공유할 수 있지만 규모가 커지면 상태 관리가 어렵게 될것이고 누군가의 도움이 필요하다. 리엑트 진영에서는 Redux같은 Flux 구현체들을 만들어 사용하듯이 뷰js에서도 Vuex라는 상태관리 솔루션을 사용할 수 있다.

내가 보기에 뷰js 컴포넌트의 꽃은 단일파일 컴포넌트다. 하나의 파일에 템플릿, 스타일, 로직을 모두 집어 넣을수 있으니 말이다. 그래서 파일 단위로 컴포넌트를 관리할 수 있다. 물론 빌드 과정을 거지치면서 다시 HTML, CSS, JS 로 나눠지겠지만 개발자가 코드를 컴포넌트 단위로 관리할수 있다는 점이 마음에 든다.

vue-component-in-one-file
vue-component-in-one-file

이런식으로 컴포넌트를 만들고 이것을 모듈시스템으로 가져와서 사용할수 있다.

import Hello from "./Hello.vue"
;<template>
  <Hello></Hello>
</template>

ES6의 모듈시스템을 사용하려면 웹팩과 바벨의 도움을 받아야 한다. 직접 개발환경을 꾸밀수도 있지만 그 전에 쉽고 빠르게 구성해주는 도구도 있다. vue-cli. 이 녀석이 만들어 내는 코드를 분석해 보는 것도 뷰js 개발 환경을 이해하는데 도움이 될듯하다.

정리

앵귤러에서는 ng- 로 시작하는 디렉티브를 사용한다. 뷰js를 사용할때 ng- 를 v-로 변경해서 사용하면 대부분 맞다. ng-repeat만 v-for로 사용하는게 다른 점이다. 그만큼 앵귤러 프레임웍을 사용해 봤다면 뷰js는 친숙하다.

가상돔, 컴포넌트 기반 개발, UI 코어에 집중하는 점은 리엑트와 유사하다. 사실 앵귤러가 적응하면 편하긴 하지만 그들의 스타일을 너무 강요하는 만큼 나만의 스타일을 보여주기가 쉽지 않았다. UI 코어만 다루는 리엑트나 뷰js 따위의 라이브러리도 사용해 보고 싶었다.

뷰js가 리엑트보다 끌리는 점은 지금 바로 시작할 수 있다는 점이다. JSX와 ES6 그리고 웹팩과 바벨을 사용하지 않아도 뷰js 컴포넌트를 내 코드에서 사용할 수 있는 점은 유연한 측면에서 상당히 매력적이다. 물론 이런 툴과 기술을 사용하면 더 재밌는 프로그래밍을 할 수 있다.

이 다음엔 이것들을 살펴보자.

  • Vuex: Flux 아이디어와 유사한 상태 관리 패턴 라이브러리
  • Vue-resource: XMLHttpRequest, JSONP 라이브러리 ($.ajax 안써도 됨)
  • Vue-router: SPA 구현을 위한 라우터 클라이언트 라이브러리
  • Weex: 모바일 크로스플랫폼 개발을 위한 라이브러리 (React Native 같은것)