Syw.Frontend

๐Ÿงฉ Vue 2๋กœ ๋ฐฐ์šฐ๋Š” ํ”„๋ก ํŠธ์—”๋“œ ๊ธฐ์ดˆ

2๋‹จ๊ณ„. ๋„ฅ์ŠคํŠธ๊ณต๋ฌด์› ๋ฉ”์ธ Vue + Express๋กœ ๋กœ ํด๋ก ํ•˜๊ธฐ

2-9. ๋ฉ”์ธ ๋ฐ์ดํ„ฐ Vuex ์ƒํƒœ๊ด€๋ฆฌ๋กœ ์ด๊ด€

๐Ÿงฉ ๊ฐ•์˜ ๋ชฉํ‘œ

  • ๋ฉ”์ธ ํŽ˜์ด์ง€์— ์‚ฌ์šฉํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‘ Vuex์—์„œ ์ค‘์•™ ๊ด€๋ฆฌํ•˜๋„๋ก ๋ฆฌํŒฉํ† ๋ง
  • ๊ธฐ์กด axios ์š”์ฒญ โ†’ Vuex์—์„œ fetch โ†’ ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” mapState ๋ฐ mapActions์œผ๋กœ ์‚ฌ์šฉ

๐Ÿ“ 1. Vuex ์„ค์น˜ ๋ฐ store ๊ตฌ์„ฑ

โ‘  ์„ค์น˜

npm install vuex

โ‘ก src/store/index.js ์ƒ์„ฑ

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

Vue.use(Vuex);

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

โ‘ข src/store/modules/main.js ์ƒ์„ฑ

import axios from '@/libs/axios';

export default {
  namespaced: true,
  state: {
    mainSlides: [],
    mainTabs: [],
    promoList: [],
  },
  mutations: {
    setMainSlides(state, slides) {
      state.mainSlides = slides;
    },
    setMainTabs(state, tabs) {
      state.mainTabs = tabs;
    },
    setPromoList(state, list) {
      state.promoList = list;
    },
  },
  actions: {
    async fetchMainSlides({ commit }) {
      const res = await axios.get('/api/main-slides');
      commit('setMainSlides', res.data);
    },
    async fetchMainTabs({ commit }) {
      const res = await axios.get('/api/main-tabs');
      commit('setMainTabs', res.data);
    },
    async fetchPromoList({ commit }) {
      const res = await axios.get('/api/promo-list');
      commit('setPromoList', res.data);
    },
  },
};

๐Ÿ›  2. main.js์— store ๋“ฑ๋ก

์ด๋ฏธ ๋˜์–ด์žˆ๋Š” ๊ฒฝ์šฐ ํŒจ์Šค

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

import VueAwesomeSwiper from "vue-awesome-swiper";
import "swiper/css/swiper.css"; // swiper 5์šฉ css

import "./assets/base.scss";

Vue.use(VueAwesomeSwiper);

Vue.config.productionTip = false;

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

๐Ÿงฉ 3. MainSlide.vue์—์„œ Vuex ์—ฐ๋™

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

export default {
  name: "MainSlide",
  computed: {
    ...mapState('main', ['mainSlides']),
  },
  created() {
    this.fetchMainSlides();
  },
  methods: {
    ...mapActions('main', ['fetchMainSlides']),
  },
};
</script>

๐Ÿงฉ 4. MainTabs.vue์—์„œ Vuex ์—ฐ๋™

<template>
  <div class="main-tabs">
    <h3 class="title">๋„ฅ์ŠคํŠธ๊ณต๋ฌด์› 1์œ„ ๊ฐ•์‚ฌ์ง„</h3>
    <div class="tab-slide">
      <div class="tabs">
        <button
          v-for="(tab, idx) in mainTabs"
          :key="idx"
          :class="{ active: currentTab === idx }"
          @click="changeTab(idx)"
        >
          {{ tab.label }}
        </button>
      </div>

      <TabSlide
        v-if="mounted && mainTabs.length"
        :key="renderKey"
        :slides="mainTabs[currentTab].slides"
      />
    </div>
  </div>
</template>

<script>
import { mapState, mapActions } from 'vuex';
import TabSlide from "./TabSlide.vue";

export default {
  components: { TabSlide },
  data() {
    return {
      currentTab: 0,
      renderKey: 0,
      mounted: true,
    };
  },
  computed: {
    ...mapState('main', ['mainTabs']),
  },
  created() {
    this.fetchMainTabs();
  },
  methods: {
    ...mapActions('main', ['fetchMainTabs']),
    changeTab(index) {
      this.currentTab = index;
      this.mounted = false;
      this.$nextTick(() => {
        this.renderKey++;
        this.mounted = true;
      });
    },
  },

};
</script>

<style scoped lang="scss">
.main-tabs {
  width: 790px;
  .title {
    font-size: 18px;
    font-weight: bold;
    margin-bottom: 16px;
  }

  .tab-slide {
    .tabs {
      display: flex;
      gap: 12px;
      margin-bottom: 20px;

      button {
        background: #f2f2f2;
        border: none;
        padding: 8px 16px;
        border-radius: 6px;
        cursor: pointer;
        font-weight: 500;
        color: #333;

        &.active {
          background: #0055ff;
          color: #fff;
        }
      }
    }
  }
}
</style>

๐Ÿงฉ 5. TeacherPromoSection.vue์—์„œ Vuex ์—ฐ๋™

<template>
  <section class="teacher-promo-section">
    <h3 class="section-title">์ง€๊ธˆ ์ฃผ๋ชฉํ•  ์„ ์ƒ๋‹˜ ๊ธฐํš์ „</h3>
    <div class="grid">
      <TeacherPromoCard v-for="(card, i) in promoList" :key="i" v-bind="card" />
    </div>
  </section>
</template>

<script>
import { mapState, mapActions } from 'vuex';
import TeacherPromoCard from "./TeacherPromoCard.vue";

export default {
  components: { TeacherPromoCard },
  computed: {
    ...mapState('main', ['promoList']),
  },
  created() {
    this.fetchPromoList();
  },
  methods: {
    ...mapActions('main', ['fetchPromoList']),
  },
};
</script>

<style scoped>
.teacher-promo-section {
  padding: 0 20px;
  width: 790px;
}

.section-title {
  font-size: 20px;
  font-weight: bold;
  margin-bottom: 24px;
}

.grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px;
}
</style>

GitHub - heroyooi/vue2-megagong at ch9

๐ŸŽฏ ๊ณผ์ œ

  • StickyPanel.vue ์ปดํฌ๋„ŒํŠธ์˜ ๋ฐฐ๋„ˆ ๋ฐ์ดํ„ฐ๋ฅผ JSON ํŒŒ์ผ์—์„œ ๊ฐ€์ ธ์™€ Express API โ†’ Axios โ†’ Vuex๋กœ ์ƒํƒœ ๊ด€๋ฆฌ ๊ตฌ์กฐ๋ฅผ ๊ตฌํ˜„ํ•˜์„ธ์š”.
  • ์‹ค์ œ ์ถœ๋ ฅ๋˜๋Š” ์ด๋ฏธ์ง€ 2~3๊ฐœ๋กœ ํ…Œ์ŠคํŠธํ•˜๊ณ , ์„œ๋ฒ„/ํ”„๋ก ํŠธ ๋‘˜ ๋‹ค ์ •์ƒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.

๐Ÿ’ฌ ๋Œ“๊ธ€

    โ€ป ๋กœ๊ทธ์ธ ํ›„ ๋Œ“๊ธ€์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.