MVC 패턴에서 Model을 변경하는 View와의 상호작용이 많아지면서 Model간 상태를 변경하는 등 커지는 데이터 흐름을 통제하지 못하는 문제점이 발생되었습니다.
이러한 문제점의 근본은 데이터의 흐름을 관리하지 못하는 데에서 기인했다고 볼 수 있습니다. 데이터의 흐름이 추적되지 않거나 추적이 어려우면 그만큼 디버깅이 어려워지고 이는 에러 발생시 소모되는 시간과 비용이 증가한다는 의미와 같습니다.
MVC 패턴을 포함한 비슷한 패턴들이 가지는 문제점들을 보완하기 위한 디자인 패턴이 바로 Flux 패턴입니다.
Vue 애플리케이션에서는 데이터 흐름에 따라 3가지의 상태관리 유형이 있습니다.
아래에서는 동일한 todolist에 대한 3가지 유형의 구조를 살펴보고 그 차이점과 상태관리의 필요성 등에 대해서 알아보도록 하겠습니다.
<!-- App.vue -->
<template>
<div id="app">
<TodoHeader />
<!-- 🛰 🕹 -->
<TodoInput v-on:addTodo="addTodo" />
<!-- 🛰 🕹🕹 📑 -->
<TodoList
v-bind:todos="todos"
v-on:removeTodo="removeTodo"
v-on:toggleCompleted="toggleCompleted"
/>
<!-- 🛰 🕹 -->
<TodoFooter v-on:allClear="allClear" />
</div>
</template>
<script>
import TodoHeader from "./components/TodoHeader";
import TodoInput from "./components/TodoInput";
import TodoList from "./components/TodoList";
import TodoFooter from "./components/TodoFooter";
export default {
name: "App",
// 📦
data() {
return {
todos: [],
id: 0,
};
},
// 🎮 🕹🕹🕹🕹
methods: {
addTodo(newTodoItem) {
const { item, completed } = newTodoItem;
this.todos = [...this.todos, { item, completed }];
localStorage.setItem(
`todo-${newTodoItem.item}`,
JSON.stringify(newTodoItem)
);
},
removeTodo(targetTodo) {
this.todos = this.todos.filter((todo) => todo.item !== targetTodo);
localStorage.removeItem(`todo-${targetTodo}`);
},
toggleCompleted(targetTodo) {
this.todos = this.todos.map((todo) =>
todo.item === targetTodo
? { ...todo, completed: !todo.completed }
: todo
);
let todo = JSON.parse(localStorage.getItem(`todo-${targetTodo}`));
localStorage.setItem(
`todo-${targetTodo}`,
JSON.stringify({
...todo,
completed: !todo.completed,
})
);
},
allClear() {
this.todos = [];
localStorage.clear();
},
},
// 🗄 📁📁📁📁
components: {
TodoHeader,
TodoInput,
TodoList,
TodoFooter,
},
created: function () {
if (localStorage.length > 0) {
for (let i = 0; i < localStorage.length; i++) {
this.todos.push(JSON.parse(localStorage.getItem(localStorage.key(i))));
}
}
},
};
</script>
<!-- Style 태그는 생략했습니다 -->
<!-- TodoInput.vue -->
<script>
export default {
// 📦
data: function () {
return {
newTodoItem: {
completed: false,
item: "",
},
showModal: false,
};
},
methods: {
addTodo: function () {
if (this.newTodoItem.item === "") {
this.showModal = true;
return;
} else {
this.$emit("addTodo", this.newTodoItem);
this.clearInput();
}
},
clearInput: function () {
this.newTodoItem.item = "";
},
},
components: {
Modal: ModalComponent,
},
};
</script>
<!-- TodoList.vue -->
<script>
export default {
// 📑
props: ["todos"],
methods: {
removeTodo: function(todo) {
// 📡
this.$emit("removeTodo", todo);
},
toggleCompleted: function(todo) {
// 📡
this.$emit("toggleCompleted", todo);
}
}
};
</script>
<!-- TodoFooter.vue -->
<script>
export default {
methods: {
allClear: function() {
// 📡
this.$emit("allClear");
}
}
};
</script>
<!-- App.vue -->
<template>
<div id="app">
<TodoHeader />
&<TodoInput />
<TodoList />
<TodoFooter />
</div>
</template>
<script>
import TodoHeader from "./components/TodoHeader";
import TodoInput from "./components/TodoInput";
import TodoList from "./components/TodoList";
import TodoFooter from "./components/TodoFooter";
export default {
name: "App",
components: {
TodoHeader,
TodoInput,
TodoList,
TodoFooter
}
};
</script>
<!-- TodoInput.vue -->
<script>
// 🚌
import EventBus from "./common/EventBus";
export default {
data: function() {
return {
newTodoItem: {
completed: false,
item: ""
},
showModal: false
};
},
methods: {
addTodo: function() {
if (this.newTodoItem.item === "") {
this.showModal = true;
return;
} else {
// 🚌 📡
EventBus.$emit("addTodo", this.newTodoItem);
this.clearInput();
}
},
clearInput: function() {
this.newTodoItem.item = "";
}
},
components: {
Modal: ModalComponent
}
};
</script>
<!-- TodoList.vue -->
<script>
// 🚌
import EventBus from "./common/EventBus";
export default {
// 📦
data: function() {
return {
todos: []
};
},
// 🎮
methods: {
// 🕹
removeTodo: function(targetTodo) {
this.todos = this.todos.filter(todo => todo.item !== targetTodo);
localStorage.removeItem(`todo-${targetTodo}`);
},
// 🕹
toggleCompleted: function(targetTodo) {
this.todos = this.todos.map(todo =>
todo.item === targetTodo
? { ...todo, completed: !todo.completed }
: todo
);
let todo = JSON.parse(localStorage.getItem(`todo-${targetTodo}`));
localStorage.setItem(
`todo-${targetTodo}`,
JSON.stringify({
...todo,
completed: !todo.completed
})
);
},
// 🕹
addTodo: function(newTodoItem) {
const { item, completed } = newTodoItem;
this.todos = [...this.todos, { item, completed }];
localStorage.setItem(
`todo-${newTodoItem.item}`,
JSON.stringify(newTodoItem)
);
},
// 🕹
allClear: function() {
this.todos = [];
localStorage.clear();
}
},
created: function() {
if (localStorage.length > 0) {
for (let i = 0; i < localStorage.length; i++) {
this.todos.push(JSON.parse(localStorage.getItem(localStorage.key(i))));
}
}
// 🛰🛰🛰🛰
EventBus.$on("addTodo", this.addTodo);
EventBus.$on("removeTodo", this.removeTodo);
EventBus.$on("toggleCompleted", this.toggleCompleted);
EventBus.$on("allClear", this.allClear);
}
};
</script>
<!-- TodoClear.vue -->
<script>
// 🚌
import EventBus from "./common/EventBus";
export default {
methods: {
allClear: function() {
// 🚌 📡
EventBus.$emit("allClear");
}
}
};
</script>
<!-- store.js -->
import Vue from "vue";
import Vuex from "vuex";
import { storage } from './mutations';
Vue.use(Vuex);
export const store = new Vuex.Store({
// 📦
state: {
todos: storage.getTodos(),
},
// 🛰 🎮
mutations: {
// 🕹
addTodo: function (state, newTodoItem) {
const { item, completed } = newTodoItem;
state.todos = [...state.todos, { item, completed }];
storage.createTodo({ item, completed });
},
// 🕹
removeTodo: function (state, targetTodo) {
state.todos = state.todos.filter((todo) => todo.item !== targetTodo);
storage.removeTodo(targetTodo);
},
// 🕹
toggleCompleted: function (state, targetTodo) {
state.todos = state.todos.map((todo) =>
todo.item === targetTodo
? { ...todo, completed: !todo.completed }
: todo
);
storage.toggleComplted(targetTodo);
},
// 🕹
clearAll: function (state) {
state.todos = [];
storage.clearAll();
},
},
});
this.$store.state.todos
와 같은 깐깐징어처럼 정의된 방법을 따라야 합니다.<!-- App.vue -->
<template>
<div id="app">
<TodoHeader />
<TodoInput />
<TodoList />
<TodoFooter />
</div>
</template>
<script>
import TodoHeader from "./components/TodoHeader";
import TodoInput from "./components/TodoInput";
import TodoList from "./components/TodoList";
import TodoFooter from "./components/TodoFooter";
export default {
name: "App",
components: {
TodoHeader,
TodoInput,
TodoList,
TodoFooter,
},
};
</script>
<!-- TodoInput.vue -->
<script>
export default {
// 📦
data: function() {
return {
newTodoItem: {
completed: false,
item: ""
},
showModal: false
};
},
methods: {
addTodo: function() {
if (this.newTodoItem.item === "") {
this.showModal = true;
return;
} else {
// 📡 🕹
this.$store.commit("addTodo", this.newTodoItem);
this.clearInput();
}
},
clearInput: function() {
this.newTodoItem.item = "";
}
},
components: {
Modal: ModalComponent
}
};
</script>
<!-- TodoList.vue -->
<template>
<div class="todolist-container">
<ul>
<!-- 📡 📑 -->
<li
v-for="todo in this.$store.state.todos"
v-bind:key="todo.item"
name="todo.item"
>
<span v-on:click="toggleCompleted(todo.item)">
<img v-show="!todo.completed" src="../assets/circle-icon.png" />
<img v-show="todo.completed" src="../assets/check-circle-icon.png" />
</span>
{{ todo.item }}
<span v-on:click="removeTodo(todo.item)">
<img src="../assets/trash-icon.png" />
</span>
</li>
</ul>
</div>
</template>
<script>
export default {
methods: {
removeTodo: function (todo) {
// 📡 🕹
this.$store.commit("removeTodo", todo);
},
toggleCompleted: function (todo) {
// 📡 🕹
this.$store.commit("toggleCompleted", todo);
},
},
};
</script>
<!-- TodoFooter.vue -->
<template>
<div class="footer-container">
<span class="all-clear-button" v-on:click="allClear">All Clear</span>
</div>
</template>
<script>
export default {
methods: {
allClear: function () {
// 📡 🕹
this.$store.commit("clearAll");
},
},
};
</script>