영광의 시대!/2022 개발자의 품격 부트캠프 1기

[개발자의품격][부트캠프][1기][24차시] Vue.js #17 | 로그인 기능(vuex-persistedstate, vue-cookies 등 사용)

DandyNow 2022. 3. 20. 20:51
728x90
반응형

로그인 기능

 

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">&copy; 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>

 

[그림 1] Bootstrap Sign-in 적용

 

<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' }) // 추가
    }

 

[그림 2] 로그인 전에는 NavBar가 보이지 않게 되었다.

 

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 page를 보여주고 있으나 Home을 선택한 것으로 표시되어 있다.

 

[그림 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
    }
  },

 

[그림 3] 로그아웃 버튼과 Sewol이라는 사용자 정보가 표시되었다.

 

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'] })]
})

 

[그림 4] 사용자 정보가 Local Storage 안에 있다.

 

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', {})
      ...
    }

 

[그림 5] 로그아웃할 때 사용자 정보가 Local Storage 안에서 사라진다.

 

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 토큰 기반으로 서버에서 관리한다.

728x90
반응형