써치킴의 우당탕탕 개발 블로그

[Vue.js][Ch4][영화검색 사이트] 에러 메시지 출력과 로딩 애니메이션 본문

완벽하게 Vue.js

[Vue.js][Ch4][영화검색 사이트] 에러 메시지 출력과 로딩 애니메이션

써치킴 2022. 4. 25. 19:02

에러메시지 출력

초기 메시지 상태 : 'Search for the movie title!'

검색하면 메시지 상태 : '' (없어짐)

에러나면 메시지 상태 : '에러 메시지'

 

movie.js

/* 영화 검색과 관련된 데이터를 취급하는 파일 */
import axios from 'axios'
import _uniqBy from 'lodash/uniqBy'   // uniqBy 기능만 사용할 것이기 때문에 lodash/uniqBy 명시

export default {
  /*
  * namespaced : 해당 파일이 모듈화돼서 사용할 수 있다는 것을 나타내는 옵션.(true / false)
  * state : 실제로 취급해야하는 데이터. store 개념에서는 data를 state라고 부른다.(data와 같은 개념)
  * getters : 계산된 상태를 만들어내는 개념.(computed와 같은 개념)
  * mutations : 변이라는 뜻. 관리하는 데이터를 변경시켜줄 수 있는 methods. mutations에서만 데이터를 수정할 수 있다.
  * actions : 비동기로 동작하는 methods. 데이터를 수정할 수 없다. state 옵션 사용 불가능 -> context 사용해야한다.
  * - context.state / context.getters / context.commit 사용 => 객체구조분해를 통해 {state, getters, commit} 으로 사용할 수 있다.
  */
  namespaced: true,
  state: () => ({
    movies: [],
    message: 'Search for the movie title!',   // 초기 상태 메시지
    loading: false
  }),
  getters: {},
  mutations: {
    /* state를 바꿔줄 수 있는건 mutations 밖에 없기 때문에
    state에 정의된 데이터를 통합적으로 갱신할 수 있는 mutation을 만들어준다. */
    updateState(state, payload) {   // 상태를 갱신할때마다 수정하는 메소드
      // Object.keys : 객체 데이터 속성의 이름만 갖고 새로운 배열 데이터 생성 (ex.['movies', 'message', 'loading'])
      Object.keys(payload).forEach(key => {   // ex. key : movies, message, loading
        // state.movies = payload.movies를 동적으로 전표기법을 제거하고 state['movies']라고 지정할 수 있다.
        // state['movies'] = payload['movies'] => state[key] = payload[key]
        state[key] = payload[key];
      })
    },
    resetMovies(state) {
      state.movies = [];
    }
  },
  actions: {
    // actions의 메소드에는 context, payload 매개변수를 제공한다.
    // context : state, getters.commit 활용
    // payload : 인수로 들어온 데이터를 받아줌
    async searchMovies({ state, commit }, payload) {    // (c, p)로도 사용이 가능하다. (매개변수 이름 변경 가능)
      commit('updateState', {   // 검색을 시작하면 message는 초기화
        message: ''
      });

      try {
        // 최초 요청은 page가 1
        const res = await _fetchMovie({
          // searchMovies의 payload에 이미 원하는 데이터가 있기 때문에 전개 연산자 사용
          ...payload,
          page : 1
        });
        
        // res 검색결과의 data에 실제 검색결과가 존재하므로 res.data 사용 
        const { Search, totalResults } = res.data;    // 검색 결과의 data를 객체구조분해
        // context의 commit 활용
        commit('updateState', {    // updateState 메소드가 실행될 때, 객체가 payload 인수로 전달된다.
          movies: _uniqBy(Search, 'imdbID'),    // imdbID 기준으로 movies 중복 제거
          message: '', 
          loading: true
        });
        console.log(totalResults) // ex) frozen을 검색했을 때, 305 => 31번 요청해야함.
        console.log(typeof totalResults); // string

        const total = parseInt(totalResults, 10); // 10진법의 정수로 형변환
        // Math.ceil : 올림 처리
        const pageLength = Math.ceil(total / 10); // 305/10을 올림 처리 -> 31번 요청 처리.

        // 1번 이상 요청해야한다면, 추가 요청 전송
        if(pageLength > 1){
          for (let page = 2; page <= pageLength; page++) {
            // 영화 목록을 몇개 가져올지(number) 조건문을 이용해 for문 종료
            if (page > (payload.number / 10)) break;    // for문 종료

            // 추가 요청은 page가 2부터 검색
            const res = await _fetchMovie({
              // searchMovies의 payload에 이미 원하는 데이터가 있기 때문에 전개 연산자 사용
              ...payload,
              page
            });

            const { Search } = res.data;
            /*
            * movies에는 이미 영화가 들어가져 있는 상태이기 때문에 
            * 새로운 요청을 할때마다 새로운 배열을 만들어서 movies에 할당
            */
            commit('updateState', {
              // ... : 전개 연산자
              movies: [
                ...state.movies, 
                ..._uniqBy(Search, 'imdbID')] // state.movies를 먼저 전개하고, Search 데이터를 전개
            });
          }
        }
      } catch (message) {
        commit('updateState', {
          movies: [],           // 에러가 발생하면 영화 정보 초기화
          message
        })
      }
    }
  } 
}

function _fetchMovie(payload) {
  const {title, type, year, page} = payload;
  const OMDB_API_KEY = '7035c60c';
  const url = `https://www.omdbapi.com/?apikey=${OMDB_API_KEY}&s=${title}&type=${type}&y=${year}&page=${page}`;

  return new Promise((resolve, reject) => {
    axios.get(url)
      .then(res => {
        if (res.data.Error) {   // 응답받은 영화정보 데이터에 에러가 있다면 예외처리
          reject(res.data.Error);
        }
        resolve(res);           // 정상동작하면 결과값 반환
      })
      .catch(err => {
        reject(err.message);    // 에러 반환
      })
  })

}

MovieList.vue

<template>
  <div class="container">
    <!-- 영화 목록이 없으면 no-result 출력 -->
    <div :class="{ 'no-resilt' : !movies.length }" class="inner">
      <!-- message에 데이터가 있을때만 message 출력
      message가 없으면 영화 목록 출력 -->
      <div 
        v-if="message" 
        class="message">
        {{ message }}
      </div>
      <div v-else class="movies">
        <!-- props 활용 : movies 배열에 있는 movie 객체를 movie라는 이름으로 MovieItem이라는 컴포넌트에 전달 -->
        <MovieItem 
          v-for="movie in movies"
          :key="movie.imdbID" 
          :movie="movie" />
      </div>
    </div>
  </div>
</template>

<script>
import MovieItem from '~/components/MovieItem'

export default {
  components: {
    MovieItem
  },
  computed: {
    movies() {
      // store의 state중 movie 모듈의 movies 데이터를 계산된 데이터로 반환하겠다.
      return this.$store.state.movie.movies;
    },
    message() {
      return this.$store.state.movie.message;
    }
  }
}
</script>

<style lang="scss" scoped> 
@import "~/scss/main";    /* 부트스트랩을 사용할 수 있게 import */

  .container {
    margin-top: 30px;
    .inner {
      background-color: $gray-200;
      padding: 10px 0;
      border-radius: 4px;
      text-align: center;
      &.no-result {   // no-result가 있으면
        padding: 70px 0;
      }
    }
    .message {
      color: $gray-400;
      font-size: 20px;
    }
    .movies {
      display: flex;      // 수평 정렬
      flex-wrap: wrap;    // 줄바꿈이 가능하도록 감싸도록 변경
      justify-content: center;    // 가운데 정렬
    }
  }
</style>

로딩 애니메이션 추가

부트스트랩에서 로딩 애니메이션이 적용되는 컴포넌트를 제공한다.

movie.js

검색 시작할 때, 로딩 중이면 로직을 실행하지 않는 조건문 추가 => 동작 중복 제거

검색을 시작하면 로딩 애니메이션 출력 =>  searchMovies 함수가 실행되면 loading : true

if(state.loading) return; // 로딩 중이면 함수를 빠져나감 (동작 중복 제거)

commit('updateState', {   // 검색을 시작하면 message는 초기화
    message: '',
    loading: true           // 로딩 중
});

 

검색이 정상 종료되던, 에러가 발생하던 로딩은 끝났으니 로딩 애니메이션 제거 => finally 문 추가

finally {       // 검색이 정상실행되던, 에러가 발생하던 로딩은 끝났으니 loading : false
  commit('updateState', {
    loading: false
  })    
}

MovieList.vue

로딩 중일때만 로딩 애니메이션 출력 (loading : true)

<!-- loading이 true일 때만 로딩 애니메이션 출력 -->
<div v-if="loading" class="spinner-border text-primary"></div>   <!-- 로딩 애니메이션 -->

로딩 애니메이션 검증을 위해 개발자 도구 > Network 

Slow 3G로 변경하여 인터넷 속도를 강제로 부하를 줘서 로딩 애니메이션이 정상적으로 동작하는지 확인한다.

Comments