Web Development/vue

[콜럼Vue스] To do list 만들기 (+리팩토링 추가)

쟤리 2024. 10. 10. 08:40
728x90
반응형

자료 : 원쌤의 vue.js 퀵스타트

 

이 예제는 Vue.js를 사용하여 간단한 할 일 목록(TodoList) 앱을 만드는 방법을 설명함. 사용자는 할 일을 입력하고, 추가, 완료, 삭제할 수 있으며, 각각의 할 일에 대해 완료 상태에 따른 스타일을 동적으로 적용할 수 있음. 또한, 이 예제에서는 Bootstrap 라이브러리를 사용하여 기본적인 UI 스타일을 제공함.


1. 화면 시안 작성

먼저, 기본적인 HTML 구조를 작성하고 Bootstrap CSS를 불러옴. 이 예제에서는 할 일 목록을 관리할 수 있는 기본 UI와 할 일 입력란을 작성함.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>06-11 TodoList App</title>
  <link rel="stylesheet" href="https://unpkg.com/bootstrap@5.2.3/dist/css/bootstrap.min.css" />
  <style>
    body { margin: 0; padding: 0; font-family: sans-serif; }
    .title { text-align: center; font-weight: bold; font-size: 20pt; }
    .todo-done { text-decoration: line-through; }
    .container { padding: 10px; }
    .pointer { cursor: pointer; }
  </style>
</head>
<body>
  <div id="app" class="container">
    <div class="card card-body bg-light">
      <div class="title">:: Todolist App</div>
    </div>
    <div class="card card-default card-borderless">
      <div class="card-body">
        <!-- 이곳에 할 일 목록 UI와 할 일 입력 UI가 삽입됩니다 -->
      </div>
    </div>
  </div>

  <script src="https://unpkg.com/vue@next"></script>
  <script type="text/javascript">
    // Vue.js 앱 초기화
  </script>
</body>
</html>

2. 데이터 및 메서드 정의

이제 할 일 목록 데이터를 정의하고, 할 일을 추가, 삭제, 완료 처리하는 메서드를 Vue.js 인스턴스에서 작성함.

<script type="text/javascript">
  var ts = new Date().getTime();  // 유니크한 ID 생성을 위한 타임스탬프
  var vm = Vue.createApp({
    name: "App",
    data() {
      return {
        todo: "",  // 사용자 입력을 저장하는 변수
        todolist: [  // 초기 할 일 목록
          { id: ts, todo: "자전거 타기", completed: false },
          { id: ts + 1, todo: "딸과 공원 산책", completed: true },
          { id: ts + 2, todo: "일요일 애견 카페", completed: false },
          { id: ts + 3, todo: "Vue 원고 집필", completed: false }
        ]
      };
    },
    methods: {
      // 새로운 할 일을 추가
      addTodo() {
        if (this.todo.length >= 2) {
          this.todolist.push({
            id: new Date().getTime(),
            todo: this.todo,
            completed: false
          });
          this.todo = "";  // 입력 필드 초기화
        }
      },
      // 할 일을 삭제
      deleteTodo(id) {
        let index = this.todolist.findIndex((item) => id === item.id);
        this.todolist.splice(index, 1);
      },
      // 완료 상태를 토글
      toggleCompleted(id) {
        let index = this.todolist.findIndex((item) => id === item.id);
        this.todolist[index].completed = !this.todolist[index].completed;
      }
    }
  }).mount("#app");
</script>

3. 템플릿 작성

할 일 입력란과 할 일 목록을 렌더링하는 템플릿을 작성함. v-for 디렉티브를 사용해 목록을 반복 렌더링하고, **v-model**로 입력 필드와 데이터 바인딩을 처리함.

<div class="row mb-3">
  <div class="col">
    <div class="input-group">
      <input id="msg" type="text" class="form-control" placeholder="할 일을 입력하세요!" v-model.trim="todo" @keyup.enter="addTodo" />
      <span class="btn btn-primary input-group-addon" @click="addTodo">추가</span>
    </div>
  </div>
</div>

<div class="row">
  <div class="col">
    <ul class="list-group">
      <li v-for="todoitem in todolist" :key="todoitem.id" class="list-group-item"
          :class="{ 'list-group-item-success': todoitem.completed }"
          @click="toggleCompleted(todoitem.id)">
        <span :class="{ 'todo-done': todoitem.completed }">{{ todoitem.todo }} {{ todoitem.completed ? "(완료)" : "" }}</span>
        <span class="float-end badge bg-secondary pointer" @click.stop="deleteTodo(todoitem.id)">삭제</span>
      </li>
    </ul>
  </div>
</div>

4. 설명

  • v-model.trim="todo": 사용자 입력을 todo 데이터에 바인딩하고, 앞뒤 공백을 자동으로 제거.
  • @keyup.enter="addTodo": 사용자가 Enter 키를 눌렀을 때 addTodo() 메서드를 실행하여 새로운 할 일을 추가함.
  • v-for: todolist 배열을 순회하며 할 일 목록을 렌더링. key 속성은 Vue.js에서 리스트 렌더링 시 필요함.
  • 동적 클래스 바인딩: completed 상태에 따라 할 일이 완료되면 list-group-item-success 클래스를 적용해 배경색을 변경하고, todo-done 클래스로 취소선을 추가.
  • 이벤트 핸들러:
    • 클릭 시 완료 상태를 토글하고,
    • 삭제 버튼을 누르면 deleteTodo() 메서드로 해당 항목을 삭제함.
    • @click.stop: 클릭 이벤트가 버블링하지 않도록 막음.

7.7 TodoList 예제 리팩토링 – 컴포넌트와 이벤트 최적화


1. 컴포넌트 분할과 정의

기존 TodoList 예제를 컴포넌트 단위로 분리하고 재구성합니다. 컴포넌트 분할의 주된 목적은 재사용성, 디버깅 편의성, 렌더링 최적화를 달성하기 위함입니다.
Vue.js에서 가상 DOM은 변경된 부분만 렌더링되지만, 컴포넌트가 잘못 구성되면 불필요한 렌더링이 발생할 수 있습니다. 따라서 데이터 단위에 따라 컴포넌트를 분리하는 것이 좋습니다.


2. 컴포넌트 분할 기준

  1. 기능별 분리: 각 컴포넌트에 2~3개 이상의 기능이 들어가지 않도록 설계.
  2. 렌더링 최적화: 변경된 데이터만 다시 렌더링되도록 컴포넌트를 나눔.
  3. 재사용성 향상: 독립적인 컴포넌트로 재사용을 용이하게 만듦.
  4. 이벤트 관리의 명확성: 이벤트 흐름을 쉽게 파악할 수 있도록 이벤트를 컴포넌트별로 나누고 mitt와 같은 전역 이벤트 관리 라이브러리 활용.

3. 컴포넌트 구조 정의

아래와 같이 App.vue를 중심으로 4개의 컴포넌트를 분리합니다:

1) App.vue (최상위 컴포넌트)

  • data: todoList 배열 관리
  • methods:
    • addTodo(todo): 새로운 할 일 추가
    • deleteTodo(id): 할 일 삭제
    • toggleCompleted(id): 완료 상태 변경
  • 이벤트 수신: add-todo, delete-todo, toggle-completed

2) InputTodo.vue

  • data: 사용자 입력값 todo 관리
  • 발신 이벤트: add-todo

3) TodoList.vue

  • props: todoList 배열 전달받기
  • 이벤트 경유: delete-todo, toggle-completed

4) TodoListItem.vue

  • props: 개별 todoItem 전달
  • 발신 이벤트: delete-todo, toggle-completed

4. 코드 작성 예제

1) App.vue

<template>
  <div id="app" class="container">
    <div class="card card-body bg-light">
      <h1>:: Todolist App</h1>
    </div>
    <InputTodo @add-todo="addTodo" />
    <TodoList :todoList="todoList" 
              @delete-todo="deleteTodo" 
              @toggle-completed="toggleCompleted" />
  </div>
</template>

<script>
import InputTodo from './components/InputTodo.vue';
import TodoList from './components/TodoList.vue';

export default {
  name: 'App',
  components: { InputTodo, TodoList },
  data() {
    return {
      todoList: [
        { id: 1, todo: "자전거 타기", completed: false },
        { id: 2, todo: "딸과 공원 산책", completed: true },
        { id: 3, todo: "일요일 애견 카페", completed: false },
        { id: 4, todo: "Vue 원고 집필", completed: false }
      ]
    };
  },
  methods: {
    addTodo(todo) {
      this.todoList.push({ id: Date.now(), todo, completed: false });
    },
    deleteTodo(id) {
      this.todoList = this.todoList.filter(item => item.id !== id);
    },
    toggleCompleted(id) {
      const todo = this.todoList.find(item => item.id === id);
      todo.completed = !todo.completed;
    }
  }
};
</script>

2) InputTodo.vue

<template>
  <div class="input-group mb-3">
    <input type="text" v-model="todo" class="form-control" placeholder="할 일을 입력하세요" 
           @keyup.enter="emitAddTodo" />
    <button class="btn btn-primary" @click="emitAddTodo">추가</button>
  </div>
</template>

<script>
export default {
  name: 'InputTodo',
  data() {
    return {
      todo: ''
    };
  },
  methods: {
    emitAddTodo() {
      if (this.todo.trim()) {
        this.$emit('add-todo', this.todo);
        this.todo = '';
      }
    }
  }
};
</script>

3) TodoList.vue

<template>
  <ul class="list-group">
    <TodoListItem v-for="item in todoList" :key="item.id" :todoItem="item" 
                  @delete-todo="$emit('delete-todo', $event)" 
                  @toggle-completed="$emit('toggle-completed', $event)" />
  </ul>
</template>

<script>
import TodoListItem from './TodoListItem.vue';

export default {
  name: 'TodoList',
  components: { TodoListItem },
  props: {
    todoList: {
      type: Array,
      required: true
    }
  }
};
</script>

4) TodoListItem.vue

<template>
  <li class="list-group-item d-flex justify-content-between align-items-center" 
      :class="{ 'list-group-item-success': todoItem.completed }">
    <span @click="$emit('toggle-completed', todoItem.id)" 
          :class="{ 'text-decoration-line-through': todoItem.completed }">
      {{ todoItem.todo }}
    </span>
    <button class="btn btn-danger btn-sm" @click.stop="$emit('delete-todo', todoItem.id)">
      삭제
    </button>
  </li>
</template>

<script>
export default {
  name: 'TodoListItem',
  props: {
    todoItem: {
      type: Object,
      required: true
    }
  }
};
</script>

5. 리팩토링 개선점 요약

  1. 일관된 네이밍: 모든 컴포넌트에 Todo 접두사를 사용해 가독성을 높임.
  2. 이벤트 관리 단순화: mitt 대신 부모-자식 간 명확한 이벤트 흐름을 구현.
  3. 컴포넌트 분리와 재사용성 강화: 각 기능을 독립된 컴포넌트로 분리하여 유지보수가 용이해짐.
728x90
반응형