Vue 3 버전을 학습하기 위해서 가장 간단하면서도, 몇 번 만들어 보았던 TodoApp 을 다시 한 번 만들어 보기로 했습니다.
목차
1 - 결과물 미리보기
2 - 학습 내용
2-1. 프로젝트 생성 및 확인
2-2. 필터와 localStorage 저장기능 구현
2-3. 컴포넌트 구현
- 글을 마치며
TodoApp 결과물
구현 기술
Vue 3
Vite 2
[Vue) Vite?, 왜 사용할까?
Vite란? Vue3버전으로 개발을 함에 있어 별다른 번들 생성 없이 ES 모듈을 바로 웹 브라우저에 렌더링 할 수 있도록 만든 개발 툴입니다. 왜 Vite 를 사용하나요? Vite 의 등장배경 👓 현재 Webpack, Rollup
jongik.tistory.com](https://jongik.tistory.com/11)
- Bootstrap 5
구현한 페이지부터 짧게 살펴보겠습니다.
- 할 일을 추가하면 하단의 '해야 할 작업들' 리스트에 추가됩니다.
- 체크박스나 할일관리를 통해 할일 완료 상태로 바꿀 수 있고, 할일 관리를 통해 해당 아이템의 삭제가 가능합니다.
- 필터를 이용해 해야 할 작업, 모든 작업, 완료한 작업, 오늘의 작업 별로 분류해서 볼 수 있습니다.
- 가장 하단의 '미완료작업' 리스트는 할 일을 추가할 때 등록한 날짜가 오늘 날짜보다 과거가 될 경우에 해당 항목이 추가됩니다.
학습 내용
1. Vite2 로 Vue 3 프로젝트 생성하기 (기초 설정)
- vite2 를 통해서 프로젝트를 생성합니다. ( 저는 Vite 2 버전을 이용했으므로 vite 1 버전을 사용하시는 분들과는 차이가 있을 수 있습니다. )
npm init @vitejs/app '프로젝트명'
# 다음 나오는 선택사항에서 사용 프레임워크 vue 선택
# 저는 타입스크립트를 적용하지 않았기에 vue 를 선택했습니다.
cd '프로젝트명'
npm install
- Bootstrap 5 설치
npm install bootstrap@next
# 특정 버전을 정의하지 않으면 최소한 Bootstrap 5 Beta 이상의 버전이 설치됩니다.
# npm install @popperjs/core
폴더 구조
- vite2로 vue 프로젝트를 생성하게 되면 아래와 같은 폴더와 파일들이 생성되고 vite.config.js 파일도 자동으로 생성됩니다.
- components 폴더 안에 구현할 컴포넌트들을, compositions 폴더에는 모듈들을 생성했습니다.
2. 필터 기능
- compositions 폴더안에는 로컬스토리지 저장을 위한 storage.js 파일과 리스트 필터 기능을 위한 filters.js 파일이 있습니다. 각각의 파일을 살펴보겠습니다.
storage.js
- 이번 프로젝트에서는 별도의 서버를 구현하지 않고 webStorage 를 이용해 데이터를 관리하는 방식을 사용했습니다.
// 변수에 반응성을 더해주기 위해 import 합니다.
// reactive 는 객체에 반응성을 더해주고 , toRefs 는 객체의 내부 속성들 모두에게 반응성을 더해줍니다.
import { reactive, toRefs } from "vue";
export const useStorage = () => {
const KEY = "todoItem"; // localStorage 에서 데이터를 저장할 KEY
const storage_obj = reactive({ storage_id: 0 }); // 리스트를 가질 todos 속성과 신규 id를 확인할 수 있는 storage_id 속성을 가진 객체
// localStorage 로부터 데이터를 불러오는 함수
const loadTodos = (initTodos) => {
// 인덱스 역할을 하는 id 를 다시 부여하는 것과 storage_id 에 배열의 길이를 저장하기 위해
// localStorage에 저장된 값을 불러와서 temp_todos 에 먼저 삽입한다
let temp_todos = JSON.parse(localStorage.getItem(KEY) || "[]");
temp_todos.forEach((todo, idx) => {
todo.id = idx;
});
/* localStorage 는 데이터를 UTF-15 DOMString 형식으로 저장하기 때문에 배열 형식의 값을 그냥 저장할 수 없다.
따라서 JOSN의 stringify를 이용해 값을 문자열 형식으로 변환해 저장하고,
불러올 때도 문자열 형식으로 불러온 값을 JSON.parse 를 이용해 객체로 변환해야한다. */
storage_obj.storage_id = temp_todos.length;
initTodos(temp_todos);
};
// localStorage 로 데이터를 저장하는 함수
const saveTodos = (todos) => {
localStorage.setItem(KEY, JSON.stringify(todos.value));
};
return {
...toRefs(storage_obj),
loadTodos,
saveTodos,
};
};
filters.js
- 날짜 순 내림차순 정렬
const dateSort = (a, b) => {
console.log("필터 : 날짜순으로 정렬");
const a_date = Date.parse(a.date);
const b_date = Date.parse(b.date);
if (a_date > b_date) return 1;
else if (a_date < b_date) return 0;
else return a.id - b.id;
};
- 날짜가 지났지만 완료 못한 작업
const getPendingTodos = (todos) => {
console.log("필터 : 오늘 해야 할 작업");
return todos.value
.filter((todo) => todo.date < today && !todo.completed)
// 작업의 날짜가 오늘보다 이전일 경우를 걸러냅니다.
.slice()
.sort(dateSort); // 위에서 작성한 dateSort 를 이용한다.
};
slice 함수 : 배열에서 필요한 값들만 잘라내 새로운 배열을 생성합니다.
복사본을 만드는 이유는 원본 데이터가 정렬되지 않은 상태로 놔두기 위해서입니다.
정렬이 되고나면 객체의 id 값이 배열의 인덱스와 달라지기 때문에 나중에 배열에서 데이터를 삭제할 때 id 를 인덱스라고 가정할 수 없게 됩니다.
- 오늘 해야 할 작업
const getActiveTodayTodos = (todos) => {
console.log("필터 : 해야 할 작업");
return todos.value
.filter((todo) => todo.date == today && !todo.completed)
// 작업의 날짜가 오늘과 같을 경우를 걸러냅니다
.slice()
.sort(dateSort);
};
- 완료한 작업
const getCompletedTodayTodos = (todos) => {
console.log("필터 : 완료한 작업");
return todos.value
.filter((todo) => todo.date == today && todo.completed)
// 작업의 날짜가 오늘과 같으며, 상태가 true 값인 작업을 걸러냅니다.
.slice()
.sort(dateSort);
};
- 오늘의 모든 기록 ( 추가, 완료 포함 )
const getAllTodayTodos = (todos) => {
console.log("필터 : 오늘의 모든 기록");
return getActiveTodayTodos(todos)
.concat(getCompletedTodayTodos(todos))
.slice().sort(dateSort);
};
- 모든 작업 ( 앞으로의 일정까지 모두 보여주기 )
const getAllTodos = (todos) => {
console.log("필터 : 모든 작업");
return todos.value.slice().sort(dateSort);
};
3. 컴포넌트 구현
컴포넌트의 구조를 그림으로 나타내면 다음과 같습니다.
- TodoListContainer 는 TodoApp 의 컴포넌트들을 관리하는 역할입니다.
- TodoListNew 에는 새로운 작업을 추가할 수 있는 UI 를 구현합니다.
- TodoListMain 는 실제 작업 목록을 관리하고 데이터를 TodoList 에 전달해주는 역할을 합니다.
- TodoListMenu 는 작업 목록에 필터를 걸어줄 수 있는 메뉴를 제공합니다.
- TodoList 는 조건에 해당하는 데이터를 보여주는 컴포넌트입니다.
TodoListContainer.vue
- template code
<todo-list-new />
<section class="container">
<div class="row justify-content-center m-2">
<todo-list-main/>
</div>
</section>
위에서 보여드린 구조와 같이 TodoListNew 컴포넌트와 TodoListMain 컴포넌트로 이루어져 있습니다.
- script code ( setup() 내부 )
TodoListNew.vue
- template code
// 설명을 위해 div 태그들은 생략했습니다.
<input
type="text"
id="todo_input"
v-model="job"
placeholder="할 일을 적어주세요"
>
<input
type="date"
v-model="date"
:min="today"
>
<button
type="button"
@click="onAddTodo"
>
추가하기
</button>
- script code
setup() {
const today = inject('today'); // App.vue 에서 설정해놓은 오늘 날짜
const addTodo = inject('addTodo');
const val_obj = reactive({
job: '',
date: today,
today: today,
})
const onAddTodo = () => {
if (val_obj.job.length > 0){
addTodo(val_obj.job, val_obj.date)
val_obj.job = '';
val_obj.date = today;
}
}
return {
...toRefs(val_obj),
onAddTodo,
}
},
Provide 가 뭔가요??
[Vue 3 ) Provide ?
Provide 란? 일반적으로 부모 컴포넌트가 자식 컴포넌트에게 데이터를 전달할 때는 props 를 이용합니다. 하지만 부모와 자식 관계가 깊어질수록 props 로 데이터를 전달하는 데는 한계가 있습니다. V
jongik.tistory.com](https://jongik.tistory.com/20)
TodoListMain.vue
- template code
// todoList의 필터링을 설정할 메뉴입니다.
<todo-list-menu v-on:change-filter="onChangeFilter" class="p-0"/>
<div v-for="key in Object.keys(filtered_todos)" :key="key" class="mb-3">
<div v-if="use_category">
<em>{{key}}</em>
</div>
// 필터링된 작업목록입니다.
<todo-list :data="filtered_todos[key]" />
</div>
<div class="my-2 mt-5">
<strong>미 완료 작업</strong>
</div>
// 완료하지 못한 작업입니다.
<todo-list :data="pending_todos"/>
- script code ( setup() 내부 )
// import 를 통해 useFilter() 기능을 사용합니다.
const {
getPendingTodos,
// ...
} = useFilter()
const filter = ref(0);
const filtered_todos = ref([]);
const pending_todos = ref([]);
const use_category = ref(false);
const todos = inject('todos');
const filters = {
0: {
str: '해야 할 작업들',
func: getActiveTodayTodos,
category: false,
},
// ... 나머지 필터링 메뉴들 생략
}
provide('filters', filters)
// filter 모듈에서 받은 배열을 날짜별로 다시 분리를 한다.
const groupBy = (todos) => {
return todos.reduce((acc, cur) => {
acc[cur['date']] = acc[cur['date']] || []
acc[cur['date']].push(cur);
return acc;
}, {})
}
const onChangeFilter = (filter_idx) => {
filter.value = Number(filter_idx)
}
// 새로운 할 일이 추가되거나, 할 일이 삭제되었을 때를 감시한다.
// 감시 내역을 실시간으로 반영하여 TodoList 컴포넌트에 새로운 데이터를 전달하는 것이 목적
watch(
[() => filter.value, todos.value],
([new_filter, new_todos], [old_filter, old_todos]) => {
pending_todos.value = getPendingTodos(todos);
if(typeof new_filter != 'undefined') {
let temp_todos = filters[new_filter].func(todos)
filtered_todos.value = groupBy(temp_todos);
use_category.value = filters[new_filter].category
}
},
{ immediate: true }
)
// return 문을 통해 export
watch : immediate 속성이 true 로 설정된 것은 TodoListMain 컴포넌트가 생성이 되었을 때 첫 변화도 즉시 감시하여 선언된 함수를 실행하게 하기 위함이다.
TodoListMenu
- template code
<button
type="button"
data-bs-toggle="dropdown"
>
필터를 선택해주세요
</button>
// 필터의 목록
<ul>
<li v-for="key in Object.keys(filters)" :key="key">
<a class="dropdown-item" @click="filter = key">{{filters[key].str}}</a>
</li>
</ul>
- script code
// watch에서 emit을 사용하기 전에 이벤트 명을 선언해줘야 한다.
emits: ['change-filter'],
setup(props, context) {
const filters = inject('filters')
const filter = ref(0)
// filters 객체로 부터 str 속성 값을 뽑아주는 역할
const state = computed(() =>{
return filters[filter.value].str
})
// 선택한 메뉴의 값이 변경될 때마다 해당 키 값을 emit 을 이용해 부모 컴포넌트인 TodoListMain 에 전달한다.
watch(
() => filter.value,
(filter) => {
context.emit('change-filter', filter)
}
)
// return 문을 통해 export
}
TodoList.vue
- template code
<div v-for="(todo, idx) in data" :key="todo.id">
// 체크박스로 할 일 완료 시키기
<input
type="checkbox"
:checked="todo.completed"
:disabled="todo.completed"
@click="completeTodo(todo.id)"
>
<ul class="dropdown-menu dropdown-menu-end">
<li v-for="item in menu" :key="item.str">
<a class="dropdown-item" @click="item.func(todo.id)">
{{item.str}}
</a>
</li>
</ul>
// 리스트메뉴를 통해 할 일 삭제하거나 완료시키기
</div>
- script code
props: {
data: {
type: Array,
default: [], // 데이터가 들어오지 않을 경우 빈 배열,
},
},
setup() {
const removeTodo = inject('removeTodo');
const completeTodo = inject('completeTodo');
const today = inject('today');
const menu = [
{
str: '할 일 삭제',
func: removeTodo,
},
{
str: '할 일 완료',
func: completeTodo,
},
]
// return 문을 통해 export
}
App.vue
- template code
<header>
<hgroup class="my-5">
<h1>To Do List</h1>
<em>{{today}}</em>
</hgroup>
</header>
<todo-list-container/>
- script code
setup() {
// 하위 컴포넌트들에서 사용하게될 today는 main.js 에서 선언한 오늘 날짜이다.
const today = inject('today')
console.log("상단에 오늘 날짜 출력 " + today)
return { today }
},
전체 코드를 보려면 여기를 눌러주세요!
글을 마치며
Vue 2 에서 Vue 3 로 넘어가면서 기본 개념을 이해하고 익힐 수 있었던 것 같습니다.
다음에는 간단한 블로그 형식의 웹페이지 구현을 계획중입니다.
"웹 애플리케이션 개발 기초부터 실전까지 한 권으로 배우는 Vue.js 3" 도서를 참고하며 진행했습니다.
'Study > Vue' 카테고리의 다른 글
Vue 2 ) 무한스크롤 페이징 구현하기 vue-infinite-loading (0) | 2022.01.18 |
---|---|
Vue 3 ) 반응형 api 를 이용해보자 (0) | 2022.01.14 |
Vue 3 ) Vite 2로 생성한 프로젝트에 TailwindCSS 적용하기 (0) | 2022.01.11 |
Vue ) <script setup> 은 또 뭐 (0) | 2022.01.10 |
Vue 3 ) Ref, Refs ?? (0) | 2022.01.09 |
댓글