FE-Weekly-Pick에서는 최근에 팀 내에서 진행했던 스터디 내용을 정리하는 의미에서, 4회에 걸쳐 자바스크립트 (프론트엔드) 프레임워크를 소개하는 시리즈를 연재할 예정입니다. 금주부터 아래와 같은 목차로 진행되니 많은 관심 부탁드립니다.
Vue.js는 Evan You가 만들었으며, 2014년 릴리즈를 시작으로 꾸준히 발전하고 있는 자바스크립트 프레임워크이다. 앞서 소개된 Angular나 React보다 덜 알려져 있으나, 최근 Vue.js 포럼에 한국어 카테고리가 추가될 정도로 인지도에 상승 곡선을 그리는 추세이다. Vue.js를 접하는 사람들의 반응은 두 가지이다. 낯설거나 혹은 친절하거나. 전자는 (현재 이 글을 읽고 있는 당신을 포함하여) Vue.js를 처음 접하는 사람일 것이고, 후자는 단 한 번이라도 Vue.js의 가이드를 읽어보거나 사용해보고 싶다는 생각을 한 사람일 것이다.
Vue.js는 발음대로 철저히 뷰(View)에 최적화 된 프레임워크이다. 컨트롤러 대신 뷰 모델을 가지는 MVVM(Model-View-ViewModel) 패턴을 기반으로 디자인 되었으며, 컴포넌트(Components)를 사용하여 재사용이 가능한 UI들을 묶고 뷰 레이어를 정리하는 것을 가장 강력한 기능으로 꼽는다. 또한 템플릿(Template) 위주의 개발을 권장한다. 이러한 특징들은 다음 목차에서 다루어질 내용의 미리보기 격으로, 앞에서 장황하게 설명하기보다 본론으로 들어가 샘플 코드와 함께 Vue.js를 이해하는 것이 더 좋을 것이다. 최근 Vue.js는 2.0 버전으로 업데이트 되었으며 앞으로 보게 될 예제 또한 최신 버전 기준으로 작성된 점을 참고하길 바란다.
기본적으로 Vue.js는 사용할 때 신경 써야 할 의존성을 가지고 있지 않다. 2.0부터 Virtual DOM의 구현체인 Snabbdom을 사용하고 있지만, 필요에 의해 수정한 소스로 내장해 사용하고 있어 따로 설치하거나 버전을 신경 쓸 필요가 없다. Vue.js는 템플릿 엔진을 따로 사용하지 않고 웹 컴포넌트 스펙과 유사하게 구현된 HTML 형식의 커스텀 엘리먼트를 이용해 Virtual DOM 렌더 함수로 컴파일하도록 내부에서 구현했다.
vue-cli는 프로젝트를 쉽게 구성할 수 있도록 미리 정의된 설정들을 사용할 수 있게 한다. Vue.js 전용 Yeoman이라고 생각하면 된다.
$ npm install -g vue-cli
npm
을 이용해 쉽게 설치가 가능하며, 아래와 같은 기본 설정들을 제공한다.
webpack
: Webpack, vue-loader, 정적 분석, 테스트 등 기본 빌드 프로세스 대부분을 설정webpack-simple
: Webpack과 vue-loader로 구성된 간단한 조합browserify
: Browserify, vueify, 정적 분석, 테스트 등 기본 빌드 프로세스 대부분을 설정browserify-simple
: Browserify와 vueify로 구성된 간단한 조합simple
: 특별한 모듈 관리 도구를 사용하지 않고 HTML 파일 1개로 구성하는 제일 간단한 조합아래와 같이 사용한다.
$ vue init webpack projectName
프로젝트 규모나 필요에 의해 옵션을 선택해서 미리 구성된 설정들을 이용할 수 있기 때문에, 일일이 설정하면서 생기는 문제들로 스트레스를 받지 않는다.
Vue.js는 인스턴스 생성 시 인자로 전달받는 data
객체를 이용해 상태를 관리하게 되는데, data
안의 내용이 변경되면 내부에서 인지하고 그 데이터를 사용하는 컴포넌트들이 스스로 업데이트를 한다. 이때 디버깅의 편의를 위해 data
에 직접 접근하지 않고 따로 스토어 패턴을 이용해서 데이터를 관리하는 객체를 구현할 수 있으며, 결국 Flux 아키텍처와 유사한 구조도 생각해 볼 수 있다. 이렇게 간단하게 스토어를 구현할 수 있으나 직접 만들지 않고, Flux와 Elm 아키텍처에서 영감을 얻어 Vue.js를 확장한 Vuex 사용도 가능하다. 여기서 직접 다루진 않겠지만, 문서 내용을 보면 Flux 아키텍처를 알고 있는 사람이라면 익숙한 단어와 그림이 보일 것이다.
요즘 프론트엔드 프레임워크는 그 자체도 중요하지만, 개발할 때 사용할 전용 개발 도구의 지원도 필수가 된 것 같다. Vue.js도 React와 Redux의 개발 도구만큼 유용한 전용 크롬 개발도구 플러그인을 지원한다. react-devtools처럼 커스텀 엘리먼트의 계층 구조와 바인드 된 컴포넌트 정보를 볼 수 있으며 Vuex 관련 도구도 지원한다.
(출처: https://github.com/vuejs/vue-devtools)
Vue.js에서는 단일 파일 컴포넌트(Single File Components)를 제공하는데, 이 단일 파일 컴포넌트를 정상적으로 사용하려면 Webpack이나 Browserify를 통해 빌드한 후 브라우저에서 사용 가능한 자바스크립트를 한 파일로 번들링하는 과정을 거쳐야 한다. 여기서 필요한 각 도구의 설정들은 직접 할 필요 없이 vue-cli를 이용해 쉽게 세팅 적용이 가능하다. (단일 파일 컴포넌트에 대한 설명은 4. 컴포넌트 목차에서 대신한다)
React에서 사용하는 JSX를 Vue.js에서도 이용하고자 한다면 render
함수를 직접 작성하고 바벨 플러그인을 이용해 번들링 단계에서 적용할 수 있다.
구현 코드는 React와 상당히 유사한데 아래와 같다.
import AnchoredHeading from "./AnchoredHeading.vue";
new Vue({
el: "#demo",
render(h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
);
}
});
React, Angular 2와 마찬가지로 대부분의 모던 브라우저를 지원하며 인터넷 익스플로러는 9 버전부터 지원한다. Compatibility Note에 적혀있는 내용에 따르면 ES5 기능 중에 폴리필이 불가능한 기능을 이용하기 때문이라고 한다. 그래서 추가적인 폴리필을 이용해도 낮은 버전의 인터넷 익스플로러 지원은 불가능해 보인다.
뷰 모델(View Model)은 Vue.js의 기초이자 절대 놓쳐서는 안 될 개념 중 하나이다. 뷰 모델은 다음 한 줄로 요약할 수 있다.
MVVM 패턴의 VM에 해당하며, MVC 패턴에서 컨트롤러 역할처럼 데이터를 관리하고 액션을 처리한다.
(출처: https://en.wikipedia.org/wiki/Model-view-viewmodel)
Vue.js에서는 Vue
생성자 함수의 인스턴스를 생성하면서 뷰 모델을 다루게 된다. 아래 예제와 같이 인스턴스를 생성할 때 뷰와 데이터를 연결하기 위한 옵션을 설정할 수 있다. 자주 사용되는 옵션 정보는 다음과 같으며, 더 많은 옵션은 API 문서에서 Options
카테고리에서 확인할 수 있다.
el
, template
data
, methods
, computed
components
created
, mounted
, updated
, destroyed
뷰 모델의 인스턴스가 정상적으로 생성되었다면 Vue.js를 사용하기 위한 절반의 준비는 끝났다.
var vm = new Vue({
el: '#example', // DOM
data: { // Plain Data
firstName: 'Foo',
lastName: 'Bar'
},
methods: {
getFullName: function() { // Methods For Data
return this.firstName + ' ' + this.lastName;
}
},
created: function() { // Lifecycle Hooks
console.log(this.firstName, this.lastName);
}
...
});
Vue.js에서 뷰 모델이 생성되는 순간부터 중요한 사이클이 시작된다. 생명 주기(Lifecycle)라고 부르며 다음 다이어그램을 이해한다면, 위에서 뷰 모델을 생성하기 위해 설정된 옵션들이 어떠한 용도로 사용되었는지 파악할 수 있다. 다이어그램에서 빨간색 박스는 생명 주기의 단계를 나타내며, 뷰 모델 생성 옵션 중 생명 주기 훅에 해당하는 콜백 메서드들이 실행되는 시점이기도 하다. 생명 주기 훅을 기준으로 생명 주기 각 단계에서 하는 일을 정리하면 다음과 같다.
beforeCreate
~ created
: 데이터 및 이벤트 초기화created
~ beforeMount
: 뷰 생성mounted
~ updated
: 데이터 바인딩, 데이터 변경 주시 및 뷰 업데이트destroyed
: 자식 컴포넌트, 이벤트 리스너 해제자, 얼마나 많은 일이 뷰 모델 인스턴스 안에서 처리되고 있는가?
(출처: http://vuejs.org)
Vue.js의 데이터를 외부로 보여주기 위해서 어떻게 해야 될까? 뷰 모델 생성에 필요한 옵션 중 template
혹은 el
프로퍼티를 기억하는가? Vue.js에서 데이터는 렌더링 된 DOM에 바인딩 되어 처리되는데, 이러한 처리를 위해 템플릿이 사용되며 HTML 기반의 템플릿 사용을 권장한다. 이는 JSX와 같은 문법을 따로 배우지 않아도, 애플리케이션 개발자가 알고 있는 HTML 기초 지식 안에서 프로그래밍할 수 있다는 뜻이다. (물론 JSX 사용도 가능하다)
다음은 뷰 모델의 템플릿을 실제 DOM과 매핑하고, 템플릿에 데이터 값 name
을 바인딩하여 <span>
에 출력하는 예제이다. Mustaches라고 불리는 {{ }}
태그 안에 data
객체의 프로퍼티 값을 출력하면 된다. 일반적인 자바스크립트 템플릿 엔진 사용 방법과 같다. 예제를 실행하면 'My framework is Vue.js!' 문장을 볼 수 있다.
<div id="example">
<p>My framework is {{ name + '!' }}</p>
</div>
var vm = new Vue({
el: "#example",
data: {
name: "Vue.js"
}
});
여기에 조금의 양념을 더해 템플릿을 반응적(Reactive)으로 만들 수 있다. 이때 필요한 것이 지시자(Directives)이다. 지시자는 HTML 컴파일러가 실행될 때 해당 DOM을 찾기 위해 사용되는 특별한 형태의 속성(Attribute)이다. Vue.js의 지시자는 그 이름답게 접두사 v-
로 시작하며, 스타일에서부터 이벤트까지 DOM을 다룰 수 있는 모든 범위에서 다양한 지시자를 제공한다. 지시자를 가진 엘리먼트에 특정 액션이 발생했을 때, 지시자에 할당된 자바스크립트 구문이 실행되고 상황에 따라 데이터와 템플릿 상태가 변경된다. 이전 예제에 v-on
지시자를 가진 버튼을 추가한 후 클릭 이벤트를 발생시키면 name
값이 자동으로 업데이트된다.
<div id="example">
<p>My framework is {{ name + '!' }}</p>
<button v-on:click="changeName">Change</button>
</div>
var vm = new Vue({
el: "#example",
data: {
name: "Vue.js"
},
methods: {
changeName: function() {
this.name = this.name.toLowerCase(); // Vue.js --> vue.js
}
}
});
뷰 모델과 템플릿으로 Vue.js의 외형을 살펴보았다면, 이번에는 깊은 범위에서 데이터를 살펴보려고 한다. 지시자를 설명하는 부분에서 반응적(Reactive)이라는 표현이 사용되었는데, 이는 Vue.js가 데이터를 다루는 방식을 명확하게 보여준다. Vue.js에서는 데이터가 변경되었을 때 해당 데이터를 참조하고 있는 뷰도 함께 업데이트 된다. 직접 DOM 엘리먼트를 찾아 텍스트를 변경해야만 하던 수고의 과정들을 Vue.js가 대신 처리하고 있는 셈이다. 어떻게 이러한 동작이 가능할까?
뷰 모델 인스턴스는 watcher
객체를 포함하고, 인스턴스 옵션 중 data
는 Vue.js의 데이터로써 순수한 자바스크립트 객체이다. watcher
의 역할은 데이터의 변경을 감시하고 있다가 값이 변경되면 지시자에게 이를 알리고 해당 지시자를 가진 DOM을 업데이트할 수 있도록 도와준다. 2.0 버전에서는 Virtual DOM을 사용하면서 DOM 업데이트 방식이 변경되었으나, watcher
라는 중요한 게이트를 두고 데이터가 이동된다는 핵심은 변함이 없다.
아래 이미지는 각각 1.0 (상), 2.0 (하) 버전에서의 데이터 흐름 상태를 보여준다.
(출처: http://vuejs.org)
컴포넌트에 대해서 들어본 적이 있는가? 컴포넌트는 웹 애플리케이션을 구성하는 최소의 단위이자, 얼마나 UI를 깔끔하게 정리하고 재사용할 수 있는가에 대한 척도가 되는 중요한 기능이다. Vue.js에서도 컴포넌트가 제공되며, 컴포넌트를 사용했을 때 비로소 Vue.js를 제대로 만져보았다고 할 수 있다. 이번 목차를 따로 분류한 이유는 아키텍처에서 설명한 뷰 모델과 템플릿을 기초로 컴포넌트가 생성되기 때문이다. 응용 단계의 스텝을 밟아보자.
(출처: http://vuejs.org)
뷰 모델 인스턴스의 생성부터 시작한다. 이때 생성되는 뷰 모델 인스턴스는 등록하려는 컴포넌트의 영역을 지정하고 컴포넌트들을 관리한다. 컴포넌트의 목적은 공통의 템플릿을 사용하여 여러 데이터를 표현하는 것이기 때문에 글로벌 영역에 컴포넌트를 등록해야 한다. (지역적으로도 등록하여 사용할 수 있지만 '공통'이라는 단어와 거리가 있어 보여 이번 설명에서는 제외한다)
이 때 새로운 개념이 등장한다. 커스텀 엘리먼트로 컴포넌트의 살을 붙이기 위한 일종의 뼈대라고 생각하면 이해하기 쉽다. 컴포넌트의 살은 템플릿에 해당하며, 컴포넌트가 등록될 때 매칭되는 커스텀 엘리먼트를 찾아 템플릿으로 교체된다. 예제에서는 <my-component>
가 커스텀 엘리먼트로, 컴포넌트 등록 이후에는 <p>My framework is Vue.js!</p>
으로 바뀐다. 커스텀 엘리먼트 대신 지시자를 사용해 실제 DOM에 매핑할 수도 있는데, 자세한 사용 방법은 조합된 컴포넌트 페이지를 참고하면 된다.
<div id="example">
<my-component></my-component>
</div>
// 컴포넌트 등록
Vue.component("my-component", {
template: "<p>My framework is Vue.js!</p>"
});
// 뷰 모델 인스턴스 생성
var vm = new Vue({
el: "#example"
});
이번에는 데이터를 바인딩하고 재사용이 가능한 컴포넌트를 생성해 보려고 한다.
아래 예제를 보았을 때 그림이 그려지는가? 3개의 버튼의 겉모습은 같지만 각 컴포넌트가 참조하는 데이터가 다르고 액션에 따라서 각각의 데이터가 변화하게 된다. 10개의 버튼을 등록한다고 할지라도 컴포넌트 등록은 한 번이면 충분하다. 컴포넌트의 데이터는 뷰 모델과 동일하게 data
옵션으로 관리되며, Vue
인스턴스의 data
와 다른 점으로 순수 객체 대신 콜백 함수를 통해 반환된 값을 사용한다. 이는 각각의 컴포넌트가 같은 데이터를 공유하지 않기 위함이다. 이러한 개념을 활용하면 여러 페이지 안에서 공통으로 사용되는 UI(예를 들어 게시판의 리스트와 같은)를 분리하고 데이터만 교체하여 사용이 가능해진다. 반복적인 일은 모두 Vue.js의 컴포넌트에게 맡기면 된다.
<div id="example">
<simple-counter></simple-counter>
<simple-counter></simple-counter>
<simple-counter></simple-counter>
</div>
Vue.component("simple-counter", {
template: '<button v-on:click="increase">{{ counter }}</button>',
data: function() {
return {
counter: 0
};
},
methods: {
increase: function() {
this.counter += 1;
}
}
});
var vm = new Vue({
el: "#example"
});
위 예제처럼 항상 같은 레벨에 위치한 컴포넌트만 생성할 수 있는 것은 아니다. 컴포넌트는 또 다른 컴포넌트를 포함할 수도 있고, 1개의 템플릿 안에서 부모 - 자식 관계를 형성하면서 여러 종류의 컴포넌트 생성과 조합이 가능하다.
여기서 눈여겨보아야 될 부분은 데이터 통신 방식이다. 부모 컴포넌트가 가지는 데이터는 자식 컴포넌트에서도 사용할 수 있으며, 부모에서 변경이 일어났을 때 관련된 자식 컴포넌트들이 최신 상태에 맞추어 업데이트된다는 특성이 있다. 자식 컴포넌트가 업데이트되었을 때는 부모에게 변경된 데이터를 전달하는 대신 '변경되었음!'을 알리는 이벤트를 발생하게 된다. 이러한 관계를 단방향 이벤트 플로우(One-Way Data Flow)라고 부르며, 규모가 큰 애플리케이션에서 데이터의 흐름을 쉽게 만들 수 있다는 장점이 있다.
(출처: http://vuejs.org)
위에서 설명한 내용을 구현한 예제 코드이다. vm
인스턴스는 부모 역할을 하며 자식 컴포넌트로 child-component
를 가지는데, 이때 자식 컴포넌트의 props
속성을 통해 부모의 데이터를 전달받게 된다. 부모의 데이터 message
는 자식 컴포넌트에서 참조 가능하며, 버튼을 클릭했을 때 message
값이 변경되면서 자식 컴포넌트의 뷰에서 보여지는 값도 자동으로 업데이트된다.
<div id="example">
<child-component v-bind:message="message"></child-component>
<button v-on:click="changeMessage">Change</button>
</div>
Vue.component("child-component", {
template: "<p>{{ fullMessage }}</p>",
props: ["message"],
data: function() {
return {
name: "Vue.js"
};
},
computed: {
fullMessage: function() {
return this.message + this.name + "!";
}
}
});
var vm = new Vue({
el: "#example",
data: {
message: "My component is "
},
methods: {
changeMessage: function() {
this.message = "Your component is ";
}
}
});
Vue.js는 컴포넌트를 좀 더 효율적으로 관리할 방법도 제공한다. Vue.component
API로 컴포넌트를 생성하는 방식은 중소규모의 프로젝트에 적합하다. 하지만 몇 가지 주의해야 할 점이 있다.
/
)를 이용해야 한다.그래서 vue
확장자로 저장되는 단일 파일 컴포넌트를 이용해, 파일 하나에 템플릿부터 CSS스타일 그리고 컴포넌트까지 모두 정의해 모듈화 할 수 있다. 단일 파일 컴포넌트를 사용하게 되면 컴포넌트의 물리적 관리가 쉬워진다. 또한 jade
, TypeScript
, SCSS
등 특정 언어의 사용도 가능해진다.
(출처: http://vuejs.org)
마지막으로 라우팅까지 더하면 Vue.js의 컴포넌트 기능은 완벽해진다. 가장 작은 단위의 컴포넌트들을 묶고 조합하면 Vue.js만의 SPA를 완성할 수 있다.
번들링 환경만큼 테스트 환경도 처음 구성할 때 여러 가지 시행착오를 겪게 되고 적지 않은 시간을 사용하게 되는데, 간단하게 vue-cli를 이용해 미리 구성된 테스트 환경 이용을 추천한다. 어떤 조합의 테스트 환경도 적용이 가능하겠지만, vue-cli는 가장 대중적인 Karma와 테스트 프레임워크 조합으로 환경을 만들어 준다. vue-cli를 이용해 Webpack이나 Browserify의 풀 설정으로 설치를 해야하며, 설치를 시작하기 전에 터미널에서 Mocha 혹은 Jasmine으로 테스트 프레임워크를 선택할 수 있다.
$ npm run test
위 스크립트를 실행하면 미리 설정된 테스트 환경에 의해 유닛 테스트가 하나씩 실행된다. 기본적으로 PhantomJS 환경만 설정되어 있지만 Karma 플러그인을 추가 설치하여 다양한 브라우저 환경도 간단히 추가할 수 있다.
컴포넌트의 테스트 케이스를 작성할 때는 컴포넌트에 옵션을 각각의 케이스에 맞게 다르게 적용한 후 컴포넌트 안에서 만들어지는 Virtual DOM 객체를 확인하는 형태로 유닛 테스트를 작성한다.
import Vue from "vue";
import Hello from "src/components/Hello";
describe("Hello.vue", () => {
it("should render correct contents", () => {
const vm = new Vue({
el: document.createElement("div"),
render: h => h(Hello)
});
expect(vm.$el.querySelector(".hello h1").textContent).to.equal(
"Hello Vue!"
);
});
});
위 예제 코드는 아주 간단한 단일 파일 컴포넌트를 테스트하는 예제이며, 컴포넌트 인스턴스의 $el
로 Virtual DOM 객체에 접근해서 DOM을 테스트하듯 테스트할 수 있다.
Vue.js는 다른 프레임워크들과 성능 비교를 시도하고 그 결과도 제공하고 있어, 프레임워크 적용 시 퍼포먼스를 고려하는 사람들에게 많은 도움이 될 것이다.
(출처: http://vuejs.org)
Vue.js는 반응적(Reactive)이고 뷰 컴포넌트를 제공한다는 유사점으로 인해 React와 자주 비교된다. 위 수치는 React와 렌더링에 대한 성능 비교를 한 결과이다. 다양한 상황에서 렌더링 속도를 측정하였으며, 어느 구간을 살펴보아도 React보다 Vue.js의 렌더링 속도가 빠르다는 것을 알 수 있다. Vue.js 1.0 버전에서는 Virtual DOM 대신 실제 DOM 템플릿을 사용하는 구조 때문에, 그리고 2.0 버전에서는 메모리 소모와 성능을 개선한 Virtual DOM을 도입함으로써 React보다 더 나은 성능을 보인다고 한다.
Angular 1과 비교하였을 때도 우세하다. Angular 1의 경우 Dirty Checking을 수행하지만, Vue.js는 비동기 큐 옵저버 시스템을 이용해 의존 관계가 명시적이지 않은 경우에는 독립적으로 이벤트를 발생하도록 처리한다. 그래서 빠르다. Angular 2와 비교하면 성능에는 큰 차이가 없다고 언급되지만, Vue.js가 23kb의 경량 프레임워크라는 점에서 앞선다.
Vue.js를 만드는 사람들은 계속해서 그들의 프레임워크를 좀 더 보기 좋고 편하게 사용할 수 있도록 개선하고 있다. 이는 가이드 페이지와 깃헙의 로그 몇 줄만 보아도 알 수 있다. Vue.js는 목표는 프론트엔드 개발자들이 부담 없이 입문하고 쉽게 애플리케이션을 생산해 낼 수 있도록 지원하는 것이 아닐까 추측된다.
Vue.js는 다른 프레임워크들에 비해서 덜 복잡하고 덜 꾸며져 있다. Vue.js를 사용하기 위해 처음부터 어려운 개념들을 익힐 필요가 없으며, 필요에 따라 플러그인과 라이브러리를 제공하기 때문에 유연하다. 또한 사용자 중심의 프레임워크가 분명하다. 2.0 버전 업데이트 내역을 살펴보면, Virtual DOM을 도입하는 등 내부적으로 큰 변화가 있었지만 API를 포함한 캡슐화 영역에서는 변동 사항이 거의 없었다. 사용자들을 당황스럽게 만들지 않기 위한 나름의 배려로 보인다. (심지어 마이그레이션 방법도 제공한다!)
Vue.js는 친절하다. 그리고 쉽다. 새로운 애플리케이션을 원한다면 지금 바로 Vue.js를 설치해보라!