ksundev 님의 블로그

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

[개발] Vue.js/중급

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

ksundev 2025. 7. 7. 17:20

할일 완료 상태 토글 기능 구현하기

이번 포스트에서는 할 일의 완료 상태를 토글하는 기능을 구현해보겠습니다. 특히 Vue.js의 반응형 시스템과 관련된 중요한 개념인 RMT 패턴에 대해서도 함께 알아보겠습니다.

1. 현재 상황과 해결해야 할 문제

현재 TodoList 컴포넌트의 상태

현재 TodoList.vue는 프레젠테이션 컴포넌트로 변경되어 데이터 관리 기능이 App 컴포넌트로 이동했습니다. 하지만 완료 상태 토글 기능은 아직 구현되지 않았습니다.

해결해야 할 문제

  • 체크 아이콘 클릭 시 완료 상태 변경
  • 완료된 할 일의 시각적 표시 (취소선, 색상 변경)
  • localStorage에 변경된 상태 저장

2. App 컴포넌트에 토글 메서드 추가하기

App.vue에 toggleComplete 메서드 추가

<template>
  <div id="app">
    <TodoHeader />
    <TodoInput @addTodo="addOneItem" />
    <TodoList :todoItems="todoItems" @removeItem="removeOneItem" @toggleComplete="toggleComplete" />
    <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);
    },
    removeOneItem(todoItem, index) {
      localStorage.removeItem(todoItem.item);
      this.todoItems.splice(index, 1);
    },
    // 완료 상태 토글 메서드
    toggleComplete(todoItem, index) {
      this.todoItems[index].completed = !this.todoItems[index].completed;
    },
  },
  components: {
    TodoHeader,
    TodoInput,
    TodoList,
    TodoFooter,
  },
};
</script>

toggleComplete 메서드 분석

toggleComplete(todoItem, index) {
  this.todoItems[index].completed = !this.todoItems[index].completed;
},

3. RMT 패턴과 접근 방식의 차이점

방법 1: Props 객체 직접 변경 (문제가 될 수 있음)

// TodoList.vue에서 props로 받은 객체 직접 변경
methods: {
  toggleComplete(todoItem, index) {
    todoItem.completed = !todoItem.completed; // 문제: Vue가 감지하지 못할 수 있음
  }
}

방법 2: 반응형 데이터 직접 접근 (권장)

// App.vue에서 반응형 데이터 직접 변경
methods: {
  toggleComplete(todoItem, index) {
    this.todoItems[index].completed = !this.todoItems[index].completed; // 권장: Vue가 확실히 감지
  }
}

4. 왜 방법 2가 더 좋은가?

RMT 패턴의 원칙

  • R(React): 반응형 데이터에 직접 접근
  • M(Mutation): 데이터 변경
  • T(Trigger): Vue의 반응형 시스템이 변경을 감지

방법 1의 문제점

// Props로 받은 객체는 Vue의 반응형 시스템 밖에 있을 수 있음
props: {
  todoItems: {
    type: Array,
    required: true,
  },
},
methods: {
  toggleComplete(todoItem, index) {
    // todoItem은 props의 참조일 수 있어서 Vue가 감지하지 못할 수 있음
    todoItem.completed = !todoItem.completed;
  }
}

방법 2의 장점

// this.todoItems는 확실히 반응형 데이터
data() {
  return {
    todoItems: [], // Vue가 완전히 추적하는 반응형 데이터
  };
},
methods: {
  toggleComplete(todoItem, index) {
    // Vue의 반응형 시스템이 확실히 감지
    this.todoItems[index].completed = !this.todoItems[index].completed;
  }
}

5. 실제 동작 과정 비교

방법 1: Props 접근 시

// 1. Props로 객체 참조 받음
const todoItem = this.todoItems[0]; // 부모에서 온 참조

// 2. 객체 속성 변경
todoItem.completed = true; // 객체는 변경되지만

// 3. Vue의 반응: "이건 props에서 온 거라서 내가 추적하지 않을게"
// 결과: UI 업데이트 안됨

방법 2: 직접 접근 시

// 1. 내가 관리하는 배열에서 직접 접근
const targetItem = this.todoItems[0]; // 내가 관리하는 데이터

// 2. 객체 속성 변경
this.todoItems[0].completed = true; // 내 데이터 변경

// 3. Vue의 반응: "아! 내가 관리하는 데이터가 변경됐구나!"
// 결과: UI 업데이트됨

6. 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,
    },
  },
};
</script>

이벤트 발생 구조

<!-- 체크 아이콘 클릭 시 부모에게 이벤트 발생 -->
@click="$emit('toggleComplete', todoItem, index)"

7. 완성된 토글 기능의 동작

데이터 흐름

  1. 사용자 클릭: 체크 아이콘 클릭
  2. 이벤트 발생: TodoList에서 toggleComplete 이벤트 발생
  3. 메서드 호출: App 컴포넌트의 toggleComplete 메서드 실행
  4. 데이터 변경: this.todoItems[index].completed 값 토글
  5. UI 업데이트: Vue의 반응형 시스템이 자동으로 화면 업데이트

시각적 피드백

  • 완료되지 않은 할 일: 체크 아이콘 파란색, 텍스트 검정색
  • 완료된 할 일: 체크 아이콘 회색, 텍스트 취소선과 회색

8. RMT 패턴의 핵심 교훈

같은 객체, 다른 접근 방식

// 같은 객체를 참조하지만
const todoItem = this.todoItems[0]; // props 참조
this.todoItems[0]; // 직접 접근

// Vue의 반응형 시스템에서는 다른 의미
todoItem.completed = true;           // Props 간접 접근 (Vue 추적 안함)
this.todoItems[0].completed = true; // 직접 접근 (Vue 확실히 추적)

결론

RMT 패턴을 고려하면 반응형 데이터에 직접 접근하는 방법 2가 더 안전하고 확실합니다. 이 방법은 Vue의 반응형 시스템과 완벽하게 호환되어 모든 상황에서 안정적으로 작동하며, UI 업데이트가 보장됩니다.

다음 포스트에서는 전체 초기화 기능을 추가하여 Todo 앱을 완전히 동작하는 상태로 만들어보겠습니다!