Syw.Frontend

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

1๋‹จ๊ณ„. Vue 2 + Express + Axios ์ธ์ฆ ๊ธฐ๋ฐ˜ ํˆฌ๋‘์•ฑ ๋งŒ๋“ค๊ธฐ

1-21. Vuex ๋ชจ๋“ˆํ™” ๊ตฌ์กฐ๋กœ ๋ฆฌํŒฉํ† ๋งํ•˜๊ธฐ

๐ŸŽฏ ํ•™์Šต ๋ชฉํ‘œ

  • ๊ธฐ์กด Vuex store๋ฅผ auth, theme ๋ชจ๋“ˆ๋กœ ๋‚˜๋ˆˆ๋‹ค
  • ๊ฐ ๋ชจ๋“ˆ์—์„œ state, mutations, actions, getters๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค
  • ๋ชจ๋“ˆ ๊ตฌ์กฐ๋ฅผ ํ†ตํ•ด ํ™•์žฅ์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๋†’์ธ๋‹ค

โœ… 1๋‹จ๊ณ„: ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ ๋งŒ๋“ค๊ธฐ

src/
โ””โ”€โ”€ store/
    โ”œโ”€โ”€ index.js       โ† ๋ฃจํŠธ ์Šคํ† ์–ด
    โ”œโ”€โ”€ modules/
    โ”‚   โ”œโ”€โ”€ auth.js     โ† ๋กœ๊ทธ์ธ/์‚ฌ์šฉ์ž ๊ด€๋ จ ์ƒํƒœ
    โ”‚   โ””โ”€โ”€ theme.js    โ† ๋‹คํฌ๋ชจ๋“œ ๊ด€๋ จ ์ƒํƒœ

โœ… 2๋‹จ๊ณ„: auth.js ๋ชจ๋“ˆ ์ƒ์„ฑ

// store/modules/auth.js
import { isTokenExpired } from '@/utils/jwt';

const state = {
  isLoggedIn: false,
  username: localStorage.getItem('username') || '',
};

const mutations = {
  login(state, token) {
    localStorage.setItem('token', token);
    state.isLoggedIn = true;
  },
  setUsername(state, name) {
    state.username = name;
    localStorage.setItem('username', name);
  },
  logout(state) {
    localStorage.removeItem('token');
    localStorage.removeItem('username');
    state.isLoggedIn = false;
    state.username = '';
  },
  syncLoginState(state) {
    const token = localStorage.getItem('token');
    const name = localStorage.getItem('username');
    if (!token || isTokenExpired(token)) {
      localStorage.removeItem('token');
      localStorage.removeItem('username');
      state.isLoggedIn = false;
      state.username = '';
    } else {
      state.isLoggedIn = true;
      state.username = name || '';
    }
  },
};

export default {
  namespaced: true,
  state,
  mutations,
};

โœ… 3๋‹จ๊ณ„: theme.js ๋ชจ๋“ˆ ์ƒ์„ฑ

// store/modules/theme.js

const state = {
  isDark: localStorage.getItem('theme') === 'dark',
};

const mutations = {
  toggleDark(state) {
    state.isDark = !state.isDark;
    localStorage.setItem('theme', state.isDark ? 'dark' : 'light');
  },
};

export default {
  namespaced: true,
  state,
  mutations,
};

โœ… 4๋‹จ๊ณ„: ๋ฃจํŠธ ์Šคํ† ์–ด index.js์—์„œ ๋ชจ๋“ˆ ๋“ฑ๋ก

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

import auth from './modules/auth';
import theme from './modules/theme';

Vue.use(Vuex);

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

โœ… 5๋‹จ๊ณ„: ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹ ๋ณ€๊ฒฝ

๋ชจ๋“ˆ์„ namespaced: true๋กœ ๋“ฑ๋กํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ ‘๊ทผ ๋ฐฉ์‹์ด ๋ฐ”๋€๋‹ˆ๋‹ค.

App.vue

import { mapState, mapMutations } from 'vuex';

export default {
  computed: {
    ...mapState('auth', ['isLoggedIn', 'username']),
    ...mapState('theme', ['isDark']),
  },
  created() {
    this.syncLoginState();
  },
  methods: {
    ...mapMutations('auth', ['logout', 'syncLoginState']),
    ...mapMutations('theme', ['toggleDark']),
    handleLogout() {
      this.logout();
      this.$router.push('/login');
    },
  },
};

๐Ÿ” ๋ชจ๋“ˆ ์ด๋ฆ„ ('auth', 'theme')์„ ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ ๋ฐ˜๋“œ์‹œ ๋„ฃ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค!

๐Ÿ”ง 1. data() ์ œ๊ฑฐ

data() {
  return {}; // ๋˜๋Š” ์ƒ๋žต
},

๐Ÿ”ง 2. watch: $route() ์ œ๊ฑฐ

Vuex๊ฐ€ ์ „์—ญ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋ฏ€๋กœ route ๋ณ€๊ฒฝ ์‹œ ๋ณ„๋„๋กœ localStorage์—์„œ ์ฝ์–ด์˜ฌ ํ•„์š” ์—†์Œ.

๐Ÿ” 3. ์ตœ์ข… ๋ฆฌํŒฉํ† ๋ง๋œ ์Šคํฌ๋ฆฝํŠธ ๋ถ€๋ถ„

<script>
import { mapState, mapMutations } from 'vuex';
export default {
  computed: {
    ...mapState('auth', ['isLoggedIn', 'username']),
    ...mapState('theme', ['isDark']),
  },
  created() {
    this.syncLoginState(); // ์ƒˆ๋กœ๊ณ ์นจ ์‹œ ๋กœ๊ทธ์ธ ์ƒํƒœ ๋ณต์›
  },
  methods: {
    ...mapMutations('auth', ['logout', 'syncLoginState']),
    ...mapMutations('theme', ['toggleDark']),
    handleLogout() {
      this.logout();
      this.$router.push('/login');
    },
  },
};
</script>

โœ… 6๋‹จ๊ณ„: Login.vue๋„ ์ˆ˜์ •

this.$store.commit('auth/login', res.data.token);
this.$store.commit('auth/setUsername', res.data.username);

โœ… 7๋‹จ๊ณ„: router.js ์ˆ˜์ •

import store from './store';

// ๊ธฐ์กด๊ณผ ๋™์ผ

router.beforeEach((to, from, next) => {
  store.commit('auth/syncLoginState');

  const isAuthRequired = to.matched.some((record) => record.meta.requiresAuth);
  const isLoggedIn = store.state.auth.isLoggedIn;

  if (isAuthRequired && !isLoggedIn) {
    if (to.path !== '/login') {
      next('/login');
    } else {
      next();
    }
  } else if ((to.path === '/login' || to.path === '/register') && isLoggedIn) {
    if (from.path !== '/todos') {
      next('/todos');
    } else {
      next(false); // ํ˜„์žฌ ํŽ˜์ด์ง€์—์„œ stay
    }
  } else {
    next(); // ์ •์ƒ ๋ผ์šฐํŒ…
  }
});
  • router.beforeEach์—์„œ ์ •ํ™•ํ•œ ์กฐ๊ฑด ๋ถ„๊ธฐ + next()๋ฅผ ์ค‘๋ณต ํ˜ธ์ถœํ•˜์ง€ ์•Š๋„๋ก ์กฐ์ •

๐Ÿ“ฆ ์š”์•ฝ

์ด ๊ฐ•์˜์—์„œ๋Š” Vuex ์Šคํ† ์–ด๋ฅผ auth, theme ๋ชจ๋“ˆ๋กœ ๋‚˜๋ˆ„์–ด ์ „์—ญ ์ƒํƒœ๋ฅผ ์—ญํ• ๋ณ„๋กœ ๋ถ„๋ฆฌํ–ˆ๋‹ค. ๋ชจ๋“ˆํ™”๋ฅผ ํ†ตํ•ด ๋” ๊ตฌ์กฐ์ ์ด๊ณ  ์œ ์ง€๋ณด์ˆ˜์— ๊ฐ•ํ•œ ์ƒํƒœ ๊ด€๋ฆฌ ์ฒด๊ณ„๋ฅผ ๊ตฌ์ถ•ํ–ˆ๋‹ค.

FE ์ €์žฅ์†Œ

GitHub - heroyooi/vue2-app at ch21

๐Ÿ’ฌ ๋Œ“๊ธ€

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