ksundev 님의 블로그

[TODO App][Refactoring] App 컴포넌트를 컨테이너로 만들기 2 본문

[개발] Vue.js/중급

[TODO App][Refactoring] App 컴포넌트를 컨테이너로 만들기 2

ksundev 2025. 7. 7. 17:11

할일 추가 & 할일 삭제 책임 App 컴포넌트로 옮기기

저번 포스트에서 컨테이너 패턴의 개념과 TodoList 컴포넌트의 리팩토링을 다뤘었죠. 이번에는 할일 추가와 삭제 기능을 App 컴포넌트로 옮겨서 완전한 컨테이너 패턴을 구현해보겠습니다.

1. 현재 문제점과 해결 방안

현재 발생하는 문제

저번 포스트에서 언급했듯이, 현재 Todo 앱에는 중요한 문제가 있습니다:

새로운 할 일을 추가해도 화면이 즉시 업데이트되지 않습니다.

문제의 원인

  1. 컴포넌트 간 통신 부재: TodoInput에서 할 일을 추가해도 TodoList가 이를 알 수 없습니다
  2. localStorage 동기화 문제: 데이터는 저장되지만 화면에 반영되지 않습니다
  3. 반응형 데이터 부족: 컴포넌트들이 서로의 상태 변화를 감지하지 못합니다

2. App 컴포넌트에 할일 추가/삭제 메서드 구현하기

App.vue에 메서드 추가

<template>
  <div id="app">
    <TodoHeader />
    <TodoInput @addTodo="addOneItem" />
    <TodoList :todoItems="todoItems" @removeItem="removeOneItem" />
    <TodoFooter />
  </div>
</template>

<script>
import TodoHeader from "./components/TodoHeader.vue";
import TodoInput from "./components/TodoInput.vue";
import TodoList from "./components/TodoList.vue";
import TodoFooter from "./components/TodoFooter.vue";

export default {
  data() {
    return {
      todoItems: [],
    };
  },
  created() {
    if (localStorage.length > 0) {
      for (var i = 0; i < localStorage.length; i++) {
        if (localStorage.key(i) !== "loglevel:webpack-dev-server") {
          this.todoItems.push(
            JSON.parse(localStorage.getItem(localStorage.key(i)))
          );
        }
      }
    }
  },
  methods: {
    // 새로운 할 일 추가 메서드
    addOneItem(todoItem) {
      var obj = { completed: false, item: todoItem };
      localStorage.setItem(todoItem, JSON.stringify(obj));
      this.todoItems.push(obj); // 배열에 직접 추가하여 즉시 UI 업데이트
    },
    // 할 일 삭제 메서드
    removeOneItem(todoItem, index) {
      localStorage.removeItem(todoItem.item);
      this.todoItems.splice(index, 1); // 배열에서 제거하여 즉시 UI 업데이트
    },
  },
  components: {
    TodoHeader,
    TodoInput,
    TodoList,
    TodoFooter,
  },
};
</script>

주요 추가사항

1) addOneItem() 메서드

addOneItem(todoItem) {
  var obj = { completed: false, item: todoItem };
  localStorage.setItem(todoItem, JSON.stringify(obj));
  this.todoItems.push(obj); // 핵심: 배열에 직접 추가
},

2) removeOneItem() 메서드

removeOneItem(todoItem, index) {
  localStorage.removeItem(todoItem.item);
  this.todoItems.splice(index, 1); // 핵심: 배열에서 직접 제거
},

3. TodoInput 컴포넌트를 이벤트 발생 컴포넌트로 변경

TodoInput.vue 수정

<template>
  <div class="inputBox shadow">
    <input
      type="text"
      placeholder="할 일을 입력하세요"
      v-model="newTodo"
      @keyup.enter="addTodo"
    />
    <span class="addContainer" @click="addTodo">
      <i class="fa-solid fa-plus addBtn"></i>
    </span>
  </div>
</template>

<script>
export default {
  data() {
    return {
      newTodo: "",
    };
  },
  methods: {
    addTodo() {
      if (this.newTodo !== "") {
        this.$emit('addTodo', this.newTodo); // 부모에게 이벤트 발생
        this.clearInput();
      }
    },
    clearInput() {
      this.newTodo = "";
    },
  },
};
</script>

TodoInput의 변화

1) 이벤트 발생으로 변경

// 이전: localStorage에 직접 저장
localStorage.setItem(this.newTodo, JSON.stringify(obj));

// 현재: 부모에게 이벤트 발생
this.$emit('addTodo', this.newTodo);

2) 책임 분리

  • 이전: 데이터 저장 + UI 관리
  • 현재: 사용자 입력 처리 + 이벤트 발생

4. TodoList 컴포넌트를 이벤트 발생 컴포넌트로 변경

TodoList.vue 수정

<template>
  <div>
    <ul>
      <li v-for="(todoItem, index) in todoItems" :key="index" class="shadow">
        <i
          class="checkBtn fa-solid fa-check"
          :class="{ checkBtnCompleted: todoItem.completed }"
          @click="$emit('toggleComplete', todoItem, index)"
        ></i>
        <span :class="{ textCompleted: todoItem.completed }">{{
          todoItem.item
        }}</span>
        <span class="removeBtn" @click="$emit('removeItem', todoItem, index)">
          <i class="fa-solid fa-trash-can"></i>
        </span>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  props: {
    todoItems: {
      type: Array,
      required: true,
    },
  },
  // data()와 methods() 제거 - 프레젠테이션 컴포넌트로 변경
};
</script>

TodoList의 변화

1) 이벤트 발생으로 변경

<!-- 이전: 직접 메서드 호출 -->
@click="removeTodo(todoItem, index)"

<!-- 현재: 부모에게 이벤트 발생 -->
@click="$emit('removeItem', todoItem, index)"

5. 완전한 컴포넌트 간 통신 구조

데이터 흐름

App 컴포넌트 (컨테이너)
├── todoItems 데이터 관리
├── localStorage 접근
├── 비즈니스 로직 처리
└── 하위 컴포넌트들과 통신
    ├── TodoInput ← addOneItem() 호출
    └── TodoList ← props로 데이터 전달, 이벤트 수신

이벤트 흐름

  1. TodoInput: @addTodo 이벤트 발생 → App의 addOneItem() 호출
  2. TodoList: @removeItem 이벤트 발생 → App의 removeOneItem() 호출

6. 리팩토링의 성과

1) 실시간 업데이트 해결

  • 새로운 할 일 추가 시 즉시 화면에 반영
  • 할 일 삭제 시 즉시 화면에서 제거

2) 완전한 관심사 분리

  • App: 데이터 관리와 비즈니스 로직
  • TodoInput: 사용자 입력 처리
  • TodoList: UI 렌더링

3) 예측 가능한 데이터 흐름

  • 모든 데이터 변경이 App 컴포넌트를 통해 이루어짐
  • 컴포넌트 간 의존성이 명확해짐

7. 다음 단계

이제 할일 추가와 삭제 기능이 완전히 동작하는 컨테이너 패턴이 구현되었습니다! 다음 포스트에서는 할 일 완료 상태 토글과 전체 초기화 기능을 추가하여 Todo 앱을 완전히 동작하는 상태로 만들어보겠습니다.