[프로젝트] 개발자 포트폴리오 - 6. [프론트엔드] 회원 가입, 로그인 구현 (vuex, localStorage)

개발자 포트폴리오 프로젝트

6. [프론트엔드] 회원 가입, 로그인 구현 (vuex, localStorage)

전체 소스 - https://github.com/vividswan/Portfolio-For-Developers


회원 가입 화면 및 기능, resister 객체, vuex 생성

source commit - a096fef

vuex 생성

store/index.js

import Vue from "vue"
import Vuex from "vuex"
import axios from "axios"

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    userInfo: null,
    isLogin: false
  },
  mutations: {
    loginSuccess(state, payload) {
      state.isLogin = true
      state.userInfo = payload
    },
    logout(state) {
      state.isLogin = false
      state.userInfo = null
      localStorage.removeItem("access_token")
    }
  },
  actions: {
    getAccountInfo({ commit }) {
      let token = localStorage.getItem("access_token")
      axios
        .get("/userinfo", {
          headers: {
            "X-AUTH-TOKEN": token
          }
        })
        .then((response) => {
          commit("loginSuccess", response.data.data)
        })
        .catch((error) => {
          console.log(error)
        })
    }
  },
  modules: {}
})

src 하위에 store 폴더를 만들고 index.js를 다음과 같이 생성한다.
state에는 user의 정보와 로그인의 여부를 저장하며 로그인과 로그아웃에 대한 mutations를 만들었다.
getAccountInfo은 localStorage에 저장된 jwt 토큰을 확인 후 이를 api에 전달하여 유저정보를 받아오는 actions이다.
후에 cookie가 web storage 보다 더 나은 방안이라고 생각이 되면 js-cookie를 이용해서 쿠키에 회원정보와 로그인 유무를 담는 것으로 수정할 계획이다.

resisterObj.js, loginObj.js

export default class ResisterObj {
  constructor(email, password, nickname) {
    this.email = email
    this.password = password
    this.nickname = nickname
  }
}
export default class LoginObj {
  constructor(email, password) {
    this.email = email
    this.password = password
  }
}

클라이언트에게 받은 정보를 담을 ResisterObjLoginObj이다.

회원가입 view

회원가입 화면

1

account-register.vue

<template>
  <v-container style="width: 450px">
    <v-layout align-center row wrap>
      <v-flex xs12>
        <v-alert v-if="isError" type="error">
          
        </v-alert>
        <v-card>
          <v-toolbar flat color="indigo">
            <v-toolbar-title
              ><span class="white--text">회원가입</span></v-toolbar-title
            >
          </v-toolbar>
          <div class="pa-5">
            <v-form ref="form" v-model="valid" lazy-validation>
              <v-text-field
                v-model="formData.email"
                :rules="emailRules"
                label="Enter E-mail"
                required
              ></v-text-field>

              <v-text-field
                v-model="formData.name"
                :counter="10"
                :rules="nameRules"
                label="Name"
                required
              ></v-text-field>

              <v-text-field
                v-model="formData.password"
                :append-icon="show ? 'mdi-eye' : 'mdi-eye-off'"
                :rules="[rules.required, rules.min]"
                :type="show ? 'text' : 'password'"
                label="Enter Password"
                hint="At least 8 characters"
                counter
                @click:append="show = !show"
              ></v-text-field>

              <v-text-field
                v-model="chkPassword"
                :append-icon="show ? 'mdi-eye' : 'mdi-eye-off'"
                :rules="[rules.required, rules.min]"
                :type="show ? 'text' : 'password'"
                label="Enter Password Again"
                hint="At least 8 characters"
                counter
                @click:append="show = !show"
              ></v-text-field>

              <h6 v-if="sameChk(chkPassword)" class="mb-5 teal--text accent-3">
                Please create the two passwords identical.
              </h6>
              <h6 v-else class="mb-5 red--text lighten-2">
                Please create the two passwords identical.
              </h6>

              <div class="mt-3 d-flex flex-row-reverse">
                <v-btn color="error" class="mr-4" @click="reset"> 리셋 </v-btn>

                <v-btn
                  :disabled="!valid"
                  color="blue"
                  class="mr-4"
                  @click="register(formData)"
                >
                  회원가입
                </v-btn>
              </div>
            </v-form>
          </div>
        </v-card>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
import RegisterObj from "../models/resisterObj"
import axios from "axios"
export default {
  data: () => ({
    formData: new RegisterObj("", "", ""),
    valid: false,
    nameRules: [
      (v) => !!v || "Name is required",
      (v) => (v && v.length <= 10) || "Name must be less than 10 characters"
    ],
    isError: false,
    errorMsg: "",
    emailRules: [
      (v) => !!v || "E-mail is required",
      (v) => /.+@.+\..+/.test(v) || "E-mail must be valid"
    ],
    show: false,
    chkPassword: "",
    rules: {
      required: (value) => !!value || "Required.",
      min: (v) => v.length >= 8 || "Min 8 characters"
    }
  }),
  methods: {
    goToMain() {
      this.$router.push({
        name: "login"
      })
    },
    sameChk(password) {
      if (this.formData.password == password) return true
      else {
        this.valid = false
        return false
      }
    },
    register(RegisterObj) {
      if (
        !this.formData.email ||
        !this.formData.name ||
        !this.formData.password
      ) {
        this.isError = true
        this.errorMsg = "이메일과 닉네임과 비밀번호를 모두 입력해주세요."
        return
      }
      axios
        .post("/signup", RegisterObj)
        .then(() => {
          this.goToMain()
        })
        .catch((err) => {
          if (err.response) {
            this.isError = true
            this.errorMsg = err.response.data.message
          }
        })
    },
    validate() {
      this.$refs.form.validate()
    },
    reset() {
      this.$refs.form.reset()
    },
    resetValidation() {
      this.$refs.form.resetValidation()
    }
  }
}
</script>

회원가입 화면의 view를 구상하는 컴포넌트이다.
입력 폼은 vuetify 문서의 forms를 참고해서 만들었다.vuetify 한글문서 - forms

// ...
<v-text-field
                v-model="formData.password"
                :append-icon="show ? 'mdi-eye' : 'mdi-eye-off'"
                :rules="[rules.required, rules.min]"
                :type="show ? 'text' : 'password'"
                label="Enter Password"
                hint="At least 8 characters"
                counter
                @click:append="show = !show"
              ></v-text-field>

              <v-text-field
                v-model="chkPassword"
                :append-icon="show ? 'mdi-eye' : 'mdi-eye-off'"
                :rules="[rules.required, rules.min]"
                :type="show ? 'text' : 'password'"
                label="Enter Password Again"
                hint="At least 8 characters"
                counter
                @click:append="show = !show"
              ></v-text-field>

<h6 v-if="sameChk(chkPassword)" class="mb-5 teal--text accent-3">
                Please create the two passwords identical.
              </h6>
              <h6 v-else class="mb-5 red--text lighten-2">
                Please create the two passwords identical.
              </h6>
// ...

// ...
sameChk(password) {
      if (this.formData.password == password) return true
      else {
        this.valid = false
        return false
      }
    },
// ...

두 비밀번호를 메소드로 값이 변할 때마다 확인하여 두 개가 같지 않을 땐 valid를 false로 바꿔준다.

// ...

register(RegisterObj) {
      if (
        !this.formData.email ||
        !this.formData.nickname ||
        !this.formData.password
      ) {
        this.isError = true
        this.errorMsg = "이메일과 닉네임과 비밀번호를 모두 입력해주세요."
        return
      } else if (!this.sameChk) {
        this.isError = true
        this.errorMsg = "두 비밀번호가 같아야 합니다."
        return
      }
      axios
        .post("/sign-up", RegisterObj)
        .then(() => {
          this.goToMain()
        })
        .catch((err) => {
          if (err.response) {
            this.isError = true
            this.errorMsg = err.response.data.messager
          }
        })
    },

// ...

valid가 true 일 때 회원가입 버튼을 누르면 register 메소드를 호출한다.
필요한 모든 값을 다 입력했는지 확인해 주고, sameChk 메소드를 통해 두 비밀번호가 일치했는지도 확인해 준다.
이상이 없으면 axios를 이용해 baseURL의 "/sign-up" 경로에 등록 객체를 post 요청으로 보내준다.
this.goToMain()는 메인 페이지로 보내주는 메소드이다.

로그인 객체 및 로그인 페이지 생성

source commit - e2835c1

로그인 화면

로그인 화면

2

account-login.vue

<template>
  <v-container style="width: 450px">
    <v-layout align-center row wrap>
      <v-flex xs12>
        <v-alert v-if="isError" type="error">
          
        </v-alert>
        <v-card>
          <v-toolbar flat color="indigo">
            <v-toolbar-title
              ><span class="white--text">로그인</span></v-toolbar-title
            >
          </v-toolbar>
          <div class="pa-5">
            <v-form ref="form" v-model="valid" lazy-validation>
              <v-text-field
                v-model="formData.email"
                :rules="emailRules"
                label="Enter E-mail"
                required
              ></v-text-field>

              <v-text-field
                v-model="formData.password"
                :append-icon="show ? 'mdi-eye' : 'mdi-eye-off'"
                :rules="[rules.required, rules.min]"
                :type="show ? 'text' : 'password'"
                label="Enter Password"
                hint="At least 8 characters"
                counter
                @click:append="show = !show"
              ></v-text-field>

              <div class="mt-3 d-flex flex-row-reverse">
                <v-btn color="error" class="mr-4" @click="reset"> 리셋 </v-btn>

                <v-btn
                  color="primary"
                  class="mr-4"
                  link
                  router
                  :to="{ name: 'register' }"
                >
                  회원가입
                </v-btn>

                <v-btn
                  :disabled="!valid"
                  color="success"
                  class="mr-4"
                  @click="login(formData)"
                >
                  로그인
                </v-btn>
              </div>
            </v-form>
          </div>
        </v-card>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
import LoginObj from "../models/loginObj"
import axios from "axios"
export default {
  data: () => ({
    formData: new LoginObj("", ""),
    valid: false,
    isError: false,
    errorMsg: "",
    emailRules: [
      (v) => !!v || "E-mail is required",
      (v) => /.+@.+\..+/.test(v) || "E-mail must be valid"
    ],
    show: false,
    rules: {
      required: (value) => !!value || "Required.",
      min: (v) => v.length >= 8 || "Min 8 characters"
    }
  }),
  methods: {
    login(LoginObj) {
      if (!this.formData.email || !this.formData.password) {
        this.isError = true
        this.errorMsg = "이메일과 비밀번호를 입력해주세요."
        return
      }
      axios
        .post("/signin", LoginObj)
        .then((res) => {
          let token = res.data.token
          localStorage.setItem("access_token", token)
          this.$store.dispatch("getAccountInfo")
          this.$router.push({ name: "Home" })
        })
        .catch((err) => {
          if (err.response) {
            this.isError = true
            this.errorMsg = err.response.data.message
          }
        })
    },
    validate() {
      this.$refs.form.validate()
    },
    reset() {
      this.$refs.form.reset()
    },
    resetValidation() {
      this.$refs.form.resetValidation()
    }
  }
}
</script>

로그인 화면 구현도 회원가입 화면과 큰 차이는 없다.
다만, axios로 api와 연결하는 부분이 다르다.

login(LoginObj) {
      if (!this.formData.email || !this.formData.password) {
        this.isError = true
        this.errorMsg = "이메일과 비밀번호를 입력해주세요."
        return
      }
      axios
        .post("/signin", LoginObj)
        .then((res) => {
          let token = res.data.token
          localStorage.setItem("access_token", token)
          this.$store.dispatch("getAccountInfo")
          this.$router.push({ name: "Home" })
        })
        .catch((err) => {
          if (err.response) {
            this.isError = true
            this.errorMsg = err.response.data.message
          }
        })
    },

우선 "/signin" 경로에 로그인 모델과 함께 post 요청을 보내준다.
백엔드가 DB에서 이를 확인하고 정보가 일치한다면 응답 값의 data.token에 jwt 토큰 값을 보내준다.
프론트에서는 localStorage.setItem 메서드를 통해 이 토큰 값을 localStorage에 저장한다.
여기까지의 과정은 토큰 값만 받은 뒤 저장한 단계이고 로그인 한 사용자의 정보를 아직 불러오지 않았다.
그러므로 저장 후 vuex의 getAccountInfo Action을 통해 localStorage에 있는 토큰 값을 담아 api를 호출해 사용자의 정보도 얻어낸다.

getAccountInfo({ commit }) {
      let token = localStorage.getItem("access_token")
      axios
        .get("/userinfo", {
          headers: {
            "X-AUTH-TOKEN": token
          }
        })
        .then((response) => {
          commit("loginSuccess", response.data.data)
        })
        .catch((error) => {
          console.log(error)
        })
    }

vuex에서 구현한 getAccountInfo Action이다.

this.$router.push({ name: “Home” })

사용자의 정보를 받은 뒤 메인 페이지로 이동한다.


소스 수정 추가

source commit - 20fe017 백엔드와 실행해보면서 url 주소가 다른 것을 수정하고, 라우터에 관한 오류, 객체 변수명 등을 변경했다.

source commit - e2f67c5 회원가입 객체의 객체 이름을 잘 못 만들어 수정하고, vue 컴포넌트의 이름을 vue.js 스타일 가이드에 맞게 합성어로 변경했다.