본문 바로가기
Dev/Vue.js

[Vue.js] Today I Learned - 학습 노트 조회

by dev_jsk 2020. 10. 27.
728x90
반응형

학습 노트 조회

Today I Learned의 핵심 기능인 학습 노트 조회 기능이다. 로그인 시 발급된 인증(토큰) 정보를 이용하여 학습 노트 데이터 조회를 할 수 있다.

로그인 토큰 값 확인

로그인 시 발급 받는 토큰을 이용하여 학습 노트 데이터를 조회 할 수 있다.

// src/components/LoginForm.vue
methods: {
  async submitForm() {
    try {
      // 비즈니스 로직
      const userData = {
        username: this.username,
        password: this.password,
      };
      const { data } = await loginUser(userData);
      // 토큰 확인
      console.log(data.token);
      this.$store.commit('setUsername', data.user.username);
      this.$router.push('/main');
    } catch (error) {
      // 에러 핸들링할 코드
      console.log(error.response.data);
      this.logMessage = error.response.data;
    } finally {
      this.initForm();
    }
  },
  initForm() {
    this.username = '';
    this.password = '';
  },
},

스토어를 이용한 HTTP 헤더에 토큰 저장 및 활용

스토어를 이용하여 로그인 시 발급 받은 토큰을 HTTP Request Header에 저장하여 인증 시 사용할 수 있도록 구현한다.

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

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    username: '',
    // 토큰 저장할 변수
    token: '',
  },
  getters: {
    isLogin(state) {
      return state.username !== '';
    },
  },
  mutations: {
    setUsername(state, username) {
      state.username = username;
    },
    clearUsername(state) {
      state.username = '';
    },
    // 토큰 저장할 함수
    setToken(state, token) {
      state.token = token;
    },
  },
});

// src/components/LoginForm.vue
methods: {
  async submitForm() {
    try {
      // 비즈니스 로직
      const userData = {
        username: this.username,
        password: this.password,
      };
      const { data } = await loginUser(userData);
      console.log(data.token);
      // 스토어를 이용하여 setToken 호출
      this.$store.commit('setToken', data.token);
      this.$store.commit('setUsername', data.user.username);
      this.$router.push('/main');
    } catch (error) {
      // 에러 핸들링할 코드
      console.log(error.response.data);
      this.logMessage = error.response.data;
    } finally {
      this.initForm();
    }
  },
  initForm() {
    this.username = '';
    this.password = '';
  },
},

// src/api/index.js
import axios from 'axios';
import store from '@/store/index';

const instance = axios.create({
  baseURL: process.env.VUE_APP_API_URL,
  // 헤더 설정
  headers: {
    // 스토어에 저장된 토큰 사용
    Authorization: store.state.token,
  },
});

토큰을 이용한 API 요청 시 의도치 않은 동작 확인

위처럼 스토어를 이용하여 저장된 토큰을 사용하여 API 요청을 하였다. 코드 상으로 보았을 때 정상 동작이 예상되었지만 결과를 보면 HTTP Request HeaderAuthorization의 값이 비어있었다.

일단 현재 로직은 Axios를 create하면서 Authorization에 스토어에 저장된 토큰을 사용하도록 구성하고 create된 Axios 인스턴스를 사용하여 로그인API를 호출한다. 자세히 보자면 토큰은 로그인이 된 후 API결과에 담겨있는데 그 전에 Axios 인스턴스에 토큰을 저장하면 빈 토큰을 저장하는 것이다.

그렇기 때문에 Axios Interceptor를 이용하여 특정 시점에 토큰 저장 처리를 해주어야 한다.

Axios Interceptor 사용

로그인 후 발급되는 토큰의 저장 시점을 제어하기 위해 Axios Interceptor를 사용한다.

 

Interceptor 모듈화

인터셉터 처리 코드를 효율적으로 관리하기 위해 모듈화를 진행한다.

// src/api/common/interceptors.js
export function setInterceptors(instance) {
  // Add a request interceptor
  instance.interceptors.request.use(
    function(config) {
      // Do something before request is sent
      return config;
    },
    function(error) {
      // Do something with request error
      return Promise.reject(error);
    },
  );

  // Add a response interceptor
  instance.interceptors.response.use(
    function(response) {
      // Any status code that lie within the range of 2xx cause this function to trigger
      // Do something with response data
      return response;
    },
    function(error) {
      // Any status codes that falls outside the range of 2xx cause this function to trigger
      // Do something with response error
      return Promise.reject(error);
    },
  );

  return instance;
}

Interceptor 연결

Axios 인스턴스에 Interceptor를 연결하여 시점 제어를 구현한다.

// src/api/index.js
import axios from 'axios';
import { setInterceptors } from './common/interceptors';

function createInstance() {
  const instance = axios.create({
    baseURL: process.env.VUE_APP_API_URL,
  });
  // Interceptor 사용
  return setInterceptors(instance);
}

const instance = createInstance();

function registerUser(userData) {
  return instance.post('signup', userData);
}

function loginUser(userData) {
  return instance.post('login', userData);
}

export { registerUser, loginUser };

Axios Interceptor 사용하여 HTTP 헤더 설정

인터셉터를 사용하여 Request 요청 시 스토어에 저장되어 있는 토큰을 인터셉터 내 config headers에 저장하여 이후 API 요청 마다 해당 토큰을 사용하여 인증처리하도록 구현한다.

// src/api/common/interceptors.js
import store from '@/store/index';

export function setInterceptors(instance) {
  // Add a request interceptor
  instance.interceptors.request.use(
    function(config) {
      // Do something before request is sent
      // config.headers 에 스토어에 저장되어 있는 토큰 저장
      config.headers.Authorization = store.state.token;
      return config;
    },
    function(error) {
      // Do something with request error
      return Promise.reject(error);
    },
  );

  // Add a response interceptor
  instance.interceptors.response.use(
    function(response) {
      // Any status code that lie within the range of 2xx cause this function to trigger
      // Do something with response data
      return response;
    },
    function(error) {
      // Any status codes that falls outside the range of 2xx cause this function to trigger
      // Do something with response error
      return Promise.reject(error);
    },
  );

  return instance;
}

 

학습 노트 조회 API 구현

학습 노트 데이터를 조회하기 위한 API 함수를 구현한다.

// src/api/index.js
import axios from 'axios';
import { setInterceptors } from './common/interceptors';

// axios 초기화 함수
function createInstance() {
  const instance = axios.create({
    baseURL: process.env.VUE_APP_API_URL,
  });

  return setInterceptors(instance);
}

const instance = createInstance();

// 학습 노트 데이터 조회 API
function fetchPosts() {
  return instance.get('posts');
}

export { fetchPosts };

학습 노트 데이터 표시

Swagger API 문서를 통해 생성한 학습 노트 데이터를 바인딩하여 화면에 표시한다.

<!-- src/views/MainPage.vue -->
<template>
  <div>
    <div class="main list-container contents">
      <h1 class="page-header">Today I Learned</h1>
      <!-- 학습 노트 데이터 표시 -->
      <ul>
        <li v-for="postItem in postItems" :key="postItem._id">
          <div class="post-title">
            {{ postItem.title }}
          </div>
          <div class="post-contents">
            {{ postItem.contents }}
          </div>
          <div class="post-time">
            {{ postItem.createdAt }}
          </div>
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
// 조회 API 함수 임포트
import { fetchPosts } from '@/api/index';

export default {
  data() {
    return {
      postItems: [],
    };
  },
  methods: {
    async fetchData() {
      const { data } = await fetchPosts();
      this.postItems = data.posts;
    },
  },
  // 라이프사이클 훅을 이용해 인스턴스가 작성된 후 호출
  created() {
    this.fetchData();
  },
};
</script>

<style></style>

학습 노트 목록 아이템 컴포넌트화

학습 노트 데이터를 표시하는 부분을 컴포넌트화 하여 코드를 간결하게 할 수 있고 props 속성을 사용하여 구현할 수 있다. 추가적으로 데이터 로딩상태를 표시하는 스피너 컴포넌트를 등록하여 구현할 수 있다.

<!-- src/views/MainPage.vue -->
<template>
  <div>
    <div class="main list-container contents">
      <h1 class="page-header">Today I Learned</h1>
      <!-- Loading Spinner -->
      <LoadingSpinner v-if="isLoading"></LoadingSpinner>
      <ul v-else>
        <!-- props 속성 사용 -->
        <PostListItem
          v-for="postItem in postItems"
          :key="postItem._id"
          :postItem="postItem"
        ></PostListItem>
      </ul>
    </div>
  </div>
</template>

<script>
// 리스트 컴포넌트
import PostListItem from '@/components/posts/PostListItem.vue';
// 로딩바 컴포넌트
import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
import { fetchPosts } from '@/api/index';

export default {
  components: {
    PostListItem,
    LoadingSpinner,
  },
  data() {
    return {
      postItems: [],
      // 로딩중 여부
      isLoading: false,
    };
  },
  methods: {
    async fetchData() {
      this.isLoading = true;
      const { data } = await fetchPosts();
      this.postItems = data.posts;
      this.isLoading = false;
    },
  },
  created() {
    this.fetchData();
  },
};
</script>

<!-- src/components/posts/PostListItem.vue -->
<template>
  <li>
    <div class="post-title">
      {{ postItem.title }}
    </div>
    <div class="post-contents">
      {{ postItem.contents }}
    </div>
    <div class="post-time">
      {{ postItem.createdAt }}
    </div>
  </li>
</template>

<script>
export default {
  // props 속성 사용
  props: {
    postItem: {
      type: Object,
      required: true,
    },
  },
};
</script>
728x90
반응형

댓글