원본 소스 : https://github.com/jyongk/vuejs-login
npm을 통해 vue-cli 최신버전을 설치하고 webpack 템플릿 기반으로 프로젝트를 생성한다.
npm install -g vue-cli
vue init webpack vuejs-login
vue-login
├── src
│ ├── main.js
│ ├── App.vue
│ └── models
│ └── user.js
│ └── router
│ └── index.js
│ └── services
│ └── auth-header.js
│ ├── auth.service.js
│ └── user.service.js
│ └── store
│ └── auth.module.js
│ ├── index.js
│ └── map.module.js
│ └── components
│ └── Home.vue
│ ├── Login.vue
│ ├── Profile.vue
│ └── Register.vue
└── index.html
프로젝트를 구성하는데 필요한 패키지들을 설치한다.
npm --save install bootstrap-vue
npm --save install bootstrap
npm --save install @fortawesome/fontawesome-svg-core
npm --save install @fortawesome/free-solid-svg-icons
npm --save install @fortawesome/vue-fontawesome
npm --save install axios
npm --save install vuex
npm install --save vee-validate@2.2.15
main.js 파일은 vue 애플리케이션의 진입점이다.
최상위 컨테이너 뷰인 App.vue 파일을 로드하는 구조로 구성된다.
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap-vue/dist/bootstrap-vue.min.css'
import VeeValidate from 'vee-validate'
import { library } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import {
faHome,
faUser,
faUserPlus,
faSignInAlt,
faSignOutAlt
} from '@fortawesome/free-solid-svg-icons'
library.add(faHome, faUser, faUserPlus, faSignInAlt, faSignOutAlt)
Vue.config.productionTip = false
Vue.use(BootstrapVue)
Vue.use(VeeValidate)
Vue.component('font-awesome-icon', FontAwesomeIcon)
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
App.vue 파일은 최상위 컨테이너이다.
본 애플리케이션의 전체 레이아웃은 상단 메뉴부와 컨텐츠 부로 구성된다.
컨텐츠부는 vue router를 통해 처리된다.
<template>
<div id="app">
<nav class="navbar navbar-expand navbar-dark bg-dark">
<div class="navbar-brand">vue-login</div>
<div class="navbar-nav mr-auto">
<li class="nav-item">
<a href="/" class="nav-link">
<font-awesome-icon icon="home" /> Home
</a>
</li>
</div>
<div class="navbar-nav ml-auto" v-if="!currentUser">
<li class="nav-item">
<a href="/register" class="nav-link">
<font-awesome-icon icon="user-plus" /> Sign Up
</a>
</li>
<li class="nav-item">
<a href="/login" class="nav-link">
<font-awesome-icon icon="sign-in-alt" /> Login
</a>
</li>
</div>
<div class="navbar-nav ml-auto" v-if="currentUser">
<li class="nav-item">
<a href="/profile" class="nav-link">
<font-awesome-icon icon="user" />
{{currentUser.username}}
</a>
</li>
<li class="nav-item">
<a href class="nav-link" @click="logOut">
<font-awesome-icon icon="sign-out-alt" /> LogOut
</a>
</li>
</div>
</nav>
<div class="container">
<router-view />
</div>
</div>
</template>
<script>
export default {
computed: {
currentUser () {
return this.$store.state.auth.user
}
},
methods: {
logOut () {
this.$store.dispatch('auth/logout')
this.$router.push('/')
}
}
}
</script>
components 디렉토리는 기능별 웹 페이지들의 화면을 구성할 컴포넌트들로 구성되어 있다.
<template>
<div class="col-md-12">
<div class="card card-container">
<img
id="profile-img"
src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
class="profile-img-card"
/>
<form name="form" @submit.prevent="handleLogin">
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
class="form-control"
name="username"
v-model="user.username"
v-validate="'required'"
/>
<div
class="alert alert-danger"
role="alert"
v-if="errors.has('username')"
>Username is required!</div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
class="form-control"
name="password"
v-model="user.password"
v-validate="'required'"
/>
<div
class="alert alert-danger"
role="alert"
v-if="errors.has('password')"
>Password is required!</div>
</div>
<div class="form-group">
<button class="btn btn-primary btn-block" :disabled="loading">
<span class="spinner-border spinner-border-sm" v-show="loading"></span>
<span>Login</span>
</button>
</div>
<div class="form-group">
<div class="alert alert-danger" role="alert" v-if="message">{{message}}</div>
</div>
</form>
</div>
</div>
</template>
<script>
import User from '../models/user'
export default {
name: 'login',
computed: {
loggedIn() {
return this.$store.state.auth.status.loggedIn
}
},
data() {
return {
user: new User('', ''),
loading: false,
message: ''
}
},
mounted() {
if (this.loggedIn) {
this.$router.push('/')
}
},
methods: {
handleLogin() {
this.loading = true
this.$validator.validateAll()
if (this.errors.any()) {
this.loading = false
return
}
if (this.user.username && this.user.password) {
this.$store.dispatch('auth/login', this.user).then(
() => {
this.$router.push('/profile')
},
error => {
this.loading = false
this.message = error.message
}
)
}
}
}
}
</script>
<style scoped>
label {
display: block;
margin-top: 10px;
}
.card-container.card {
max-width: 350px !important;
padding: 40px 40px;
}
.card {
background-color: #f7f7f7;
padding: 20px 25px 30px;
margin: 0 auto 25px;
margin-top: 50px;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
border-radius: 2px;
-moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
-webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}
.profile-img-card {
width: 96px;
height: 96px;
margin: 0 auto 10px;
display: block;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
border-radius: 50%;
}
</style>
<template>
<div class="col-md-12">
<div class="card card-container">
<img
id="profile-img"
src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
class="profile-img-card"
/>
<form name="form" @submit.prevent="handleRegister">
<div v-if="!successful">
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
class="form-control"
name="username"
v-model="user.username"
v-validate="'required|min:3|max:20'"
/>
<div
class="alert-danger"
v-if="submitted && errors.has('username')"
>{{errors.first('username')}}</div>
</div>
<div class="form-group">
<label for="email">Email</label>
<input
type="email"
class="form-control"
name="email"
v-model="user.email"
v-validate="'required|email|max:50'"
/>
<div
class="alert-danger"
v-if="submitted && errors.has('email')"
>{{errors.first('email')}}</div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
class="form-control"
name="password"
v-model="user.password"
v-validate="'required|min:6|max:40'"
/>
<div
class="alert-danger"
v-if="submitted && errors.has('password')"
>{{errors.first('password')}}</div>
</div>
<div class="form-group">
<button class="btn btn-primary btn-block">Sign Up</button>
</div>
</div>
</form>
<div
class="alert"
:class="successful ? 'alert-success' : 'alert-danger'"
v-if="message"
>{{message}}</div>
</div>
</div>
</template>
<script>
import User from '../models/user'
export default {
name: 'register',
computed: {
loggedIn() {
return this.$store.state.auth.status.loggedIn
}
},
data() {
return {
user: new User('', '', ''),
submitted: false,
successful: false,
message: ''
}
},
mounted() {
if (this.loggedIn) {
this.$router.push('/')
}
},
methods: {
handleRegister() {
this.message = ''
this.submitted = true
this.$validator.validate().then(valid => {
if (valid) {
this.$store.dispatch('auth/register', this.user).then(
data => {
this.message = data.message
this.successful = true
},
error => {
this.message = error.message
this.successful = false
}
).then(
this.$router.push('/')
)
}
})
}
}
}
</script>
<style scoped>
label {
display: block;
margin-top: 10px;
}
.card-container.card {
max-width: 350px !important;
padding: 40px 40px;
}
.card {
background-color: #f7f7f7;
padding: 20px 25px 30px;
margin: 0 auto 25px;
margin-top: 50px;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
border-radius: 2px;
-moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
-webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}
.profile-img-card {
width: 96px;
height: 96px;
margin: 0 auto 10px;
display: block;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
border-radius: 50%;
}
</style>
<template>
<div class="container">
<header class="jumbotron">
<h3>Home content</h3>
</header>
</div>
</template>
<script>
export default {
name: 'Home',
data() {
return {
content: ''
}
},
mounted() {
}
}
</script>
<template>
<div class="container">
<header class="jumbotron">
<h3>user name : {{content.username}}</h3>
<h3>email : {{content.email}}</h3>
</header>
</div>
</template>
<script>
import UserService from '../services/user-service'
export default {
name: 'profile',
data() {
return {
content: ''
}
},
mounted() {
UserService.getUserContent().then(
response => {
this.content = response.data
},
error => {
this.content = error.response.data.message
}
)
}
}
</script>
store는 로그인 상태, 데이터 상태 등 각종 상태 관리를 위한 일종의 모듈같은것이다.
vue.js는 store를 쉽게 구성할 수 있는 vuex라는 라이브러리를 자체적으로 제공한다.
import Vue from 'vue'
import Vuex from 'vuex'
import { auth } from './auth.module'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
auth
}
})
import AuthService from '../services/auth-service'
const user = JSON.parse(localStorage.getItem('user'))
const initialState = user
? { status: { loggedIn: true }, user }
: { status: {}, user: null }
export const auth = {
namespaced: true,
state: initialState,
actions: {
login ({ commit }, user) {
return AuthService.login(user).then(
user => {
commit('loginSuccess', user)
return Promise.resolve(user)
},
error => {
commit('loginFailure')
return Promise.reject(error.response.data)
}
)
},
logout({ commit }) {
AuthService.logout()
commit('logout')
},
register({ commit }, user) {
return AuthService.register(user).then(
response => {
commit('registerSuccess')
return Promise.resolve(response.data)
},
error => {
commit('registerFailure')
return Promise.reject(error.response.data)
}
)
}
},
mutations: {
loginSuccess(state, user) {
state.status = { loggedIn: true }
state.user = user
},
loginFailure(state) {
state.status = {}
state.user = null
},
logout(state) {
state.status = {}
state.user = null
},
registerSuccess(state) {
state.status = {}
},
registerFailure(state) {
state.status = {}
}
}
}
vue router의 각종 path들이 정의될 공간입니다.
import Vue from 'vue'
import Router from 'vue-router'
import Home from '../components/Home.vue'
import Login from '../components/Login.vue'
import Register from '../components/Register.vue'
import Profile from '../components/Profile.vue'
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{
path: '/',
component: Home
},
{
path: '/login',
component: Login
},
{
path: '/register',
component: Register
},
{
path: '/profile',
name: 'profile',
component: Profile
}
]
})
services 디렉토리는 api 요청이나 데이터 처리들을 용도별로 모듈화시켜 놓은 공간이다.
export default function authHeader () {
let user = JSON.parse(localStorage.getItem('user'))
if (user && user.accessToken) {
return { Authorization: 'Bearer ' + user.accessToken }
} else {
return {}
}
}
import axios from 'axios'
const API_URL = 'http://localhost:8080/api/auth/'
class AuthService {
login(user) {
return axios
.post(API_URL + 'signin', {
username: user.username,
password: user.password
})
.then(this.handleResponse)
.then(response => {
if (response.data.accessToken) {
localStorage.setItem('user', JSON.stringify(response.data))
}
return response.data
})
}
logout() {
localStorage.removeItem('user')
}
register(user) {
return axios.post(API_URL + 'signup', {
username: user.username,
email: user.email,
password: user.password
})
}
handleResponse(response) {
if (response.status === 401) {
this.logout()
location.reload(true)
const error = response.data && response.data.message
return Promise.reject(error)
}
return Promise.resolve(response)
}
}
export default new AuthService()
import axios from 'axios'
import authHeader from './auth-header'
const API_URL = 'http://localhost:8080/api/user/'
class UserService {
getUserContent() {
return axios.get(API_URL + 'userContent', { headers: authHeader() })
}
}
export default new UserService()
이것으로 프론트 구성이 완료되었다.
다음 세션에서는 실제 데이터 처리를 담당할 API 서버에 해당하는 백엔드 구성을 진행하겠다.