Frontend/Vue.js

[Vue CLI] 할 일 관리 앱 만들기(2)

gamzaggang7 2024. 6. 8. 18:42
728x90

애플리케이션 구조 개선

현재 todoApp은 데이터를 입력했을 때 목록에 바로 반영되지 않으며, clear all 버튼을 눌렀을 때로 목록에 바로 반영되지 않는다. 4개의 컴포넌트로 분리했기 때문에 한 영역에서의 처리 결과를 다른 영역에서 감지하지 못하는 것이다. 그렇다고 하나의 컴포넌트 안에서 데이터 저장, 조회, 삭제를 모두 처리하는 것은 비효율적이다. 

TodoInput의 newTodoItem과 TodoList의 todoItems은 모두 '할 일' 이라는 같은 데이터 속성을 사용한다. 최상위 컴포넌트인 App 컴포넌트에서 '할 일' 데이터를 정의하고 하위 컴포넌트에 props로 전달한다면 위의 문제점들을 해결할 수 있다. App 컴포넌트에서 데이터 조화, 추가, 삭제를 하고 하위 컴포넌트들은 요청(이벤트 발생)을 하도록 구조를 개선한다.

 

먼저 App 컴포넌트에 todoItems 데이터와 addTodo() 메서드를 추가한다.

export default {
  data() {
    return {
      todoItems: [],
    };
  },
  methods: {
    addTodo() {
     
      }
    },
  },
...

 

선언한 todoItems 속성을 TodoList 컴포넌트에 props로 전달하고 TodoList.vue에 props 속성을 추가한다.

<TodoList v-bind:propsdata="todoItems"></TodoList>
export default {
  props: ['propsdata'],

 

할 일 추가 버튼을 눌렀을 때 App 컴포넌트로 이벤트 전달할 수 있게 todoInput 컴포넌트에 v-on 디렉티브를 추가한다.

<TodoInput v-on:addTodo="addTodo"></TodoInput>

 

TodoInput.vue 수정)

addTodo() 메서드에 이벤트 전달할 때 입력값인 value 객체를 인자로 전달한다. 기존에 있던 로컬 스토리지에 데이터를 저장하는 코드는 삭제한다.

  methods: {
    addTodo() {
      if (this.newTodoItem !== "") {
        var value = this.newTodoItem && this.newTodoItem.trim();
        this.$emit('addTodo', value)
        this.clearInput();
      }
    },

그리고 App.vue 의 addTodo() 메서드에 코드를 추가한다.

  methods: {
    addTodo(todoItem) {
      localStorage.setItem(todoItem, todoItem)
      this.todoItems.push(todoItem)
    },
  },

addTodo()의 인자 값 todoItem은 TodoInput.vue 에서 올려 보낸 입력 값이다. 이 값을 로컬 스토리지에 저장하고 App.vue의 todoItems 데이터 속성에도 추가한다. 

+ 버튼을 누르면 TodoInput.vue에서 App.vue로 신호(이벤트)를 보내 App.vue의 addTodo() 메서드를 실행한다.

 

TodoList.vue 수정)

v-for 디렉티브의 반복 대상을 propsdata로 변경한다.

<li v-for="(todoItem, index) in propsdata" :key="todoItem">

App.vue의 todoItems 데이터 개수만큼 목록 아이템을 생성한다. 이제 할 일을 추가하면 새로고침하기 않아도 목록이 갱신된다.

기존의 data() 코드는 삭제하고 created() 는 App.vue로 옮긴다.

export default {
  props: ['propsdata'],
  methods: {
    removeTodo(todoItem, index) {
      localStorage.removeItem(todoItem)
      this.todoItems.splice(index, 1)
    }
  }
};
export default {
  data() {
    return {
      todoItems: [],
    };
  },
  created() {
    if (localStorage.length > 0) {
      for (var i = 0; i < localStorage.length; i++) {
        this.todoItems.push(localStorage.key(i));
      }
    }
  },

 

 

이제 Clear All 버튼을 누르면 자동으로 화면이 갱신되도록 한다.

하위 컴포넌트에서 발생시킬 이벤트 이름을 removeAll, 상위 컴포넌트에서 받아 실행시킬 이벤트 이름을 clearAll()로 한다.

 

상위 컴포넌트에서 TodoFooter에 이벤트 전달 속성을 추가한다.

<TodoFooter v-on:removeAll="clearAll"></TodoFooter>

그리고 methods에 clearAll 메서드를 추가한다.

    clearAll() {
      localStorage.clear();
      this.todoItems = [];
    },

 

TodoFooter.vue의 clearTodo()를 수정한다.

  methods: {
    clearTodo() {
      this.$emit("removeAll");
    },
  },

Clear All 버튼을 누르면 removeAll 이벤트를 발생시켜 상위 컴포넌트인 App.vue로 전달하고, 상위 컴포넌트에서 이벤트를 받아 상위 컴포넌트에서 정의된 clearAll() 메서드를 실행한다.

이제 버튼을 클릭하면 화면이 자동 갱신된다.

 

TodoList에서 todoItems 배열을 삭제했기 때문에 removeTodo() 메서드에서 오류가 발생한다. TodoList 컴포넌트의 각 아이템 삭제하는 로직에도 이벤트 전달 방식을 적용한다.

 

먼저 App.vue의 TodoList 컴포넌트 태그에 v-on 디렉티브를 추가한다.

<TodoList v-bind:propsdata="todoItems" @removeTodo="removeTodo"></TodoList>

@removeTodo는 v-on:removeTodo와 같다. TodoList 컴포넌트의 removeTodo() 메서드에서 removeTodo 이벤트를 발생시켜 App.vue로 전달한다. 전달할 때는 입력 텍스트와 인덱스를 같이 보낸다.

  methods: {
    removeTodo(todoItem, index) {
      this.$emit('removeTodo', todoItem, index)
    }
  }

기존의 removeTodo() 메서드 코드는 App.vue로 옮긴다.

  methods: {
    ...
    removeTodo(todoItem, index) {
      localStorage.removeItem(todoItem)
      this.todoItems.splice(index, 1)
    }
  },
728x90

 

기능 추가

1) 뷰 애니메이션 추가

뷰 애니메이션은 뷰 프레임워크에서 지원하는 기능으로 데이터 추가, 변경, 삭제에 대해 페이드 인, 페이드 아웃 등 여러 애니메이션 효과를 지원한다. js 애니메이션 라이브러리나 css 애니메이션 라이브러리도 같이 사용할 수 있다.

 

할 일 목록에 애니메이션 효과를 추가하기 위해 기존 ul 태그를 transition-group 태그로 변경한다. transition-group 태그의 tag 속성에 애니메이션이 들어갈 html 태그 이름을 지정하면 된다.

<template>
  <section>
    <transition-group name="list" tag="ul">

 

transition-group 태그에 적용할 css 속성을 추가한다. transition-group 태그와 함께 사용할 수 있는 css 클래스 이름에는 규칙이 있다.

  • *-enter-active: 요소가 삽입될 때 효과가 적용되는 동안 활성화되는 클래스
  • *-leave-active: 요소가 제거될 때 효과가 적용되는 동안 활성화되는 클래스
  • *-enter-from: 요소가 삽입되기 시작할 때 초기 상태를 정의하는 클래스
  • *-leave-to: 요소가 제거될 때 최종 상태를 정의하는 클래스
.list-enter-active, .list-leave-active {
  transition: all 1s;
}
.list-enter-from, .list-leave-to {
  opacity: 0;
  transform: translateY(30px);
}

이제 데이터를 추가하고 삭제할 때 부드럽게 들어오고 나가는 애니매이션이 동작한다.

 

2) 뷰 모달

입력 값이 없을 때의 예외 처리를 추가한다. js의 기본 경고 창으로 예외 처리할 수도 있지만 뷰 공식 사이트에서 제공하는 팝업 대화상자인 모달을 사용한다.

 

컴포넌트 폴더에 common 폴더를 만들고 그 안에 모달 컴포넌트를 생성한다.

https://v2.ko.vuejs.org/v2/examples/modal.html

 

모달 컴포넌트 — Vue.js

Vue.js - 프로그레시브 자바스크립트 프레임워크

v2.ko.vuejs.org

위 사이트에서 html의 transition 코드를 template 태그 안에, css 코드를 style 태그 안에 복붙한다. .modal-body 태그는 필요 없으므로 지운다.

헤더 텍스트 색은 붉은색으로 바꿨다.

<template>
  <transition name="modal">
    <div class="modal-mask">
      <div class="modal-wrapper">
        <div class="modal-container">

          <div class="modal-header">
            <slot name="header">
              default header
            </slot>
          </div>

          <div class="modal-footer">
            <slot name="footer">
              default footer
              <button class="modal-default-button" @click="$emit('close')">
                OK
              </button>
            </slot>
          </div>
        </div>
      </div>
    </div>
  </transition>
</template>

<script>

</script>

<style>
.modal-mask {
  position: fixed;
  z-index: 9998;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, .5);
  display: table;
  transition: opacity .3s ease;
}

.modal-wrapper {
  display: table-cell;
  vertical-align: middle;
}

.modal-container {
  width: 300px;
  margin: 0px auto;
  padding: 20px 30px;
  background-color: #fff;
  border-radius: 2px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .33);
  transition: all .3s ease;
  font-family: Helvetica, Arial, sans-serif;
}

.modal-header h3 {
  margin-top: 0;
  color: #5f0000;
}

.modal-default-button {
  float: right;
}

.modal-enter {
  opacity: 0;
}

.modal-leave-active {
  opacity: 0;
}

.modal-enter .modal-container,
.modal-leave-active .modal-container {
  -webkit-transform: scale(1.1);
  transform: scale(1.1);
}
</style>

 

TodoInput.vue에서 모달 컴포넌트를 사용할 수 있도록 모달 컴포넌트를 등록한다.

import ModalComponent from "./common/ModalComponent.vue";

export default {
  components: {
    ModalComponent,
  },

 

template의 span 태그 밑에 모달에 표시할 header와 footer를 정의하는 코드를 추가한다.

    <ModalComponent v-if="showModal" @click="showModal = false">
      <template v-slot:header>
        <h3>경고</h3>
      </template>
      <template v-slot:footer>
        <span @click="showModal = false">
          할 일을 입력하세요.
          <i class="closeModalBtn fas fa-times" aria-hidden="true"></i>
        </span>
      </template>
    </ModalComponent>
  • v-if="showModal": showModal이 true일 때만 모달 컴포넌트가 렌더링된다.
  • @click="showModal=false": 모달 컴포넌트가 close 이벤트를 발생시키면 showModal값을 false로 변경하여 모달 창을 닫는다.
  • <template v-slot:>: 모달 컴포넌트의 해당 슬롯에 내용을 전달한다.

모달 창 표시 여부를 제어하는 데이터를 추가한다. 기본 값은 false이다.

  data() {
    return {
      newTodoItem: "",
      showModal: false,
    };
  },

 

addTodo() 메서드에서 텍스트 값이 없을 시 showModal 값을 true로 설정해 모달 창이 동작하도록 한다.

  methods: {
    addTodo() {
      if (this.newTodoItem !== "") {
        var value = this.newTodoItem && this.newTodoItem.trim();
        this.$emit("addTodo", value);
        this.clearInput();
      } else {
        this.showModal = !this.showModal;
      }
    },

 

 

728x90