ksundev 님의 블로그

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

[개발] Vue.js/중급

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

ksundev 2025. 7. 7. 15:08

컨테이너란?

저번 포스트에서 간단히 언급했었죠?
컨테이너라는 것은 비즈니스 로직을 관리하고, 하위 컴포넌트들에게 데이터를 전달하는 역할을 하는 컴포넌트입니다.
간단히 생각하자면 애플리케이션의 두뇌역할을 한다고 생각하면 됩니다.

컨테이너의 주요 특징

1) 데이터 관리

  • 애플리케이션의 상태(state)를 중앙에서 관리
  • 여러 컴포넌트가 공유하는 데이터를 보관
  • 데이터의 변경과 업데이트를 담당

2) 비즈니스 로직 처리

  • 복잡한 계산이나 데이터 처리 로직을 포함
  • API 호출이나 외부 서비스와의 통신
  • 이벤트 핸들링과 상태 변경 로직

3) 하위 컴포넌트 제어

  • 자식 컴포넌트들에게 필요한 데이터를 props로 전달
  • 자식 컴포넌트에서 발생한 이벤트를 처리
  • 컴포넌트 간의 통신을 중재

리팩토링

1. TodoList 컴포넌트의 책임(역할) App 컴포넌트로 옮기기

현재 TodoList 컴포넌트의 책임

현재 TodoList.vue는 다음과 같은 책임을 가지고 있습니다:

  1. 데이터 관리: todoItems 배열을 관리
  2. localStorage 접근: 데이터 로드 및 저장
  3. 비즈니스 로직: 할 일 삭제, 완료 상태 토글
  4. UI 렌더링: 할 일 목록을 화면에 표시

리팩토링 목표

TodoList 컴포넌트의 데이터 관리와 비즈니스 로직을 App 컴포넌트로 옮겨서 컨테이너 패턴을 구현하겠습니다.

2. App 컴포넌트에 데이터와 메서드 추가하기

App.vue 수정

<template>
  <div id="app">
    <TodoHeader />
    <TodoInput />
    <TodoList :propsdata="todoItems" />
    <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)))
          );
        }
      }
    }
  },
  components: {
    TodoHeader,
    TodoInput,
    TodoList,
    TodoFooter,
  },
};
</script>
<style>
  <!-- 생략 -->
</style>

주요 변경사항

1) 데이터 중앙화

data() {
  return {
    todoItems: [], // App 컴포넌트에서 중앙 관리
  };
},

2) 생명주기 훅 이동

created() {
  // localStorage에서 데이터 로드하는 로직을 App으로 이동
  if (localStorage.length > 0) {
    // ... 데이터 로드 로직
  }
},

3. 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('toggle-complete', todoItem, index)"
        ></i>
        <span :class="{ textCompleted: todoItem.completed }">{{
          todoItem.item
        }}</span>
        <span class="removeBtn" @click="$emit('remove-todo', 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) props로 데이터 받기

props: {
  todoItems: {
    type: Array,
    required: true
  }
},

2) 책임 분리

  • 이전: 데이터 관리 + UI 렌더링
  • 현재: UI 렌더링만 담당

4. 컴포넌트 간 통신 구조

데이터 흐름

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

이벤트 흐름

  1. TodoInput: @add-todo 이벤트 발생 → App의 addTodo() 호출
  2. TodoList: @remove-todo, @toggle-complete 이벤트 발생 → App의 메서드 호출
  3. TodoFooter: @clear-all 이벤트 발생 → App의 clearAll() 호출

5. 리팩토링의 장점

1) 실시간 업데이트 해결

  • App 컴포넌트에서 데이터를 중앙 관리하므로 즉시 화면에 반영
  • 새로운 할 일 추가 시 todoItems 배열에 직접 추가되어 UI 업데이트

2) 관심사 분리

  • App: 데이터 관리와 비즈니스 로직
  • TodoList: UI 렌더링만 담당

3) 재사용성 향상

  • TodoList는 다양한 데이터 소스와 함께 사용 가능
  • 비즈니스 로직 변경 시 App 컴포넌트만 수정하면 됨

4) 테스트 용이성

  • 비즈니스 로직과 UI 로직을 분리해서 테스트 가능

6. 다음 단계

이제 TodoInput과 TodoFooter 컴포넌트도 같은 방식으로 리팩토링하여 완전한 컨테이너 패턴을 구현할 수 있습니다. 이를 통해 컴포넌트 간 통신 문제를 완전히 해결하고, 더 유지보수하기 쉬운 구조를 만들 수 있을 것입니다.