티스토리 뷰

728x90
반응형

vue 정리 - login(로그인) part1

vue.config.js 설정을 끝낸 후 여러 가지 것들을 셋팅해야 하지만 우선

로그인 부분을 먼저 다루어야 vuex / vue-router 등을 자연스럽게 한번에 세팅 할 수 있다.

 

대체적으로 아래 이미지와 같은 비즈니스 로직으로 처리 된다.

사항요약해 보면 다음과 같다.

1. login call ---> 로그인 화면에서 특정 아이디/패스워드로 로그인 시도.-> 서버측에 로그인 요청

2. token 처리 ---> 요청한 아이디/패스워드가 멤버가 맞다면 jwt(json web token) 값을 client 측에 전송

    전송받은  jwt(json web token) 을 client 측에서 localstorage 에 담아 두고

    요청 headers 의 Authorization 에  jwt(json web token)을 넣어서 로그인한 사용자 정보를 요청 전송한다.

3. me call --->  최종적으로 서버에서  jwt(json web token)을 확인하고 인증처리하여 로그인 사용자 정보를 client 측에 전달해 준다.

4. user 정보 ---> client 에서 사용자 확인 후 main 으로 router 를 변경해 준다.

 

4가지로 요약했지만 그 사이사이에 많은 것들이 숨어있다.

 

일단 위 그림처럼 하려면 먼저 준비할 것들이 필요하다.

a. vuex-module-decorators( vuex 모듈 플러그인 )

b. vuex-class ( vuex 모듈 네임 바인딩 처리 플러그인 )

c. axios ( api 통신 라이브러리 )

 

위 3가지를 npm install 로 설치한다.

 

더 상세하게 기술해 보겠다.

 

1번에서 login call 말 그대로 단순히 로그인 정보를 전송하는 게 전부이다.

물론 전송 하는 부분은 스토어의 action 을 이용하긴 한다. 

 

하지만 여기서 그것보다 전송 후가 중요한 부분이다. 다시 한번 더 짚어 보면 아래와 같다.

 

- 로그인 데이터 전송 >

- vue store action 과 연동되어 있는 login api 통신(axios 이용)으로 서버 접근. 사용자 인증 >

- 인증 후 서버에서 전달받은 token 을  vue store 에 token 정보 저장 >

- vue 에서 token 정보 저장 후 바로 headers 의 Authorization 에 token 을 담아서 사용자 정보 호출 >

- 서버에서 Authorization 체크하여 해당 사용자 정보 client ( vue ) 에 전송 

 

이제 위 사항들을 하나씩 살펴 보겠다.

우선 로그인 데이터 전송부분 부터 살펴 보자.

 

LoginForm.ts

import { Vue, Component } from 'vue-property-decorator';
import { namespace } from 'vuex-class';
import { UserActionType } from '@/store/moduleType/AuthActionTypes';
......................중략.............................
import WithRender from './LoginForm.html';

const Auth=namespace( 'Auth' );

@WithRender
@Component( {
    components: {
        ......................중략.............................
    }
} )
export default class LoginForm extends Vue {

    @Auth.Action
    private [UserActionType.LOGIN]!: (payload: { id: string, password: string })=>Promise<any>;

    private userId: string='';
    private userPw: string='';
    
    get isFieldRequired(): boolean {
      return this.userId !== '' && this.userPw !== '';
    }

    private validate(): void {
        if (this.isFieldRequired) {
            this[UserActionType.LOGIN]( { id: this.userId, password: this.userPw } )
                .then( (data: any)=>{
                   this.$router.push('/');
                } );
            } ).catch( (error: any)=>{
                // 로그인 에러의 경우 
                ......................중략.............................
            } );
        } else {
           // validation 체크 통과 못한 경우 
             ......................중략.............................
        }

    }

}

LoginForm.html

<div class="container">
    .....................중략............................
  <div class="contents-wrap">
    <div class="contents">
      <div class="login-form">
        <div class="form-cnt">
         
          <div class="form-input">
            <div class="form-group">
              <txt-field size="100%"
                         label-type="form-label"
                         label="ID"
                         id="userID"
                         placeholder="아이디를 입력해 주세요."
                         v-model="userId"
                         rules="required"
                         valid-name="아이디"></txt-field>
            </div>

            <div class="form-group mgt-20">
              <txt-field input-type="password"
                         size="100%"
                         label-type="form-label"
                         label="Password"
                         id="userPw"
                         placeholder="비밀번호를 입력해 주세요."
                         v-model="userPw"
                         rules="required"
                         valid-name="비밀번호"
                         @keyup.enter.native="validate"></txt-field>
            </div>

            <div class="login-btn d-grid mgt-35">
              <btn type="primary" @click="validate">Login</btn>
            </div>
          </div>

        </div>
      </div>
    </div>
  </div>
.....................중략............................
</div>

LoginForm.ts 에서 보면 

해당 부분은 Store 의 Action 을 통해 통신을 주고 받고 성공시 라우터를 main페이지로 보낸다.

 

@Auth.Action
private [UserActionType.LOGIN]!: (payload: { id: string, password: string })=>Promise<any>;

 

라는 구문이 나온다.  이 부분은 store action 부분이다. 

@Auth.Action 이라고 데코레이터 속성이다.

해당 부분은 스토어의 Auth 모듈 내에 action 을 선언했던 부분 그대로 가져와 똑같은 타입선언만 해주면 된다.

정말 간편하다.

 

const Auth=namespace( 'Auth' ); 라고 상단에 선언한 vuex-class와

vuex 모듈 클래스에서 사용하는 vuex-module-decorators 플러그인 덕분이다.

 

vuex 모듈에 선언해둔 store 클래스들을 손쉽게 모듈네임을 변경하여 쓸 수 있게 한다. 

 

만약에 해당 플러그인이 없다면 좀 손이 많이 가게 된다.  아래와 같이 component 선언 부분에 mapActions 라는

promise 를 반환하는 Mapper 타입 함수를 써야만 한다.

@Component({
    computed: { 
        ...mapGettes({
            checkCount: `Auth/checkCount`,
        }),
    },
    methods: { 
        ...mapActions({
            [UserActionType.LOGIN]: `Auth/${UserActionType.LOGIN}`,
        }),
    },
})

 

하지만 vuex-class 의 namespace 를 사용하면 아래와 같이 명확해진다.

import { Vue, Component } from 'vue-property-decorator';
import { namespace } from 'vuex-class';
import { UserActionType } from '@/store/moduleType/AuthActionTypes';
......................중략.............................
import WithRender from './LoginForm.html';

const Auth=namespace( 'Auth' );

@WithRender
@Component( {
    components: {
        ......................중략.............................
    }
} )
export default class LoginForm extends Vue {

    @Auth.Action
    private [UserActionType.LOGIN]!: (payload: { id: string, password: string })=>Promise<any>;

    ......................중략.............................

    private validate(): void {
        if (this.isFieldRequired) {
        
            //Action 부분 
            this[UserActionType.LOGIN]( { id: this.userId, password: this.userPw } )
                .then( (data: any)=>{
                   this.$router.push('/');
                } );
            } ).catch( (error: any)=>{
                // 로그인 에러의 경우 
                ......................중략.............................
            } );
            
        } else {
           // validation 체크 통과 못한 경우 
             ......................중략.............................
        }

    }

}

 

참고로 아래 코드는 store/index.ts 에 있는 내용이다.

아래 코드에선 AuthModule 클래스로 되어 있는 것을 Auth 라고 네임을 변경해서 쓴다. 

import Vue from 'vue';
import Vuex from 'vuex';
import { TokenActionType } from '@/store/moduleType/AuthActionTypes';
import { getToken } from '@/core/auth/utils';

import AuthModule from '@/store/AuthModule';


Vue.use( Vuex );

const store=new Vuex.Store( {
    state: {},
    modules: {
        Auth: AuthModule,
        ................중략..................
    }
} );
const { token, refreshToken }=localStorage;
//새로고침시 토큰이 있는지 체크
if (getToken()) {
    //token 이 로컬스토리지에 존재할 경우 action 을 통해 토큰값을 다시 저장해 둔다.
    store.dispatch( `Auth/${ TokenActionType.SIGN_IN_BY_TOKEN }`, { token, refreshToken } )
        .catch( (error: any)=>{
            console.log( 'SIGN_IN_BY_TOKEN=', error );
        } );
}

export default store;

위 store/index.ts 마지막에 getToken() 함수로 토큰이 있는지 여부 체크하는 부분이 있는데 해당 부분은 

만약 새로고침시 store 에 담아 두었던 데이터는 몽땅 리셋이 되어 버리기 때문에 token 을 다시 저장해 두어야 연동되는 다른 컨포넌트들이 지장을 안받는다. 해당 getToken() 함수는 아래 utils.ts 에 지정되어 있다.

 

auth 에 쓰이는 utils.ts

import AuthConfig from '@/core/auth/config';

..................중략...........................

export const isUser=()=>{
    return localStorage.getItem( AuthConfig.ME_KEY );
};

// token  가져오기
export const getToken=()=>{
    return localStorage.getItem( AuthConfig.TOKEN_KEY );
};
// refresh token  가져오기
export const getRefreshToken=()=>{
    return localStorage.getItem( AuthConfig.REFRESH_TOKEN_KEY );
};

..................중략...........................

 

아래는 vuex 모듈 클래스 인 AuthModule.ts 와 type 별 선언한 클래스들이다.  

AuthMutationType.ts

enum TokenMutationType {
    GET_TOKEN='GET_TOKEN',
    GET_REFRESH_TOKEN='GET_REFRESH_TOKEN',
    SIGN_IN_BY_TOKEN='SIGN_IN_BY_TOKEN'
}

enum UserMutationType {
    INFO='INFO',
    LOGOUT='LOGOUT',
    LOG_IN='LOG_IN',
    ERROR_LOG='ERROR_LOG',
    INFO_UPDATE='INFO_UPDATE'
}

enum PageAuthMutationType{
    IS_AUTH='IS_AUTH'
}


export { TokenMutationType, UserMutationType, PageAuthMutationType };

AuthActionTypes.ts

enum UserActionType{
  LOGIN='LOGIN'
}
enum TokenActionType{
  SIGN_IN_BY_TOKEN='SIGN_IN_BY_TOKEN'
}
export { UserActionType, TokenActionType };

TokenMutationType.GET_TOKEN, AuthConfig.ME_KEY 등 함수이름, 인자값 등으로 입력되는 문자열들을 볼 수 있다.

대문자를 통해 해당 값이 상수값이다 라고 유추해 볼 수 있는 데...상수값이라 하면 일단 변하지 말아야 하는 값이다.

그렇다. 해당 값은 절대로 변해서 안되는 값인 것이다.

말인 즉슨 해당 값이 매번 어떠한 이유로 변한다면 무지막지한 에러 및 실행이 불가하다.

특히나 토큰정보/유저정보 등의 여러 컨포넌트 및 라우터에서 수시로 체크하는 것들은 오타가 발생할 수도 있고

잘못된 정보를 입력할 수 있다. 그럴 때 오류 및 에러를 방지하는 목적이 있다.

또 반대로 한번 설정하면 변경하지 말아야 할 저 상수값들을 프로젝트 어떠한 시점에 수정해야만 하는 경우가 간혹 생기기도 한다.

그럴 때 선언된 상수 문자열들은 그대로 두고 값만 변경하면 되기에 관리면에서도 유용하다. 

전역적으로 혹은 자주 사용되는 값들은 상수로 선언. 즉 한 곳에서 중앙 관리가 키포인트이다.

 

AuthModule.ts

import { Action, Module, Mutation, VuexModule } from 'vuex-module-decorators';
import { PageAuthMutationType, TokenMutationType, UserMutationType } from '@/store/moduleType/AuthMutationTypes';
import { TokenActionType, UserActionType } from '@/store/moduleType/AuthActionTypes';
import { AuthService } from '@/restApi';
import { IAdminUserMe } from '@/model/admin/AdminUserModel';
import { AdminService } from '@/restApi/service/AdminService';
import AuthConfig from '@/core/auth/config';
import axios from 'axios';
import { LoadingMutationType } from '@/store/moduleType/LoadingMutationType';
import { isUser } from '@/core/auth/utils';

@Module( {
    namespaced: true,
} )
export default class AuthModule extends VuexModule {
    //멤버 변수는 state 로 이용된다.
    public token!: string | null; 
    public me: IAdminUserMe | null=null;
    private refreshToken: string | null=null;

    get isAuth(): boolean {
        return !!this.token;
    }

    get isRefreshAuth(): boolean {
        return !!this.refreshToken;
    }

    get isMe(): IAdminUserMe | null {
        return this.me;
    }

    @Mutation
    public [TokenMutationType.GET_TOKEN](token: string | null): void {
        if (token !== null) {
            this.token=token;
            localStorage.setItem( AuthConfig.TOKEN_KEY, this.token );
        }
    }

    @Mutation
    public [TokenMutationType.GET_REFRESH_TOKEN](refreshToken: string | null): void {
        if (refreshToken !== null) {
            this.refreshToken=refreshToken;
            localStorage.setItem( AuthConfig.REFRESH_TOKEN_KEY, this.refreshToken );
        }
    }
    
    @Mutation
    public [UserMutationType.INFO](me: IAdminUserMe): void {
        this.me=me;
        localStorage.setItem( AuthConfig.ME_KEY, JSON.stringify( this.me ) );
    }
    
    

    .................중략...........................
    
   //새로고침시 localstorage 에 있는 token 값 존재 확인 후 저장 
    @Action( { rawError: true } )
    public [TokenActionType.SIGN_IN_BY_TOKEN](payload: { token: string, refreshToken: string }) {

        const { token, refreshToken }=payload;

        this.context.commit( TokenMutationType.GET_TOKEN, token );
        this.context.commit( TokenMutationType.GET_REFRESH_TOKEN, refreshToken );
    }
    

    /**
     * 로그인~
     * @param payload
     */
    @Action( { rawError: true } )
    public [UserActionType.LOGIN](payload: { id: string, password: string }): Promise<any> {
        const { id, password }=payload;
        //LOGIN_ACTION
        return AuthService.login( id, password )
            .then( (data: any)=>{
               
                this.context.commit( TokenMutationType.GET_TOKEN, data.accessToken );
                this.context.commit( TokenMutationType.GET_REFRESH_TOKEN, data.refreshToken );

                return AdminService.getMe()
                    .then( (data: any)=>{
                        this.context.commit( UserMutationType.INFO, data );
                        return Promise.resolve( 'signin status' );
                    } );
            } ).catch( (error: any)=>{
                return Promise.reject( error );
            } );
    }


}

위 코드에서 보면 

this.context.commit( TokenMutationType.GET_TOKEN, data.accessToken );
this.context.commit( TokenMutationType.GET_REFRESH_TOKEN, data.refreshToken )

라는 부분이 인증 후 서버에서 전달받은 token 을  vue store 에 token 정보 저장 단계이다.

 

이제 나머지 jwt 와 관련된 부분을 할 차례이다.

( 아래 다음 파트로 이어짐 )

 

vue 정리 - login (로그인) part2

 

vue 정리 - login (로그인) part2

vue 정리 - login (로그인) part2 login par1. 에 이어서 jwt 즉 json web token 설정에 대해서 정리해보고자 한다. 이제 vue 에서 token 정보 저장 후 바로 headers 의 Authorization 에 token 을 담아서..

uxicode.tistory.com

 

 

 

728x90
반응형

'Programming language > vuejs' 카테고리의 다른 글

vue 정리 - login (로그인) part2  (0) 2022.05.19
vue 정리 part3 - Vue-router  (0) 2022.05.17
vue 정리 part1 - vue 설정.  (0) 2022.05.02
Vue.js 요약  (0) 2019.04.04
vue-cli 및 typescript 설정  (0) 2019.04.03
댓글