• [Vue] Vue.js를 사용하여 Todo List 만들기

    2021. 5. 10.

    by. 늅

     

     

    Vue.js를 이용하여 Todo List를 만들어 볼 예정이다.

    상태관리는 Vuex를 사용하며, localStorage에 데이터를 저장해보려한다.

     

    1. 화면설계

    1) 오늘 날짜 노출

    2) 리스트 총 개수와 리스트 완료여부 개수 나타남

    3) 인풋

    4) 인풋에 텍스트 입력 후 등록 버튼 누르면 리스트 추가

    5) 최신순, 오래된 순에 따라 리스트 필터링

    6) 전체삭제 버튼을 누르면 리스트 전체 삭제

    7) 체크박스 클릭 시 완료된 리스트

    8) 등록한 날짜 노출

    9) 한개의 리스트 삭제

     

     

    2. 폴더 구조

    폴더 구조는 이러하다.

    router와, store도 사용할 예정이니 프로젝트 생성할때 옵션을 선택해주자~

     

     

     

    우선 layouts 폴더에 Header, Footer를 만들고 layouts/index.vue에 import 시킨 뒤,

    내부 컨텐츠는 slot에 넣어줄 것이다.

    layouts/index.vue

    <template>
        <div>
            <todo-header></todo-header>
            <slot></slot> <!-- 여기에 컨텐츠 영역이 들어감. -->
            <todo-footer></todo-footer>
        </div>
    </template>
    
    <script>
    import TodoHeader from './TodoHeader.vue'
    import TodoFooter from './TodoFooter.vue'
    
    export default {
        components: { TodoHeader, TodoFooter },
    
    }
    </script>

     

     

    그런 뒤 views 폴더에 Todo.vue를 만들어준다.

    Todo.vue에는 layouts/index.vue를 import 시켜주자.

    views/Todo.vue

    <template>
    	<base-layout>
        	<!-- 여기에 마크업이 들어갈 예정이에요 -->
        </base-layout>
    </template>
    
    <script>
    import BaseLayout from "@/layouts/index.vue";

     

     

    이제 router에 views/Todo.vue 를 import 시켜줘야 한다.

    그래야 화면에 Todo.vue가 보여질수 있으니까

    router/index.js

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import TodoList from '../views/Todo.vue'
    
    Vue.use(VueRouter)
    
    const routes = [
      {
        path: '/',
        name: 'Todo',
        component: TodoList
      }
    ]
    
    const router = new VueRouter({
      mode: 'history',
      base: process.env.BASE_URL,
      routes
    })
    
    export default router
    

     

    assets

    assets폴더에는 css,images폴더를 만들어주었다.

     

     

    main.js

    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'
    // css
    import '@/assets/css/reset.css';
    import '@/assets/css/font.css';
    
    Vue.config.productionTip = false
    
    new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount('#app')
    

     

     

    3. 컴포넌트 생성

    프로젝트에 필요한 컴포넌트다.

    TodoButton.vue - 리스트를 등록할 버튼 컴포넌트

    TodoCheckButton.vue - 리스트 완료 여부 체크 버튼 컴포넌트

    TodoInput.vue - input 컴포넌트

    TodoSelect.vue - 최신순, 오래된순을 선택하는 셀렉트박스 컴포넌트

    TodoList.vue(List/Index.vue) - TodoListItem.vue를 여러개 뿌려주는 컴포넌트

    TodoListItem.vue - 리스트 한개의 컴포넌트

    TodoModal.vue(Modal/index.vue) - 알럿 팝업 컴포넌트

     

    이외의 나머지 기능들은 일회성이기 때문에 컴포넌트화 시키지 않았다.

    (물론 따지고 보면 이 프로젝트에서 나머지 컴포넌트들도 일회성이지만 실제 프로젝트에서 사용했을때를 생각하고 작업했음!)

     

     

    TodoHeader

    Header에는 오늘날짜가 노출된다.

    <template>
        <header>
            <div class="wrap">
                <div class="date">
                    5/11 Fri
                </div>
            </div>
        </header>
    </template>
    
    <script>
    export default {
    
    }
    </script>
    
    <style lang="scss" scoped>
    header {
        padding: 40px 0 80px;
        .wrap {
            display: flex;
            justify-content: flex-end;
            padding: 0 30px;
        }
        .date {
            font-size: 18px;
            font-weight: 600;
            color: #fff;
        }
    }
    </style>

     

    TodoFooter

    <template>
        <footer>
            <div class="wrap">
                made by youbin
            </div>
        </footer>
    </template>
    
    <script>
    export default {
    
    }
    </script>
    
    <style lang="scss" scoped>
    footer {
        padding: 80px 30px;
        .wrap {
            padding: 20px 0;
            color: rgba(255,255,255,.2);
            font-size: 14px;
            text-align: right;
            border-top: 1px solid rgba(255,255,255,.2);
        }
    }
    </style>

     

    TodoButton

    버튼은 btn-add/btn-clear 이렇게 두가지 타입으로 나눴다.

    btnType을 props을 통해서 부모창에서 btnType="btn-add" or btnType="btn-clear"으로 값을 받으면

    그에 따라 class가 들어가게 된다.

    <template>
    	<button 
    		:btnType="btnType"
    		:class="btnType"
    		class="button">
    		<slot></slot>
    	</button>
    </template>
    
    <script>
    export default {
    	name: "TodoButton",
    	props: {
    		btnType: {
    			type: String,
    			default: ''
    		}
    	}
    }
    </script>
    
    <style lang="scss" scoped>
    .button {
    	// 추가버튼
    	&.btn-add {
    		display: inline-flex;
    		justify-content: center;
    		align-items: center;
    		width: 30px;
    		height: 30px;
    		border-radius: 50%;
    		background: #fff;
    		box-shadow: 0 0px 10px rgba(0,0,0,.3);
    		transition: all 0.3s ease-out;
    
    		img {
    			transition: all 0.3s ease-out;
    		}
    
    		&:hover {
    			img {
    				transform: translateX(3px);
    			}
    		}
    	}
        // 모두 삭제 버튼
    	&.btn-clear {
    		width: 80px;
    		height: 30px;
    		padding: 5px;
    		font-size: 12px;    
    		color: #fff;
    		background: rgba(255,255,255,.2);
    		border-radius: 20px;
    		text-align: center;
    	}
    }
    </style>

     

    TodoCheckBox

    <template>
    	<button class="checkbox"></button>
    </template>
    
    <script>
    export default {
    	name: 'TodoCheckButton',
    	props: {
            
    	},
    	data() {
    		return {
    
    		}
    	},
    	methods: {
    	}
    }
    </script>
    
    <style lang="scss" scoped>
    .checkbox {
        &.checked {
            &:before {
                left: 3px;
                top: 2px;
                width: 10px;
                height: 7px;
                border: 2px solid #fff;
                border-top: 0;
                border-right: 0;
                transform: rotate(-45deg);
                z-index: 1;
            }
        }
        position: relative;
        display: block;
        width: 20px;
        height: 20px;
        border: 2px solid #d4d4d4;
        &:before {
            content: '';
            display: inline-block;
            position: absolute;
            margin: auto;
        }  
    }
    </style>
    

     

    TodoInput

    input 도 역시 type, placeholder, value를 props로 받아 재활용이 가능한 컴포넌트로 만들어 주었다.

    v-model을 컴포넌트에서 사용하기 위해 input 이벤트를 바인딩준다!

    반드시 input 이벤트로 바인딩 해준다. v-model은 input이벤트로 받기 때문이다.

    this.$emit('input', 인자값)을 하여 값을 넘겨주면 실시간으로 값이 바인딩 된다.

    <template>
    	<input 				
    		:type="type"
    		:placeholder="placeholder"
    		:value="value"		
    		@input="change" 
    		>
    </template>
    
    <script>
    export default {
    	name: 'VueInput',
    	props: {
    		type: {
    			type: String,
    			default: ''
    		},	
    		placeholder: {
    			type: String,
    			default: ''
    		},	
    		value: {
    			type: String,
    			default: ''
    		},	
    	},
    	data() {
    		return {
    			isActive: false,
    		}
    	},
    	methods: {
    		change: function($event) {
    			this.$emit('input', $event.target.value)
    		}
    	}
    }
    </script>
    
    <style lang="scss" scoped>
    input {
    	width: 100%;
    	height: 100%;
    	padding: 10px 20px;
    	font-size: 14px;
    	color: #fff;
    	border: 2px solid #fff;
    	border-radius: 25px;
    	transition: all .2s;
    	&.on {
    		box-shadow: 0 10px 10px rgba(0,0,0,.05);
    		background-color: #fff;
    	}
    }
    
    input {
    	&::placeholder{
    		font-size: 14px;
    		color: #ddd;
    	}
    }
    
    </style>
    

     

    TodoSelect

    <template>
        <select name="" id="">
            <option value="">Latest</option>
            <option value="">Oldest</option>
        </select>
    </template>
    
    <script>
    export default {
        name: 'TodoSelect',
    
    }
    </script>
    
    <style lang="scss" scoped>
    select {
        width: 100px;
        height: 30px;
        padding: 5px;
        color: #fff;
        font-size: 12px;
        text-align-last: center;
        background: rgba(255,255,255,.2);
        border-radius: 20px;
        option {
            color: #000;
        }
    }
    
    </style>

     

    TodoListItem

    list 한개에 대한 컴포넌트이다.

    리스트 안에 체크박스, 삭제버튼, 날짜 기능이 같이 들어있으니 함께 마크업 해준다!

    체크박스 버튼은 컴포넌트를 만들어두었으니 import해준뒤 사용하자.

    <template>
        <div class="list-item">
            <div class="left-area">
                <TodoCheckButton/>
                <p class="label">label</p>
            </div>
            <div class="right-area">
                <button class="close">close button</button>
                <div class="date">5/11 Fri</div>
            </div>
        </div>
    </template>
    
    <script>
    import TodoCheckButton from '@/components/Button/TodoCheckButton.vue'
    
    export default {    
        name: 'TodoListItem',
        components: { TodoCheckButton },
        props: {
        },
        methods: {
            
        }
    }
    </script>
    
    <style lang="scss" scoped>
    .list-item {
        position: relative;
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 25px;
        .left-area {        
            display: flex;
            align-items: center;
            .label {
                padding-left: 10px;
                color: #fff;
                letter-spacing: 1px;
            }
        }
        .right-area {        
            .close {            
                position: relative;
                width: 12px;
                height: 12px;
                top: -5px;
                right: 0;
                font-size: 0;
                &::before, &::after {
                    content: '';
                    display: block;
                    position: absolute;
                    top: 50%;
                    left: 50%;
                    background-color: #fff;
                    transform: translate(-50%,-50%) rotate(45deg);
                }
                &::before {
                    width: 1px;
                    height: 100%;
                }
                &::after {
                    width: 100%;
                    height: 1px;
                }
            }
            .date {
                position: absolute;
                right: 0;
                top: 15px;
                color: #fff;
                font-size: 10px;
                letter-spacing: 1px;
            }
        }
    }
    </style>

     

    TodoList

    TodoListItem를 v-for를 이용해 뿌리기 위한 컴포넌트다.

    <template>
        <div>
            <TodoListItem 
                v-for="(item, key) in items"
                :key="key"/>
        </div>
    </template>
    
    <script>
    import TodoListItem from '@/components/List/TodoListItem.vue'
    export default {
        name: 'TodoList',
        components: { TodoListItem },
        props: {
            items: {
                type: Array,
            },
        },
        data() {
            return {
                
            }
        },
        methods: {
        }
    }
    </script>
    
    <style lang="scss" scoped>
    .no-list {
        font-size: 14px;
        color: rgba(255,255,255,.7);
    }
    </style>

     

    TodoModal

    팝업이 띄워질때 transition을 주기 위해 transition으로 묶어준다.

    name을 주면 css에서 처리할때 name값을 class처럼 앞에 붙여주면 transition효과를 줄 수있다!

    .'name값'-leave-active {
    
        transition: opacity 0.3s ease;
    
    } 

     

    @click이벤트로 $emit('close')를 부모에게 넘겨주어 닫기버튼을 누르면 close이벤트가 일어나게 하였다.

    <template>
    	<transition name="modal" appear>
    		<div 
    			@click.self="$emit('close')"
    			class="modal">
    			<div class="modal-box">
    				<button
    				@click="$emit('close')">닫기</button>
    				<slot></slot>
    			</div>
    		</div>
    	</transition> 
    </template>
    
    <script>
    export default {
    	name: 'Modal',  
    }
    </script>
    
    <style lang="scss">
    .modal {
    	position: fixed;
    	top: 0;
    	left: 0;
    	right: 0;
    	bottom: 0;
    	background-color: rgba(0,0,0,.6);
    	display: flex;
    	justify-content: center;
    	align-items: center;
    	z-index: 100;
    	.modal-box {
    		position: relative;    
    		padding: 50px 30px 30px;
    		background-color: #fff;
    		button {
    			position: absolute;
    			top: 20px;
    			right: 20px;
    			width: 18px;
    			height: 18px;
    			background: url('~@/assets/images/close_btn.png') no-repeat;
    			background-size: 100%;
    			font-size: 0;
    		}
    	}
    }
    
    .modal-enter-active, .modal-leave-active {
    	transition: opacity 0.3s;
    
    	.modal-box {
    		transition: opacity 0.3s, transform 0.3s;
    	}
    }
    
    .modal-leave-active {
    	transition: opacity 0.3s ease;
    }
    
    .modal-enter, .modal-leave-to {
    	opacity: 0;
    
    	.modal-box {
    		opacity: 0;
    		transform: translateY(-10px);
    	}
    }
    </style>
    

     

    3. 마크업

    만들어둔 컴포넌트를 이용하여 마크업을 해보자.

    <template>
    	<base-layout>
            <div class="wrap">
                <ul class="count-area">
                    <li class="complete">1</li>
                    <li class="all">10</li>
                </ul>
            
                <div class="add-area">
                    <todo-input
                        v-model="text"
                        placeholder="please enter here"
                    ></todo-input>
                    <todo-button
                        btnType="btn-add">
                        <img src="@/assets/images/btn_go.png" alt="">
                    </todo-button>
                </div>
    
                <div class="filter-area">
                    <todo-select></todo-select>
                    <todo-button
                        btnType="btn-clear">All Clear</todo-button>
                </div>
    
                <div class="list-area">
                    <todo-list
                        :items="todoList"
                    ></todo-list>
                </div>
            </div>        
        </base-layout>
    </template>
    
    <script>
    import BaseLayout from "@/layouts/index.vue";
    import TodoList from "@/components/List/index.vue";
    import TodoInput from '@/components/Form/TodoInput.vue';
    import TodoButton from '@/components/Button/TodoButton.vue';
    import TodoSelect from '@/components/Form/TodoSelect.vue';
    
    export default {
        name: 'Todo',
        components: {
            BaseLayout,
            TodoList,
            TodoInput,
            TodoButton,
            TodoSelect
        },
        data() {
            return {
                text: '',
                todoList: [
                	{
                    	title: 'youbin list',
                        complete: false
                    },
                    {
                    	title: 'list youbin',
                        complete: false
                    }
                ]
            }
        },
        methods: {
        }
    }
    </script>
    
    <style lang="scss" scoped>
    .wrap {
        width: 100%;
        padding: 0 30px;
    }
    
    .count-area {
        display: flex;
        align-items: baseline;
        margin-bottom: 40px;
        color: #fff;
        .complete {
            position: relative;
            padding-right: 25px;
            margin-right: 5px;
            font-size: 60px;
            &::after {
                content: '/';
                display: block;
                position: absolute;
                right: 0;
                bottom: 3px;
                font-size: 50px;
            }
        }
        .all {
            font-size: 30px;
        }
    }
    
    .add-area {
        position: relative;
        width: 100%;
        height: 50px;
        margin-bottom: 20px;
        input {
            position: absolute;
            width: 100%;
        }
        button {
            position: absolute;
            right: 15px;
            top: 0;
            bottom: 0;
            margin: auto;
        }
    }
    
    .filter-area {
        display: flex;
        justify-content: space-between;
        margin-bottom: 20px;
    }
    
    .list-area {
        width: 100%;
        min-height: 300px;
        padding: 40px 30px;
        background-color: rgba(255,255,255,.1);
        border-radius: 25px;
    }
    
    </style>

     

    부모에서 자식컴포넌트(TodoList.vue)에 items라는 이름으로 값을 바인딩 해주면,

    <!-- 부모컴포넌트(Todo.vue) -->
    
    <todo-list
       :items="todoList"
       @check="todoCheck"
    ></todo-list>

    자식컴포넌트(TodoList.vue)에서 props로 값을 받아준다.

    여기서도 역시 item이라는 이름을 자식(TodoListItem.vue)에게 한번 더 바인딩해주자.

    <!-- 자식 컴포넌트(TodoList.vue) -->
    
    <template>
        <div>
            <TodoListItem 
                v-for="(item, key) in items"
                :key="key"
                :item="item"/>
        </div>
    </template>
    
    <script>
    import TodoListItem from '@/components/List/TodoListItem.vue'
    export default {
        name: 'TodoList',
        components: { TodoListItem },
        props: {
            items: {
                type: Array,
            },
        },
        data() {
            return {
                
            }
        },
        methods: {
        }
    }
    </script>
    
    <style lang="scss" scoped>
    .no-list {
        font-size: 14px;
        color: rgba(255,255,255,.7);
    }
    </style>

    그래야 자식(TodoListItem.vue)에서 최상단 부모(Todo.vue)에 있는 todoList 데이터를 뿌려줄 수 있다!

     

    <!-- 자식 컴포넌트(TodoListItem.vue) -->
    
    <template>
        <div class="list-item">
            <div class="left-area">
                <TodoCheckButton 
                    :class="{ 'checked' : item.complete }"/>
                <p class="label">{{ item.title }}</p>
            </div>
            <div class="right-area">
                <button class="close">close button</button>
                <div class="date">5/11 Fri</div>
            </div>
        </div>
    </template>
    
    <script>
    import TodoCheckButton from '@/components/Button/TodoCheckButton.vue'
    
    export default {    
        name: 'TodoListItem',
        components: { TodoCheckButton },
        props: {
            item: {
                type: Object,
                require: true
            },
        },
        methods: {
            
        }
    }
    </script>
    
    <style lang="scss" scoped>
    .list-item {
        position: relative;
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 25px;
        .left-area {        
            display: flex;
            align-items: center;
            .label {
                padding-left: 10px;
                color: #fff;
                letter-spacing: 1px;
            }
        }
        .right-area {        
            .close {            
                position: relative;
                width: 12px;
                height: 12px;
                top: -5px;
                right: 0;
                font-size: 0;
                &::before, &::after {
                    content: '';
                    display: block;
                    position: absolute;
                    top: 50%;
                    left: 50%;
                    background-color: #fff;
                    transform: translate(-50%,-50%) rotate(45deg);
                }
                &::before {
                    width: 1px;
                    height: 100%;
                }
                &::after {
                    width: 100%;
                    height: 1px;
                }
            }
            .date {
                position: absolute;
                right: 0;
                top: 15px;
                color: #fff;
                font-size: 10px;
                letter-spacing: 1px;
            }
        }
    }
    </style>

     

    이렇게 까지 하면 일단 마크업, 임의로 만들어 준 데이터까지 뿌려주기 완성!!! 

     

     

     

     

    이제 다음번엔 vuex를 사용해보도록 해야징~~! ♥

    댓글