๐ฏ ํ์ต ๋ชฉํ
- 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>
โ ์์ ํฌ์ธํธ
- โ
token
โthis.token
์ผ๋ก ๋ณ๊ฒฝ - โ
๋ชจ๋ Axios ์์ฒญ์
Authorization: Bearer ${this.token}
์ถ๊ฐ - โ
userId
๋ ๋ ์ด์ ์ฌ์ฉํ์ง ์์ผ๋ฏ๋ก ์ ๊ฑฐ (์๋ฒ๋ JWT์์ ์ ์ ์ ๋ณด ์ถ์ถ)
๐งช ํ ์คํธ ์ฒดํฌ๋ฆฌ์คํธ
ํญ๋ชฉ | ๊ฒฐ๊ณผ |
---|---|
๋ก๊ทธ์ธ ํ ํ ํฐ์ด localStorage ์ ์ ์ฅ๋จ |
โ |
ํฌ๋ ๋ฑ๋ก/์กฐํ ์ ํ ํฐ์ด ํฌํจ๋์ด ์์ฒญ๋จ | โ |
๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ง๋ค ๋ณ๋ ํฌ๋ ๊ด๋ฆฌ๋จ | โ |
๋ค๋ฅธ ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ ์ ๊ทผ ๋ถ๊ฐ | โ |
๐ฆ ์์ฝ
- ์ฌ์ฉ์ ๋ก๊ทธ์ธ ์ ๋ณด๋ฅผ ํ ํฐ์ ๋ด๊ณ ์๋ฒ์์ ์์ ํ๊ฒ ๊บผ๋ด ์ฌ์ฉํ๋ ๊ตฌ์กฐ๋ฅผ ์์ฑํ๋ค.
- ์ด์ ์ฌ์ฉ์๋ณ๋ก Todo ๋ฐ์ดํฐ๊ฐ ๋ถ๋ฆฌ๋๊ณ , ์ธ์ฆ๋ ์ฌ์ฉ์๋ง ์์ ์ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃฐ ์ ์๋ค.
FE ์ ์ฅ์
GitHub - heroyooi/vue2-app at ch16
BE ์ ์ฅ์
๐ฌ ๋๊ธ
โป ๋ก๊ทธ์ธ ํ ๋๊ธ์ ์์ฑํ ์ ์์ต๋๋ค.