vue의 state 흐름이 어떻게 이루어지 알아보기 위해 간단하게 CRUD를 구현할 수 있는 TodoList를 만들어봤다. 리액트는 useState를 통해 상태를 업데이트할 수 있었는데 vue와는 어떤 차이가 있을지도 궁금했다.
공식 문서를 보면 프로젝트를 시작하는 방법부터 자세하게 나와있어서 차근차근 따라해보았는데 생각보다 크게 어렵지는 않았던 것 같다.
1. Vue 프로젝트 시작하기
npm init vue@latest
해당 명령어를 입력하면 아래와 같이 옵션을 선택할 수 있는데, 내가 원하는 상황에 맞게 옵션을 선택해주면 빠르게 만들 수 있다. 나는 router 설정과 Eslint, prettier 옵션을 선택한 후 프로젝트를 생성했다.
✔ Project name: … <your-project-name>
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit testing? … No / Yes
✔ Add Cypress for both Unit and End-to-End testing? … No / Yes
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
Scaffolding project in ./<your-project-name>...
Done.
프로젝트가 제대로 생성 되었다면 해당 프로젝트 폴더로 이동 후, 해당 프로젝트를 실행시킨다.
npm install
npm run dev
2. 옵션 API(Options API)
API 의 종류는 크게 두가지로 나뉘는 것 같았는데 옵션 api와 컴포지션 api이다. 먼저 options API에 대해서 살펴보면 각각의 옵션 별로 그룹화해놓은 것처럼 보였다. <template> 태그 안에는 화면에 보여줄 부분을 작성하고 <script> 태그 안에서는 함수나 데이터, 컴포넌트 등의 옵션에 해당하는 부분들을 작성하게 된다.
<template>
<h2>{{ count }}</h2>
<button @click="update">증가</button>
<button @click="delete">감소</button>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
update() {
this.count++
},
delete() {
this.count--
}
},
}
</script>
위의 예제처럼 options API를 이용해 간단한 todoList를 구현해보았다. 처음 해보는 것이기 때문에 컴포넌트는 따로 나누지 않았다.
<template>
<div class="todo">
<h2>TodoList 만들기</h2>
<div class="inputBox">
<input type="text" placeholder="할 일을 입력하세요" :value="todoText" @input="typing" @keyup.enter="addTodo"/>
<button @click="addTodo">추가하기</button>
</div>
<div class="todoListBox">
<div class="todoList" v-for="todo in todoList" :key="todo.id">
<li :class="todo.isDone === false ? 'contents' : 'contentsDone'">{{ todo.content }}</li>
<div>
<button @click ="doneTodo(todo.id)">{{ todo.isDone === false ? '완료' : '취소'}}</button>
<button @click ="deleteTodo(todo.id)">삭제</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Home",
data() {
return {
todoList: [
{
id: 1,
content: '마트 가기',
isDone: false
},
{
id: 2,
content: '강아지 산책 시키기',
isDone: false
}
],
todoText: ''
};
},
methods: {
addTodo() {
if(this.todoText === '') {
alert('할 일을 입력하세요!');
return;
}
this.todoList = [...this.todoList, {id: this.todoList.length + 1, content: this.todoText, isDone: false}]
this.todoText = ''
},
deleteTodo(id) {
this.todoList = this.todoList.filter((ele) => ele.id !== id)
},
doneTodo(id) {
console.log(id)
this.todoList = this.todoList.map((todo) => todo.id === id ? {...todo, isDone: !todo.isDone} : todo)
},
typing(e) {
this.todoText = e.target.value
}
},
}
</script>
3. 컴포지션 API(Composition API)
컴포지션 API는 리액트를 사용하는 것과 조금 비슷했던 것 같은데 기능별로 만들 때 보기 편할 것 같다는 생각이 들었다. Composition API는 <script setup> 스크립트 태그 안에 setup이라는 속성을 사용한다.
<script setup>
import { ref } from 'vue'
const count = ref(0)
function update() {
count.value++
}
function delete() {
count.value++
}
</script>
<template>
<h2>{{ count }}</h2>
<button @click="update">증가</button>
<button @click="delete">감소</button>
</template>
컴포지션 API를 사용해서 구현한 TodoList는 다음과 같다. 리액트에서 사용하는 useRef와 vue에서 사용하는 ref의 개념은 살짝 다른 것 같았는데 여기선 state를 ref를 가지고 관리하는 것 같았다.
<script setup>
import { ref } from 'vue'
const todoList = ref([])
const todoText = ref('')
function addTodo() {
if(todoText.value === "") {
alert('내용을 입력하세요!')
return;
}
todoList.value = [...todoList.value, {id: todoList.value.length + 1, content: todoText.value, isDone:false}]
todoText.value = ""
}
function doneTodo(id) {
todoList.value = todoList.value.map((todo) => todo.id === id ? {...todo, isDone: !todo.isDone} : todo)
}
function deleteTodo(id) {
todoList.value = todoList.value.filter((ele) => ele.id !== id)
}
function typing(e) {
todoText.value = e.target.value
}
</script>
<template>
<div class="todo">
<h2>TodoList 만들기</h2>
<div class="inputBox">
<input type="text" placeholder="할 일을 입력하세요" :value="todoText" @input="typing" @keyup.enter="addTodo"/>
<button @click="addTodo">추가하기</button>
</div>
<div class="todoListBox">
<div class="todoList" v-for="todo in todoList" :key="todo.id">
<li :class="todo.isDone === false ? 'contents' : 'contentsDone'">{{ todo.content }}</li>
<div>
<button @click ="doneTodo(todo.id)">{{ todo.isDone === false ? '완료' : '취소'}}</button>
<button @click ="deleteTodo(todo.id)">삭제</button>
</div>
</div>
</div>
</div>
</template>
간단하게 todoList를 구현하며 리액트와의 차이를 조금 알게되었는데 ref뿐만 아니라 화면에 뿌려줄 때 map 대신 v-for 반복문을 사용하는 것이 일반적이라는 것을 알게되었다. 그렇다고 map을 쓰지 못하는 것은 아니었다.