PromleeBlog
sitemap
aboutMe

posting thumbnail
Vue와 TypeScript로 실전 Todo 앱 만들기 - 10편
Building a Todo App with Vue and TypeScript - Part 10

📅

🚀

들어가기 전에 🔗

드디어 Vue.js 기본기 다지기 시리즈의 마지막 편입니다.
이번 시간에는 이전과 다르게
TypeScript
를 Vue 프로젝트에 적용하여, 더욱 안정적이고 견고한 'Todo 앱'을 만들어 보는 것입니다.

TypeScript는 코드에 타입을 명시하여 개발 과정에서 발생할 수 있는 수많은 실수를 미리 방지해주고, 코드 자동 완성과 같은 편리한 기능들을 제공합니다.
Vue의 공식 프로젝트 생성 도구를 사용해 타입스크립트 개발 환경을 손쉽게 구축하고, 모든 코드를 타입과 함께 작성하는 실전 개발 흐름을 경험해 보겠습니다.
TODO 앱 만들기
TODO 앱 만들기

🚀

1. 프로젝트 생성하기 🔗

Vue는 타입스크립트를 완벽하게 지원하며, 공식 프로젝트 생성 도구를 사용하면 아주 간단하게 타입스크립트 개발 환경을 만들 수 있습니다.

1단계: 공식 도구로 프로젝트 생성 🔗

터미널을 열고 다음 명령어를 입력하세요.
이 명령어는 Vue 프로젝트 생성을 위한 공식 인터페이스를 실행합니다.
npm create vue@latest
이 명령어를 실행하면, 프로젝트 설정을 위한 몇 가지 질문이 나타납니다.
우리는 Todo 앱에 필요한 기능들을 선택하여 답변하겠습니다.
Need to install the following packages:
create-vite@6.5.0
Ok to proceed? (y) y
│
◇  Project name:
│  vue-todo-app
│
◇  Select a framework:
│  Vue
│
◇  Select a variant:
│  TypeScript
│
◇  Scaffolding project in C:\projects\vue-todo-app...
│
└  Done. Now run:

  cd vue-todo-app
  npm install
  npm run dev

2단계: 프로젝트 설정 완료 🔗

생성이 완료되면, 안내에 따라 폴더로 이동하여 필요한 라이브러리들을 설치합니다.
cd vue-ts-todo-app
npm install
이제 npm run dev 명령어로 개발 서버를 시작할 수 있습니다.
npm run dev

🚀

2. 프로젝트 구조 설계 및 초기 설정 🔗

코드를 작성하기에 앞서, 프로젝트의 구조를 파악하고 데이터 타입을 정의하는 것은 좋은 습관입니다.

1단계: 생성된 폴더 구조 이해하기 🔗

npm create vue@latest 도구는 실무에서 쓰기 좋은 표준적인 폴더 구조를 자동으로 만들어 줍니다.
우리의 Todo 앱은 다음과 같은 구조로 파일을 구성하겠습니다.
📦src
 ┣ 📂components  // 재사용 가능한 작은 UI 조각들
 ┃ ┣ 📜TodoForm.vue  // 새 할 일을 입력받는 폼 컴포넌트
 ┃ ┣ 📜TodoItem.vue  // 개별 할 일 항목 하나하나를 나타내는 아이템 컴포넌트
 ┃ ┗ 📜TodoList.vue  // 할 일 목록 전체를 보여주는 리스트 컴포넌트
 ┣ 📂router  // 라우팅 설정 파일
 ┃ ┗ 📜index.ts  // Vue Router 설정 파일
 ┣ 📂stores  // Pinia 상태 관리 로직
 ┃ ┗ 📜todoStore.js  // 할 일 목록 데이터와 관련 함수들을 관리하는 스토어
 ┣ 📂types  // 데이터 타입 정의
 ┃ ┗ 📜index.ts  // Todo 객체의 구조를 정의하는 파일
 ┣ 📂views  // 페이지 단위의 컴포넌트
 ┃ ┗ 📜HomeView.vue  // 우리의 Todo 앱 메인 페이지
 ┣ 📜App.vue
 ┗ 📜main.ts

2단계: 데이터 타입 정의하기 (interface) 🔗

TypeScript를 사용할 때 가장 먼저 할 일 중 하나는 애플리케이션에서 사용할 데이터의 구조를 정의하는 것입니다.
src 폴더 아래에
types
라는 새 폴더를 만들고, 그 안에 index.ts 파일을 생성하여 Todo 객체의 구조를 정의합니다.
src/types/index.ts
export interface Todo {
  id: number;
  text: string;
  completed: boolean;
}
이렇게 interface를 정의해두면, 앞으로 Todo 타입의 객체는 반드시 위 세 가지 속성을 가져야 함을 코드상에서 강제할 수 있어 실수를 방지합니다.

🚀

3. 타입이 적용된 Pinia 스토어 만들기 🔗

다음 명령어로 Pinia를 설치합니다. Pinia는 Vue의 공식 상태 관리 라이브러리로, Vuex의 대안으로 많이 사용됩니다.
npm install pinia
이제 Pinia를 Vue 애플리케이션에 통합합니다. src/main.ts 파일을 열고 다음 코드를 추가합니다.
src/main.ts
import { createApp } from "vue";
import { createPinia } from "pinia";
import "./style.css";
import App from "./App.vue";
 
const app = createApp(App);
app.use(createPinia());
app.mount("#app");
이제 Pinia 스토어에 타입스크립트를 적용해 보겠습니다. src/stores/ 폴더 안에 기본으로 생성된 counter.ts 파일을 todoStore.ts로 이름을 바꾸고, 아래와 같이 수정합니다.
src/stores/todoStore.ts
import { ref } from 'vue'
import { defineStore } from 'pinia'
import type { Todo } from '../types' // 2단계에서 정의한 Todo 타입을 불러옵니다.
 
export const useTodoStore = defineStore('todo', () => {
  // ref에 제네릭(<>)을 사용하여 이 데이터가 Todo 객체들의 배열임을 명시합니다.
  const todos = ref<Todo[]>([])
  const nextId = ref(1)
 
  function fetchTodos() {
    todos.value = [
      { id: nextId.value++, text: 'Vue.js + TypeScript 공부하기', completed: true },
      { id: nextId.value++, text: '타입 적용하기', completed: false },
    ]
  }
 
  // 함수의 매개변수에도 타입을 지정하여 의도를 명확히 합니다.
  function addTodo(newTodoText: string) {
    if (!newTodoText.trim()) return
    todos.value.unshift({
      id: nextId.value++,
      text: newTodoText,
      completed: false
    })
  }
 
  function deleteTodo(idToDelete: number) {
    todos.value = todos.value.filter(todo => todo.id !== idToDelete)
  }
 
  function toggleTodo(idToToggle: number) {
    const todo = todos.value.find(todo => todo.id === idToToggle)
    if (todo) {
      todo.completed = !todo.completed
    }
  }
 
  return { todos, fetchTodos, addTodo, deleteTodo, toggleTodo }
})
ref<Todo[]>와 같이 타입을 명시하면, todos 변수는 항상 Todo 인터페이스를 따르는 객체들의 배열이어야 한다는 규칙이 생깁니다.

🚀

4. 컴포넌트 만들기 🔗

이제 화면을 구성하는 컴포넌트들을 타입스크립트로 작성해 보겠습니다. 모든 <script> 태그는 <script setup lang="ts">로 시작합니다.

TodoItem.vue 🔗

개별 할 일 목록
먼저 src/components/ 폴더에 TodoItem.vue 파일을 생성합니다. 이 컴포넌트는 할 일 하나를 표시합니다.
src/components/TodoItem.vue
<script setup lang="ts">
import type { Todo } from '../types'
 
defineProps<{
  todo: Todo
}>()
 
const emit = defineEmits<{
  (e: 'toggle-todo'): void
  (e: 'delete-todo'): void
}>()
</script>
 
<template>
  <li :class="{ completed: todo.completed }">
    <span @click="emit('toggle-todo')">{{ todo.text }}</span>
    <button @click="emit('delete-todo')">삭제</button>
  </li>
</template>
 
<style scoped>
li { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #eee; }
.completed span { text-decoration: line-through; color: #999; }
span { cursor: pointer; }
</style>

TodoList.vue 🔗

할 일 목록
다음은 TodoItem들을 모아 목록으로 보여줄 TodoList.vue 파일을 src/components/ 폴더에 생성합니다.
src/components/TodoList.vue
<script setup lang="ts">
import { useTodoStore } from '../stores/todoStore'
import TodoItem from './TodoItem.vue'
 
const store = useTodoStore()
 
function onToggle(id: number) {
  store.toggleTodo(id)
}
 
function onDelete(id: number) {
  store.deleteTodo(id)
}
</script>
 
<template>
  <ul>
    <TodoItem
      v-for="todo in store.todos"
      :key="todo.id"
      :todo="todo"
      @toggle-todo="onToggle(todo.id)"
      @delete-todo="onDelete(todo.id)"
    />
  </ul>
</template>
 
<style scoped>
ul {
  list-style: none;
  padding: 0;
}
</style>

TodoForm.vue 🔗

새 할 일 입력 폼
마지막으로 새 할 일을 입력받을 TodoForm.vue 파일을 src/components/ 폴더에 생성합니다.
src/components/TodoForm.vue
<script setup lang="ts">
import { ref } from 'vue'
import { useTodoStore } from '../stores/todoStore'
 
const newTodoText = ref<string>('')
const store = useTodoStore()
 
function handleSubmit() {
  store.addTodo(newTodoText.value)
  newTodoText.value = '' // 입력창 비우기
}
</script>
 
<template>
  <form @submit.prevent="handleSubmit">
    <input
      v-model.trim="newTodoText"
      placeholder="새로운 할 일을 입력하세요"
    />
    <button type="submit">추가</button>
  </form>
</template>
 
<style scoped>
form {
  display: flex;
  margin-bottom: 16px;
}
input {
  flex-grow: 1;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
}
button {
  padding: 8px 12px;
  margin-left: 8px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

🚀

5. 최종 조립 및 라우터 연결 🔗

이제 모든 재료가 준비되었습니다. 이들을 한데 모아 우리만의 애플리케이션을 완성할 차례입니다.
먼저, 다음 명령어로 Vue Router를 설치합니다. Vue Router는 Vue 애플리케이션의 라우팅을 관리하는 공식 라이브러리입니다.
npm install vue-router@4
이제 Vue Router를 설정합니다. src/router/index.ts 파일을 생성하고 다음 코드를 작성합니다.
src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
 
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    }
    // '/about' 경로는 깨끗하게 지워주세요.
  ]
})
 
export default router
pinia와 마찬가지로, Vue Router도 Vue 애플리케이션에 통합해야 합니다.
src/main.ts 파일을 열고 다음 코드를 작성합니다.
src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
 
import App from './App.vue'
import router from './router'
 
const app = createApp(App)
 
app.use(createPinia())
app.use(router)
 
app.mount('#app')

1단계: App.vue 정리하기 🔗

가장 먼저, 우리 앱의 껍데기 역할을 하는 src/App.vue 파일을 수정해야 합니다.
프로젝트를 처음 만들면 이 파일은 Vue의 기본 환영 페이지 코드로 가득 차 있습니다.
우리는 이 내용을 모두 지우고, 앞으로 만들 페이지들이 보여질 무대인 <RouterView />만 남겨두어야 합니다.
src/App.vue
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
 
<template>
  <RouterView />
</template>
 
<style>
/* 전역 스타일을 이곳이나 src/assets/main.css에 작성할 수 있습니다. */
body {
  background-color: #f0f2f5;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
</style>

2단계: HomeView.vue 페이지 구성하기 🔗

이제 src/views/HomeView.vue 파일에서 우리가 만든 컴포넌트들을 조립하여 Todo 앱 페이지를 완성합니다.
src/views/HomeView.vue
<script setup lang="ts">
import { onMounted } from 'vue'
import { useTodoStore } from '../stores/todoStore'
import TodoForm from '../components/TodoForm.vue'
import TodoList from '../components/TodoList.vue'
 
const store = useTodoStore()
 
// 컴포넌트가 화면에 그려질 때 초기 데이터를 불러옵니다.
onMounted(() => {
  store.fetchTodos()
})
</script>
 
<template>
  <main>
    <h1>My Todo App</h1>
    <TodoForm />
    <TodoList />
  </main>
</template>
 
<style scoped>
main {
  max-width: 500px;
  margin: 40px auto;
  padding: 20px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  border-radius: 8px;
  background-color: white;
}
h1 {
  text-align: center;
  color: #333;
}
</style>
모든 파일을 저장한 후, 터미널에서 npm run dev를 실행하여 우리가 만든 Todo 앱을 확인해 보세요.
TODO 앱 완성
TODO 앱 완성

🚀

결론 🔗

우리는 Vue와 TypeScript를 사용하여, 처음부터 끝까지 하나의 완전한 웹 애플리케이션을 만들어 보았습니다.

이번 최종 프로젝트를 통해 우리는 다음의 과정을 경험했습니다.

TypeScript를 사용하는 것은 프로젝트의 규모가 커질수록 유지보수성을 높이는 좋은 방법입니다.
오늘 배운 내용을 바탕으로 앞으로의 프로젝트에도 자신 있게 적용해 보시길 바랍니다.

참고 🔗