vue 3 사용해보기
실무자가 아닌 개인 공부하는 사람의 글임을 참고하세요.
자주 쓰지 않다 보니 자꾸 까먹어서 그냥 공부한 과정을 순서대로 기록하였습니다.
vue가 3 버전이 나오면서 nuxt 같은 라이브러리가 아직 지원이 되지 않고 있다.
2 버전의 프로젝트를 3으로 마이그레이션 하는 것은 현재 권장하지 않고 있다.
https://v3.ko.vuejs.org/guide/migration/introduction.html
당분간은 계속 이 상태일듯하여 이왕 이렇게 된 거 nuxt 없이 vue3로 프로젝트를 만들어보려고 한다.
설치
https://v3.ko.vuejs.org/guide/installation.html
vue cli를 사용하면 편하다.
vue cli는 vue를 개발하기 위해 제공되는 도구인데 손쉽게 프로젝트를 생성할 수도 있고 웹 ui 기반 관리 툴을 제공하여 vue 프로젝트의 생성, 삭제, 관리 및 의존성 관리를 하기 편하게 해 준다.
node 설치
vue cli를 사용하기 위해 우선 node를 설치한다.
vue cli 설치
command 창에서 아래와 같이 명령어로 vue cli를 전역 설치한다.
npm install -g @vue/cli
프로젝트 생성
cli가 설치되면 아래처럼 vue create [생성할 프로젝트] 명령을 사용해 vue 프로젝트를 생성할 수 있다.
현재는 vuejs 2와 3을 선택하여 생성할 수 있다.
또한 수동 설정을 통해 여러 설정을 자동화해주는데 이는 자세한 설정을 원하는 경우 사용하면 된다.
생성 시 프로젝트 이름에 대문자는 사용하지 못하는 듯하다.
vue create bluesky-vuejs-study
vue ui 사용하기
이후 vue ui를 사용하여 해당 프로젝트를 실행하고 종료한다.
아래 명령어를 사용하면 해당 웹 ui가 호출된다.
vue ui
아래와 같은 관리 화면이 나타난다.
vue ui 프로젝트 매니저
이 화면의 왼쪽 상단을 선택하면 프로젝트 매니저를 선택할 수 있다.
vue 프로젝트 매니저를 선택하면 다음처럼 프로젝트를 관리할 수 있는 메뉴가 있다.
이를 통해 vue 프로젝트를 가져올 수 있다.
vue ui 프로젝트 관리
프로젝트를 선택하면 해당 프로젝트의 대시보드가 보인다.
대시보드, 플러그인, 의존성, 설정, 작업 목록 항목이 있다.
플러그인, 의존성에서 관련 설정을 추가/삭제할 수 있다.
javascript 라이브러리가 정말 쉴 새 없이 버전이 업데이트되는 데 사용하는 라이브러리가 많으면 업데이트되는 버전을 관리하기 힘들어진다.
일일이 package.json의 버전을 변경하면 참조된 의존성의 버전이 문제없는지 확인하기 어려운데 이 대시보드의 의존성 관리 메뉴를 사용하여 버전을 관리하면 편하다.
(vue 3으로 생성된 프로젝트는 최신 버전보다 설치된 버전이 높아서 느낌표가 표시되는 단점이 있긴 하다.)
라이브러리를 추가하고 싶다면 우상단의 의존성 설치 메뉴를 선택하면 추가할 수 있다.
또한 그 옆의... 을 선택하면 버전이 업데이트된 라이브러리를 한꺼번에 업데이트할 수 있다.
작업 목록에서 서버를 구동시키거나 빌드를 하는 등의 처리를 할 수 있다.
이 항목에서 실행을 하면 해당 프로젝트의 서버가 시작되고 해당 서버의 index 페이지까지 웹브라우저에 띄워준다.
이후 서버가 구동되어 있는 상태에서 프로젝트의 소스코드를 수정하면 실시간으로 브라우저에 수정된 소스코드가 반영되는 것을 볼 수 있다.
개발하기
기본 프로젝트 구성
vue cli에서 create 명령을 통해 생성한 프로젝트의 구조를 보면 다음과 같다.
- public/
- favicon.ico
- index.html
- src/
- assets/
- logo.png
- components/
- HelloWorld.vue
- App.vue
- main.js
- assets/
main.js가 가장 최초로 실행되는 스크립트 파일이고 이 파일에서 App.vue를 호출하여 app을 생성한다.
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
이 파일에서 App.vue를 호출하여 createApp을 한 결과를 index.html에 있는 app id의 attribute에 할당한다.
App.vue는 다음과 같다.
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
index.html은 다음과 같다.
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
vue router 사용하기
기본은 호출 시 App.vue와 index.html을 조합하여 내려주고 있지만 페이지 분기를 하려면 vue-router를 추가한다.
이때 vue cli를 사용하는 경우 플러그인 메뉴에서 우상단에 vue-router와 vuex 추가 메뉴가 있는데 그걸 사용하면 알아서 vue 3과 연관된 vue-router와 cli-plugin-router를 설치하고 관련한 기본 페이지까지 생성해준다.
vue 3는 vue-router 4.x를 사용한다.
https://next.router.vuejs.org/
vue-router plugin을 추가하면 아래처럼 프로젝트 구조에 router 관련 기초적인 설정이 추가되어 있다.
- public/
- favicon.ico
- index.html
- src/
- assets/
- logo.png
- components/
- HelloWorld.vue
- router/
- index.js
- views/
- About.vue
- Home.vue
- App.vue
- main.js
- assets/
이제 요청에 대한 처리는 router/index.js에 설정하면 된다.
기본 설정은 다음과 같이 되어 있다.
path를 추가하면 주소별 페이지를 사용할 수 있게 된다.
상세한 설정은 vue-router 홈페이지의 가이드 문서를 참고하면 된다.
import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '../views/Home.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
main.js는 router를 추가하면 다음과 같이 변경되어 있다.
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
기존 코드에 router를 사용한다는 설정이 추가된다.
App.vue는 다음과 같이 변경되어 있다.
<template>
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
</div>
<router-view />
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
}
</style>
여기에서 router-view로 지정된 부분이 router에 설정된 정보가 매칭 되어 처리된다.
Nested Routes
예제의 경우 About과 Home 두 개의 주소에 대해 route처리를 하는 것을 보여준다.
많이 사용할 법한 경우는 중첩 route일 것 같다.
최상위 레이아웃 아래 특정 주소 하위에 서브 레이아웃을 구성하고 그 아래에서 메뉴별 호출을 처리하는 경우인데 이에 대해서도 vue-router가 제공해준다.
https://next.router.vuejs.org/guide/essentials/nested-routes.html
routes 설정에 children을 추가하여 해당 설정을 할 수 있다.
예를 들어 다음처럼 추가했다면
import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '../views/Home.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
},
{
path: '/test',
name: "test",
component: () => import('../views/test/Index.vue'),
children: [
{
path: 'testSub',
component: () => import('../views/test/TestSub.vue')
}
]
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
/test로 접근하면 /views/test/Index.vue를 바라보고
/test/testSub로 접근하면 /views/test/TestSub를 바라본다.
다만 TestSub를 보기 전에 /views/test/Index.vue를 바라보기 때문에 해당 페이지에 <router-view /> 설정이 있어야 해당 위치에 /views/test/TestSub.vue가 렌더링 된다.
이렇게 중첩 구조가 구성되기 때문에 공통으로 사용할 것들은 위로 계속 올려서 설정하면 그 하위 vue들은 그걸 같이 사용할 수 있게 되는 것 같다.
예를 들어 이후 추가하는 bootstrap 설정은 최상위 App.vue에 선언하면 그 하위에서는 같이 사용한다.
bootstrap 사용하기
기존의 경우 vue 2와 bootstrap 4를 사용하는 bootstrapvue 라이브러리가 있었다.
이 라이브러리를 쓰면 bootstrap의 class를 일일이 사용하지 않고 custom html 태그와 속성만으로 bootstrap을 사용할 수 있었고 버전이 변경되더라도 이 태그의 사용이 크게 바뀌지 않으면 버전 변경이 용이할 것이라고 생각했다.
하지만 릴리즈 된 지 시간이 좀 지난 vue3와 bootstrap 5를 아직 지원하지 않고 있다.
따라서 그냥 bootstrap 5를 직접 참조해서 써보기로 하였다.
vue ui에서 의존성 메뉴의 우상단의 의존성 설치에서 bootstrap을 선택하여 설치하면 package.json에 해당 dependency가 추가된다.
{
// 중간 생략
"dependencies": {
"bootstrap": "^5.1.1",
"core-js": "^3.6.5",
"vue": "^3.0.0",
"vue-router": "^4.0.0-0"
},
// 중간 생략
}
추가 후 서버를 실행하면 boostrap이 참조하는 popperjs를 참조하라는 메시지가 나온다.
This dependency was not found:
* @popperjs/core in ./node_modules/bootstrap/dist/js/bootstrap.esm.js
To install it, you can run: npm install --save @popperjs/core
bootstrap 5의 경우 설치 시 popperjs core가 필요하다.
https://getbootstrap.com/docs/5.1/getting-started/introduction/
관련하여 의존성에 @popperjs/core를 추가한다.
사용하려는 페이지에 다음처럼 bootstrap과 css를 추가하면 이제 사용할 수 있게 된다.
<template>
<div>
test
<button type="button" class="btn btn-primary">Primary</button>
</div>
</template>
<script>
import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap";
export default {};
</script>
<style></style>
vuex 사용하기
vuex는 상태 관리 패턴 라이브러리라고 한다.
그냥 어떤 값을 여러 페이지에서 같이 쓰기 위한 라이브러리로 이해하고 있다.
또한 component를 중첩해서 사용하면 계속 prop 값을 넘기는 게 복잡도가 증가하기 때문에 이럴 경우에도 좀 더 간결하게 사용할 수 있게 해주지 않을까 싶다.
vue router와 마찬가지로 plugin 메뉴에서 vuex를 추가하면 기본적인 설정까지 추가해준다.
vuex를 추가하면 다음과 같은 path가 추가된다.
- public/
- favicon.ico
- index.html
- src/
- assets/
- logo.png
- components/
- HelloWorld.vue
- router/
- index.js
- store/
- index.js
- views/
- About.vue
- Home.vue
- App.vue
- main.js
- assets/
또한 main.js에 vuex를 사용한다는 설정이 추가된다.
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
createApp(App).use(store)..mount('#app')
기존 vue 확장자 파일에 설정하던 props, data의 변수를 /store 아래로 옮겨 선언하고 그걸 가져다 쓰도록 변경하면 페이지를 이동하더라도 store에서 참조한 값은 유지가 되게 된다.
store 하위 선언의 시작점은 store/index.js이다.
store/index.js는 다음과 같이 처음 설정되어 있다.
import { createStore } from "vuex";
export default createStore({
state: {},
mutations: {},
actions: {},
modules: {},
});
기본 설정은 저렇게 state, mutations, actions, modules가 있는데 문서상으론 여기에 getters가 추가되어 있다.
따라서 vuex에 설정하는 항목은 총 5가지이다.
항목 | 설명 |
state | 저장할 값을 선언하는 부분 |
getters | 공통으로 사용할 호출 함수 선언 부분 |
mutations | state에 저장된 값을 변경하는 함수 선언 부분 |
actions | state에 저장된 값을 변경하는 함수 선언 부분 비동기 작업의 경우 actions에 추가해야함 |
modules | state를 나누어 관리하기 위해 제공됨 |
vuex가 저장하는 state는 single state tree이다.
store 아래 여러 모듈로 나누어 state를 설정해도 그 모든 값들은 $store.state에 함께 저장된다.
vuex의 사용 예제를 참고하면 어떻게 사용하면 되는지 파악하기 좋다.
https://github.com/vuejs/vuex/tree/4.0/examples/classic/shopping-cart
전체적인 예제 파일의 구성은 다음과 같다.
├── index.html
├── main.js
├── api
│ └── ... # abstractions for making API requests
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # where we assemble modules and export the store
├── actions.js # root actions
├── mutations.js # root mutations
└── modules
├── cart.js # cart module
└── products.js # products module
해당 예제의 경우 최상위 store/index.js에 다음과 같이 선언되어 있다.
import { createStore, createLogger } from 'vuex'
import cart from './modules/cart'
import products from './modules/products'
const debug = process.env.NODE_ENV !== 'production'
export default createStore({
modules: {
cart,
products
},
strict: debug,
plugins: debug ? [createLogger()] : []
})
index.js에 모든 설정을 추가하지 않고 모듈로 나누어 설정을 하였고 store/index.js에 해당 모듈을 선언하여 사용할 수 있게 된다.
이렇게 추가된 product 모듈은 다음과 같다.
https://github.com/vuejs/vuex/blob/4.0/examples/classic/shopping-cart/store/modules/products.js
import shop from '../../api/shop'
// initial state
const state = () => ({
all: []
})
// getters
const getters = {}
// actions
const actions = {
async getAllProducts ({ commit }) {
const products = await shop.getProducts()
commit('setProducts', products)
}
}
// mutations
const mutations = {
setProducts (state, products) {
state.all = products
},
decrementProductInventory (state, { id }) {
const product = state.all.find(product => product.id === id)
product.inventory--
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
all이라는 state가 선언되었고 이 all에 products를 추가하고 찾고, 제거하는 등의 함수들이 제공되고 있다.
이렇게 선언된 내용을 /components/ProductList.vue에서 다음과 같이 호출하여 사용하고 있다.
<template>
<ul>
<li
v-for="product in products"
:key="product.id">
{{ product.title }} - {{ currency(product.price) }}
<br>
<button
:disabled="!product.inventory"
@click="addProductToCart(product)">
Add to cart
</button>
</li>
</ul>
</template>
<script>
import { mapState, mapActions } from 'vuex'
import { currency } from '../currency'
export default {
computed: mapState({
products: state => state.products.all
}),
methods: {
...mapActions('cart', [
'addProductToCart'
]),
currency
},
created () {
this.$store.dispatch('products/getAllProducts')
}
}
</script>
vuex에 설정된 state, getters, mutations, actions를 vue파일에서 쉽게 가져다 쓸 수 있도록 mapState, mapGetters, mapMutations, mapActions helper를 제공해주고 있어 이를 사용하면 쉽게 해당 vue파일에서 state와 각 함수를 사용할 수 있다.