Vue (2.x)를 이용해 빠르게 애플리케이션을 개발하는데 필요한 최소한의 것을 정리해 본다.
Vue 로 개발할 때 빠르게 숙지하면 좋을만한 것들, 실제로 작업하면서 찾아보았던 것들 위주로 정리했다.

Vue 공식 가이드 문서에는 아래와 같은 문장이 있다.

Vue를 시작하기 위해 일반적으로 개발자가 일상적인 애플리케이션을 빌드하는데 필요한 충분한 지식을 얻기 위해 가이드를 읽는데 하루가 걸리지 않습니다.

가이드를 참고하여 알아두면 좋을 만한 부분만 일부 발췌하여 작성했다.

Vue.js ?

Vue는 고전적인 웹기술을 받아들여서 그 기반위에 만들어 졌다.
HTML 기반 템플릿을 이용하면 기존의 어플리케이션을 Vue로 점진적으로 이전하기가 훨씬 쉽다.
(React에서는 모든 것이 JavaScript이다. JSX를 통해서 HTML 구조가 들어와있고, CSS관리도 JavaScript에서 하는 추세)
일부 Vue의 문법은 Angular와 매우 유사하다. (예: v-if / ng-if)

Vue는 Angular에 비해서 유연하고, 러닝커브가 적다. HTML 및 기존 JavaScript(ES5)에 익숙해진 것 만으로 개발을 시작할 수 있다. (공식 Typings공식 decorator를 통해 TypeScript와 함께 사용할 수도 있다.)

Angular는 TypeScript가 필수다.

개인적인 생각으로는 React의 경우 프론트엔드 조직이 갖추어진 회사이고, 해당 어플리케이션이 메인 서비스인 경우 (고객이 있는) 주로 사용하거나 사용할 수 있는 것 같고 상대적으로.. Angular, Vue의 경우 서비스 운영자를 위한 백오피스 개발에 적합한 것 같다.

Vue의 경우에 SI 프로젝트에서 상대적으로 많이 채택하는 것 같은 이유가 있다면 러닝커브가 적고 유연하기 때문일 것 같다. 다만 강제성이 적고 유연하다는 이유로 jquery와 믹스해서 사용한다던지(기존 프로젝트를 점진적으로 전환하기 위한 목적도 있겠지만..) 등의 다양한 형태를 볼 수 있다…

다른 프레임워크와의 비교

알아두기

Vue 인스턴스

  • 각 Vue 인스턴스의 data가 변경되면 화면은 다시 렌더링 된다.

  • Vue 인스턴스는 데이터 속성 이외에도 유용한 인스턴스 속성 및 메소드를 제공한다. 다른 사용자 정의 속성과 구분하기 위해 $ 접두어를 붙인다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var data = { a: 1 }
    var vm = new Vue({
    el: '#example',
    data: data,
    created: function () {
    // `this` 는 vm 인스턴스를 가리킵니다. "a is: 1"
    console.log('a is: ' + this.a)
    }
    })

    vm.$data === data // => true
    vm.$el === document.getElementById('example') // => true

    API reference 참고

    • 인스턴스가 마운트 된 이후, 그 엘리먼트는 vm.$el 로 엑세스 할 수 있다.
    • 인스턴스가 생성된 이후 원래 데이터 객체는 vm.$data로 접근할 수 있다. Vue 인스턴스는 데이터 객체에 있는 모든 속성을 프록시 한다. vm.avm.$data.a와 동일하다.
  • 모든 라이프 사이클 훅은 this 컨텍스트가 호출하는 Vue 인스턴스를 가리키며 호출 된다.

라이프 사이클 다이어그램

각 Vue 인스턴스는 생성될 때 일련의 초기화 단계를 거친다.
그 과정에서 사용자 정의 로직을 실행할 수 있는 라이프사이클 훅 도 호출된다.

https://kr.vuejs.org/images/lifecycle.png

Creation (Initialization)

  • 라이프 사이클 중에 가장 처음 실행 된다.
  • 컴포넌트가 DOM에 추가되기 전 필요한 작업
  • 이 단계에서 제공되는 Hook은 beforeCreate, Created
  • 서버 측 렌더링 중에도 실행
beforeCreate
  • 모든 Hook 중에 가장 먼저 실행 된다.
  • 아직 data와 events(vm.$on, vm.$once, vm.$off, vm.$emit) 가 세팅되지 않은 시점.
created
  • 컴포넌트 초기에 세팅되어야할 데이터를 패치한다.
  • data와 events가 활성화되어 접근할 수 있는 상태.
  • 부모의 created Hook이 자식의 created Hook 보다 먼저 실행된다.
  • 템플릿과 가상돔(virtual DOM)은 마운트 및 렌더링 되지 않은 상태.
    • $el 을 사용하여 엘리먼트에 접근할 수 없다.

Mounting (DOM Insertion)

  • 초기 렌더링 직전과 직후에 컴포넌트에 직접 접근할 수 있다.
beforeMount
  • 초기 렌더링이 발생하기 직전과 템플릿 또는 렌더링 함수가 컴파일 된 후에 실행 된다.
  • 대부분의 경우 사용하지 않는 것이 좋다.
  • 서버 사이드 렌더링 중에는 실행되지 않는다.
mounted
  • 컴포넌트, 템플릿, 렌더링된 돔에 접근할 수 있다. ($el 사용 가능)
  • 단, 모든 하위 컴포넌트가 마운트된 상태를 보장하지는 않는다.
  • 단, 부모와 자식 관계의 컴포넌트에서 우리가 생각한 순서로 mounted가 발생하지 않는다.
    • 부모의 mounted Hook이 자식의 mounted Hook 보다 먼저 실행되지 않는다.
    • 부모의 mounted Hook을 실행하기 전에 자식의 mounted Hook이 끝나기를 기다린다.

Updating (Diff & Re-render)

  • 컴포넌트에서 사용되는 반응형 속성들이 변경되거나 다른 이유로 재 렌더링 될 때마다 실행된다.
  • 디버깅이나 프로파일링 등을 위해 컴포넌트 재 랜더링 시점을 알고 싶을 때 사용하면 된다.
beforeUpdate
  • DOM이 재 렌더링 되고, 패치되기 직전에 실행된다.
  • 실제 렌더링 되기 전에 컴포넌트에 있는 데이터의 새로운 상태를 알 수 있다.
updated
  • DOM이 업데이트 완료된 상태
    • 컴포넌트의 데이터가 변해서 재 렌더링이 일어난 후에 실행
  • DOM 업데이트 후 엑세스 해야할 경우 사용

Destruction (Teardown)

beforeDestroy
  • 해체 (뷰 인스턴스 제거) 되기 직전에 호출된다.
  • 컴포넌트는 원래 모습과 모든 기능을 그대로 가지고 있다.
  • 이벤트 리스너를 제거하거나 reactive subscription을 제거할 때 유용.
destroyed
  • 컴포넌트가 DOM에서 제거 될 때 발생한다.

참고

템플릿 문법

디렉티브

  • v- 접두사가 있는 특수 속성
  • v-bind, v-if, v-on, v-model, ..
    • v-bind : 단반향 바인딩
    • v-model : 양방향 바인딩
약어
1
2
3
4
5
6
7
8
9
10
11
<!-- 전체 문법 -->
<a v-bind:href="url"> ... </a>

<!-- 약어 -->
<a :href="url"> ... </a>

<!-- 전체 문법 -->
<a v-on:click="doSomething"> ... </a>

<!-- 약어 -->
<a @click="doSomething"> ... </a>
v-for

Vue에서 개별 DOM 노드들을 추적하고 기존 엘리먼트를 재사용, 재정렬하기 위해서 v-for의 각 항목들에 고유한 key 속성이 필요하다.
key에 대한 이상적인 값은 각 항목을 식별할 수 있는 고유한 ID로 문자열이나 숫자를 사용해야 한다.
v-bind를 사용하여 동적 값에 바인딩 해야한다.

1
2
3
<div v-for="item in items" v-bind:key="item.id">
<!-- content -->
</div>

왜냐하면..

Vue가 v-for에서 렌더링된 엘리먼트 목록을 갱신할 때 기본적으로 “in-place patch” 전략을 사용합니다. 데이터 항목의 순서가 변경된 경우 항목의 순서와 일치하도록 DOM 요소를 이동하는 대신 Vue는 각 요소를 적절한 위치에 패치하고 해당 인덱스에서 렌더링할 내용을 반영하는지 확인합니다.

computed

1
2
3
4
5
6
7
<p>뒤집힌 메시지: "{{ reversedMessage() }}"</p>
// 컴포넌트 내부
methods: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
}
}

computed 속성은 종속 대상을 따라 저장(캐싱)된다.

computed 속성은 해당 속성이 종속된 대상이 변경될 때만 함수를 실행한다. 즉 message가 변경되지 않는 한, computed 속성인 reversedMessage를 여러 번 요청해도 계산을 다시 하지 않고 계산되어 있던 결과를 즉시 반환한다.

또한 Date.now()처럼 아무 곳에도 의존하지 않는 computed 속성의 경우 절대로 업데이트 되지 않는다.

1
2
3
4
5
computed: {
now: function () {
return Date.now()
}
}

이에 비해 메소드를 호출하면 렌더링을 다시 할 때마다 항상 함수를 실행한다.

필터

Vue는 텍스트 형식화를 적용할 수 있는 필터를 지원한다.

1
2
3
4
5
<!-- 중괄호 보간법 -->
{{ message | capitalize }}

<!-- v-bind 표현 -->
<div v-bind:id="rawId | formatId"></div>

로컬필터 정의

1
2
3
4
5
6
7
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}

전역필터 정의

1
2
3
4
5
6
7
8
9
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
// Vue 인스턴스 생성하기 전에 전역필터 정의
new Vue({
// ...
})

그밖에

  • 필터는 체이닝이 가능하다.
  • 필터는 두개 이상의 인수를 받을 수 있다.
    1
    2
    {{ message | filterA | filterB }}
    {{ message | filterA('arg1', arg2) }}

컴포넌트

v-on 을 이용한 사용자 지정 이벤트

자식 컴포넌트가 로드되었을 때 (마운트가 완료된 이후에) 부모컴포넌트에서 처리해야 하는 작업이 있다면?

자식 컴포넌트ChildComponent.vue의 라이프사이클 mounted훅에

1
this.$emit('init');

그리고 부모 컴포넌트에서 자식 컴포넌트를 사용할 때

1
<child-component @init="initChildComponent" ..

와 같이 자식 컴포넌트가 로드된 이후에 실행할 method를 정의해서 사용하면 된다.

.sync 수식어

부모 컴포넌트와 자식 컴포넌트가 양방향으로 데이터 바인딩이 필요할 때 사용.
부모컴포넌트에서 자식 컴포넌트를 사용할 떄

1
2
3
<more :flag.sync="flag"></more>

// flag 는 false

하위 컴포넌트에서 flag 를 갱신하려면 명시적으로 이벤트를 보내면 된다.

1
2
3
this.$emit('update:flag', true)

// this.$emit('update:prop이름', 값);

이렇게 하면 부모와 자식 컴포넌트 모두 값이 변경된다.

예를들어

버튼이 있는 공통 컴포넌트인데 해당 버튼이 클릭되었을 때 상위 컴포넌트의 특정영역을 노출 또는 비노출 되도록 구현하는 경우 사용할 수 있다.
또는 상위컴포넌트에 2개의 A, B 하위컴포넌트가 있고, 하위컴포넌트에서 모두 영향을 미치는 공통의 값이 필요한 경우 사용할 수 있다.
(부모 컴포넌트와 다수의 자식 컴포넌트간의 양방향 데이터 바인딩이 가능하다.)

반응형에 대해서 깊이 알아보기

https://kr.vuejs.org/v2/guide/reactivity.html

비동기 갱신 큐

https://kr.vuejs.org/v2/guide/reactivity.html#%EB%B9%84%EB%8F%99%EA%B8%B0-%EA%B0%B1%EC%8B%A0-%ED%81%90

Vue는 DOM 업데이트를 비동기로 한다.
데이터 변경이 발견될 때마다 큐를 열고, 같은 이벤트 루프에서 발생하는 모든 데이터 변경을 버퍼에 담는다.
단, 같은 Watcher가 여러 번 발생하면 대기열에서 한번만 푸시된다.
이벤트 루프 tick에서 Vue는 대기열을 비우고(Flush) 작업을 수행한다.
내부적으로 Vue는 비동기 큐를 위해 네이티브 Promise.then와 MessageChannel를 시도하고, setTimeout(fn, 0) 으로 돌아간다.

예를 들어,vm.someData = 'new value'를 설정하면, 컴포넌트는 즉시 재 렌더링되지 않습니다. 큐가 플러시 될 때 다음 “tick” 에서 업데이트됩니다

업데이트 후의 DOM 상태에 의존하는 작업을 수행하려는 경우
데이터 변경 후 DOM 업데이트를 마친 후에 작업이 필요한 경우 Vue.nextTick (콜백 function)을 사용할 수 있다.
콜백은 DOM이 업데이트된 후에 호출된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vue.component('example', {
template: '<span>{{ message }}</span>',
data: function () {
return {
message: '갱신 안됨'
}
},
methods: {
updateMessage: function () {
this.message = '갱신됨'
console.log(this.$el.textContent) // => '갱신 안됨'
this.$nextTick(function () {
console.log(this.$el.textContent) // => '갱신됨'
})
}
}
})

슬롯 (Slots)

컴포넌트를 만들 때 슬롯을 사용해서 만들어 두면 좀 더 유연하게 컴포넌트를 사용할 수 있다.
컴포넌트를 렌더링할 때 <slot></slot>영역에 전달된 내용으로 교체되어 렌더링 된다.

슬롯 사용해보기

<base-layout> 컴포넌트

1
2
3
4
5
6
7
8
9
10
11
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot>TEST</slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
  • name이 지정되지 않은 <slot>에는 암묵적으로 “default” 값이 사용됩니다.
  • 슬롯에 아무것도 전달되지 않을 때 <slot> 태그 사이에 기본 값을 지정해 둘 수 있다.

슬롯에 내용 전달해보기

<base-layout> 컴포넌트 사용 시 슬롯에 내용 전달해 보기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>

<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>

<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
  • 이름이 있는 슬롯에 내용을 전달하려면 <template>v-slot 디렉티브를 쓰고 그 속성에 앞에서 지정한 ‘name’을 넣으면 된다.
  • 단축

    <template v-slot:header><template #header>로 표현할 수 있다.

slot 인수를 일반 요소(element)에 바로사용하는 것은 삭제될 문법이다.
1
2
3
<base-layout>
<h1 slot="header">Here might be a page title</h1>
</base-layout>

v-slot 디렉티브는 slotslot-scope 인수들을 대체하는, 더 발전된 API로 Vue 2.6.0에 도입되었습니다. 새 문법이 도입된 이유는 이 RFC에서 찾아볼 수 있습니다. slotslot-scope 인수는 앞으로도 2.x 버전에서는 계속 지원될 것입니다. 하지만 Vue 3에서는 공식적으로 삭제될 예정입니다.

삭제될 문법

믹스인

컴포넌트 구현 시 공통으로 사용할 옵션(함수, 데이터, 공통컴포넌트로드 등)을 구현할 수 있다.
믹스인은 다중 상속이 가능하다.

mixin 객체는 모든 구성 요소 옵션을 포함할 수 있다.

믹스인을 이용해서 공통된 기능을 분리 시키고, 컴포넌트에서 특정 믹스인 객체를 사용하여 확장구현할 수 있다. 이때 해당 믹스인 객체에 오버라이딩이 필요한 함수등을 만들어 컴포넌트에서 특정 함수를 구현하도록 가이드 할 수도 있다.

옵션병합 전략

data 오브젝트의 내용이 상충하는 경우, 컴포넌트에 선언된 data 오브젝트를 우선 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var mixin = {
data: function () {
return {
message: 'hello',
foo: 'abc'
}
}
}

new Vue({
mixins: [mixin],
data: function () {
return {
message: 'goodbye',
bar: 'def'
}
},
created: function () {
console.log(this.$data)
// => { message: "goodbye", foo: "abc", bar: "def" }
}
})
mixin 훅은 컴포넌트 자체의 훅 이전에 호출된다.
methods,components,directives와 같은 객체 값을 요구하는 옵션은 같은 객체에 병합된다.

(충돌하는 키가 있을 경우 컴포넌트의 옵션을 우선순위를 갖음.)

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
var mixin = {
methods: {
foo: function () {
console.log('foo')
},
conflicting: function () {
console.log('from mixin')
}
}
}

var vm = new Vue({
mixins: [mixin],
methods: {
bar: function () {
console.log('bar')
},
conflicting: function () {
console.log('from self')
}
}
})

vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"

같은 병합 전략이 Vue.extend()에서 사용된다.

HTML 주입

Vue는 자동으로 HTML 컨텐츠를 이스케이프 해서 스크립트 삽입을 방지한다.

1
2
3
4
5
6
7
<h1>{{ userProvidedString }}</h1>
...
<!-- userProvidedString 가 다음 값을 가지고 있다면 -->
'<script>alert("hi")</script>'
...
<!-- 다음 HTML로 이스케이프 된다. -->
&lt;script&gt;alert(&quot;hi&quot;)&lt;/script&gt;

HTML을 명시적으로 렌더링 할 수있다.

  • 템플릿을 사용하는 경우
1
<div v-html="userProvidedHtml"></div>
  • 렌더 함수를 사용하는 경우
1
2
3
4
5
h('div', {
domProps: {
innerHTML: this.userProvidedHtml
}
})

웹사이트에서 임의의 HTML을 동적으로 렌더링하려면 XSS 취약점으로 쉽게 이어질 수 있으므로 매우 위험할 가능성이 있습니다. 신뢰할 수 있는 콘텐츠에서만 HTML 보간을 사용하고 사용자가 제공한 콘텐츠에서는 절대 사용하면 안됩니다.

라우팅

vue-router

여러 Vue 인스턴스에서 공유해야하는 상태가 있다면

https://kr.vuejs.org/v2/guide/state-management.html

veux

컴포넌트가 store에 속한 상태를 직접 변경하지 않고, store에 이벤트를 보내는 방식으로 동작한다.

vuex

예시

모든 페이지에서 API 호출 전/후로 응답대기 상태를 알려주는 이미지를 노출하고자 할때 를 가정한다.

공통으로 구현해서 사용할 수 있는 방법을 생각해보면 로딩 컴포넌트를 공통으로 만들고, 로딩 이미지 노출을 제어하는 값은 공통으로 사용되면 좋겠다.

레이아웃 컴포넌트 내에 작성

1
2
3
...
<loading v-show="this.$store.state.loading"></loading>
...

store.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from "vue"
import Vuex from "vuex"

Vue.use(Vuex)

export const store = new Vuex.Store({
state: {
loading: false
},
mutations: {
updateLoading(state, loadingState) {
state.loading = loadingState;
},
}
})

api.js

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
import axios from 'axios'
import { store } from '@/shared/store'

const API = axios.create({
baseURL: process.env.VUE_APP_BASE_URL || 'http://localhost:8080'
})

API.interceptors.request.use(function (config) {
if (config.method == "post") {
store.commit('updateLoading', true);
}
return config;
}, function (error) {
store.commit('updateLoading', false);
return Promise.reject(error);
});

API.interceptors.response.use(function (response) {
if (response.config.method == "post") {
store.commit('updateLoading', false);
}
return response;
}, function (error) {
store.commit('updateLoading', false);
return Promise.reject(error);
});

...

시작하기

Vue 를 이용해 빠르게 어플리케이션을 개발할 때 검토했던 것들과 필요한 것들을 정리해둔다.

검토

bootstrap-vue.js
coreui
vuemeterial
element-ui

비교 및 추천

https://levelup.gitconnected.com/18-useful-vuejs-ui-libraries-in-2019-1c622c6a4184

Vue CLI

Vue.js는 단일 페이지 애플리케이션를 빠르게 구축할 수 있는 공식CLI (Vue CLI / github)를 제공한다. 사전구성된 빌드 설정을 제공한다.

개발도구

VSCode
EXTENSIONS

Vetur

  • Syntax-highlighting
  • Snippet
  • Emmet
  • Linting / Error Checking
  • Formatting
  • Auto Completion
  • Debugging

크롬 확장프로그램

vue-devtools

설치

nodeJS 설치
Vue CLI 설치
1
> npm install -g @vue/cli
Vue 프로젝트 생성
1
> vue create portal
Vue 실행
1
> npm run serve
Vue CLI UI 실행
1
> vue ui
Vue 개발에 필요한 라이브러리 설치
1
2
3
> npm install vue-router
> npm install axios
> npm install bootstrap-vue bootstrap

배포

nginx - docker