할일 목록을 만들면서 Vue3 배워보자 - 01

> 할일 목록을 만들면서 Vue3 배워보자 - 02

   할일 목록을 만들면서 Vue3 배워보자 - 03

 


 

먼저 Store 없이, emits을 통해서 데이터 통신하는 방식으로 개발해보겠습니다.

Style 영역은 01과 달라진 부분이 없으므로 코드에 나타내지 않겠습니다.

 

 

기능 개발 - 01 등록하기

 

views/App.vue

<template>
  <div class="container">
    <div class="row">
      <base-header />
    </div>
    <div class="row">
      <todo-input @update:todo="handleUpdateTodoList" />
    </div>
    <div class="row">
      <router-view :todoList="todoList" />
    </div>
  </div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import BaseHeader from "@/components/header.vue";
import TodoInput from "@/components/todo-input.vue";

interface TodoItem {
  id: number;
  title: string;
  status: "active" | "clear";
}

const todoList = ref([
  {
    id: 0,
    title: "청소하기",
    status: "active",
  },
  {
    id: 1,
    title: "공과금 내기",
    status: "active",
  },
  {
    id: 2,
    title: "운동 30분하기",
    status: "clear",
  },
] as TodoItem[]);

const handleUpdateTodoList = (item: TodoItem) => {
  todoList.value.push(item);
};
</script>

데이터 정의는 ref를 사용합니다.

1. App.vue가 가장 최상위 파일이므로 todoList 데이터를 만들어줍니다. 

TodoItem에 대한 interface도 작성해줍니다. 

이때, status는 string으로 해도되지만 'active'와 'clear'만 사용할 것이므로 문자열로 적어줍니다. (vue에서 props받을 때, validator 생각하면 될 듯 합니다.) 이 외의 문자열이 오거나 타입이 다르면 에러가 납니다.

2. todo-input에서 emit을 통해 이벤트(update:todo)가 발생하면 handleUpdateTodoList 함수를 호출하여 todoList에 데이터를 추가해줄 것입니다. 

 

 

components/todo-input.vue

<template>
  <input
    type="text"
    class="form-control"
    placeholder="할일을 입력해주세요."
    :value="inputValue"
    @keyup.enter="handleAddItem"
  />
</template>
<script lang="ts">
export default {
  name: "TodoInput",
};
</script>
<script lang="ts" setup>
import { ref, defineEmits } from "vue";

const inputValue = ref("");
const emit = defineEmits(["update:todo"]);

const handleAddItem = (event: Event) => {
  const value = (event.target as HTMLInputElement).value;
  if (!value) return;
  inputValue.value = value;
  emit("update:todo", {
    id: 1,
    title: value,
    status: "active",
  });
  inputValue.value = "";
};
</script>

이제 App.vue까지 통신하기위해서 데이터를 입력하는 가장작은 단위의 컴포넌트인, todo-input 컴포넌트 작업을 하겠습니다.

데이터 inputValue를 ref를 통해 선언합니다.

1. input에 :value로 inputValue를 등록하고, enter를 했을 경우에 대한 keyup 이벤트를 등록합니다.

2. defineEmits을 사용해 emit을 등록해줍니다. 상위 컴포넌트에서 이벤트를 받을때와 동일한 이름이어야합니다.

위의 App.vue에서 등록한 이름과 동일하게 (update:todo) 작성합니다. 

3. handleAddItem 이벤트가 발생하게 되면 실행될 로직을 작성합니다.

emit에 params로 TodoItem에서 받는 값 id, title, status를 전달합니다. 그 후 inputValue를 비워줍니다.

 

 

components/todo-item.vue

<template>
  <div class="input-group">
    <div class="input-group-text">
      <input
        class="form-check-input mt-0"
        type="checkbox"
        :checked="props.status === 'clear'"
      />
    </div>
    <input type="text" class="form-control" :value="props.title" disabled />
    <button class="btn btn-outline-secondary" type="button">X</button>
  </div>
</template>
<script lang="ts">
export default {
  name: "TodoItem",
};
</script>
<script lang="ts" setup>
import { defineProps } from "vue";

interface TodoItem {
  id: number;
  title: string;
  status: "active" | "clear";
}

const props = defineProps<TodoItem>();
</script>

1. checkbox의 checked가 'clear' 일 경우에 체크될 수 있도록 적용해줍니다.

2. views/item-list에서 todo-item에 필요한 props를 내려줄 것입니다. 입력할때 id, title, status를 보내고 있으니 내려오는 값도 동일합니다. defineProps를 통해 props 선언해줍니다.

이때 interface에 TodoItem을 또 등록하고, props에 타입을 선언해주면 내부에 따로 등록을 하지 않아도 됩니다.

만얀 interface를 사용하지 않을 것이라면, 아래와 같이 다른 방식으로 작업해도 됩니다.

defineProps 내부에서 선언

const props = defineProps({
  id: {
    type: Number,
    required: true,
  },
  title: {
    type: String,
    required: true,
  },
  status: {
    type: String,
    required: true,
    validator: (val: string) => ["active", "clear"].includes(val),
  },
});

 

 

views/item-list.vue

v-for를 통해 todo-item 컴포넌트들을 여러개 생성해줍니다.

<template>
  <todo-item
    v-for="item in props.todoList"
    :key="item.id"
    :id="item.id"
    :title="item.title"
    :status="item.status"
  />
</template>
<script lang="ts">
export default {
  name: "ItemList",
};
</script>
<script lang="ts" setup>
import { defineProps } from "vue";
import TodoItem from "@/components/todo-item.vue";

interface ITodoItem {
  id: number;
  title: string;
  status: "active" | "clear";
}

interface Props {
  todoList: ITodoItem[];
}

const props = defineProps<Props>();
</script>

 

렌더링

할일을 입력한 후, 엔터키를 누르면 아이템이 추가되는 것을 확인할 수 있습니다.

할일 등록된 모습

 

 

기능 개발 - 02 삭제하기

작은 단위 컴포넌트의 기능부터 큰 범위로 올라가면서 개발해보도록 하겠습니다.

 

component/todo-item.vue

<template>
  <div class="input-group">
    <div class="input-group-text">
      <input
        class="form-check-input mt-0"
        type="checkbox"
        :checked="props.status === 'clear'"
      />
    </div>
    <input type="text" class="form-control" :value="props.title" disabled />
    <button
      class="btn btn-outline-secondary"
      type="button"
      @click="handleRemoveItem"
    >
      X
    </button>
  </div>
</template>
<script lang="ts">
export default {
  name: "TodoItem",
};
</script>
<script lang="ts" setup>
import { defineProps, defineEmits } from "vue";

interface TodoItem {
  id: number;
  title: string;
  status: "active" | "clear";
}

const props = defineProps<TodoItem>();
const emit = defineEmits(["remove:todo"]);
const handleRemoveItem = () => {
  emit("remove:todo", props.id);
};
</script>

1. x 버튼(삭제) 클릭시에 handleRemoveItem 이벤트 바인딩 해줍니다.

해당 이벤트가 실행되면 emit을 통해 remove:todo가 실행됩니다. 상위 컴포넌트(item-list)에서 이벤트를 캐치할 수 있습니다.

이때, 기억해야할 것은 TodoList는 최상위 컴포넌트(App.vue)에 있으므로 App.vue까지 이벤트가 닿아야할 것입니다. (props로 전달받은 데이터는 수정을 하면 안됩니다.)

2. TodoList에서 지워야하는 아이템을 찾아야하므로 유니크한 값(id)를 param로 전달합니다.

 

 

views/item-list.vue

todo-item의 바로 부모 컴포넌트입니다. (App.vue는 item-list를 호출하고 있습니다.)

그렇기 때문에 todo-item에서 받은 remove:todo 이벤트에서 다시한번 emit 해줍니다. (네이밍을 동일하게 해줍니다만, 다르게 하고 App.vue에서 동일한 이름으로 받으면됩니다.)

간단히 말하면, 부모-자식 간의 emit의 이벤트 이름이 동일하면됩니다.

<template>
  <todo-item
    v-for="item in props.todoList"
    :key="item.id"
    :id="item.id"
    :title="item.title"
    :status="item.status"
    @remove:todo="(id) => emit('remove:todo', id)"
  />
</template>
<script lang="ts">
export default {
  name: "ItemList",
};
</script>
<script lang="ts" setup>
import { defineProps, defineEmits } from "vue";
import TodoItem from "@/components/todo-item.vue";

interface ITodoItem {
  id: number;
  title: string;
  status: "active" | "clear";
}

interface Props {
  todoList: ITodoItem[];
}

const props = defineProps<Props>();
const emit = defineEmits(["remove:todo"]);
</script>

 

 

App.vue

<template>
  <div class="container">
    <div class="row">
      <base-header />
    </div>
    <div class="row">
      <todo-input @update:todo="handleUpdateTodoList" />
    </div>
    <div class="row">
      <router-view :todoList="todoList" @remove:todo="handleRemoveTodoItem" />
    </div>
  </div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import BaseHeader from "@/components/header.vue";
import TodoInput from "@/components/todo-input.vue";

interface TodoItem {
  id: number;
  title: string;
  status: "active" | "clear";
}

const todoList = ref([
  {
    id: 0,
    title: "청소하기",
    status: "active",
  },
  {
    id: 1,
    title: "공과금 내기",
    status: "active",
  },
  {
    id: 2,
    title: "운동 30분하기",
    status: "clear",
  },
] as TodoItem[]);

const handleUpdateTodoList = (item: TodoItem) => {
  todoList.value.push(item);
};
const handleRemoveTodoItem = (id: number) => {
  todoList.value = todoList.value.filter((item: TodoItem) => item.id !== id);
};
</script>
<style lang="scss">
#app {
  padding: 50px 0;

  .row + .row {
    margin-top: 15px;
  }
}
</style>

최상위 컴포넌트에서 자식 컴포넌트(item-list.vue)에서 emit한 이벤트를 캐치해줍니다.

handleRemoveTodoItem을 바인딩합니다. 해당 이벤트는 todoList에서 id를 찾아서 제거한 값을 새로운 값으로 넣어줍니다.

x 버튼 클릭 시 해당 item 삭제

 

자, 지금까지 작업한 것들은 몇가지 문제가 있습니다.

- changed 개발을 까먹었다.(머쓱) 

1. input 등록 시 id를 정적으로 넣고있어서 등록 시 id값이 다 동일하게 저장되고 있다. 이것은 삭제 시 문제가 된다.

2. 새로고침 시 수정한 데이터가 사라진다.

3. interface가 여러군데 중복 선언 되었다.

4. remove:todo 두번에 걸쳐서 emit하고 있다. (즉, 실수해서 버그나기 딱 좋다)

 

 

다음편 보기

 

 

 

+ Recent posts