January 14, 2021
vue-cli 설치 및 Vue SPA Application 생성
$ npm i -g @vue/cli
$ vue create {project_name}
ESLint 란?
에러가 화면에 표시되지 않게 하는 방법
vue.config.js
module.exports = {
devServer: {
overlay: false
}
}
.eslintrc.js
Prettier
rules: { ... }
부분에 정의한다..eslintrc.js
module.exports = {
...
rules: {
...
"prettier/prettier": ['error', {
singleQuote: true,
semi: true,
useTabs: true,
tabWidth: 2,
trailingComma: 'all',
printWidth: 80,
bracketSpacing: true,
arrowParens: 'avoid',
}]
},
...
}
Reference
../../../
’@/
‘로 접근할 수 있게 처리하자.(VSCode IDE를 사용하는 경우) 프로젝트 루트 경로에 jsconfig.json 파일 생성
jsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": [
"./*"
],
"@/*": [
"./src/*"
],
}
},
"exclude": [
"node_modules",
"dist"
]
}
src/router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
// 플러그인을 실행하기 위해서 필요한 코드
Vue.use(VueRouter);
// VueRouter로 인스턴스를 생성하고 export default 로 꺼냄.
export default new VueRouter();
src/main.js - 인스턴스 라우터를 연결해줌
...
import router from '@/router/index';
...
new Vue({
...
router,
}).$mount('#app');
src/router/index.js
...
import LoginPage from '@/view/LoginPage.vue';
import SignupPage from '@/view/SignupPage.vue';
...
export default new VueRouter({
// routes: Vue Router에 의해서 컨트롤되는 페이지의 정보를 담는 것
routes: [
{
path: '/login',
component: LoginPage,
},
{
path: '/signup',
component: SignupPage,
},
]
});
src/router/index.js
...
routes: [
{
path: '/login',
component: () => import ('@/views/LoginPage.vue'),
},
{
path: '/signup',
component: () => import ('@/views/SignupPage.vue'),
},
]
...
src/router/index.js
...
routes: [
{
path: '/',
redirect: '/login',
},
{
path: '/login',
component: () => import ('@/views/LoginPage.vue'),
},
{
path: '/signup',
component: () => import ('@/views/SignupPage.vue'),
},
]
...
정의되어 있지 않은 모든 url에 대해서 반응하기 위한 정의
...
routes: [
...
{
path: '*',
component: () => import ('@/views/NotFoundPage.vue'),
},
]
...
...
export default new VueRouter({
mode: 'history',
routes: [
...
]
});
mode: 'history'
를 추가하면 url #
제거됨기본적으로 url 에 #
이 붙어있는 이유 (해시 모드)
Reference
Axios 설치
$ npm i axios
Axios로 통신하는 방법
방법 1 : axios 요청을 날리는 Vue 컴포넌트에 직접 axios를 import하여 작성한다
components/SignupForm.vue
<script>
import axios from 'axios';
export default {
...
methods: {
submitForm() {
console.log('폼 제출');
axios.post();
}
},
}
</script>
방법 2 : API 구조화 (추천)
src/api/index.js 파일 생성
import axios from 'axios';
function registerUser(userData) {
const url = 'http://localhost:3000/signup';
return axios.post(url, userData);
}
export { registerUser };
registerUser
함수를 Vue 컴포넌트에서 import 해와서 호출할 수 있게 된다.axios.post()
결과가 Promise 이기 때문에 Promise 를 return을 해줘야 registerUser
를 호출한 이후의 비동기 동작을 수행할 수 있게 된다.components/SignupForm.vue
<script>
import { registerUser } from '@/api/index';
export default {
...
methods: {
submitForm() {
console.log('폼 제출');
const userData = {
username: this.username,
password: this.password,
nickname: this.nickname
}
registerUser(userData);
}
},
}
</script>
비동기 처리
submitForm
을 비동기 처리해줘야 한다.registerUser
함수 호출 결과, Promise가 resolve 된 형태로 들어오게 된다.<script>
import { registerUser } from '@/api/index';
export default {
...
methods: {
async submitForm() {
console.log('폼 제출');
const userData = {
username: this.username,
password: this.password,
nickname: this.nickname
}
const { data } = await registerUser(userData);
console.log(data.username);
this.initForm();
},
initForm() {
this.username = '';
this.password = '';
this.nickname = '';
}
},
}
</script>
url 공통화 - src/api/index.js
import axios from 'axios';
const axiosService = axios.create({
baseURL: 'http://localhost:3000/',
});
function registerUser(userData) {
return axiosService.post('signup', userData);
}
export { registerUser };
axios.create()
라고 하는 axios의 api를 이용하여 인스턴스를 생성한다. API 요청할 때 필요한 공통 설정을 미리 넣을 수 있다..env
파일을 생성한다.{root}/config/dev.env.js
와 같이 생성해야 한다.
In old Vue versions environment variables were defined in e.g. config/dev.env.js instead of the .env files in root.env
VUE_APP_API_URL=http://localhost:3030/
VUE_APP_
을 꼭 포함시켜야 한다.VUE_APP_
가 포함된 환경변수는 아래의 파일에서 따로 import 하지 않아도 자동으로 쓸 수 있게 해준다.src/api/index.js
import axios from 'axios';
const axiosService = axios.create({
baseURL: process.env.VUE_APP_API_URL,
});
function registerUser(userData) {
return axiosService.post('signup', userData);
}
export { registerUser };
.env.development
파일을 새로 생성한다. 이 파일은 개발 모드에서만 동작한다..env
파일은 우선순위가 낮다. 그래서 개발 모드에서는 .env.development
파일이 우선순위가 높아서 이 파일에 있는 환경 변수를 가져온다. 이 파일에 값이 없으면 .env
파일에 있는 값을 찾는다..env
파일은 .env.development
와 .env.production
파일에 없는 공통의 값을 지정하기 위해 사용한다..env.production
파일을 만들 수 있는데 이 파일은 배포 모드에서만 동작한다.vue 예전 버전 : /config/dev.env.js
, /config/prod.env.js
Example - /config/dev.env.js
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
VUE_APP_API_URL:'"http://localhost:3030/"',
})
components/signupForm.vue
<script>
import { registerUser } from '@/api/index';
export default {
...
methods: {
async submitForm() {
try {
// 비즈니스 로직
const userData = {
username: this.username,
password: this.password,
nickname: this.nickname
}
const { data } = await registerUser(userData);
this.logMessage = `${data.user.username}님 회원 가입이 완료되었습니다.`;
} catch (error) {
// 에러 핸들링할 코드
console.log(error.response);
this.logMessage = error.response.data;
} finally {
this.initForm();
}
},
...
},
}
</script>
ID 입력창에 이메일 형식으로 입력하지 않았거나 입력창에 입력값이 없을 경우 회원가입 버튼 비활성화
src/utils/validation.js
function validateEmail(email) {
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
export { validateEmail };
components/signupForm.vue
<template>
<form>
...
<button :disabled="!isUsernameValid || !password" type="submit">회원가입</button>
</form>
</template>
<script>
...
import { validateEmail } from '@utils/validation';
export default {
...
computed: {
isUsernameValid() {
return validateEmail(this.username);
}
},
...
}
</script>
this.username
이 한 글자 한 글자 바뀔 때마다 validateEmail
이라는 함수가 실행이 될 것이다.설치
$ npm i vuex
vuex는 package.json의 dependencies에 들어가게 된다.
dependencies
: 프로젝트를 빌드하면 최종적으로 자원이 압축이 되어 함께 나가게 된다.devDependencies
: 빌드 결과물에 포함되지 않는다.src/store/index.js 파일 생성
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
username: '',
},
mutations: {
setUsername(state, username) {
state.username = username;
}
}
});
state
에 추가해준다.state
: 여러 컴포넌트에서 공유되는 데이터mutations
: 데이터(state)를 변경하는 것
username
을 받아서 state.username
에 넣는다.src/main.js
...
import store from '@/store/index';
new Vue({
...
store,
}).$mount('#app');
LoginForm.vue
<script>
...
export default {
...
methods: {
...
async submitForm() {
const userData = {
username: this.username,
password: this.password,
}
const { data } = await loginUser(userData);
this.$store.commit('setUsername', data.user.username);
this.$router.push('/main');
...
},
...
},
}
</script>
this.$store.commit('setUsername', data.user.username)
: mutations를 호출하여 Store의 state 데이터 값을 변경한다.Header.vue
<template>
...
<span>{{ $store.state.username }}</span>
</template>
$store.state.username
: Store의 state 데이터 중 username
값을 가져와서 출력할 수 있다.store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
username: '',
},
getters: {
isLogin(state) {
return state.username !== '';
},
},
mutations: {
setUsername(state, username) {
state.username = username;
}
}
});
Getters 값 사용하기
방법 1
<template>
...
<template v-if="$store.getters.isLogin">
<span>{{ $store.state.username }}</span>
</template>
<template>
<router-link to="/login">로그인</router-link>
</template>
</template>
<template>
으로 분기처리할 수 있다.$store.getters.isLogin
: Store의 Getters에 직접 접근할 수 있다.방법 2
<template>
...
<template v-if="isUserLogin">
<span>{{ $store.state.username }}</span>
</template>
<template>
<router-link to="/login">로그인</router-link>
</template>
</template>
<script>
export default {
computed: {
isUserLogin() {
return this.$store.getters.isLogin;
}
}
}
</script>
src/api/index.js
import axios from 'axios';
const axiosService = axios.create({
baseURL: process.env.VUE_APP_API_URL,
headers: {
Authorization: 'token_test',
}
});
function registerUser(userData) {
return axiosService.post('signup', userData);
}
...
export { registerUser };
store/index.js
...
export default new Vuex.Store({
state: {
...
token: '',
},
...
mutations: {
...
setToken(state, token) {
state.token = token;
}
}
});
LoginForm.vue
...
<script>
...
methods: {
async submitForm() {
try {
...
const { data } = await loginUser(userData);
this.$store.commit('setToken', data.token);
...
} catch (error) {
...
} finally {
...
}
}
}
</script>
src/api/index.js
import axios from 'axios';
import store from '@/store/index';
const axiosService = axios.create({
baseURL: process.env.VUE_APP_API_URL,
headers: {
Authorization: store.state.token,
}
});
function registerUser(userData) {
return axiosService.post('signup', userData);
}
...
export { registerUser };
현재 로직
src/api/index.js
import axios from 'axios';
import store from '@/store/index';
const axiosService = axios.create({
baseURL: process.env.VUE_APP_API_URL,
headers: {
Authorization: store.state.token,
}
});
function loginUser(userData) {
return axiosService.post('login', userData);
}
...
export { loginUser };
token
값 (defaults: 빈 문자열) 을 axiosService
인스턴스에 설정token
값을 store에 저장token
값을 axiosService
인스턴스에 설정Axios Interceptors 사용해보기
Axios Interceptors:
You can intercept requests or responses before they are handled by then
or catch
.
HTTP 요청과 응답을 컴포넌트 단에서 처리하기 전에 추가 로직을 넣을 수 있다.
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// 요청을 보내기 전에 어떤 처리를 할 수 있다.
return config;
}, function (error) {
// 요청이 잘못되었을 때 에러가 컴포넌트 단으로 오기 전에 어떤 처리를 할 수 있다.
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// 서버에 요청을 보내고 나서 응답을 받기 전에 어떤 처리를 할 수 있다.
return response;
}, function (error) {
// 응답이 에러인 경우에 미리 전처리할 수 있다.
return Promise.reject(error);
});
src/api/common/interceptors.js 파일 생성
export function setInterceptors(axiosService) {
axiosService.interceptors.request.use(function (config) {
// 요청을 보내기 전에 어떤 처리를 할 수 있다.
return config;
}, function (error) {
// 요청이 잘못되었을 때 에러가 컴포넌트 단으로 오기 전에 어떤 처리를 할 수 있다.
return Promise.reject(error);
});
axiosService.interceptors.response.use(function (response) {
// 서버에 요청을 보내고 나서 응답을 받기 전에 어떤 처리를 할 수 있다.
return response;
}, function (error) {
// 응답이 에러인 경우에 미리 전처리할 수 있다.
return Promise.reject(error);
});
return axiosService;
}
src/api/index.js
import axios from 'axios';
import store from '@/store/index';
import { setInterceptors } from './common/interceptors';
function createAxiosService() {
const axiosService = axios.create({
baseURL: process.env.VUE_APP_API_URL,
headers: {
Authorization: store.state.token,
}
});
return setInterceptors(axiosService);
}
const axiosService = createAxiosService();
function loginUser(userData) {
return axiosService.post('login', userData);
}
...
export { loginUser };
인터셉터를 사용하는 것은 필수는 아니다. api/index.js에서 Axios 인스턴스를 만들 때 Authorization에 token 값을 설정해줘도 된다.
먼저 api/index.js 의 token 설정 부분을 제거한다.
src/api/index.js
import axios from 'axios';
import { setInterceptors } from './common/interceptors';
function createAxiosService() {
const axiosService = axios.create({
baseURL: process.env.VUE_APP_API_URL,
});
return setInterceptors(axiosService);
}
const axiosService = createAxiosService();
function loginUser(userData) {
return axiosService.post('login', userData);
}
...
export { loginUser };
inspectors 설정 파일에 store 를 import해주고 요청을 보내기 직전에 config.headers.Authorization
에 token 값을 넣어준다.
src/api/common/interceptors.js
import store from '@/store/index';
export function setInterceptors(axiosService) {
axiosService.interceptors.request.use(
function (config) {
config.headers.Authorization = store.state.token;
return config;
},
function (error) {
return Promise.reject(error);
}
);
axiosService.interceptors.response.use(
function (response) {
return response;
},
function (error) {
return Promise.reject(error);
}
);
return axiosService;
}
일단 여기까지 최종 로그인 로직 (추후에 브라우저 저장소를 이용하는 로직을 추가하며 약간 변경될 예정임)
/src/utils/cookies.js 생성
function saveAuthToCookie(value) {
document.cookie = `auth=${value}`;
}
function saveUserToCookie(value) {
document.cookie = `user=${value}`;
}
function getAuthFromCookie() {
return document.cookie.replace(
/(?:(?:^|.*;\s*)auth\s*=\s*([^;]*).*$)|^.*$/,
'$1',
);
}
function getUserFromCookie() {
return document.cookie.replace(
/(?:(?:^|.*;\s*)user\s*=\s*([^;]*).*$)|^.*$/,
'$1',
);
}
function deleteCookie(value) {
document.cookie = `${value}=; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
}
export {
saveAuthToCookie,
saveUserToCookie,
getAuthFromCookie,
getUserFromCookie,
deleteCookie,
};
LoginForm.vue
<script>
...
import { saveAuthToCookie, saveUserToCookie } from '@/utils/cookies';
export default {
...
methods: {
async submitForm() {
try {
...
const { data } = await loginUser(userData);
this.$store.commit('setToken', data.token);
this.$store.commit('setUsername', data.user.username);
saveAuthToCookie(data.token);
saveUserToCookie(data.user.username);
this.$router.push('/main');
} catch (error) {
...
} finally {
...
}
}
}
}
</script>
token
값과 user.username
값을 Cookies 에 저장store/index.js
...
import { getAuthFromCookie, getUserFromCookie } from '@/utils/cookies';
...
export default new Vuex.Store({
state: {
username: getUserFromCookie() || '',
token: getAuthFromCookie() || '',
},
getters: {
...
},
mutations: {
...
},
});
store/index.js
import { saveAuthToCookie, saveUserToCookie } from '@/utils/cookies';
import { loginUser } from '@/api/index';
...
actions: {
async LOGIN({ commit }, userData) {
const { data } = await loginUser(userData); // api 호출
commit('setToken', data.token);
commit('setUsername', data.user.username);
saveAuthToCookie(data.token);
saveUserToCookie(data.user.username);
return data;
}
}
return data
를 해준다.loginForm.vue
<script>
...
export default {
...
methods: {
async submitForm() {
try {
...
// const { data } = await loginUser(userData);
// this.$store.commit('setToken', data.token);
// this.$store.commit('setUsername', data.user.username);
// saveAuthToCookie(data.token);
// saveUserToCookie(data.user.username);
await this.$store.dispatch('LOGIN', userData);
this.$router.push('/main');
} catch (error) {
...
} finally {
...
}
}
}
}
</script>
/api/index.js 기존 코드
import axios from 'axios';
import { setInterceptors } from './common/interceptors';
function createAxiosService() {
const axiosService = axios.create({
baseURL: process.env.VUE_APP_API_URL,
});
return setInterceptors(axiosService);
}
const axiosService = createAxiosService();
function registerUser(userData) {
return axiosService.post('signup', userData);
}
function loginUser(userData) {
return axiosService.post('login', userData);
}
function fetchPosts() {
return instance.get('posts');
}
...
export { registerUser, loginUser, fetchPosts, ... };
registerUser
, loginUser
와 같은 로그인 이전 단계의 요청들에도 token 값이 같이 들어갈 수 있게 된다. 백엔드에서 엄격하게 코드를 짰다면 이 부분에서 에러가 날 것이다./api/index.js 함수를 두 개로 분리한 코드
import axios from 'axios';
import { setInterceptors } from './common/interceptors';
function createAxiosService() {
return axios.create({
baseURL: process.env.VUE_APP_API_URL,
});
}
function createAxiosServiceWithAuth(url) {
const axiosService = axios.create({
baseURL: `${process.env.VUE_APP_API_URL}/${url}`,
});
return setInterceptors(axiosService);
}
const axiosService = createAxiosService();
const posts = createAxiosServiceWithAuth('posts');
function registerUser(userData) {
return axiosService.post('signup', userData);
}
...
export { registerUser, loginUser, fetchPosts, ... };
createAxiosService
: 인증값이 필요없는 요청이 일어날 때createAxiosServiceWithAuth
: 인증값이 필요한 요청이 일어날 때
url
을 매개변수로 받아와서 baseURL
을 설정해준다. (restful api)src/api/auth.js 파일 생성 - 로그인, 회원가입 등 계정과 관련된 API
import { axiosService } from './index';
function registerUser(userData) {
return axiosService.post('signup', userData);
}
function loginUser(userData) {
return axiosService.post('login', userData);
}
export { registerUser, loginUser };
src/api/posts.js 파일 생성 - posts 와 관련된 API
import { posts } from './index';
function fetchPosts() {
return posts.get('/');
}
function createPosts(postData) {
return posts.post('/', postData);
}
export { fetchPosts, createPosts };
src/api/index.js
import axios from 'axios';
import { setInterceptors } from './common/interceptors';
function createAxiosService() {
return axios.create({
baseURL: process.env.VUE_APP_API_URL,
});
}
function createAxiosServiceWithAuth(url) {
const axiosService = axios.create({
baseURL: `${process.env.VUE_APP_API_URL}/${url}`,
});
return setInterceptors(axiosService);
}
export const axiosService = createAxiosService();
export const posts = createAxiosServiceWithAuth('posts');
{{ message | capitalize }}
필터 함수 이름을 우측에 넣어주면 데이터를 필터링한 결과를 화면에 보여준다.Vue filters 정의
<script>
...
filters: {
capitalize() {
if (!value) return '';
value = value.toString();
return value.charAt(0).toUpperCase() + value.slice(1);
}
}
</script>
Reference
Date 형식 정의하는 Filter
<template>
...
<span>{{ postItem.createDT | formatDate }}</span>
...
</template>
<script>
...
filters: {
formatDate(value) {
return new Date(value);
}
}
...
</script>
필터를 여러 컴포넌트에 적용할 수 있는 형태로 재사용 가능하게 하는 방법
utils/filters.js 파일 생성
export function formatDate(value) {
const date = new Date(value);
const year = date.getFullYear();
let month = date.getMonth() + 1;
month = month > 9 ? month : `0${month}`;
const day = date.getDate();
let hours = date.getHours();
hours = hours > 9 ? hours : `0${hours}`;
const minutes = date.getMinutes();
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
main.js
...
import { formatDate } from '@/utils/filters';
Vue.filter('formatDate', formatDate);
...
router/index.js
...
{
path: '/post/:id',
component: () => import('@/views/PostEditPage.vue');
}
...
PostListItem.vue
...
methods: {
...
routeEditPage() {
const id = this.postItem.id;
this.$router.push(`/post/${id}`);
}
}
...
router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
const router = new VueRouter({
routes: [
{
path: '/',
redirect: '/login',
},
{
path: '/login',
component: () => import('@/views/LoginPage.vue'),
},
{
path: '/signup',
component: () => import('@/views/SignupPage.vue'),
},
{
path: '/main',
component: () => import('@/views/MainPage.vue'),
meta: { auth: true },
},
{
path: '/add',
component: () => import('@/views/PostAddPage.vue'),
meta: { auth: true },
},
{
path: '/post/:id',
component: () => import('@/views/PostEditPage.vue'),
meta: { auth: true },
},
{
path: '*',
component: () => import('@/views/NotFoundPage.vue'),
},
],
});
router.beforeEach((to, from, next) => {
if (to.meta.auth && !store.getters.isLogin) {
console.log('인증이 필요합니다');
next('/login');
return;
}
next();
});
export default router;
meta
속성을 추가한다.new VueRouter
인스턴스를 새로운 변수 router
에 담아준다.router
변수에 beforeEach
를 사용한다. (라우터 네비게이션 가드)
to
: 이동하려는 페이지from
: 현재 페이지next
: 다음 페이지로 이동할 수 있도록 호출하는 API위 코드에서는 라우트 이동이 일어나기 전에 페이지 접근 권한이 있는지 확인하고 있다.
to.meta.auth
: 인증이 필요한 페이지인지 확인store.getters.isLogin
: 현재 로그인되어 있는 상태인지 확인next('/login')
으로 로그인 페이지로 이동시킨다.return;
을 꼭 넣어줘야 한다. next('/login')
을 하고나서 next();
를 또 호출하는 불필요한 현상이 발생하기 때문이다.ex) 로그인 기능
JS 테스팅 도구 사용량 통계
Jest 공식문서
파일명
{파일명}.spec.js
이나 {파일명}.test.js
으로 중간에 spec
이나 test
를 넣어줘야 한다.jest.config.js 파일을 생성해서 테스트 대상을 지정할 수 있다.
module.exports = {
preset: '@vue/cli-plugin-unit-jest',
testMatch: [
'<rootDir>/src/**/*.spec.(js|js|ts|tsx)|**/__tests__/*.(js|js|ts|tsx)';
],
}
tests
폴더 안에 JS 테스트 파일을 작성해야 한다. 이러한 방식은 개인의 선호도가 있겠지만 테스트 파일은 테스트할 파일과 가장 근접하게 두는 것을 추천한다./components/__test__
폴더를 생성해서 테스트 파일을 작성하는 방식이 있다.eslint에 jest 문법을 이해할 수 있도록 설정
.eslintrc 파일에 아래와 같이 추가
{
"env": {
"jest": true
},
}
Jest에서 제공하는 API
describe
: 연관된 테스트 케이스를 그룹화하는 APItest
: 하나의 테스트 케이스를 검증하는 APIexpect
: 테스트 결과를 예상하는 API테스트 작성해보기
math.js
export function sum(a, b) {
return a + b;
}
math.spec.js
import { sum } from './math';
describe('math.js', () => {
test('10 + 20 = 30', () => {
const result = sum(10, 20);
expect(result).toBe(30);
});
});
ex) LoginForm.vue 컴포넌트 테스트
LoginForm.spec.js 파일 생성
import Vue from 'vue';
import LoginForm from './LoginForm.vue';
describe('LoginForm.vue', () => {
test('컴포넌트가 마운팅되면 username이 존재하고 초기값으로 설정되어 있어야 한다.', () => {
const instance = new Vue(LoginForm).$mount();
expect(instance.username).toBe('');
});
});
Vue Test Utils 라이브러리를 사용하면 LoginForm.spec.js을 아래와 같이 수정할 수 있다.
import { shallowMount } from '@vue/test-utils';
import LoginForm from './LoginForm.vue';
describe('LoginForm.vue', () => {
test('컴포넌트가 마운팅되면 username이 존재하고 초기값으로 설정되어 있어야 한다.', () => {
const wrapper = shallowMount(LoginForm);
expect(wrapper.vm.username).toBe('');
});
});
LoginForm 컴포넌트에서 테스트해야 할 기능
LoginForm.spec.js
import { shallowMount } from '@vue/test-utils';
import LoginForm from './LoginForm.vue';
describe('LoginForm.vue', () => {
test('ID는 이메일 형식이어야 한다.', () => {
const wrapper = shallowMount(LoginForm);
const idInput = wrapper.find('#username');
...
});
});
find
: Vue Test Utils 에서 제공하는 것으로, 컴포넌트가 화면에 부착이 되었을 때 Template 안에 있는 Tag들, HTML 요소를 쫓아갈 수 있는 APIidInput
: <input id="username" type="text">
LoginForm.spec.js
import { shallowMount } from '@vue/test-utils';
import LoginForm from './LoginForm.vue';
describe('LoginForm.vue', () => {
test('ID는 이메일 형식이어야 한다.', () => {
const wrapper = shallowMount(LoginForm, {
data() {
return {
username: 'test',
}
}
});
const idInput = wrapper.find('#username');
console.log('인풋 박스의 값', idInput.element.value);
console.log(wrapper.vm.isUsernameValid);
});
});
shallowMount
의 두 번째 인자는 컴포넌트 마운팅할 때 옵션이다. 실제로 컴포넌트 정의할 때 넣는 데이터 속성처럼 넣어줄 수 있다.wrapper.vm.isUsernameValid
: LoginForm 컴포넌트에 정의되어 있는 computed 속성인 isUsernameValid
값에 접근할 수 있다.사용자 관점의 테스트 - LoginForm.spec.js
import { shallowMount } from '@vue/test-utils';
import LoginForm from './LoginForm.vue';
describe('LoginForm.vue', () => {
test('ID가 이메일 형식이 아니면 경고 메세지가 출력된다.', () => {
const wrapper = shallowMount(LoginForm, {
data() {
return {
username: 'test',
}
}
});
const warningText = wrapper.find('.warning');
expect(warningText.exists()).toBeTruthy();
});
});
username
이 이메일 형식에 맞지 않았을 때 LoginForm.vue 에 있는 warning
이라는 class를 가진 태그가 화면에 보여줘야 한다.exists
: 있으면 true
를 반환하고 없으면 false
를 반환하는 Vue Test Utils 의 기능이다.ID와 PW가 입력되지 않은 경우 테스트 추가
import { shallowMount } from '@vue/test-utils';
import LoginForm from './LoginForm.vue';
describe('LoginForm.vue', () => {
test('ID가 이메일 형식이 아니면 경고 메세지가 출력된다.', () => {
const wrapper = shallowMount(LoginForm, {
data() {
return {
username: 'test',
}
}
});
const warningText = wrapper.find('.warning');
expect(warningText.exists()).toBeTruthy();
});
test('ID와 PW가 입력되지 않으면 로그인 버튼이 비활성화 된다.', () => {
const wrapper = shallowMount(LoginForm, {
data() {
return {
username: '',
password: '',
}
}
});
const button = wrapper.find('button');
expect(button.element.disabled).toBeTruthy();
});
});
테스트 코드가 잘 작성되었는지 확인하려면 항상 반대 케이스를 넣어봐야 한다.
import { shallowMount } from '@vue/test-utils';
import LoginForm from './LoginForm.vue';
describe('LoginForm.vue', () => {
...
test('ID와 PW가 입력되지 않으면 로그인 버튼이 비활성화 된다.', () => {
const wrapper = shallowMount(LoginForm, {
data() {
return {
username: 'a@a.com',
password: '1234',
}
}
});
const button = wrapper.find('button');
expect(button.element.disabled).toBeTruthy(); // 테스트 실패!
});
});