Syw.Frontend

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

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

1-16. JWT ๊ธฐ๋ฐ˜ ์‚ฌ์šฉ์ž๋ณ„ Todo ๊ด€๋ฆฌ ์™„์„ฑํ•˜๊ธฐ

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

  • JWT ์ธ์ฆ ํ† ํฐ์„ ํ™œ์šฉํ•ด ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋งŒ ์ž์‹ ์˜ Todo๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ๋“ฑ๋ก/์ˆ˜์ •/์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ•œ๋‹ค.
  • ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” ํ† ํฐ๋งŒ ๋ณด๋‚ด๊ณ , ์„œ๋ฒ„๋Š” ๊ทธ ํ† ํฐ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ž๋™์œผ๋กœ ์ถ”์ถœํ•œ๋‹ค.

๐Ÿ“ 1. ์„œ๋ฒ„ ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด ์ƒ์„ฑ

๐Ÿ”ง middlewares/auth.js

const jwt = require('jsonwebtoken');
const SECRET = 'my-vue-jwt-secret';

module.exports = function (req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader) return res.status(401).json({ message: '์ธ์ฆ ํ† ํฐ์ด ์—†์Šต๋‹ˆ๋‹ค.' });

  const token = authHeader.split(' ')[1];
  try {
    const decoded = jwt.verify(token, SECRET); // { username }
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(403).json({ message: '์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค.' });
  }
};

๐Ÿ›  2. routes/todos.js ์ˆ˜์ • โ€“ ์‚ฌ์šฉ์ž๋ณ„ Todo ์ฒ˜๋ฆฌ

โœ… ๋ณ€๊ฒฝ ํ•ต์‹ฌ

  • req.user.username์œผ๋กœ ์‚ฌ์šฉ์ž ๊ตฌ๋ถ„
  • ํด๋ผ์ด์–ธํŠธ์—์„œ userId ๋”ฐ๋กœ ๋ณด๋‚ผ ํ•„์š” ์—†์Œ

๐Ÿ” ์ˆ˜์ •๋œ routes/todos.js

const express = require('express');
const fs = require('fs').promises;
const path = require('path');
const auth = require('../middlewares/auth');

const router = express.Router();
const DATA_FILE = path.join(__dirname, '../data/todos.json');

let todosByUser = {};

const loadTodos = async () => {
  try {
    const data = await fs.readFile(DATA_FILE, 'utf-8');
    todosByUser = JSON.parse(data);
  } catch {
    todosByUser = {};
  }
};

const saveTodos = async () => {
  await fs.writeFile(DATA_FILE, JSON.stringify(todosByUser, null, 2));
};

loadTodos();

// โœ… ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด ์ „์—ญ ์ ์šฉ
router.use(auth);

// โœ… GET /todos
router.get('/', (req, res) => {
  const username = req.user.username;
  res.json(todosByUser[username] || []);
});

// โœ… POST /todos
router.post('/', async (req, res) => {
  const { title, completed = false } = req.body;
  const username = req.user.username;

  if (!title) return res.status(400).json({ message: 'ํ•  ์ผ์„ ์ž…๋ ฅํ•˜์„ธ์š”.' });

  const newTodo = {
    id: Date.now(),
    title,
    completed,
  };

  if (!todosByUser[username]) {
    todosByUser[username] = [];
  }

  todosByUser[username].unshift(newTodo);
  await saveTodos();
  res.status(201).json(newTodo);
});

// โœ… PUT /todos/:id
router.put('/:id', async (req, res) => {
  const username = req.user.username;
  const id = Number(req.params.id);
  const { title, completed } = req.body;

  if (!todosByUser[username]) return res.sendStatus(404);

  todosByUser[username] = todosByUser[username].map((todo) =>
    todo.id === id
      ? {
          ...todo,
          ...(title !== undefined && { title }),
          ...(completed !== undefined && { completed }),
        }
      : todo
  );

  await saveTodos();
  res.sendStatus(200);
});

// โœ… DELETE /todos/:id
router.delete('/:id', async (req, res) => {
  const username = req.user.username;
  const id = Number(req.params.id);

  if (!todosByUser[username]) return res.sendStatus(404);

  todosByUser[username] = todosByUser[username].filter((todo) => todo.id !== id);
  await saveTodos();
  res.sendStatus(200);
});

module.exports = router;

๐ŸŒ 3. server.js๋Š” ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ

app.use('/todos', todosRouter); // ์ธ์ฆ์€ ๋‚ด๋ถ€์—์„œ ์ฒ˜๋ฆฌ๋จ
app.use('/auth', authRouter);   // ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ์€ ์ธ์ฆ ๋ถˆํ•„์š”

โœ… 4. src\components\TodoApp.vue ์ˆ˜์ •

โœ… token ์ •์˜ ์œ„์น˜

data() {
  return {
    newTodo: '',
    todos: [],
    editingId: null,
    token: localStorage.getItem('token'), // โœ… ์—ฌ๊ธฐ์—์„œ ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ
  };
},

โœ… Axios ์š”์ฒญ ์‹œ ํ—ค๋”์— ํ† ํฐ ํฌํ•จํ•˜๋Š” ๋ฐฉ๋ฒ•

axios.get, axios.post ๋“ฑ ๋ชจ๋“  ์š”์ฒญ์— ์•„๋ž˜์ฒ˜๋Ÿผ headers ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค:

const config = {
  headers: {
    Authorization: `Bearer ${this.token}`,
  },
};
<script>
import axios from 'axios';
import TodoItem from './TodoItem.vue';

const API_URL = '/api/todos';

export default {
  components: { TodoItem },
  data() {
    return {
      newTodo: '',
      todos: [],
      editingId: null,
      token: localStorage.getItem('token'), // โœ… token ์ €์žฅ
    };
  },
  mounted() {
    this.fetchTodos();
  },
  methods: {
    // โœ… ๊ณตํ†ต ํ—ค๋”
    authHeader() {
      return {
        headers: {
          Authorization: `Bearer ${this.token}`,
        },
      };
    },

    async fetchTodos() {
      try {
        const res = await axios.get(API_URL, this.authHeader());
        this.todos = res.data;
      } catch (err) {
        console.error('ํ•  ์ผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์‹คํŒจ:', err);
      }
    },

    async addTodo() {
      const title = this.newTodo.trim();
      if (!title) return;
      try {
        const res = await axios.post(
          API_URL,
          { title, completed: false },
          this.authHeader()
        );
        this.todos.unshift(res.data);
        this.newTodo = '';
      } catch (err) {
        console.error('ํ•  ์ผ ์ถ”๊ฐ€ ์‹คํŒจ:', err);
      }
    },

    async removeTodo(id) {
      try {
        await axios.delete(`${API_URL}/${id}`, this.authHeader());
        this.todos = this.todos.filter((todo) => todo.id !== id);
      } catch (err) {
        console.error('์‚ญ์ œ ์‹คํŒจ:', err);
      }
    },

    startEdit(id) {
      this.editingId = id;
    },

    async updateTodo(id, newTitle) {
      if (newTitle === null) {
        this.editingId = null;
        return;
      }
      const title = newTitle.trim();
      if (!title) return;
      try {
        await axios.put(`${API_URL}/${id}`, { title }, this.authHeader());
        this.todos = this.todos.map((todo) =>
          todo.id === id ? { ...todo, title } : todo
        );
        this.editingId = null;
      } catch (err) {
        console.error('์ˆ˜์ • ์‹คํŒจ:', err);
      }
    },

    async toggleComplete(id) {
      const target = this.todos.find((todo) => todo.id === id);
      if (!target) return;
      const updated = { ...target, completed: !target.completed };
      try {
        await axios.put(
          `${API_URL}/${id}`,
          { completed: updated.completed },
          this.authHeader()
        );
        this.todos = this.todos.map((todo) =>
          todo.id === id ? updated : todo
        );
      } catch (err) {
        console.error('์ฒดํฌ ์ƒํƒœ ๋ณ€๊ฒฝ ์‹คํŒจ:', err);
      }
    },
  },
};
</script>

โœ… ์ˆ˜์ • ํฌ์ธํŠธ

  1. โœ… token โ†’ this.token ์œผ๋กœ ๋ณ€๊ฒฝ
  2. โœ… ๋ชจ๋“  Axios ์š”์ฒญ์— Authorization: Bearer ${this.token} ์ถ”๊ฐ€
  3. โœ… userId๋Š” ๋” ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ œ๊ฑฐ (์„œ๋ฒ„๋Š” JWT์—์„œ ์œ ์ €์ •๋ณด ์ถ”์ถœ)

๐Ÿงช ํ…Œ์ŠคํŠธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

ํ•ญ๋ชฉ ๊ฒฐ๊ณผ
๋กœ๊ทธ์ธ ํ›„ ํ† ํฐ์ด localStorage์— ์ €์žฅ๋จ โœ…
ํˆฌ๋‘ ๋“ฑ๋ก/์กฐํšŒ ์‹œ ํ† ํฐ์ด ํฌํ•จ๋˜์–ด ์š”์ฒญ๋จ โœ…
๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋งˆ๋‹ค ๋ณ„๋„ ํˆฌ๋‘ ๊ด€๋ฆฌ๋จ โœ…
๋‹ค๋ฅธ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋Š” ์ ‘๊ทผ ๋ถˆ๊ฐ€ โœ…

๐Ÿ“ฆ ์š”์•ฝ

  • ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์ •๋ณด๋ฅผ ํ† ํฐ์— ๋‹ด๊ณ  ์„œ๋ฒ„์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ๊บผ๋‚ด ์‚ฌ์šฉํ•˜๋Š” ๊ตฌ์กฐ๋ฅผ ์™„์„ฑํ–ˆ๋‹ค.
  • ์ด์ œ ์‚ฌ์šฉ์ž๋ณ„๋กœ Todo ๋ฐ์ดํ„ฐ๊ฐ€ ๋ถ„๋ฆฌ๋˜๊ณ , ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๋‹ค.

FE ์ €์žฅ์†Œ

GitHub - heroyooi/vue2-app at ch16

BE ์ €์žฅ์†Œ

GitHub - heroyooi/vue2-api at ch16

๐Ÿ’ฌ ๋Œ“๊ธ€

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