6 — 애플리케이션 제작 파트 — 스토어 구현

source: categories/study/vue-beginner-lv3/vue-beginner-lv3_6.md

6.1 Vuex 설치 및 Vuex가 적용된 앱 구조 소개

  • Before: NewsView라는 페이지 컴포넌트에서 API의 함수를 그대로 가져다가 호출한 모습
  • After: NewsView에서 바로 API 함수를 불러다 쓰는 것이 아니라 중간에 Vuex에 있는 state에 API로 불러온 데이터를 담아서 그걸로 화면에 표시

npm i vuex
# OR
yarn add vuex

6.2 Vuex 모듈화 및 state 적용

src/store/index.js 폴더 및 파일 생성



import Vue from "vue";
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
    state: {
        news: [],
    }
})

export default store


src/main.js



import Vue from 'vue'
import App from './App.vue'
import router from "./routes";
import store from "./store";

Vue.config.productionTip = false

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


6.3 NewsView에 actions와 mutations 적용

6.3.1 비동기 데이터 요청 로직 Vuex의 store의 actions로 옮기기

src/store/index.js



import Vue from "vue";
import Vuex from 'vuex';
import {fetchNewsList} from "../api";

Vue.use(Vuex);

const store = new Vuex.Store({
    state: {
        news: [],
    },
    actions: {
        FETCH_NEWS() {
            fetchNewsList()
                .then(response => console.log(response))
                .catch(error => console.log(error))
        }
    }
})

export default store


src/views/NewsView.vue



<template>
<div>
  <div v-for="(user, index) in users" v-bind:key="index">{{user.title}}</div>
</div>
</template>

<script>
export default {
  name: "NewsView",
  data() {
    return {
      users: [],
    }
  },
  created() {
    this.$store.dispatch('FETCH_NEWS');
  }
}
</script>

<style scoped>

</style>


Note

위와 같이 actions에서 Backend API에서 꺼내온 data를 바로 State에 넣고싶은데, 그렇게하면 안된다.
그 이유는 현재 Vuex의 구조는 비동기 로직은 전부 actions에서 하게되어있고,
거기서 받아온 데이터를 mutations를 통해서 State에 넣어주게끔
그런 상태관리 구조로 되어있기 때문이다.

이는 데이터 흐름 추적이 용이하게 하기위한 구조이다.
비동기 안에 데이터(state)까지 조작하는 로직이 들어있으면 언제 어떻게 데이터(state)가 바뀌는지 흐름 파악하기가 어렵다.

그래서 각각 역할을 정확하게 나눈 것이다.

actions는 비동기 로직만을 다룸으로써 Backend API로부터 데이터를 꺼내오기만 하고, State에 담기위한 Mutations를 별도로 호출하는 역할만 담당한다.

6.3.2 actions에서 mutations 호출하기

store/index.js



import Vue from "vue";
import Vuex from 'vuex';
import {fetchNewsList} from "../api";

Vue.use(Vuex);

const store = new Vuex.Store({
    state: {
        news: [],
    },
    mutations: {
        SET_NEWS(state, news) {
            state.news = news;
        },
    },
    actions: {
        FETCH_NEWS(context) {
            fetchNewsList()
                .then(response => {
                    context.commit('SET_NEWS', response.data);
                })
                .catch(error => console.log(error))
        },
    }
})

export default store


src/views/NewsView.vue



<template>
<div>
  <div v-for="(user, index) in this.$store.state.news" v-bind:key="index">{{user.title}}</div>
</div>
</template>

<script>

export default {
  name: "NewsView",
  created() {
    this.$store.dispatch('FETCH_NEWS');
  }
}
</script>

<style scoped>

</style>


Note

위와 같이 하면 아까 말씀드렸던 위 이미지의 After로 구조가 바뀌게된다.
actions에서 API 로직을 호출해서 Backend API로부터 데이터를 받으면, 그걸 mutations로 넘겨주면서 mutations를 호출하고,
mutations에서 state 값을 수정한 다음, 이것이 NewsView 컴포넌트가 렌더링되면서 반영된다.

6.4 [실습 안내] JobsView와 AskView 실습 안내

src/store/index.js



import Vue from "vue";
import Vuex from 'vuex';
import {fetchNewsList, fetchJobsList, fetchAskList} from "../api";

Vue.use(Vuex);

const store = new Vuex.Store({
    state: {
        news: [],
        jobs: [],
        ask: [],
    },
    mutations: {
        SET_NEWS(state, news) {
            state.news = news;
        },
        SET_JOBS(state, jobs) {
            state.jobs = jobs;
        },
        SET_ASK(state, ask) {
            state.ask = ask;
        }
    },
    actions: {
        FETCH_NEWS(context) {
            fetchNewsList()
                .then(response => {
                    context.commit('SET_NEWS', response.data);
                })
                .catch(error => console.log(error))
        },
        FETCH_JOBS(context) {
            fetchJobsList()
                .then(response => {
                    context.commit('SET_JOBS', response.data);
                })
                .catch(error => console.log(error))
        },
        FETCH_ASK(context) {
            fetchAskList()
                .then(response => context.commit('SET_ASK', response.data))
                .catch(error => console.log(error))
        }
    }
})

export default store


src/views/JobsView.vue



<template>
<div>
  <div v-for="(job, index) in this.$store.state.jobs" v-bind:key="index">{{job.title}}</div>
</div>
</template>

<script>
export default {
  name: "NewsView",
  created() {
    this.$store.dispatch('FETCH_JOBS');
  }
}
</script>

<style scoped>

</style>


src/views/AskView.vue



<template>
<div>
  <div v-for="(item, index) in this.$store.state.ask" v-bind:key="index">{{item.title}}</div>
</div>
</template>

<script>
export default {
  name: "NewsView",
  created() {
    this.$store.dispatch('FETCH_ASK');
  }
}
</script>

<style scoped>

</style>


6.5 [실습] JobsView에 스토어 적용

Destructuring 문법 적용



import Vue from "vue";
import Vuex from 'vuex';
import {fetchNewsList, fetchJobsList, fetchAskList} from "../api";

Vue.use(Vuex);

const store = new Vuex.Store({
    state: {
        news: [],
        jobs: [],
        ask: [],
    },
    mutations: {
        SET_NEWS(state, news) {
            state.news = news;
        },
        SET_JOBS(state, jobs) {
            state.jobs = jobs;
        },
        SET_ASK(state, ask) {
            state.ask = ask;
        }
    },
    actions: {
        FETCH_NEWS({commit}) {
            fetchNewsList()
                .then(({data}) => commit('SET_NEWS', data))
                .catch(error => console.log(error))
        },
        FETCH_JOBS({commit}) {
            fetchJobsList()
                .then(({data}) => commit('SET_JOBS', data))
                .catch(error => console.log(error))
        },
        FETCH_ASK({commit}) {
            fetchAskList()
                .then(({data}) => commit('SET_ASK', data))
                .catch(error => console.log(error))
        }
    }
})

export default store


6.6 [실습] map 헬퍼 함수를 이용한 AskView 풀이

src/views/AskView.vue - mapState 사용



<template>
<div>
  <div v-for="(item, index) in ask" v-bind:key="index">{{item.title}}</div>
</div>
</template>

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

export default {
  name: "AskView",
  computed: {
    ...mapState(['ask'])
  },
  created() {
    this.$store.dispatch('FETCH_ASK');
  }
}
</script>

<style scoped>

</style>


Vuex getters 사용

src/store/index.js



import Vue from "vue";
import Vuex from 'vuex';
import {fetchNewsList, fetchJobsList, fetchAskList} from "../api";

Vue.use(Vuex);

const store = new Vuex.Store({
    state: {
        news: [],
        jobs: [],
        ask: [],
    },
    getters: {
        fetchedAsk(state) {
            return state.ask;
        }
    },
    mutations: {
        SET_NEWS(state, news) {
            state.news = news;
        },
        SET_JOBS(state, jobs) {
            state.jobs = jobs;
        },
        SET_ASK(state, ask) {
            state.ask = ask;
        }
    },
    actions: {
        FETCH_NEWS({commit}) {
            fetchNewsList()
                .then(({data}) => commit('SET_NEWS', data))
                .catch(error => console.log(error))
        },
        FETCH_JOBS({commit}) {
            fetchJobsList()
                .then(({data}) => commit('SET_JOBS', data))
                .catch(error => console.log(error))
        },
        FETCH_ASK({commit}) {
            fetchAskList()
                .then(({data}) => commit('SET_ASK', data))
                .catch(error => console.log(error))
        }
    }
})

export default store


src/views/AskView.vue



<template>
<div>
  <div v-for="(item, index) in fetchedAsk" v-bind:key="index">{{item.title}}</div>
</div>
</template>

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

export default {
  name: "AskView",
  computed: {
    // # 3.
    ...mapGetters(['fetchedAsk'])

    // 이름을 바꾸고싶다면
    // ...mapGetters({
    //   ask: 'fetchedAsk',
    // })

    // # 2.
    // ...mapState(['ask'])

    // # 1.
    // ask() {
    //   return this.$store.state.ask;
    // }
  },
  created() {
    this.$store.dispatch('FETCH_ASK');
  }
}
</script>

<style scoped>

</style>


6.7 스토어 속성 모듈화 (state, getters, mutations, actions 속성별 모듈화입니다)

src/store/index.js



import Vue from "vue";
import Vuex from 'vuex';
import mutations from "./mutations";
import actions from "./actions";

Vue.use(Vuex);

const store = new Vuex.Store({
    state: {
        news: [],
        jobs: [],
        ask: [],
    },
    getters: {
        fetchedAsk(state) {
            return state.ask;
        }
    },
    mutations,
    actions,
})

export default store


src/store/actions.js



import {fetchAskList, fetchJobsList, fetchNewsList} from "../api";

export default {
    FETCH_NEWS({commit}) {
        fetchNewsList()
            .then(({data}) => commit('SET_NEWS', data))
            .catch(error => console.log(error))
    },
    FETCH_JOBS({commit}) {
        fetchJobsList()
            .then(({data}) => commit('SET_JOBS', data))
            .catch(error => console.log(error))
    },
    FETCH_ASK({commit}) {
        fetchAskList()
            .then(({data}) => commit('SET_ASK', data))
            .catch(error => console.log(error))
    }
}


src/store/mutations.js



export default {
    SET_NEWS(state, news) {
        state.news = news;
    },
    SET_JOBS(state, jobs) {
        state.jobs = jobs;
    },
    SET_ASK(state, ask) {
        state.ask = ask;
    }
}


스토어 기능별 모듈화

src/store/index.js



import Vue from "vue";
import Vuex from 'vuex';
import FetchNews from "./modules/FetchNews";
import FetchJobs from "./modules/FetchJobs";
import FetchAsk from "./modules/FetchAsk";

Vue.use(Vuex);

const store = new Vuex.Store({
    modules: {
        FetchNews,
        FetchJobs,
        FetchAsk,
    },
})

export default store


src/store/modules/FetchAsk.js



import {fetchAskList} from "../../api";

const state = {
    ask: [],
}

const getters = {
    fetchedAsk(state) {
        return state.ask;
    },
}

const mutations = {
    SET_ASK(state, ask) {
        state.ask = ask;
    },
}

const actions = {
    FETCH_ASK({commit}) {
        fetchAskList()
            .then(({data}) => commit('SET_ASK', data))
            .catch(error => console.log(error))
    }
}

export default {
    state,
    getters,
    mutations,
    actions,
}


src/store/modules/FetchNews.js



import {fetchNewsList} from "../../api";

const state = {
    news: [],
}

const getters = {
    fetchedNews(state) {
        return state.news;
    },
}

const mutations = {
    SET_NEWS(state, news) {
        state.news = news;
    },
}

const actions = {
    FETCH_NEWS({commit}) {
        fetchNewsList()
            .then(({data}) => commit('SET_NEWS', data))
            .catch(error => console.log(error))
    },
}

export default {
    state,
    getters,
    mutations,
    actions,
}


src/store/modules/FetchJobs.js



import {fetchJobsList} from "../../api";

const state = {
    jobs: [],
}

const getters = {
    fetchedJobs(state) {
        return state.jobs;
    }
}

const mutations = {
    SET_JOBS(state, jobs) {
        state.jobs = jobs;
    },
}

const actions = {
    FETCH_JOBS({commit}) {
        fetchJobsList()
            .then(({data}) => commit('SET_JOBS', data))
            .catch(error => console.log(error))
    },
}

export default {
    state,
    getters,
    mutations,
    actions,
}


src/views/NewsView.vue



<template>
<div>
  <div v-for="(user, index) in fetchedNews" v-bind:key="index">{{user.title}}</div>
</div>
</template>

<script>
import {mapGetters, mapActions} from "vuex";

export default {
  name: "NewsView",
  computed: {
    ...mapGetters(['fetchedNews']),
  },
  methods: {
    ...mapActions(['FETCH_NEWS']),
  },
  created() {
    this.FETCH_NEWS();
  }
}
</script>

<style scoped>

</style>


src/views/AskView.vue



<template>
<div>
  <div v-for="(item, index) in fetchedAsk" v-bind:key="index">{{item.title}}</div>
</div>
</template>

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

export default {
  name: "AskView",
  computed: {
    // # 3.
    ...mapGetters(['fetchedAsk']),

    // 이름을 바꾸고싶다면
    // ...mapGetters({
    //   ask: 'fetchedAsk',
    // })

    // # 2.
    // ...mapState(['ask'])

    // # 1.
    // ask() {
    //   return this.$store.state.ask;
    // }
  },
  methods: {
    ...mapActions(['FETCH_ASK']),
  },
  created() {
    this.FETCH_ASK();
  }
}
</script>

<style scoped>

</style>


src/views/JobsView.vue



<template>
<div>
  <div v-for="(job, index) in fetchedJobs" v-bind:key="index">{{job.title}}</div>
</div>
</template>

<script>
import {mapGetters, mapActions} from "vuex";

export default {
  name: "JobsView",
  computed: {
    ...mapGetters(['fetchedJobs']),
  },
  methods: {
    ...mapActions(['FETCH_JOBS']),
  },
  created() {
    this.FETCH_JOBS();
  }
}
</script>

<style scoped>

</style>