Dandy Now!
  • [개발자의품격][부트캠프][1기][24차시] Vue.js #17 | 로그인 기능(vuex-persistedstate, vue-cookies 등 사용)
    2022년 03월 20일 20시 51분 42초에 업로드 된 글입니다.
    작성자: DandyNow
    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
    반응형
    댓글