[개발자의품격][부트캠프][1기][24차시] Vue.js #17 | 로그인 기능(vuex-persistedstate, vue-cookies 등 사용)
로그인 기능
src/views/LoginView.vue 컴포넌트를 생성한 후 src/router/index.js에 경로 설정을 한다. vuex 관련 코드는 일단 주석 처리한다.
// src/router/index.js
...
import LoginView from '../views/LoginView.vue'
// import store from '../store'
...
const routes = [
...
{
path: '/login',
name: 'LoginView',
component: LoginView
},
...
// router.beforeEach((to, from, next) => {
// if (to.path === '/') {
// next()
// } else if (to.path === '/vuex/todo') {
// next()
// } else {
// if (store.getters['user/isLogin']) {
// next()
// } else {
// next('/vuex/todo')
// }
// }
// })
Bootstrap > Examples > Sign-in(https://getbootstrap.com/docs/5.1/examples/sign-in/)에서 소스보기(Ctrl+U)하여 HTML 소스는 <template> 안에 CSS 소스는 <style scope> 안에 각각 복붙 한다. <form> 태그를 <div>로 변경한다. 이번 실습에서는 <form> 태그를 사용하지 않는다. 완료되면 기존 화면은 [그림 1]과 같은 화면으로 변경된다.
<!-- src/views/LoginView.vue -->
<template>
<main class="form-signin">
<div>
<img class="mb-4" src="@/assets/logo.png" alt="" width="72" height="72" />
<h1 class="h3 mb-3 fw-normal">Please sign in</h1>
<div class="form-floating">
<input
type="email"
class="form-control"
id="floatingInput"
placeholder="name@example.com"
/>
<label for="floatingInput">Email address</label>
</div>
<div class="form-floating">
<input
type="password"
class="form-control"
id="floatingPassword"
placeholder="Password"
/>
<label for="floatingPassword">Password</label>
</div>
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me" /> Remember me
</label>
</div>
<button class="w-100 btn btn-lg btn-primary" type="submit">
Sign in
</button>
<p class="mt-5 mb-3 text-muted">© 2017–2021</p>
</div>
</main>
</template>
<script>
export default {
components: {},
data() {
return {
sampleData: ''
}
},
setup() {},
created() {},
mounted() {},
unmounted() {},
methods: {}
}
</script>
<style scoped>
html,
body {
height: 100%;
}
body {
display: flex;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.form-signin {
width: 100%;
max-width: 330px;
padding: 15px;
margin: auto;
}
.form-signin .checkbox {
font-weight: 400;
}
.form-signin .form-floating:focus-within {
z-index: 2;
}
.form-signin input[type='email'] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type='password'] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
</style>
<script> 태그 내 data()에 email, pw 데이터를 넣는다. <template> 태그 내 <input type="email"...>, <input type="password"...> 태그 내에 email, pw 데이터를 각각 v-model로 바인딩한다.
<template> 태그 내 Sign-in 버튼의 type="submit"을 @click="login"으로 변경하고, <script> 태그 내 methods 내에 login() 함수를 선언한다.
<!-- src/views/LoginView.vue -->
<template>
<main class="form-signin">
<div>
...
<div class="form-floating">
<input
type="email"
...
v-model="email"
/>
...
</div>
<div class="form-floating">
<input
type="password"
...
v-model="pw"
/>
...
<button class="w-100 btn btn-lg btn-primary" @click="login">
Sign in
</button>
...
</template>
<script>
export default {
...
data() {
return {
email: '',
pw: ''
}
},
...
methods: {
login() {
this.$store.commit('user/setUser', {
name: 'Sewol',
email: 'sewol@gmail.com'
})
}
}
...
route path에 navigation이 보이지 않게 처리
routes의 index.js에서 루트 경로를 LoginView.vue 컴포넌트와 연결하는 route를 추가한다.
// src/router/index.js
...
const routes = [
{
path: '/',
name: 'login',
component: LoginView
},
{
path: '/home',
name: 'home',
component: HomeView
},
{
path: '/login',
name: 'login2',
component: LoginView
},
...
components 폴더 내에 layouts/HeaderLayout.vue를 생성하고 App.vue의 모든 소스코드를 HeaderLayout.vue에 복사한다. 이때 아래와 같이 일부 코드를 주석 처리(또는 삭제)한다.
<!-- src/components/layouts/HeaderLayout.vue -->
<template>
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
<router-link to="/hello">Hello</router-link>
</nav>
<!-- <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>
App.vue에 HeaderLayout.vue를 import 하고, 경로가 루트가 아닐 때 HeaderLayout을 보여주도록 한다.
<!-- src\App.vue -->
<template>
<!-- <nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
<router-link to="/hello">Hello</router-link>
</nav> -->
<HeaderLayout v-if="$route.path != '/'" />
<router-view />
</template>
<script>
import HeaderLayout from '@/components/layouts/HeaderLayout.vue'
export default {
components: { HeaderLayout }
}
</script>
...
LoginView.vue 컨포넌트의 login() 함수에서 $router를 이용해 로그인이 되면 /home으로 이동시킨다.
// src/views/LoginView.vue
...
<script>
export default {
...
methods: {
login() {
this.$store.commit('user/setUser', {
name: 'Sewol',
email: 'sewol@gmail.com'
})
this.$router.push({ path: '/home' }) // 추가
}
HeaderLayout.vue 컴포넌트에 Bootstrap를 적용해 NavBar를 꾸며준다. Bootstrap의 Examples에서 Carousel(https://getbootstrap.com/docs/5.1/examples/carousel/을 선택하고 소스보기에서 <header> 부분만 <template> 태그 내에 복붙 한다. 코드 내 주석 처리된 부분은 수정한 코드이다.
<!-- src/components/layouts/HeaderLayout.vue -->
<template>
<header>
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Carousel</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarCollapse"
aria-controls="navbarCollapse"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item">
<!-- <a class="nav-link active" aria-current="page" href="#">Home</a> -->
<a
class="nav-link active"
aria-current="page"
@click="goToMenu('/home')"
>Home</a
>
</li>
<li class="nav-item">
<!-- <a class="nav-link" href="#">Link</a> -->
<a class="nav-link" @click="goToMenu('/about')">About</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link disabled">Disabled</a>
</li> -->
</ul>
<form class="d-flex">
<input
class="form-control me-2"
type="search"
placeholder="Search"
aria-label="Search"
/>
<button class="btn btn-outline-success" type="submit">
Search
</button>
</form>
</div>
</div>
</nav>
</header>
</template>
<script> 태그 내에 goTomenu() 함수를 선언한다.
// src/components/layouts/HeaderLayout.vue
<script>
export default {
methods: {
goToMenu(path) {
this.$router.push({ path: path })
}
}
}
</script>
<style> 태그 내에 header의 스타일을 지정한다. NavBar에 페이지가 가려지는 문제를 해결하기 위해서이다.
// src/components/layouts/HeaderLayout.vue
<style>
header {
margin-bottom: 70px;
}
</style>
[그림 2]는 About 페이지를 선택했는데 페이지는 정상적으로 보여주나 NavBar는 Home을 선택한 것으로 표시하고 있다. 이 문제를 해결하기 위해서 :class 코드를 추가한다.
<!-- src/components/layouts/HeaderLayout.vue -->
<template>
...
<li class="nav-item">
<a
class="nav-link"
:class="{ active: $route.path == '/home' }"
aria-current="page"
@click="goToMenu('/home')"
>Home</a>
</li>
<li class="nav-item">
<a
class="nav-link"
:class="{ active: $route.path == '/about' }"
@click="goToMenu('/about')"
>About</a>
</li>
...
로그인한 경우 [그림 3]과 같이 userInfo가 표시되도록 하고, 로그아웃 버튼을 만든다.
<!-- src/components/layouts/HeaderLayout.vue -->
<template>
...
<div class="d-flex">
<span v-if="userInfo.name" class="text-white">{{
userInfo.name
}}</span>
<button class="btn btn-outline-success">로그아웃</button>
</div>
...
사용자 정보를 가져오는 userInfo() 함수를 선언한다.
// src/components/layouts/HeaderLayout.vue
export default {
computed: {
userInfo() {
return this.$store.state.user.userInfo
}
},
methods에 logout() 함수를 선언하여 로그아웃 기능을 구현한다.
<!-- src/components/layouts/HeaderLayout.vue -->
<template>
...
<button class="btn btn-outline-success" @click="logout">
로그아웃
</button>
...
<script>
...
methods: {
...
logout() {
this.$store.commit('user/setUser', {})
this.$router.push({ path: '/' })
}
...
vuex-persistedstate
새로고침 하면 사용자 정보가 사라진다. 사용자 정보가 사라지지 않도록 하고자 한다. 터미널 창에서 vuex-persistedstate를 설치한다.
npm install vuex-persistedstate
store폴더의 index.js에 vuex-persistedstate를 import 한다. persistedstate 객체의 paths의 값에 상태를 영속적으로 유지하고자 하는 사용자 정보를 넣으면 새로고침이 일어나더라도 사용자 정보가 영구히 유지된다. 이것이 가능한 이유는 [그림 4]와 같이 사용자 정보가 Local storage 안에 있기 때문이다.
// src/store/index.js
...
import persistedstate from 'vuex-persistedstate'
export default createStore({
...
plugins: [persistedstate({ paths: ['user.userInfo'] })]
})
Local storage 내에 민감한 정보가 남아 있을 경우 문제가 될 수 있다. 따라서 [그림 5]와 같이 로그아웃할 때 사용자 정보가 사라지게 만들어야 한다. (이러한 처리를 해도 로그아웃 없이 창을 닫으면 사용자 정보가 남아있게 된다.)
// src/store/user.js
export const user = {
...
mutations: {
...
logout(state) {
state.userInfo = {}
}
...
// src/components/layouts/HeaderLayout.vue
<script>
export default {
...
methods: {
...
logout() {
this.$store.commit('user/logout', {})
...
}
vue-cookies
vue-cookies 모듈을 설치한다. vue-cookies는 설정된 시간에 자동 로그아웃되게 할 수 있다.
npm install vue-cookies
// src/store/user.js
import VueCookies from 'vue-cookies'
export const user = {
...
getters: {
isLogin(state) {
// if (state.userInfo.name) {
// return true
// } else {
// return false
// }
if (VueCookies.get('userInfo')) {
return true
} else {
return false
}
}
},
mutations: {
setUser(state, userInfo) {
...
VueCookies.set('userInfo', userInfo, '1MIN')
},
logout(state) {
...
VueCookies.remove('userInfo')
}
...
// src/router/index.js
...
import store from '../store'
...
...
router.beforeEach((to, from, next) => {
if (to.path === '/') {
next()
} else {
if (store.getters['user/isLogin']) {
next()
} else {
store.commit('/user/logout')
next('/')
}
}
})
...
실무에서는 위와 같이 클라이언트에서 인증 섹션을 관리하지 않고 JSON 토큰 기반으로 서버에서 관리한다.