Vue.js CRUD - 로그인/회원가입(Front-end)

김정용·2020년 1월 1일
7

원본 소스 : 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


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


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


components 디렉토리는 기능별 웹 페이지들의 화면을 구성할 컴포넌트들로 구성되어 있다.

  • components/Login.vue
    로그인 컴포넌트이다.
<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>
  • components/Register.vue
    회원가입 컴포넌트 이다.
<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>
  • components/Home.vue
    홈 화면 컴포넌트이다.
<template>
  <div class="container">
    <header class="jumbotron">
      <h3>Home content</h3>
    </header>
  </div>
</template>

<script>

export default {
  name: 'Home',
  data() {
    return {
      content: ''
    }
  },
  mounted() {
  }
}
</script>
  • components/Profile.vue
    로그인 된 사용자의 프로필 컴포넌트이다.
<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


store는 로그인 상태, 데이터 상태 등 각종 상태 관리를 위한 일종의 모듈같은것이다.
vue.js는 store를 쉽게 구성할 수 있는 vuex라는 라이브러리를 자체적으로 제공한다.

  • store/index.js
    vuex 인스턴스 생성부이다.
    로그인 관련 상태를 저장할 auth.module을 import한다.
import Vue from 'vue'
import Vuex from 'vuex'

import { auth } from './auth.module'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    auth
  }
})
  • store/auth.module.js
    회원 인증 관련 상태관리 모듈이다.
    로그인, 로그아웃, 회원가입 등의 과정에서 발생하는 상태 데이터들이 이곳에 저장되고 관리된다.
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 = {}
    }
  }
}

router


vue router의 각종 path들이 정의될 공간입니다.

  • index.js
    최상위 라우터이다.
    본 예제 프로젝트에서는 라우팅될 페이지가 많지 않으므로 이곳에만 라우터를 정의하였다.
    애플리케이션의 규모에 따라 라우터의 구조는 다양한 구조로 구성될 수 있다.
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


services 디렉토리는 api 요청이나 데이터 처리들을 용도별로 모듈화시켜 놓은 공간이다.

  • auth-header.js
    로그인을 통해 발급받은 토큰을 auth api 요청시 헤더에 설정하는 모듈이다.
    본 예제에서는 서버로부터 jwt 방식의 토큰을 발급받는 방식을 사용하였다.
export default function authHeader () {
  let user = JSON.parse(localStorage.getItem('user'))
  if (user && user.accessToken) {
    return { Authorization: 'Bearer ' + user.accessToken }
  } else {
    return {}
  }
}
  • auth-service.js
    auth 데이터 처리 모듈이다.
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()
  • user-service.js
    사용자 데이터 처리 모듈이다.
    발급받은 로그인 토큰의 인증을 통해 데이터를 가져오는 함수가 선언되어있다.
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 서버에 해당하는 백엔드 구성을 진행하겠다.

profile
Network Programming, Cloud, IOT, Web

0개의 댓글