Disclaimer
- vue.js 맛보기
- 서버개발자 (사용자) 관점
- vue.js internals는 관심없음
- 어려운 테크닉, 팁은 다루지 않음
- 개념 설명보다는 실습
frontend 기술의 역사
- vanilla javascript
jquery2006- ajax와 element selector 사용이 용이함
- mustache 2009
- template
- knockout 2010
- 2-way binding, observable
- ember.js, backbone.js, ...
angular 12010- MVVM (Model-View-ViewModel)
- modern frontend
- react.js 2013
- vue.js 2014
- anuglar 2 2016
vue.js
소개
- Evan You가 창시
- 현대적인 frontend 기술 3대장 중 하나
- react.js
- vue.js
- angular.js
- export와 import, 변수 타입 등의 짝 맞추기의 향연
(library) react.js << (hybrid) vue.js << (framework) angular.js
특징
- 완만한 learning curve
- framework (vs library)
- vue에서 정한 인터페이스에 맞춰서 사용해야 함
- component-based
- 2-way binding
- virtual dom
- react.js와 달리 이벤트를 이용해 컴퍼넌트에 전달하여 업데이트
- react.js는 데이터의 참조가 변경되면 컴퍼넌트 업데이트
- 한글 문서
2-way binding
- view와 model 사이에서 공유하는 변수가 있을 때 어느 쪽에서 수정하든 다른 쪽에도 변경이 반영되는 것

vue cli로 프로젝트 구성하기
- node.js 설치
- brew install node
- vue-cli 설치
sudo npm install -g @vue/cli @vue/cli-init
- 프로젝트 생성하기
vue create frontend- Default Vue2 - 2022.07 기준으로 Vue3는 의존성과 충돌이 많음
cd frontendnpm install -s axios vue-router@3 bootstrap bootstrap-vue lodash- 이쯤에서
git add/commit
- 개발 서비스 실행하기
npm run serve- http://localhost:8080
- 다음 화면이 잘 보이는지 확인할 것

- 본격적인 개발 전에 이 화면을 담당하는 컴퍼넌트를 지우게 됨
- 리얼 배포는?
- 개발과 달리 서비스가 사용되지 않음
- bundler로 빌드해서 리소스 형태로 생성
npm run build
- 리소스를 백엔드 웹서비스를 통해 제공(브라우저에 다운로드되도록)
핵심 요소
components
- vue는 화면과 로직을 컴퍼넌트 단위로 관리함
- App.vue가 하위 컴퍼넌트를 계층적으로 포함하는 방식
- 접근경로에 따라 특정 컴퍼넌트를 불러오는방법은 vue-router에서 다룸
- 사용법
- 다른 컴퍼넌트를 import한 다음, 사용할 때 지정
예제 (실습)
- App.vue - 최상위 컴퍼넌트
- Book.vue 파일의 객체를 읽어다가 Book이라는 컴퍼넌트로 import/declare하고
<book>이라는 요소로 사용
- Book.vue 파일의 객체를 읽어다가 Book이라는 컴퍼넌트로 import/declare하고
<template>
<div id="app">
<book/>
</div>
</template>
<script>
import Book from './components/Book.vue'
export default {
name: 'App',
components: {
Book,
}
}
</script>
- Book.vue 파일의 객체를 읽어다가 Book이라는 컴퍼넌트로 import/declare하고 이라는 요소로 사용
- 아직까지는 Book.vue 파일이 없어서 에러가 발생함
data
- 반응형 데이터
- vue가 변경을 감시함
- function 타입
예제 (실습)
Book.vue
<template>
<div id="book">
<p>title: {{ title }}</p>
<p>number: {{ number }}</p>
<p>book data: {{ bookData }}</p>
</div>
</template>
<script>
export default {
name: 'Book',
data: function () {
return {
title: 'Books',
number: 30,
bookData: [
{'author': '헤밍웨이', 'title': '노인과 바다'},
{'author': '피츠제럴드', 'title': '위대한 개츠비'},
],
status: true,
errorMessage: '',
}
},
}
</script>
- 주의사항
- javascript 코드에서는 항상 this.를 붙여서 사용할 것
- 템플릿에서는 this. 없이 사용할 것
- Troubleshooting
- 11:9 error Component name "Book" should always be multi-word vue/multi-word-component-names 에러가 발생함
- 다음 파일을 생성하고 npm run serve 다시 실행
.eslintrc.js
module.exports = {
root: true,
parserOptions: {
parser: '@babel/eslint-parser'
},
env: {
browser: true,
node: true,
},
extends: [
'plugin:vue/essential',
'eslint:recommended'
],
// required to lint *.vue files
plugins: [
'vue'
],
// add your custom rules here
rules: {
'vue/multi-word-component-names': 'off'
}
}
computed
- view에서 값을 가공하지 말 것 -> computed에서 처리
예제 (실습)
<p>{{ '실행 ' + (status ? '중단' : '재개' }}</p>
-->
<p>status: {{ status }}</p>
<p>status message: {{ statusMessage }}</p>
data: function () {
return {
status: true,
}
},
computed: {
statusMessage: function () {
return '실행 ' + (this.status ? '중단' : '재개');
}
},
- 변수의 값을 조합하거나 분해
- 캐싱되며 자동으로 재계산됨
- getter/setter 지정가능
- 함수처럼 정의하고 변수처럼 사용함
watch
- 어떤 변수의 값이 바뀌면 다른 변수를 바꾼다든가 하는 목적으로 변경에 대한 이벤트 핸들링
- 주의사항
- 두 개 이상의 변수들이 서로를 감시하면 안 됨
예제 (실습)
<p>error message: {{ errorMessage }}</p>
<b-input-group class="mt-3">
제목 입력: <b-form-input v-model="title"></b-form-input>
</b-input-group>
data: function () {
return {
errorMessage: '',
}
},
watch: {
title: function () {
this.errorMessage = '제목 변경됨';
}
},
- Troubleshooting
- bootstrap이 적용되지 않아서 화면 요소를 식별하기 어려움
- 다음 코드를 main.js에 추가할 것
- bootstrap이 적용되지 않아서 화면 요소를 식별하기 어려움
main.js
import { BootstrapVue } from 'bootstrap-vue';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';
Vue.use(BootstrapVue);
methods
예제 (실습)
methods: {
getApiUrlPath: function () {
let pathPrefix = 'https://api.mydomain.com/myapp';
if (process.env.NODE_ENV === 'development') {
pathPrefix = 'http://127.0.0.1:5000';
}
return pathPrefix;
},
- localhost:5000으로 보내는 요청은 아직 API가 준비되어 있지 않으므로 뒤에서 다시 살펴볼 예정
mounted, created, ...
- vue 앱이 마운트되고 생성될 때 초기에 실행해야 할 작업들
created: function () {
console.log("component created");
},
mounted: function () {
console.log("component mounted");
},
Lifecycle

- 응용 레벨에서는 mounted 단계만 잘 처리하면 됨
렌더링 기법
v-text와 v-html
- v-text는 {{ }}와 동일함
- HTML이 렌더링되지 않음 (HTML이 텍스트로 노출됨)
- v-html은 HTML이 렌더링됨
v-if를 이용한 요소 삭제
<div v-if="status">
<h1>작품 목록</h1>
<ul v-if="bookData">
</ul>
</div>
- v-else-if나 v-else도 있으나 사실 상 잘 쓰이지 않음
- v-show는 요소 감추기 (DOM에서 삭제되지 않음)
v-for를 이용한 목록 출력
예제 (실습)
<div v-if="status">
<h1>작품 목록</h1>
<ul v-if="bookData">
<li v-for="item in bookData" :key="item.title">{{ item.title }} by {{ item.author }}</li>
</ul>
</div>
- 다음 데이터가 사용되고 있음
- bookData를 삭제한다면 v-if에 의해 ul 요소가 모두 사라짐
bookData: [
{'author': '헤밍웨이', 'title': '노인과 바다'},
{'author': '피츠제럴드', 'title': '위대한 개츠비'},
],
v-on을 이용한 이벤트 핸들링
<a v-on:click="doSomething"> ... </a>
@이벤트명으로 줄여서 사용할 것
<button @click="run()">실행</button>
- 실제로 클릭하면 run()이 정의되어 있지 않아서 브라우저 콘솔에서 에러가 발생함
클래스 적용
<div class="memo" v-bind:class="{ active: isActive, 'text-danger': hasError }">
</div>
isActive: true,
hasError: false,
- isActive나 hasError는 모두 boolean 타입이고 이 변수의 값에 따라 해당 클래스가 적용될지 결정됨
<dlv class="memo active">
</div>
스타일 적용
<div class="memo" v-bind:style="{ color: activeColor }">
메모
</div>
- data에 추가
activeColor: 'red',
바인딩
v-model
- 2-way binding
- form 요소를 변경하면 그게 거꾸로 vue model에도 반영되게 하는 용도
- 그런데 form 요소를 변경하면 그게 거꾸로 vue model에도 반영되는 메커니즘
- 이렇게 화면에서 model로 데이터 변경을 반영하는 것까지 가능해지면 "2-way binding"이라고 부름

v-model="title"
예제 (revisited)
<b-input-group class="mt-3">
제목 입력:
<b-form-input v-model="title"></b-form-input>
</b-input-group>
data: function () {
return {
title: 'Books',
}
}
v-bind
- 1-way binding
- HTML 요소나 하위 컴퍼넌트에게 값을 전달하기 위한 용도
- 반면에 HTML의 text는 {{ }}를 이용하여 값을 전달할 수 있음
예제 (실습 x)
<div v-bind:id="dynamicId"></div>
<my-button v-bind:class="dynamicClass"></my-button>
- 문법
- v-bind:variable="value"
:variable="value"로 줄여서 사용할 것
- bind나 콜론을 사용할 때의 주의사항
- vue와 상관없이 HTML에서 속성의 값을 문자열로 지정하는 것
- variable="John Smith"
- "" 내부는 그냥 문자열
- vue 컴퍼넌트에게 코드(표현식)로 전달하는 것
- :variable="'Mr. ' + loginUserName"
- "" 내부가 vue.js의 표현식에 해당됨
- 두 가지 방식 모두 자식 컴퍼넌트에 prop으로 전달됨
- vue와 상관없이 HTML에서 속성의 값을 문자열로 지정하는 것
- 하위 컴퍼넌트로 값을 전달하고 이벤트를 받는 예제는 뒤에서 자세히 살펴볼 예정
API를 이용한 동적 바인딩
backend
- 백엔드 디렉토리 생성
- backend 디렉토리는 frontend 디렉토리와 형제
- mkdir backend
- cd backend
- pip install Flask Flask-CORS
예제 (실습)
app.py
#!/usr/bin/env python
from flask import Flask, jsonify, request, render_template
from flask_cors import CORS
app = Flask(__name__, static_folder="../frontend/dist", template_folder="../frontend/dist")
CORS(app, resources={r"/*": {"origins": "*"}})
@app.route("/books")
def books():
data = {"status": False, "message": "실패", "books": []}
data["status"] = True
data["message"] = "성공"
data["books"] = [{"author": "필립 K. 딕", "title": "안드로이드는 전기양의 꿈을 꾸는가"},
{"author": "아서 클라크", "title": "유년기의 끝"},
{"author": "H.G. 웰즈", "title": "우주 전쟁"}]
print(data)
return data
if __name__ == "__main__":
app.run(host="0.0.0.0")
- 의존성 설치
- pip install Flask Flask-CORS
- 실행
flask --debug run
frontend
- axios 모듈 설치하기
npm install axios
예제 (실습)
import axios from 'axios';
methods: {
getBooks() {
const url = this.getApiUrlPath() + "/books";
axios.get(url)
.then((res) => {
if (!res.data.status) {
this.errorMessage = res.data.message;
} else {
this.bookData = res.data['books'];
}
})
.catch((error) => {
this.errorMessage = error;
})
},
}
-
에러 핸들링이 두 군데에서 처리되고 있음에 주목할 것
.catch((error) => { }- HTTP 통신 레벨의 예외 처리
- res.data.status가 아닌 res.status (HTTP Status값)을 참고하는 것도 필요함
if (res.data.status === 'failure') { }- HTTP 통신 레벨이 아니라 API가 backend 로직의 성공/실패 여부를 확인해서 알려주는 필드 몇 가지가 필요함
- 여기서는 res.data.status와 res.data.message를 사용하고 있음에 주목할 것
-
getBooks()를 mounted()에서 호출하거나, 이벤트 리스너로 연결해볼 것
mounted: function() {
this.getBooks();
},
<button @click="bookButtonClicked">책 불러오기</button>
methods: {
bookButtonClicked() {
return this.getBooks();
},
}
주의사항
- 위 예제는 CRUD가 모두 구현되어 있지 않음
- axios의 verb와 flask route의 request.method가 일치해야 함
- 정석적인 예제
@app.route("/groups/<group_name>/feeds/<feed_name>", methods=["DELETE", "POST", "GET"])
def get_feed_info(group_name, feed_name):
response_object = {"status": "failure"}
if request.method == "GET":
...
elif request.method == "DELETE":
...
axios
.get(`/groups/${groupName}/feeds/${feedName}`)
...
axios
.delete(`/groups/${groupName}/feeds/${feedName}`)
...
개발 팁
- backend와 frontend 개발 준비가 다 되었으니 이 쯤에서 run configuration을 등록할 것
- backend:
env FLASK_ENV=developent flask run - frontend:
npm run serve
- backend:
컴퍼넌트
- 컴퍼넌트란 우리 눈에 보이는 html 조각과 그 뒤에서 동작하는 javascript 로직을 함께 묶어둔 단위
props
- 부모 컴퍼넌트에서 attribute나 v-bind를 이용해 값을 넘길 때, 자식 컴퍼넌트에서는 property로 받아서 처리
- 자식 컴퍼넌트에서는 이벤트를 올려보내서 부모 컴퍼넌트가 자식의 변화를 인지할 수 있도록 함

- 타입 및 default 선언 가능
- default는 가급적 literal 대신 함수로 정의할 것
- data와 props의 차이점
- data는 component-
private
- data는 component-
예제 (실습 x)
- MySubComponent.vue
export default {
name: 'MySubComponent',
props: {
variant: {
type: String,
default: 'info'
},
initialIcon: {
type: Array,
default: function () {
return [];
}
},
},
}
-
props에서 정의된 property로는 variant와 initialIcon이 있으며 이 property들의 타입과 초기값을 지정함
-
ParentComponent.vue
<my-sub-component
:initial-icon="['fas', 'search']"
variant="dark"/>
- attribute로 variant를 내려보내고, v-bind를 통해 initial-icon을 내려보냄
- 네이밍 컨벤션이 서로 다름에 주의할 것
- MySubComponent.vue vs <my-sub-component>
- initialIcon vs initial-icon
- MySubComponent.vue vs <my-sub-component>
$emit
- 자식 컴퍼넌트에서 부모 컴퍼넌트로 이벤트 전달
- 자식 컴퍼넌트는 $emit을 호출함
- 부모 컴퍼넌트는 v-on:이벤트명(또는 @이벤트명)으로 처리함
예제 (실습)
두 개의 버튼에서 발생하는 클릭 수를 합해서 보여주는 예제
ButtonCounter.vue
<button v-on:click="incrementCounter">
{{ counter }}
</button>
export default {
name: "ButtonCounter",
data: function () {
return {
counter: 0
}
},
methods: {
incrementCounter: function () {
this.counter += 1
this.$emit('increment')
}
},
}
- 클릭 이벤트가 발생할 때마다 this.$emit('increment')로 increment 이벤트를 올려보냄
TotalCounter.vue
<div id="counter-event-example">
<p>클릭 수 합계: {{ total }}</p>
버튼1: <button-counter v-on:increment="incrementTotal"></button-counter>
버튼2: <button-counter v-on:increment="incrementTotal"></button-counter>
</div>
- ButtonCounter를 import해서 <button-counter>라는 이름의 요소로 사용함
- 자식인 ButtonCounter에서 올라온 increment 이벤트에 대해 v-on:increment로 받아서 이벤트 핸들러인 incrementTotal을 호출해서 처리함
import ButtonCounter from './ButtonCounter.vue';
export default {
name: "TotalCounter",
components: {
ButtonCounter
},
data: function () {
return {
total: 0
}
},
methods: {
incrementTotal: function () {
this.total += 1
}
}
}
Book.vue
<template>
<div id="app">
<total-counter></total-counter>
</div>
</template>
<script>
import TotalCounter from './TotalCounter.vue'
export default {
name: 'Book',
components: {
TotalCounter,
}
}
</script>
- TotalCounter를 import하여 <total-counter>라는 이름의 요소로 사용함
참고사항
- 예제에서는 다루지 않았지만, 이벤트 전파 시에 파라미터를 전달할 수도 있음
this.$emit('decrement', e.target.value)
<parent-component @decrement="decrementWithValue"/>
decrementWithValue(value) {
this.total -= value;
}
$refs
- HTML 요소마다 다른 DOM ID를 부여하기는 어려움
- user-defined ID의 용도로 refs를 사용할 수 있음
- view와 모델 어느 곳에서도 사용 가능함
예제 (실습 x)
<button ref="saveFeedButton1" @click="saveFeedButtonClicked">Save</button>
methods: {
saveFeedButtonClicked: function() {
this.$refs.saveFeedButton1.innerText = "Saved";
}
}
- $refs'saveFeedButton1'로 접근함
- 그러나 이 방법은 응용 코드에서 DOM을 직접 access하는 방식이기 때문에 권장하지 않음 -> 다른 방법이 있는지 한 번 더 생각해보자!
심화 주제
vue-router
- 설치
npm install vue-router@3
- 사용법
- 링크는
<router-link>로 지정 - 대상 컴퍼넌트는
<router-view>에 렌더링됨
- 링크는
예제 (실습)
Result.vue
<template>
<div id="result">
결과 내용
</div>
</template>
<script>
export default {
name: "Result"
}
</script>
Management.vue
<template>
<div id="mgmt">
관리 요소들
</div>
</template>
<script>
export default {
name: "Management"
}
</script>
App.vue
<template>
<div id="app">
<router-link to="/"><h1>홈</h1></router-link>
<router-link to="/result"><h1>결과</h1></router-link>
<router-link to="/management"><h1>관리</h1></router-link>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
components: {
}
}
</script>
- 일단 components 항목을 비웠음. Book 컴퍼넌트 import도 지울 것
UI 개선 버전
<b-navbar>
<b-navbar-nav>
<b-nav-item>
<router-link to="/">
<b-nav-text>홈</b-nav-text>
</router-link>
</b-nav-item>
<b-nav-item>
<router-link to="/result">
<b-nav-text>결과</b-nav-text>
</router-link>
</b-nav-item>
<b-nav-item>
<router-link to="/management">
<b-nav-text>관리</b-nav-text>
</router-link>
</b-nav-item>
</b-navbar-nav>
</b-navbar>
<router-view></router-view>
router/index.js
import Vue from 'vue';
import Router from 'vue-router';
import Book from '../components/Book.vue';
import Result from '../components/Result.vue';
import Management from '../components/Management.vue';
Vue.use(Router);
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'Home',
component: Book
},
{
path: '/result',
name: 'Result',
component: Result
},
{
path: '/management',
name: 'Management',
component: Management,
children: [
{
path: ':group/:feed',
component: Management
}
]
}
]
})
main.js
import router from './router';
new Vue({
render: h => h(App),
router,
}).$mount('#app')
- 접근 가능한 경로
- 주의사항
- name이 중복되면 안 됨
- 하위 경로가 겹치는 경우 children 속성을 이용하여 선언할 필요가 있음
- this.$route.params'group'처럼 참조 가능함
- Troubleshooting
- 개발 서비스와 브라우저 콘솔에 아무 에러가 없는데 라우터 링크를 클릭해도 화면에 아무 것도 노출되지 않고 변화가 없다면
<router-view> 요소가 빠져있는 것임
- 개발 서비스와 브라우저 콘솔에 아무 에러가 없는데 라우터 링크를 클릭해도 화면에 아무 것도 노출되지 않고 변화가 없다면
this
- vue 최상위 객체를 this로 계속 사용하고 싶으면
function(obj) { }와 같은 callback은 쓰지 말것(obj) => { }을 대신 사용할 것
예제 (실습 x)
axios
.get(url)
.then(function (res) {
this.feeds = res.data;
})
.catch(function (res) {
console.error(err);
});
-->
axios
.get(url)
.then((res) => {
this.feeds = res.data;
})
.catch((err) => {
console.error(err);
});
functional programming
- javascript는 원래 functional한 언어임
- 그러나 개발자들은 여전히 for/if로 코드를 작성하고 있음
- functional한 메소드를 최대한 활용하거나 외부 패키지인 underscore나 lodash를 사용할 것
- lodash
- 설치
npm install -s lodashimport _ from 'lodash';
예제 (실습 x)
newData = _.filter(data, (o) => {
return o['sum'] > 100;
}).map(function (o) {
o['extra'] = {'name': 'John', 'age': 14};
o['job'] = 'Typist';
return o;
});
예제 (실습)
Book.vue
import _ from 'lodash';
getBooks() {
const url = this.getApiUrlPath() + "/books";
axios.get(url)
.then((res) => {
if (!res.data.status) {
this.errorMessage = res.data.message;
} else {
this.bookData = res.data['books'].filter((o) => {
return o['author'] !== '아서 클라크';
}).map((o) => {
o['date'] = new Date();
return o;
});
}
})
.catch((error) => {
this.errorMessage = error;
})
},
탐구 과제
Slot
- 컴퍼넌트를 사용할 때 HTML 코드 조각도 끼워넣어 사용하기
- props나 v-html로 HTML 코드 조각을 끼워넣을 수 있지만 slot을 사용하면 view와 model을 분리하기에 용이함
Vuex
- 글로벌한 데이터 저장소
- 전역변수와 달리 commit을 해야 변경이 반영됨