Vue.js_7일차(완벽가이드)

써니·2023년 1월 2일
0

vue.js

목록 보기
7/15

Vue.js

v-if 속성

속성을 사용해서
v-if / v-else if / v-else를 사용할 수 있다.
또한 router-link 속성안에서도 v-if 속성을 사용할 수 있다.

  • ListItem.vue
<template>
  <div>
    <ul class="news-list">
      <li v-for="(item, index) in listItems" :key="index" class="post">
        <div class="points">
          {{ item.points || 0}}
        </div>
        <div>
          <!-- 타이틀 영역 -->
          <p class="news-title">
            <template v-if="item.domain">
              <a v-bind:href="item.url">
                {{ item.title }}
              </a>
            </template>
            <template v-else>
              <router-link v-bind:to="`item/${item.id}`">
                {{ item.title }}
              </router-link>
            </template>
          </p>
          <small class="link-text">
            {{ item.time_age }} by 
            <router-link
              v-if="item.user"
              v-bind:to="`/user/${item.user}`" class="link-text">{{ item.user }}</router-link>
              <a :href="item.url" v-else>
                {{ item.domain }}
              </a>
          </small>
        </div>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  created() {
    console.log(this.$route.path === '/news');
    const name = this.$route.name;
    if( name === 'news' ) {
      this.$store.dispatch('FETCH_NEWS');
    } else if( name === 'ask' ) {
      this.$store.dispatch('FETCH_ASK');
    } else if( name === 'jobs' ) {
      this.$store.dispatch('FETCH_JOBS');
      // actionName = 'FETCH_JOBS';
    }
    // this.$route.dispatch.dispatch(actionName);
  },
  computed: {
    // eslint-disable-next-line vue/return-in-computed-property
    listItems() {
      const name = this.$route.name;
      const state = this.$store.state;
      if( name === 'news' ) {
        return state.news;
      } else if( name === 'ask' ) {
        return state.ask;
      } else if( name === 'jobs' ) {
        return state.jobs;
      }
    }
  }
}
</script>

<style scoped>
.news-list {
  padding: 0;
  margin: 0;
}
.post {
  list-style: none;
  display: flex;
  align-items: center;
  border-bottom: 1px solid #eee;
}
.points {
  width: 80px;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center; 
  color: #42b883;
}
.news-title {
  margin: 0;

}
.link-text {
  color: #828282;
}
</style>

디자인 끝




사용자 프로필 컴포넌트

  • UserView.vue
<template>
  <div>
    <user-profile></user-profile>
    <!-- <p>name: {{ userInfo.id}}</p>
    <p>karam: {{ userInfo.karma}}</p>
    <p>created: {{ userInfo.created}}</p> -->
  </div>
</template>

<script>
import UserProfile from '../components/UserProfile.vue'

export default {
  components: {
    UserProfile,
  },
  computed: {
    userInfo() {
      return this.$store.state.user;
    }
  },
  created() {
    console.log(this.$route.params.id);
    const userName = this.$route.params.id;
    this.$store.dispatch('FETCH_USER', userName);
    // axios.get(`https://api.hnpwa.com/v0/user/${userName}.json`);
  },
}
</script>

<style>

</style>

📂 components

  • UserProfile.vue

    ItmeView.vue에서 가져옴

<template>
  <div class="user-container">
      <div>
        <!-- 사용자 프로필 -->
        <i class="fa-solid fa-user"></i>
      </div>
      <div class="user-description">
        <!-- 사용자 정보 -->
        <!-- <router-link :to="`/user/${fetchedItem.user}`">
          {{ fetchedItem.user }}
        </router-link>
        <div class="time">
          {{ fetchedItem.time_ago }}
        </div> -->
      </div>
    </div>
</template>

<script>
export default {

}
</script>

<style>
.user-container {
  display: flex;
  align-items: center;
  padding: 0.5rem;
}
.fa-user {
  font-size: 2.5rem;
}
.user-description {
  padding-left: 8px;
}
.time {
  font-size: 0.7rem;
}
</style>


사용자 컴포넌트 데이터 흐름 처리1

UserView.vue에서 FETCH_USER를 호출하고 받아온 데이터를 UserView에서 바로 호출하는 것이 아니라 컴포넌트로 저장되어 있는 UserProfile에서 Computed를 한다.

  • UserProfile.vue
<template>
  <div class="user-container">
      <div>
        <!-- 사용자 프로필 -->
        <i class="fa-solid fa-user"></i>
      </div>
      <div class="user-description">
        <div>
          {{ userInfo.id }}
        </div>
        <!-- <router-link :to="`/user/${userInfo.user}`">
          {{ userInfo.id }}
        </router-link> -->
        <div class="time">
          {{ userInfo.created }}
        </div>
      </div>
    </div>
</template>

<script>
export default {
    computed: {
    userInfo() {
      return this.$store.state.user;
    }
  },
}
</script>

<style>
.user-container {
  display: flex;
  align-items: center;
  padding: 0.5rem;
}
.fa-user {
  font-size: 2.5rem;
}
.user-description {
  padding-left: 8px;
}
.time {
  font-size: 0.7rem;
}
</style>


사용자 컴포넌트 데이터 흐름 처리2

Props 사용하기

props 데이터를 info로 보낸다

  • UserView.vue
<template>
  <div>
    <user-profile :info="userInfo"></user-profile>
    <!-- <p>name: {{ userInfo.id}}</p>
    <p>karam: {{ userInfo.karma}}</p>
    <p>created: {{ userInfo.created}}</p> -->
  </div>
</template>

<script>
import UserProfile from '../components/UserProfile.vue'

export default {
  components: {
    UserProfile,
  },
  computed: {
    userInfo() {
      return this.$store.state.user;
    }
  },
  created() {
    console.log(this.$route.params.id);
    const userName = this.$route.params.id;
    this.$store.dispatch('FETCH_USER', userName);
    // axios.get(`https://api.hnpwa.com/v0/user/${userName}.json`);
  },
}
</script>

<style>

</style>
  • UserProfile.vue
<template>
  <div class="user-container">
      <div>
        <!-- 사용자 프로필 -->
        <i class="fa-solid fa-user"></i>
      </div>
      <div class="user-description">
        <div>
          {{ info.id }}
        </div>
        <!-- <router-link :to="`/user/${userInfo.user}`">
          {{ userInfo.id }}
        </router-link> -->
        <div class="time">
          {{ info.created }}
        </div>
      </div>
    </div>
</template>

<script>
export default {
    props: {
        info: Object
    },
}
</script>

<style>
.user-container {
  display: flex;
  align-items: center;
  padding: 0.5rem;
}
.fa-user {
  font-size: 2.5rem;
}
.user-description {
  padding-left: 8px;
}
.time {
  font-size: 0.7rem;
}
</style>

사용자 컴포넌트 데이터 흐름 처리 비교


slot 사용

슬롯(slot)은 컴포넌트의 재사용성을 높여주는 기능입니다. 특정 컴포넌트에 등록된 하위 컴포넌트의 마크업을 확장하거나 재정의할 수 있습니다.
각각 프로필을 불러오는 부분에서 id를 호출하는 것이 있고 user를 호출하는 것에서 차이가 있기 때문에 slot를 사용한다.

  • UserProfile.vue
<template>
  <div class="user-container">
      <div>
        <!-- 사용자 프로필 -->
        <i class="fa-solid fa-user"></i>
      </div>
      <div class="user-description">
        <!-- <div>{{ info.id }}</div> -->
        <slot name="username"></slot>
        <!-- <router-link :to="`/user/${userInfo.user}`">
          {{ userInfo.id }}
        </router-link> -->
        <div class="time">
          <!-- time: {{ info.created }} -->
          <slot name="time">
            <!-- 상위 컴포넌트에서 정의할 영역 -->
          </slot>
        </div>
        <slot name="karma"></slot>
      </div>
    </div>
</template>

<script>
export default {
    props: {
        info: Object
    },
}
</script>

<style>
.user-container {
  display: flex;
  align-items: center;
  padding: 0.5rem;
}
.fa-user {
  font-size: 2.5rem;
}
.user-description {
  padding-left: 8px;
}
.time {
  font-size: 0.7rem;
}
</style>
  • ItemView.vue
<template>
  <div>
    <section>
    <!-- 사용자 상세 정보 -->
    <user-profile :info="fetchedItem">
      <div slot="username">{{ fetchedItem.user }}</div>
      <template slot="time">{{ fetchedItem.time_ago }}</template>
    </user-profile>
    </section>
    <section>
      <h2>{{ fetchedItem.title }}</h2>
    </section>
    <section>
      <!-- 질문 댓글 -->
      <div v-html="fetchedItem.content">
      </div>
    </section>
  </div>
</template>

<script>
import UserProfile from '../components/UserProfile.vue';
import { mapGetters } from 'vuex';

export default {
  components: {
    UserProfile,
  },
  computed: {
    ...mapGetters(['fetchedItem']),
  },
  created() {
    console.log(this.$route.params.id);
    const itemId = this.$route.params.id;
    this.$store.dispatch('FETCH_ITEM', itemId);
  }
}
</script>

<style scoped>
.user-container {
  display: flex;
  align-items: center;
  padding: 0.5rem;
}
.fa-user {
  font-size: 2.5rem;
}
.user-description {
  padding-left: 8px;
}
.time {
  font-size: 0.7rem;
}
</style>
  • UserView.vue
<template>
  <div>
    <user-profile :info="userInfo">
      <div slot="username">{{ userInfo.id }}</div>
      <template slot="time">{{ userInfo.created }}</template>
      <div slot="karma">{{ userInfo.karma}}</div>
    </user-profile>
  </div>
</template>

<script>
import UserProfile from '../components/UserProfile.vue'

export default {
  components: {
    UserProfile,
  },
  computed: {
    userInfo() {
      return this.$store.state.user;
    }
  },
  created() {
    console.log(this.$route.params.id);
    const userName = this.$route.params.id;
    this.$store.dispatch('FETCH_USER', userName);
    // axios.get(`https://api.hnpwa.com/v0/user/${userName}.json`);
  },
}
</script>

<style>

</style>




코드 정리

  • ItemView.vue
<template>
  <div>
    <section>
    <!-- 사용자 상세 정보 -->
    <user-profile :info="fetchedItem">
      <!-- <div slot="username">{{ fetchedItem.user }}</div> -->
      <router-link slot="username" :to="`/user/${fetchedItem.user}`">
          {{ fetchedItem.user }}
      </router-link>
      <template slot="time">{{ 'Posted ' + fetchedItem.time_ago }}</template>
    </user-profile>
    </section>
    <section>
      <h2>{{ fetchedItem.title }}</h2>
    </section>
    <section>
      <!-- 질문 댓글 -->
      <div v-html="fetchedItem.content">
      </div>
    </section>
  </div>
</template>

<script>
import UserProfile from '../components/UserProfile.vue';
import { mapGetters } from 'vuex';

export default {
  components: {
    UserProfile,
  },
  computed: {
    ...mapGetters(['fetchedItem']),
  },
  created() {
    console.log(this.$route.params.id);
    const itemId = this.$route.params.id;
    this.$store.dispatch('FETCH_ITEM', itemId);
  }
}
</script>

<style scoped>
.user-container {
  display: flex;
  align-items: center;
  padding: 0.5rem;
}
.fa-user {
  font-size: 2.5rem;
}
.user-description {
  padding-left: 8px;
}
.time {
  font-size: 0.7rem;
}
</style>
  • UserView.vue
<template>
  <div>
    <user-profile :info="userInfo">
      <div slot="username">{{ userInfo.id }}</div>
      <span slot="time">{{ 'Joined ' + userInfo.created }}, </span>
      <span slot="karma">{{ userInfo.karma}}</span>
    </user-profile>
  </div>
</template>

<script>
import UserProfile from '../components/UserProfile.vue'

export default {
  components: {
    UserProfile,
  },
  computed: {
    userInfo() {
      return this.$store.state.user;
    }
  },
  created() {
    console.log(this.$route.params.id);
    const userName = this.$route.params.id;
    this.$store.dispatch('FETCH_USER', userName);
    // axios.get(`https://api.hnpwa.com/v0/user/${userName}.json`);
  },
}
</script>

<style>

</style>




이벤트 버스 - 스피너 구현

스피너(Spinner)

📂components

  • Spinner.vue
<template>
  <div class="lds-facebook" v-if="loading">
    <div>
    </div>
    <div>
    </div>
    <div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    loading: {
      type: Boolean,
      required: true,
    },
  },
}
</script>

<style>
.lds-facebook {
  display: inline-block;
  position: absolute;
  width: 64px;
  height: 64px;
  top: 47%;
  left: 47%;
}
.lds-facebook div {
  display: inline-block;
  position: absolute;
  left: 6px;
  width: 13px;
  background: #42b883;
  animation: lds-facebook 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite;
}
.lds-facebook div:nth-child(1) {
  left: 6px;
  animation-delay: -0.24s;
}
.lds-facebook div:nth-child(2) {
  left: 26px;
  animation-delay: -0.12s;
}
.lds-facebook div:nth-child(3) {
  left: 45px;
  animation-delay: 0;
}
@keyframes lds-facebook {
  0% {
    top: 6px;
    height: 51px;
  }
  50%, 100% {
    top: 19px;
    height: 26px;
  }
}
</style>

📂utils

  • bus.js
import Vue from 'vue';

export default new Vue();
  • NewView.vue
<template>
  <div>
    <list-item></list-item>
  </div>
</template>

<script>
import ListItem from '../components/ListItem.vue';
import bus from '../utils/bus.js';

export default {
  components: {
    ListItem,
  },
  created() {
    bus.$emit('start:spinner');
    this.$store.dispatch('FETCH_NEWS');
    bus.$emit('end:spinner');
  }
}
</script>
  • App.vue

    Start / End spinner 메소드를 만들어 created 될 때 on 끝나고 나서 beforeDestroy를 설정해줘야한다.

<template>
  <div id="app">
    <tool-bar></tool-bar>
    <transition name="page">
      <router-view></router-view>
    </transition>
    <spinner :loading="loadingStatus"></spinner>
  </div>
</template>

<script>
import ToolBar from './components/ToolBar.vue';
import Spinner from './components/Spinner.vue';
import bus from './utils/bus.js';

export default {
  components: {
    ToolBar,
    Spinner,
  },
  data() {
    return {
      loadingStatus: false,
    };
  },
  methods: {
    startSpinner() {
      this.loadingStatus = true;
    },
    endSpinner() {
      this.loadingStatus = false;
    }
  },
  created() {
    bus.$on('start:spinner', this.startSpinner);
    bus.$on('end:spinner', this.endSpinner);
  },
  beforeDestroy() {
    bus.$off('start:spinner', this.startSpinner);
    bus.$off('end:spinner', this.endSpinner);
  }
}
</script>

<style>
#app {
  padding: 0;
  margin: 0;
}

a {
  color: #34495e;
  text-decoration: none;
}
a.router-link-exact-active {
  text-decoration: underline;
}
a:hover {
  color: #42b883;
  text-decoration: underline;
}

/* router Transition */
.page-enter-active,
.page-leave-active {
  transition: opacity 0.5s ease;
}

.page-enter-from,
.page-leave-to {
  opacity: 0;
}
</style>


스피너 실행 및 종료시점

  • NewsView.vue

    스피너 작동 확인을 위해 setTimeOut를 설정해준다.

<template>
  <div>
    <list-item></list-item>
  </div>
</template>

<script>
import ListItem from '../components/ListItem.vue';
import bus from '../utils/bus.js';

export default {
  components: {
    ListItem,
  },
  created() {
    bus.$emit('start:spinner');
    setTimeout(() => {
       this.$store.dispatch('FETCH_NEWS')
      .then(() => {
        console.log('fetched');
        bus.$emit('end:spinner');
      })
      .catch((error) => {
        console.log(error);
      });
    }, 3000);
  }
}
</script>




하이 오더 컴포넌트1

컴포넌트의 로직을 재사용하기 위한 고급 기술
📂router

  • index.js
import Vue from 'vue'
import VueRouter from 'vue-router';
// import NewsView from '../views/NewsView.vue';
// import AskView from '../views/AskView.vue';
// import JobsView from '../views/JobsView.vue';
import ItemView from '../views/ItemView.vue';
import UserView from '../views/UserView.vue';
import createListView from '../views/CreateListView.js';

Vue.use(VueRouter);

export const router = new VueRouter({
  mode: 'history',
  routes: [
    {
        path: '/',
        redirect: '/news',
    },
    {   
        // path: url 주소
        path: '/news',
        // component: url 주소로 갔을 때 표시될 컴포넌트
        name: 'news',
        //component: NewsView,
        component: createListView('NewsView'),
    },
    {
        path: '/ask',
        name: 'ask',
        // component: AskView,
        component: createListView('AskView'),
    },
    {
        path: '/jobs',
        name: 'jobs',
        // component: JobsView,
        component: createListView('JobsView'),
    },
    {
        path: '/user/:id',
        component: UserView,
    },
    {
        path: '/item/:id',
        component: ItemView,
    }
  ]
});

📂views

  • CreateListView.js

    하이 오더 컴포넌트(news, ask, jobs의 route의 행위가 동일함)

import ListView from './ListView.vue';
import bus from '../utils/bus.js';

export default function createListView(name) {
    return {
        //  재사용할 인스턴스(컴포넌트) 옵션들이 들어갈 자리
        name: name,
        created() {
            bus.$emit('start:spinner');
            setTimeout(() => {
            this.$store.dispatch('FETCH_LIST', this.$route.name)
            .then(() => {
                console.log('fetched');
                bus.$emit('end:spinner');
            })
            .catch((error) => {
                console.log(error);
            });
            }, 3000);
        },
        render(createElement) {
            return createElement(ListView);
        },
    }
}

📂components

  • ListView.vue

    랜더용

<template>
  <div>
    <list-item></list-item>
  </div>
</template>

<script>
import ListItem from '../components/ListItem.vue'

export default {
   components: {
    ListItem,
  },
}
</script>

<style>

</style>

📂api

  • index.js
import axios from 'axios';

const config = {
    baseUrl: 'https://api.hnpwa.com/v0/'
}

function fetchNewsList() {
    // return axios.get(config.baseUrl + 'news/1.json');
    return axios.get(`${config.baseUrl}news/1.json`);
}

function fetchAskList() {
    return axios.get(`${config.baseUrl}ask/1.json`);
}

function fetchJobsList() {
    return axios.get(`${config.baseUrl}jobs/1.json`);
}

function fetchUserInfo(username) {
    return axios.get(`${config.baseUrl}user/${username}.json`);
}

function fetchCommentInfo(id) {
    return axios.get(`${config.baseUrl}item/${id}.json`);
}

function fetchList(pageName) {
    return axios.get(`${config.baseUrl}${pageName}/1.json`);
}

export {
    fetchNewsList,
    fetchAskList,
    fetchJobsList,
    fetchUserInfo,
    fetchCommentInfo,
    fetchList,
}

📂store

  • index.js
    리스트 선언
import Vue from 'vue';
import Vuex from 'vuex';
import mutations from './mutations.js';
import actions from './actions.js';

Vue.use(Vuex);

export const store = new Vuex.Store({
  state: {
    news: [],
    ask: [],
    jobs: [],
    user: {},
    item: {},
    list: [],
  },
  getters:{
    fetchedAsk(state) {
        return state.ask;
    },
    fetchedItem(state) {
      return state.item;
    }
  },
  mutations,
  actions,
});
  • action.js
import { fetchAskList, fetchCommentInfo, fetchJobsList, fetchList, fetchNewsList, fetchUserInfo } from '../api/index.js';

export default {
    FETCH_NEWS(context) {
        fetchNewsList()
        .then(response=> {
            console.log(response.data);
            context.commit('SET_NEWS', response.data);
            return response;
        })
        .catch(error=>{
            console.log(error);
        })
    },
    FETCH_ASK(context) {
        fetchAskList()
        .then(response => {
            context.commit('SET_ASK', response.data);
        })
        .catch(error=>{
            console.log(error);
        })
    },
    FETCH_JOBS({commit}) {
        fetchJobsList()
        .then(({ data })=>{
            commit('SET_JOBS', data);
        })
        .catch(error=> {
            console.log(error);
        })
    },
    FETCH_USER({commit}, name) {
        fetchUserInfo(name)
        .then(({data}) => {
            commit('SET_USER', data);
        })
        .catch(error => {
            console.log(error);
        });
    },
    FETCH_ITEM({commit}, id) {
        fetchCommentInfo(id)
        .then(({data}) =>{
            commit('SET_ITEM', data);
        })
        .catch(error=>{
            console.log(error);
        });
    },
    FETCH_LIST({commit}, pageName) {
        fetchList(pageName)
        .then(({data}) => {
            commit('SET_LIST', data)
        })
        .catch(error => {
            console.log(error)
        });
    },
  }
  • mutations.js
export default {
    SET_NEWS(state, news) {
        state.news = news; // state 변경 가능
    },
    SET_ASK(state, ask) {
        state.ask = ask;
    },
    SET_JOBS(state, jobs) {
        state.jobs = jobs;
    },
    SET_USER(state, user) {
        state.user = user;
    },
    SET_ITEM(state, item) {
        state.item = item;
    },
    SET_LIST(state, list) {
        state.list = list;
    },
  }
  • ListItem.vue
<template>
  <div>
    <ul class="news-list">
      <li v-for="(item, index) in listItems" :key="index" class="post">
        <div class="points">
          {{ item.points || 0}}
        </div>
        <div>
          <!-- 타이틀 영역 -->
          <p class="news-title">
            <template v-if="item.domain">
              <a v-bind:href="item.url">
                {{ item.title }}
              </a>
            </template>
            <template v-else>
              <router-link v-bind:to="`item/${item.id}`">
                {{ item.title }}
              </router-link>
            </template>
          </p>
          <small class="link-text">
            {{ item.time_age }} by 
            <router-link
              v-if="item.user"
              v-bind:to="`/user/${item.user}`" class="link-text">{{ item.user }}</router-link>
              <a :href="item.url" v-else>
                {{ item.domain }}
              </a>
          </small>
        </div>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  computed: {
    listItems() {
      return this.$store.state.list;
    }
  }
}
</script>

<style scoped>
.news-list {
  padding: 0;
  margin: 0;
}
.post {
  list-style: none;
  display: flex;
  align-items: center;
  border-bottom: 1px solid #eee;
}
.points {
  width: 80px;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center; 
  color: #42b883;
}
.news-title {
  margin: 0;

}
.link-text {
  color: #828282;
}
</style>


하이 오더 컴포넌트2

컴포넌트 레벨이 깊어짐..
컴포넌트 통신에 불편함이 생김

MIXINS

모달이나 스피너를 믹스인으로 빼는 것이 효율적이다.

📂mixins

  • ListMixin.js
// mixin
import bus from '../utils/bus.js'

export default {
    //재사용할 컴포넌트 옵션
    created() {
        bus.$emit('start:spinner');
        this.$store.dispatch('FETCH_LIST', this.$route.name)
        .then(() => {
            console.log('fetched');
            bus.$emit('end:spinner');
        })
        .catch((error) => {
            console.log(error);
        });
    }
}

📂router

  • indes.js
import Vue from 'vue'
import VueRouter from 'vue-router';
import NewsView from '../views/NewsView.vue';
import AskView from '../views/AskView.vue';
import JobsView from '../views/JobsView.vue';
import ItemView from '../views/ItemView.vue';
import UserView from '../views/UserView.vue';
// import createListView from '../views/CreateListView.js';

Vue.use(VueRouter);

export const router = new VueRouter({
  mode: 'history',
  routes: [
    {
        path: '/',
        redirect: '/news',
    },
    {   
        // path: url 주소
        path: '/news',
        // component: url 주소로 갔을 때 표시될 컴포넌트
        name: 'news',
        component: NewsView,
        // component: createListView('NewsView'),
    },
    {
        path: '/ask',
        name: 'ask',
        component: AskView,
        // component: createListView('AskView'),
    },
    {
        path: '/jobs',
        name: 'jobs',
        component: JobsView,
        // component: createListView('JobsView'),
    },
    {
        path: '/user/:id',
        component: UserView,
    },
    {
        path: '/item/:id',
        component: ItemView,
    }
  ]
});
  • NewView.vue(ask, jobs 동일)
<template>
  <div>
    <list-item></list-item>
  </div>
</template>

<script>
import ListItem from '../components/ListItem.vue';
// import bus from '../utils/bus.js';
import ListMixin from '../mixins/ListMixin.js';

export default {
    components: {
        ListItem,
    },
    mixins: [ListMixin],
}
</script>

NewsView에서 바로 ListItem에 접근할 수 있다.
mixins가 깊어지는 단점을 보안




UX를 고려한 데이터 호출 시점

  1. 라우터 네비게이션 가드(먼저시작)
    -> 네비게이션 가드(navigation guard)란 뷰 라우터로 특정 URL에 접근할 때 동작을 정의하는 속성
    네이게이션 가드

  2. 컴포넌트 라이프 사이클 훅
    -> created : 컴포넌트가 생성되자마자 호출되는 로직


라이프 사이클 훅

라이프 사이클 훅을 이용한 데이터 호출 방법의 문제와 비동기 처리 코드 수정

  • actions.js

    비동기 처리의 순서를 정의하지 않아서 mixins가 먼저 시작되기 때문에 return을 해서 순서를 보장시켜줘야한다.
    이 이유때문에 스피너가 잘 작동을 하지 않았다.

import { fetchAskList, fetchCommentInfo, fetchJobsList, fetchList, fetchNewsList, fetchUserInfo } from '../api/index.js';

export default {
    FETCH_NEWS(context) {
        return fetchNewsList()
        .then(response=> {
            console.log(response.data);
            context.commit('SET_NEWS', response.data);
            return response;
        })
        .catch(error=>{
            console.log(error);
        })
    },
    FETCH_ASK(context) {
        return fetchAskList()
        .then(response => {
            context.commit('SET_ASK', response.data);
        })
        .catch(error=>{
            console.log(error);
        })
    },
    FETCH_JOBS({commit}) {
        return fetchJobsList()
        .then(({ data })=>{
            commit('SET_JOBS', data);
        })
        .catch(error=> {
            console.log(error);
        })
    },
    FETCH_USER({commit}, name) {
        return fetchUserInfo(name)
        .then(({data}) => {
            commit('SET_USER', data);
        })
        .catch(error => {
            console.log(error);
        });
    },
    FETCH_ITEM({commit}, id) {
        return fetchCommentInfo(id)
        .then(({data}) =>{
            commit('SET_ITEM', data);
        })
        .catch(error=>{
            console.log(error);
        });
    },
    FETCH_LIST({commit}, pageName) {
        return fetchList(pageName)
        .then(({data}) => {
            commit('SET_LIST', data)
        })
        .catch(error => {
            console.log(error)
        });
    },
  }

오류수정


라우터 네비게이션 가드

공식문서

to: 이동할 url의 라우팅 정보
from: 현재 url의 라우팅 정보
next: 함수

📂router

  • index.js
import Vue from 'vue'
import VueRouter from 'vue-router';
import NewsView from '../views/NewsView.vue';
import AskView from '../views/AskView.vue';
import JobsView from '../views/JobsView.vue';
import ItemView from '../views/ItemView.vue';
import UserView from '../views/UserView.vue';
// import createListView from '../views/CreateListView.js';
import bus from '../utils/bus.js'
import { store } from '../store/index.js';

Vue.use(VueRouter);

export const router = new VueRouter({
  mode: 'history',
  routes: [
    {
        path: '/',
        redirect: '/news',
    },
    {   
        // path: url 주소
        path: '/news',
        // component: url 주소로 갔을 때 표시될 컴포넌트
        name: 'news',
        component: NewsView,
        // component: createListView('NewsView'),
        beforeEnter: (to, from, next) => {
            bus.$emit('start:spinner');
            store.dispatch('FETCH_LIST', to.name)
            .then(() => {
                console.log('fetched');
                bus.$emit('end:spinner');
                next();
            })
            .catch((error) => {
                console.log(error);
            });
        },
    },
    {
        path: '/ask',
        name: 'ask',
        component: AskView,
        // component: createListView('AskView'),
    },
    {
        path: '/jobs',
        name: 'jobs',
        component: JobsView,
        // component: createListView('JobsView'),
    },
    {
        path: '/user/:id',
        component: UserView,
    },
    {
        path: '/item/:id',
        component: ItemView,
    }
  ]
});

(실습) ask/jobs

용어 정리

라이프 사이클 훅

created

인스턴스가 작성된 후 동기적으로 호출된다.
부모, 자식 관계의 컴퍼넌트가 랜더링 될 때 mounted보다 먼저 호출되며 부모, 자식순으로 실행한다. 데이터 초기화 선언을 created에서 많이 한다. 가상돔을 건드릴 수 없음

mounted

부모, 자식 관계의 컴퍼넌트가 랜더링 될때 created 다음으로 호출되며, 자식, 부모순으로 실행된다. el이 새로 생성된 vm.$el로 대체된 인스턴스가 마운트 된 직후 호출된다. 돔조작관련을 mounted 영역에 기술한다.

📂router

  • index.js
import Vue from 'vue'
import VueRouter from 'vue-router';
import NewsView from '../views/NewsView.vue';
import AskView from '../views/AskView.vue';
import JobsView from '../views/JobsView.vue';
import ItemView from '../views/ItemView.vue';
import UserView from '../views/UserView.vue';
// import createListView from '../views/CreateListView.js';
import bus from '../utils/bus.js'
import { store } from '../store/index.js';

Vue.use(VueRouter);

export const router = new VueRouter({
  mode: 'history',
  routes: [
    {
        path: '/',
        redirect: '/news',
    },
    {   
        // path: url 주소
        path: '/news',
        // component: url 주소로 갔을 때 표시될 컴포넌트
        name: 'news',
        component: NewsView,
        // component: createListView('NewsView'),
        beforeEnter: (to, from, next) => {
            bus.$emit('start:spinner');
            store.dispatch('FETCH_LIST', to.name)
            .then(() => {
                console.log('fetched');
                next();
            })
            .catch((error) => {
                console.log(error);
            });
        },
    },
    {
        path: '/ask',
        name: 'ask',
        component: AskView,
        // component: createListView('AskView'),
        beforeEnter: (to, from, next) => {
            bus.$emit('start:spinner');
            store.dispatch('FETCH_LIST', to.name)
            .then(() => {
                console.log('fetched');
                next();
            })
            .catch((error) => {
                console.log(error);
            });
        },
    },
    {
        path: '/jobs',
        name: 'jobs',
        component: JobsView,
        // component: createListView('JobsView'),
        beforeEnter: (to, from, next) => {
            bus.$emit('start:spinner');
            store.dispatch('FETCH_LIST', to.name)
            .then(() => {
                console.log('fetched');
                next();
            })
            .catch((error) => {
                console.log(error);
            });
        },
    },
    {
        path: '/user/:id',
        component: UserView,
    },
    {
        path: '/item/:id',
        component: ItemView,
    }
  ]
});
  • ListMixin.js
// mixin
import bus from '../utils/bus.js'

export default {
    //재사용할 컴포넌트 옵션
    mounted() {
        bus.$emit('end:spinner');
    },
}
  • NewView.vue(jobs, ask 동일)
<template>
  <div>
    <list-item></list-item>
  </div>
</template>

<script>
import ListItem from '../components/ListItem.vue';
// import bus from '../utils/bus.js';
import ListMixin from '../mixins/ListMixin.js';

export default {
    components: {
        ListItem,
    },
    mixins: [ListMixin],
}
</script>



Async & Await

자바스크립트 비동기 처리 패턴의 최신 문법.
Promise, callback에서 주는 단점을 해결
자바스크립트의 비동기적 사고 방식에서 벗어나 동기적으로 코드 작성 가능

promise로 반환된 로직을 await로 호출한다.


예제 실습

async-await 프로젝트 생성

npm create async-await
npm cd async-await
npm install axios
npm run serve
  • App.vue
<template>
  <div>
    <button @click="loginUser1">login</button>
    <h1>List</h1>
    <ul>
      <li v-for="(item, index) in items" :key="index">{{ item }}</li>
    </ul>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      items: [],
    }
  },
  methods: {
    loginUser() {
      axios.get('http://jsonplaceholder.typicode.com/users/1')
        .then(response => {
          if(response.data.id === 1) {
            console.log('사용자가 인증되었습니다.');
            axios.get('http://jsonplaceholder.typicode.com/todos')
              .then(response => {
                this.items = response.data;
              })
              .catch(error => console.log(error))
          }
        })
        .catch(error => console.log(error));
    },
    async loginUser1() {
      var response = await axios.get('http://jsonplaceholder.typicode.com/users/1'); // 첫번째 비동기
      if (response.data.id === 1) {
        console.log('사용자가 인증되었습니다.');
        var list = await axios.get('http://jsonplaceholder.typicode.com/todos'); // 두번째 비동기
        this.items = list.data;
      }
    }
  },
}
</script>

<style>

</style>


async await 에러 처리 방법과 공통화 함수 작성 방법

promise를 사용할 때는 .then() / .catch()
async await 사용할 때는 try/catch

공통화 함수 에러작성

에러들에 처리를 빨리 낼 수 있다.

📂utils

  • hander.js
export function handleException() {
    ...
}
  • App.vue
<template>
  <div>
    <button @click="loginUser1">login</button>
    <h1>List</h1>
    <ul>
      <li v-for="(item, index) in items" :key="index">{{ item }}</li>
    </ul>
  </div>
</template>

<script>
import axios from 'axios';
import { handleException } from './utils/handler.js';

export default {
  data() {
    return {
      items: [],
    }
  },
  methods: {
    loginUser() {
      axios.get('http://jsonplaceholder.typicode.com/users/1')
        .then(response => {
          if(response.data.id === 1) {
            console.log('사용자가 인증되었습니다.');
            axios.get('http://jsonplaceholder.typicode.com/todos')
              .then(response => {
                this.items = response.data;
              })
              .catch(error => console.log(error))
          }
        })
        .catch(error => console.log(error));
    },

    async loginUser1() {
      try {
        var response = await axios.get('http://jsonplaceholder.typicode.com/users/1'); // 첫번째 비동기
        if (response.data.id === 1) {
          console.log('사용자가 인증되었습니다.');
          var list = await axios.get('http://jsonplaceholder.typicode.com/todos'); // 두번째 비동기
          this.items = list.data;
        }
      } catch (error) {
        handleException(error);
        console.log(error);
      }
    }
  },
}
</script>

<style>

</style>

async함수를 이용한 코드 리팩토링

FETCH_NEWS에 적용하기

  • actions.js
import { fetchAskList, fetchCommentInfo, fetchJobsList, fetchList, fetchNewsList, fetchUserInfo } from '../api/index.js';

export default {
    // promise
    // FETCH_NEWS(context) {
    //     return fetchNewsList()
    //     .then(response=> {
    //         console.log(response.data);
    //         context.commit('SET_NEWS', response.data);
    //         return response;
    //     })
    //     .catch(error=>{
    //         console.log(error);
    //     })
    // },
    // async
    async FETCH_NEWS(context) {
        var response = await fetchNewsList();
        context.commit('SET_NEWS', response.data);
        return response;
    },

    FETCH_ASK(context) {
        return fetchAskList()
        .then(response => {
            context.commit('SET_ASK', response.data);
        })
        .catch(error=>{
            console.log(error);
        })
    },
    FETCH_JOBS({commit}) {
        return fetchJobsList()
        .then(({ data })=>{
            commit('SET_JOBS', data);
        })
        .catch(error=> {
            console.log(error);
        })
    },
    FETCH_USER({commit}, name) {
        return fetchUserInfo(name)
        .then(({data}) => {
            commit('SET_USER', data);
        })
        .catch(error => {
            console.log(error);
        });
    },
    FETCH_ITEM({commit}, id) {
        return fetchCommentInfo(id)
        .then(({data}) =>{
            commit('SET_ITEM', data);
        })
        .catch(error=>{
            console.log(error);
        });
    },
    FETCH_LIST({commit}, pageName) {
        return fetchList(pageName)
        .then(({data}) => {
            commit('SET_LIST', data)
        })
        .catch(error => {
            console.log(error)
        });
    },
  }

(실습)

  • action.js
import { fetchAskList, fetchCommentInfo, fetchJobsList, fetchList, fetchNewsList, fetchUserInfo } from '../api/index.js';

export default {
    // promise
    // FETCH_NEWS(context) {
    //     return fetchNewsList()
    //     .then(response=> {
    //         console.log(response.data);
    //         context.commit('SET_NEWS', response.data);
    //         return response;
    //     })
    //     .catch(error=>{
    //         console.log(error);
    //     })
    // },
    // async
    async FETCH_NEWS(context) {
        var response = await fetchNewsList();
        context.commit('SET_NEWS', response.data);
        return response;
    },

    async FETCH_ASK({commit}) {
        var response = await fetchAskList();
        commit('SET_ASK', response.data);
        return response;
    },

    async FETCH_JOBS({commit}) {
        try {
            const response = await fetchJobsList();
            commit('SET_JOBS', response.data);
            return response;
        } catch (error) {
            console.log(error);
        }
    },
    async FETCH_USER({commit}, name) {
        try {
            const response = await fetchUserInfo(name);
            commit('SET_USER', response.data);
            return response;
        } catch (error) {
            console.log(error);
        }
    },
    async FETCH_ITEM({commit}, id) {
        try {
            const response = await fetchCommentInfo(id);
            commit('SET_ITEM', response.data);
            return response;
        } catch (error) {
            console.log(error);
        }
    },
    async FETCH_LIST({commit}, pageName) {
        try {
            const response = await fetchList(pageName);
            commit('SET_LIST', response.data);
            return response;
        } catch (error) {
            console.log(error);
        }
    },
  }import { fetchAskList, fetchCommentInfo, fetchJobsList, fetchList, fetchNewsList, fetchUserInfo } from '../api/index.js';

export default {
    // promise
    // FETCH_NEWS(context) {
    //     return fetchNewsList()
    //     .then(response=> {
    //         console.log(response.data);
    //         context.commit('SET_NEWS', response.data);
    //         return response;
    //     })
    //     .catch(error=>{
    //         console.log(error);
    //     })
    // },
    // async
    async FETCH_NEWS(context) {
        var response = await fetchNewsList();
        context.commit('SET_NEWS', response.data);
        return response;
    },

    async FETCH_ASK({commit}) {
        var response = await fetchAskList();
        commit('SET_ASK', response.data);
        return response;
    },

    async FETCH_JOBS({commit}) {
        try {
            const response = await fetchJobsList();
            commit('SET_JOBS', response.data);
            return response;
        } catch (error) {
            console.log(error);
        }
    },
    async FETCH_USER({commit}, name) {
        try {
            const response = await fetchUserInfo(name);
            commit('SET_USER', response.data);
            return response;
        } catch (error) {
            console.log(error);
        }
    },
    async FETCH_ITEM({commit}, id) {
        try {
            const response = await fetchCommentInfo(id);
            commit('SET_ITEM', response.data);
            return response;
        } catch (error) {
            console.log(error);
        }
    },
    async FETCH_LIST({commit}, pageName) {
        try {
            const response = await fetchList(pageName);
            commit('SET_LIST', response.data);
            return response;
        } catch (error) {
            console.log(error);
        }
    },
  }

actions에서 에러를 처리할 수도 있지만, api단에서 처리할 수도 있다

📂api

  • index.js
import axios from 'axios';

const config = {
    baseUrl: 'https://api.hnpwa.com/v0/'
}

function fetchNewsList() {
    // return axios.get(config.baseUrl + 'news/1.json');
    return axios.get(`${config.baseUrl}news/1.json`);
}

async function fetchAskList() {
    try {
        const response = await axios.get(`${config.baseUrl}ask/1.json`);
        return response;
    } catch (error) {
        console.log(error);
    }
}

function fetchJobsList() {
    return axios.get(`${config.baseUrl}jobs/1.json`);
}

function fetchUserInfo(username) {
    return axios.get(`${config.baseUrl}user/${username}.json`);
}

function fetchCommentInfo(id) {
    return axios.get(`${config.baseUrl}item/${id}.json`);
}

async function fetchList(pageName) {
    try {
        var response = await axios.get(`${config.baseUrl}${pageName}/1.json`);
        return response;
    } catch (error) {
        console.log(error);
    }
    
}

export {
    fetchNewsList,
    fetchAskList,
    fetchJobsList,
    fetchUserInfo,
    fetchCommentInfo,
    fetchList,
}

0개의 댓글