ToDo List vue3

๊ฐ๋จํ CRUD ๊ธฐ๋ฅ์ ToDo-List๋ฅผ ๋ง๋ค์๋ค!
ํ
์คํธ ๋ชฉ์ ์ธ์ง๋ผ UI์ค์ ์ ์ด ์๋๋ผ.. ๋ง์์ ์๋ค์ง๋ง ์ด์จ๋ !
์ผ๋จ Composable์ ํ ์ผ๋ก ๋นผ๋์ useTodo.js๋ฅผ ๋ณด์!
src/composable/useTodo.js
import { onMounted, ref } from 'vue';
function useTodo(){
const itemList = ref([]);
const selectedItem = ref({});
function fetchList(){
const result = [];
for(let i=0; i<localStorage.length; i++){
const key = localStorage.key(i);
const todoItem = localStorage.getItem(key);
try {
const parsedItem = JSON.parse(todoItem);
result.push(parsedItem); // ๊ฐ์ฒด๋ก ๋ณํํ์ฌ ๋ฐฐ์ด์ ์ถ๊ฐ
} catch (error) {
console.error("Invalid JSON format for key:", key, todoItem);
}
}
itemList.value = result;
}
function newItem(item){
const idx = crypto.randomUUID();
const newItemAbout = {
idx: idx,
content: item,
completed: false,
}
localStorage.setItem(idx, JSON.stringify(newItemAbout));
itemList.value.push(newItemAbout);
}
function removeTodo(item, i){
itemList.value.splice(i, 1);
localStorage.removeItem(item.idx);
}
function modifyTodo(item){
// Object.assign(selectedItem.value, item);
selectedItem.value = {...item};
}
function toggleCompleted(item){
localStorage.setItem(item.idx, JSON.stringify(item));
}
function changeTodo(item){
const index = itemList.value.findIndex(e => e.idx === item.idx);
if(index !== -1){
itemList.value[index] = {...item};
localStorage.setItem(item.idx, JSON.stringify(item))
}
}
function onCancelTodo(){
selectedItem.value = '';
}
onMounted(() => {
fetchList();
})
return { newItem, itemList, removeTodo, modifyTodo, selectedItem, toggleCompleted, changeTodo, onCancelTodo }
}
export default useTodo;
ํ ์คํธ ํ์ผ ์์ฑ
์ฐ์ ํ ์คํธ ํ์ผ์ ์์ฑํด๋ณด์
src/composable/__tests__/useTodo.test.js๋ผ๋ ํ์ผ์ ์์ฑํฉ๋๋ค.
ํ
์คํธ ํ์ผ ์ด๋ฆ์ ๋ณดํต ํ์ผ๋ช
.test.js ํ์์ผ๋ก ์ง์ต๋๋ค.
import { nextTick } from 'vue';
import useTodo from '../useTodo';
describe('useTodo composable', () => {
let todo;
beforeEach(() => {
// ์ด๊ธฐํ
localStorage.clear();
todo = useTodo();
});
test('์ด๊ธฐ ๋ก๋: fetchList()', async () => {
// Arrange: ๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ํญ๋ชฉ ์ถ๊ฐ
const mockItem = { idx: '1', content: 'Test Todo', completed: false };
localStorage.setItem('1', JSON.stringify(mockItem));
// Act: fetchList ํธ์ถ
todo.fetchList();
// Assert: itemList๊ฐ ๋ก๋๋์๋์ง ํ์ธ
expect(todo.itemList.value).toEqual([mockItem]);
});
test('์๋ก์ด ํ ์ผ ์ถ๊ฐ: newItem()', async () => {
// Act: ์๋ก์ด ํ ์ผ์ ์ถ๊ฐ
todo.newItem('New Todo');
// Assert: itemList์ ํญ๋ชฉ์ด ์ถ๊ฐ๋์๋์ง ํ์ธ
expect(todo.itemList.value.length).toBe(1);
expect(todo.itemList.value[0].content).toBe('New Todo');
});
test('ํ ์ผ ์ญ์ : removeTodo()', () => {
// Arrange: ์ด๊ธฐ ๋ฐ์ดํฐ ์ถ๊ฐ
const mockItem = { idx: '1', content: 'Delete Me', completed: false };
todo.itemList.value.push(mockItem);
localStorage.setItem('1', JSON.stringify(mockItem));
// Act: ์ญ์ ์ํ
todo.removeTodo(mockItem, 0);
// Assert: itemList์ localStorage์์ ์ญ์ ๋์๋์ง ํ์ธ
expect(todo.itemList.value).toEqual([]);
expect(localStorage.getItem('1')).toBeNull();
});
test('ํ ์ผ ์๋ฃ ํ ๊ธ: toggleCompleted()', () => {
// Arrange: ์ด๊ธฐ ๋ฐ์ดํฐ ์ถ๊ฐ
const mockItem = { idx: '1', content: 'Toggle Me', completed: false };
todo.itemList.value.push(mockItem);
localStorage.setItem('1', JSON.stringify(mockItem));
// Act: ์๋ฃ ์ํ ํ ๊ธ
mockItem.completed = true;
todo.toggleCompleted(mockItem);
// Assert: localStorage์ ์
๋ฐ์ดํธ๋์๋์ง ํ์ธ
const updatedItem = JSON.parse(localStorage.getItem('1'));
expect(updatedItem.completed).toBe(true);
});
test('ํ ์ผ ์์ : changeTodo()', () => {
// Arrange: ์ด๊ธฐ ๋ฐ์ดํฐ ์ถ๊ฐ
const mockItem = { idx: '1', content: 'Old Content', completed: false };
todo.itemList.value.push(mockItem);
localStorage.setItem('1', JSON.stringify(mockItem));
const updatedItem = { ...mockItem, content: 'Updated Content' };
// Act: ์์ ์ํ
todo.changeTodo(updatedItem);
// Assert: itemList์ localStorage๊ฐ ์
๋ฐ์ดํธ๋์๋์ง ํ์ธ
expect(todo.itemList.value[0].content).toBe('Updated Content');
const storedItem = JSON.parse(localStorage.getItem('1'));
expect(storedItem.content).toBe('Updated Content');
});
test('์ ํ ์ทจ์: onCancelTodo()', () => {
// Arrange: ์ ํ๋ ํญ๋ชฉ ์ถ๊ฐ
const mockItem = { idx: '1', content: 'Selected', completed: false };
todo.selectedItem.value = mockItem;
// Act: ์ ํ ์ทจ์
todo.onCancelTodo();
// Assert: selectedItem์ด ์ด๊ธฐํ๋์๋์ง ํ์ธ
expect(todo.selectedItem.value).toBe('');
});
});
jest ๋ฌธ๋ฒ์ผ๋ก ์์ฑ์ ์๋ฃ ํ!
npm run test
ํ
์คํธ ์คํ!

์..! localStorage is not defined" ์๋ฌ๋ฐ์..
ํ
์คํธ๊ฐ ์คํจํ๋ ์ด์ ๋ Jest๊ฐ ์คํ๋๋ ํ๊ฒฝ์์ localStorage๊ฐ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณต๋์ง ์๊ธฐ ๋๋ฌธ์
๋๋ค.
localStorage๋ ๋ธ๋ผ์ฐ์ ํ๊ฒฝ์์๋ง ์ฌ์ฉํ ์ ์๋๋ฐ, Jest๋ Node.js ํ๊ฒฝ์์ ์คํ๋๋ฏ๋ก localStorage๋ฅผ ์ง์ํ์ง ์์ต๋๋ค.
Jest ํ
์คํธ ํ๊ฒฝ์์๋ localStorage์ฒ๋ผ ๋์ํ๋ Mock(๊ฐ์ง ๊ตฌํ)์ ๋ง๋ค์ด์ผ ํฉ๋๋ค.
์ด์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํ ๋จ๊ณ๋ฅผ ์ฐจ๊ทผ์ฐจ๊ทผ ์งํํด ๋ณด๊ฒ ์ต๋๋ค.
global.localStorage = {
store: {},
getItem(key) {
return this.store[key] || null;
},
setItem(key, value) {
this.store[key] = String(value);
},
removeItem(key) {
delete this.store[key];
},
clear() {
this.store = {};
},
};
module.exports = {
preset: '@vue/vue3-jest',
transform: {
'^.+\\.vue$': '@vue/vue3-jest',
'^.+\\.js$': 'babel-jest',
},
testEnvironment: 'jest-environment-jsdom', // ํ
์คํธ ํ๊ฒฝ ์ค์
setupFilesAfterEnv: ['./jest.setup.js'],
};
.... ์ฌ์ ํ ๋ฐ์. ์๋ง๋ Jest๊ฐ ์ด ์ค์ ์ ์ ๋๋ก ์ฝ์ง ๋ชปํ๊ฑฐ๋, localStorage๊ฐ ํ
์คํธ ํ์ผ์ ์ ๋๋ก ์ ์ฉ๋์ง ์๋๋ฏ ํฉ๋๋ค..
jest.setup.js์ ์ถ๊ฐ๋ localStorage Mock์ด ์ ์ ์ฉ๋์ง ์์๋ค๋ฉด, ํ
์คํธ ํ์ผ์ ์ง์ Mock์ ์ถ๊ฐํด ๋ณด๊ฒ ์ต๋๋ค.
์ ์ฝ๋๋ฅผ ์๋จ๋ถ์ ์ง์ ์ถ๊ฐ!
npm run test

์ ์์ ์ผ๋ก ์๋ํ์ง๋ง.. fetchList()์ crypto๋ฅผ ์ธ์ํ์ง ๋ชปํ๋ ๋ฌธ์ ๊ฐ ๋ฐ์..!
useTodo.js
function useTodo(){
...
return { newItem, itemList, removeTodo, modifyTodo, selectedItem, toggleCompleted, changeTodo, onCancelTodo, fetchList }
}
// fetchList๋ฅผ ๋ฃ์ง์์ ์๋ฌ!.. ๋ด๋ถ์ ์ผ๋ก ์ฌ์ฉํ๋๊ฑฐ๋ผ ์ถ๊ฐํ์ง ์์๋ ๋๋ค. ์๊ฐํ๋๋ฐ.. ํ
์คํธ์์ ํ์ํ๋ค๋๊ฒ์ ๊ฐ๊ณผ!
crypto.randomUUID๋ Node.js๋ ๋ธ๋ผ์ฐ์ ์์๋ง ์ฌ์ฉ ๊ฐ๋ฅํ ์ต์ API ์ค ํ๋์
๋๋ค. Jest ํ
์คํธ ํ๊ฒฝ์ธ jest-environment-jsdom์ ์ด API๋ฅผ ์ง์ํ์ง ์์ผ๋ฏ๋ก ์๋ฌ๊ฐ ๋ฐ์ํฉ๋๋ค.
UUID ์์ฑ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ
1) uuid๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น:
npm install uuid
2) useTodo.js์์ uuid์ฌ์ฉ:
import { v4 as uuidv4 } from 'uuid';
...
function useTodo() {
function newItem(item) {
const idx = uuidv4(); // crypto.randomUUID ๋์ ์ฌ์ฉ
...

์์!! ์ฑ๊ณต! ๐