Vuex for Oauth Flow

Image of Vuex

차례

  • Introduction
  • File Structure
  • General Oauth Flow Overview
  • Connecting Vuex to Vue
  • Auth Module State and Getters
  • Initial Auth Module Design
  • Updating and Mutating State Values
  • Separate API Helper
  • Wiring in the Auth Module
  • Oauth Request
  • Extracting Access Token

Introduction

기능이 많지 않은 간단한 앱의 경우 Vue만 사용해서 만들수 있습니다. 하지만, 앱이 조금만 커지거나 또는 향후 scale-up 계획이 있다면 Vue만 의존해 작업하기가 쉽지 않을 것입니다.

Prop을 부모 컴포넌트에서 여러 차례 중첩된 자식 컴포넌트에 전달하는 방식으로 작업하다보면. 좀 더 좋은 방법이 있지 않을까 고민하게 됩니다.

또한, 부모 컴포넌트, 자식 컴포넌트의 인스턴스가 서로 직접 참조하거나, 이벤트를 통해 state의 여러 사본을 변경 및 동기화 하는 작업은 깨지기 쉽고 앱의 규모가 커지면 유지보수가 어려워 질 수 있습니다.

위의 어려움을 해결하기 위해 컴포넌트 간 공유하고 있는 state를 컴포넌트 밖으로 꺼내어 관리함으로써, "view"와 state를 분리해 관리하게 되었습니다.

Vuex는 vue에서 state를 관리하는 라이브러리로 Flux, Redux, The Elm Architectue로부터 영향을 받았습니다.

저는 React로 앱을 만들며 Redux만 사용해 봤습니다. 향후 Flux, Redux, Vuex를 더 공부 후 공통적으로 공유하고 있는 패턴에 대해서도 포스팅을 하고 싶습니다.

지금은 Vuex를 이용해 User Login에 대한 정보를 관리하는 코드를 작성하며, Vuex에 대해서 알아보고자 합니다.

File Structure

로그인 유무만을 판단하는 간단한 앱이며 파일 구조는 아래와 같습니다. 아래의 틀을 이용해 앱을 더 확장해 갈 수도 있을 것입니다.

public
|  |__favicon.ico
|  |__index.html
|
src
   |__api
   |   |__rest.js
   |__components
   |   |__AppHeader.vue
   |   |__AuthHandler.vue
   |__store
   |   |__modules
   |   |     |__auth.js
   |   |__index.js
   |__App.vue
   |__main.js


General Oauth Flow Overview

Oauth 위의 사진은 단순화한 Oauth Flow는 입니다. User가 로그인 버튼을 클릭하면 Imgur 사이트의 Oauth Flow를 따를 수 있도록 브라우저 주소를 Imgur Oauth flow로 변경해줍니다. 접속한 유저가 우리가 만든 웹앱이 유저 정보에 접근할 수 있도록 허가를 해 주면, Imgur는 'access token'과 함께 브라우저를 다시 우리의 웹앱으로 리다이렉션합니다. 그리고 우리의 앱은 유저를 대신해 Imgur 앱에 대해 유저가 허락한 범위의 어떠한 행위를 할 수 있습니다. 한 예로 유저의 어카운트로 사진을 업로드하는 것을 들 수 있습니다.

Connecting Vuex to Vue

Vue와 Vuex는 별개의 라이브러리이기 때문에, 우리는 이 두 라이브러리를 함께 사용하기 위해 연결고리를 만들어줘야 합니다. 아래와 같이 store 폴더 내 index.js에서 Vue와 Vuex를 연결 시킨 후, Vuex store를 export 합니다.

// src/store/index.tsx
import Vuex from 'vuex';
import Vue from 'vue';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {}
});

그리고, main.js 파일에서 Vue 인스턴스를 생성할 때, 위에서 만든 store를 아래와 같이 함께 전달해 줍니다. store를 전달 함으로써 Vue 인스턴스와 store를 연결짓고 데이터를 전달할 수 있습니다.

// src/main.js
import Vue from 'vue';
import App from './App';
import store from './store/index';

new Vue({
  store,
  render: h => h(App)
}).$mount('#app');


Auth Module State and Getters

User 로그인 정보를 관리할 auth module에서 초기 state와, 현재 user가 로그인을 했는지 안 했는지 여부를 확인하는 getter function, isLoggedIn을 아래와 같이 작성할 수 있습니다. 초기값으로 null을 할당한 후, user가 로그인 했다면 token값이 할당이 됩니다.

// src/sotre/modules/auth.js
const state = {
  token: null
};

const getters = {
  isLoggedIn: state => !!state.token
};

Updating and Mutating State Values

state의 값을 업데이트 하는 작업만 보면 단순합니다. mutation의 함수로 인자로 들어오는 state값에 원하는 값을 할당하면 됩니다.

// src/sotre/modules/auth.js
const mutations = {
  setToken: (state, token) => {
    state.token = token;
  }
};

하지만, mutation의 메소드를 실행하기 위해서는, 각 mutation method와 연관이 있는 action이 선행되어야 합니다. 만약 Redux의 action type에 따라 실행되는 reducer 함수와 맥락과 비슷하다고 생각했습니다.

action을 통해 mutation 함수를 실행하기 위해서 함수에 인자를 전달 받는 객체의 commit property를 사용해야합니다. 두 번째 인자로는 mutation 함수에 전달하고자 하는 값을 추가해줍니다. 아래는 유저가 로그아웃 시 state의 token 값을 null로 변경해야하기 때문에 인자로 null을 넘겨주었습니다.

// src/sotre/modules/auth.js
const actions = {
  logout: ({ commit }) => {
    commit('setToken', null);
  }
};

왜 mutation 내에서 직접 state를 변경하지 않는지 의문을 가질 수 있습니다. 위의 코드는 간소화 한 것이지만, 특정 action에서 여러 차레의 mutation을 할 수도 있고, action 내에서 어떠한 복잡한 비동기 작업 후 mutation 작업을 할 수도 있습니다.

Separate API Helper

logut 메소드를 만들었으니 login에 대한 action 함수도 작성을 해야합니다. 현재는 로그인 기능만 구축을 하지만, 우리가 만드는 앱이 사진, 동영상, 블로그 글 등을 요청하는 작업이 있다고 가정해봅시다. 각 모듈마다 API 관련 작업이 생길 수 있고, 그럴 경우 중복되는 코드가 많이 발생할 수 있습니다. 그래서 여기서는 API와 관련된 코드를 별도의 파일로 한 곳에서 관리하도록 하겠습니다.

// src/api/rest.js
import qs from 'qs';

const CLIENT_ID = 'SOME_CLIENT_ID';
const ROOT_URL = 'SOME_ROOT_URL';

export default {
  login() {
    const queryString = {
      client_id: CLIENT_ID,
      RESPONSE_TYPE_EXAMPLE: 'token'
    };

    window.location = `${ROOT_URL}/oauth2/authorize?${qs.stringify(querystring)}`;
  }
};

login 함수를 실행하면 브라우저의 location을 Root url의 oauth flow가 진행될 수 있도록 변경합니다. 여기서 query 작성을 쉽게 해주는 qs 라이브러리를 사용했습니다.

Wiring in the Auth Module

우리는 앞서 main.js 파일에서 Vuex store를 Vue 인스턴스에 추가했습니다. 이번에는 Store에 우리가 만든 auth 모듈을 추가하도록 하겠습니다. 우리가 store에 전달하는 key 값은 store의 state의 key 값이 되기 때문에 주의해서 이름을 지어야합니다.

// src/store/index.tsx

import Vuex from 'vuex';
import Vue from 'vue';
import auth from '../modules/auth';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    auth
  }
});

Oauth Request

oauth 진행을 위한 코드를 작성했으니, 이제 실제 로그인 버튼을 만들어 oauth flow를 타도록 하겠습니다.

// src/components/AppHeader.vue
<template>
  <div>
    <div>
      <a href="/">
        Menu
      </a>
      <div>
        <a href="#" @click="login">
          Login
        </a>
      </div>
    </div>
  </div>
</template>

<script>
import { mapActions } from 'vuex';

export default {
  name: 'AppHeader',
  methods: mapActions(['login']),
};
</script>

mapActions합수에 string이 요소인 배열을 인자로 전달하면, store에서 string으로 전달 받은 key값의 action method를 담은 객체를 반환합니다. 우리는 이 객체를 AppHeader Component 안에서 위와 같이 사용할 수 있습니다. 여기서는 로그인 버튼 클릭 시 login 함수가 실행되도록 했습니다.

실제 브라우저로 돌아가 로그인 버튼을 클릭하면 store 내 action의 login 함수가 실행됩니다. login 함수는 clientID를 query로 포함하고 브라우저의 location을 변경합니다. 유저가 타켓 앱의 접근을 우리가 만든 앱에 허가해 주면, 미리 타켓 앱의 api에 추가한 우리 앱의 redirect url로 다시 브라우저의 location이 변경됩니다. 이 때 url에 accesstoken, expiresin, token_type 등 우리가 해당 앱에 접근 시 필요한 정보를 포함하고 있습니다.

Extracting Access Token

redirect된 url에서 가장 중요한 정보는 access token이며, 우리는 이 정보를 url로부터 추출 후 이를 이용해 유저 대신 해당 앱에서 필요한 작업을 할 수 있습니다.

url에서 필요한 정보인 access token을 추줄하는 작업은 그리 어렵지 않을 것입니다. 하지만, 이 작업을 위한 코드를 어디에 위치시켜야 할까요? 그리고 이 코드가 유저가 로그인 후 리다이렉트 된 경우에만 실행이 되게 하려면 어떻게 해야 할까요?

바로 리다이렉트된 url을 기준으로 별도의 route를 설정하고, 해당 라우터에서 access token을 추출하는 코드를 실행할 수 있습니다. 이를 위해서 라우터를 설정하도록 하겠습니다.

// src/main.js

import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App';
import store from './store/index';
import AuthHandler from './components/AuthHandler';

Vue.use(VueRouter);

const router = new VueRouter({
  mode: 'history',
  routes: [
    {
      path: '/oauth2/callback',
      component: AuthHandler
    }
  ]
});

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#App');

oauth2/callback으로 리다이렉트 시 AuthHandler 컴포넌트를 추가해 주었으니, AuthHandler 컴포넌트를 src/components 폴더에 추가하도록 하겠습니다.

// src/components/AuthHandler

<template>
  <div>
    Please wait...
  </div>
</template>

<script>
import { mapActions } from 'vuex';

export default {
  name: 'AuthHandler',
  methods: mapActions(['finalizeLogin']),
  created() {
    this.finalizeLogin(window.location.hash);
  },
};
</script>

AuthHandler가 렌더되면, 우리는 url에서 access token을 추출하기 위해, auth module의 finalizeLogin 함수를 실행할 것입니다. finalizeLogin 함수에 인자로 window.location.hash를 전달했습니다. 그럼 다음으로 auth 모듈의 action에 finalizeLogin 메소드를 추가합니다.

import { router } from '../../main';

const actions = {
  // some previous codes
  finalizeLogin({ commit }, hash) {
    const query = qs.parse(hash.replace('#', ''));

    commit('setToken', query.access_token);

    router.push('/');
  }
  //
};

url에서 access token을 추출한 후 브라우저를 다시 "/"로 리다이렉트 했습니다. 필요한 경우 access token을 브라우저 local storage에 저장 후 클라이언트의 로그인을 계속 유지시킬 수 있습니다.