Syw.Frontend

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

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

1-15. Vue ์•ฑ์—์„œ ๋กœ๊ทธ์ธ UI ๊ตฌ์„ฑ ๋ฐ JWT ์ธ์ฆ ์ ์šฉ

โœ… 1. ํด๋” ๊ตฌ์กฐ

src/
โ”œโ”€โ”€ views/
โ”‚   โ”œโ”€โ”€ Login.vue
โ”‚   โ””โ”€โ”€ Register.vue
โ”œโ”€โ”€ components/
โ”‚   โ””โ”€โ”€ TodoApp.vue
โ”œโ”€โ”€ App.vue
โ”œโ”€โ”€ main.js
โ””โ”€โ”€ router.js   โ† ์ƒˆ๋กœ ์ถ”๊ฐ€

๐Ÿ”ง 2. vue-router ์„ค์น˜ ๋ฐ ์„ค์ •

npm install vue-router@3

๐Ÿ“„ src/router.js

import Vue from 'vue';
import VueRouter from 'vue-router';
import Login from './views/Login.vue';
import Register from './views/Register.vue';
import TodoApp from './components/TodoApp.vue';

Vue.use(VueRouter);

const routes = [
  { path: '/login', component: Login },
  { path: '/register', component: Register },
  {
    path: '/todos',
    component: TodoApp,
    meta: { requiresAuth: true },
  },
  { path: '*', redirect: '/login' },
];

const router = new VueRouter({ mode: 'history', routes });

// โœ… ์ธ์ฆ ์—ฌ๋ถ€ ์ฒดํฌ
router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('token');
  if (to.matched.some((r) => r.meta.requiresAuth) && !token) {
    next('/login');
  } else {
    next();
  }
});

export default router;

๐Ÿ›  3. main.js์—์„œ ๋ผ์šฐํ„ฐ ์—ฐ๊ฒฐ

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import './assets/base.css';

Vue.config.productionTip = false;

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

๐Ÿ–ฅ 4. Login.vue โ€“ ๋กœ๊ทธ์ธ ์ปดํฌ๋„ŒํŠธ

<template>
  <div class="auth">
    <h2>๋กœ๊ทธ์ธ</h2>
    <form @submit.prevent="handleLogin">
      <input v-model="username" placeholder="ID" />
      <input v-model="password" type="password" placeholder="Password" />
      <button type="submit">๋กœ๊ทธ์ธ</button>
    </form>
    <router-link to="/register">ํšŒ์›๊ฐ€์ž…</router-link>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  name: 'LoginPage',
  data() {
    return { username: '', password: '' };
  },
  methods: {
    async handleLogin() {
      try {
        const res = await axios.post('/api/auth/login', {
          username: this.username,
          password: this.password,
        });
        localStorage.setItem('token', res.data.token);
        localStorage.setItem('username', res.data.username);
        this.$router.push('/todos');
      } catch (e) {
        alert('๋กœ๊ทธ์ธ ์‹คํŒจ');
      }
    },
  },
};
</script>

๐Ÿ–ฅ 5. Register.vue โ€“ ํšŒ์›๊ฐ€์ž… ์ปดํฌ๋„ŒํŠธ

<template>
  <div class="auth">
    <h2>ํšŒ์›๊ฐ€์ž…</h2>
    <form @submit.prevent="handleRegister">
      <input v-model="username" placeholder="ID" />
      <input v-model="password" type="password" placeholder="Password" />
      <button type="submit">ํšŒ์›๊ฐ€์ž…</button>
    </form>
    <router-link to="/login">๋กœ๊ทธ์ธ</router-link>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  name: 'RegisterPage',
  data() {
    return { username: '', password: '' };
  },
  methods: {
    async handleRegister() {
      try {
        await axios.post('/api/auth/register', {
          username: this.username,
          password: this.password,
        });
        alert('ํšŒ์›๊ฐ€์ž… ์™„๋ฃŒ! ๋กœ๊ทธ์ธ ํ•ด์ฃผ์„ธ์š”.');
        this.$router.push('/login');
      } catch (e) {
        alert('ํšŒ์›๊ฐ€์ž… ์‹คํŒจ');
      }
    },
  },
};
</script>

๐Ÿ” 6. Axios์— ํ† ํฐ ์ž๋™ ์ถ”๊ฐ€

๐Ÿ“„ main.js

import axios from 'axios';

axios.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

โœ… 7. /todos ์ ‘๊ทผ ์ œ์–ด ํ™•์ธ

  • ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ /todos ์ ‘๊ทผ ์‹œ ์ž๋™์œผ๋กœ /login์œผ๋กœ ์ด๋™
  • ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ /todos ์ •์ƒ ์ ‘๊ทผ ๊ฐ€๋Šฅ
  • ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ์ธ์ฆ ์œ ์ง€๋จ (๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€ ๊ธฐ๋ฐ˜)

โœ… 8. App.vue (๋„ค๋น„๊ฒŒ์ด์…˜ + ๋‹คํฌ๋ชจ๋“œ + ๋ผ์šฐํ„ฐ๋ทฐ + ๋กœ๊ทธ์•„์›ƒ)

<template>
  <div :class="['app-container', { dark: isDark }]">
    <header class="nav-bar">
      <div class="left">
        <router-link to="/todos">ํ•  ์ผ</router-link>
        <router-link to="/login">๋กœ๊ทธ์ธ</router-link>
        <router-link to="/register">ํšŒ์›๊ฐ€์ž…</router-link>
      </div>

      <div class="right">
        <button @click="toggleDark">
          {{ isDark ? 'โ˜€๏ธ ๋ฐ์€๋ชจ๋“œ' : '๐ŸŒ™ ๋‹คํฌ๋ชจ๋“œ' }}
        </button>
        <button v-if="isLoggedIn" @click="logout">๐Ÿ”’ ๋กœ๊ทธ์•„์›ƒ</button>
      </div>
    </header>

    <!-- ๋ผ์šฐํŒ…์— ๋”ฐ๋ผ ํŽ˜์ด์ง€ ๋ถ„๊ธฐ -->
    <router-view />
  </div>
</template>

<script>
export default {
  data() {
    return {
      isDark: false,
      isLoggedIn: !!localStorage.getItem('token'),
    };
  },
  created() {
    const saved = localStorage.getItem('theme');
    this.isDark = saved === 'dark';
  },
  methods: {
    toggleDark() {
      this.isDark = !this.isDark;
      localStorage.setItem('theme', this.isDark ? 'dark' : 'light');
    },
    logout() {
      localStorage.removeItem('token');
      this.isLoggedIn = false;
      this.$router.push('/login');
    },
  },
  watch: {
    // ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ์—๋„ ๊ฐฑ์‹ ๋  ์ˆ˜ ์žˆ๋„๋ก localStorage ๊ฐ์‹œ
    $route() {
      this.isLoggedIn = !!localStorage.getItem('token');
    },
  },
};
</script>

<style lang="scss" scoped>
.app-container {
  min-height: 100vh;
  background-color: #fff;
  color: #222;
  transition: all 0.3s;

  &.dark {
    background-color: #1e1e1e;
    color: #f1f1f1;

    a {
      color: #f1f1f1;
    }
  }
}

.nav-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 16px;
  border-bottom: 1px solid #ddd;
  background: #f5f5f5;

  .left a {
    margin-right: 16px;
    text-decoration: none;
    color: #333;
  }

  .right button {
    margin-left: 8px;
    padding: 6px 12px;
    border-radius: 4px;
    border: 1px solid #aaa;
    cursor: pointer;
    background: none;
  }
}

.dark .nav-bar {
  background: #2a2a2a;
  border-color: #444;

  .left a {
    color: #f1f1f1;
  }

  .right button {
    color: #f1f1f1;
    border-color: #666;
  }
}
</style>

BE ์ €์žฅ์†Œ

GitHub - heroyooi/vue2-app at ch15

๐Ÿ’ฌ ๋Œ“๊ธ€

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