2020. 6. 13. 23:02

Vue.js로 To Do List 애플리케이션 만들기(상)

할일 관리 앱은 프론드앤드 프레임워크를 배울 때 가장 대중적으로 도전하는 활용 예제이다. 제일 무난하게 시작할 수 있는 낮은 입문턱이지만 데이터 조작(CRUD)이 어떻게 사용되는지, 컴포넌트의 구조와 컴포넌트간의 통신이 어떻게 이루어지는지도 알수있는, 간단해보이지만 이 안에 많은 것들이 함축되어있고 또한 이것을 알게된다면 여기서 응용하여서 다른 더 고도화된 앱들을 구성할 수 있다.

단순하게 보이지만 여러가지가 함축되어있는 콤비네이션!

TO DO APP 구상하기


위 사진은 우리가 만들어볼 할일 관리 앱의 완성본이다. 반응형으로 구성되어 웹 뿐만 아니라 모바일에서 봐도 문제가 없도록 UI를 구성하였다. 할 일을 input에 적어서 +버튼을 누르면 목록에 추가되고 휴지통 아이콘을 눌러서 낱개로 지우거나 Clear All버튼을 눌러서 한번에 다 지우는 것도 가능하다.

추가적으로 뷰 애니메이션을 이용해 뷰 자체에서 지원하는 애니메이션 기능으로 데이터를 추가, 삭제시에 여러가지 애니메이션 효과를 줘서 UI를 좀더 다채롭게 바꾸고 아무 값도 입력이 안되었을 때 모달창을 띄워 경고를 표시해주는 것까지 해보도록 하겠다.

이 할일 관리 앱은 하나처럼 보이지만 사실 4개의 컴포넌트가 합쳐진 모양을 하고 있다. 맨위에 박스부터 차례대로 다음과 같은 역할을 하고 있다.

TodoHeader: 애플리케이션 이름 표시
Todoinput: 할 일 입력 및 추가
TodoList: 할 일 목록 표시 및 특정 할 일 삭제
TodoFooter: 할 일 모두 삭제

이 컴포넌트라는 개념은 프론트앤드 프레임워크들의 공통적인 특징으로서 재사용성과 유지보수를 용이하게 해주므로 귀찮더라도 컴포넌트를 기능별로 나눠서 처음부터 계획부터 잘 잡고 디자인 해나가야 나중에 더 힘든 일이 생기지 않을 수 있다.

 

프로젝트 생성


개인적으로 npm보다는 yarn을 주로 사용하기 때문에 yarn으로 프로젝트를 진행한다. yarn 설치법이나 사용법은 여기서 다루지 않겠지만 npm으로 진행하더라도 크게 지장은 없다.

Vue Cli를 통해 프로젝트를 생성하기 위해 아래 명령어로 터미널에서 vue cli를 설치해준다.

yarn global add @vue/cli
# npm의 경우에는 아래 명령어로 cmd에서 설치해준다.
npm install -g @vue/cli

이 프로젝트는 @vue/cli 4.4.x 버전을 기준으로 되어있으나 이미 설치되어있는 버전이 3버전 이상이라면 무리없이 진행할 수 있다.

버전 확인은 아래 명령어를 통해 확인할 수 있다.

vue --version

버전이 만약 3버전 미만이라면 아래 명령어를 통해 업그레이드를 진행해주자.

yarn global upgrade --latest @vue/cli
# npm의 경우에는 아래 명령어로 업그레이드 가능하다
npm update -g @vue/cli

설치가 끝났으면 create명령어를 통해 프로젝트를 생성해준다.

vue create to_do_app

주의해야할 점으로 프로젝트 이름으로 카멜케이스는 안되니(대문자를 이용한 표기법)스네이크 케이스 또는 일반적인 소문자 단어로 작성을 해야한다. 여기서는 to_do_app으로 이름을 지었지만 다른 것으로 바꾸어도 무방하다.

명령어를 입력하면 default 설정으로 갈지 다른 옵션들을 설정할지를 결정하게 되는데 default로 프로젝트를 생성하자. 나중에 프로젝트에서 타입스크립트를 사용한다던지, 뷰엑스(상태 관리)나 라우터를 사용한는 일이 생긴다면 여기 프로젝트 구성 단계에서 기본적으로 설정해줄 수가 있으니 참고하도록 하자.

yarn serve
# 또는
yarn run serve

이제 만들어진 프로젝트 폴더로 이동해서 위 명령어를 쳐서 앱을 실행시켜보면 프로젝트가 생성되어 localhost:8080으로 접속하면 기본페이지로 연결되는 것을 확인할 수 있다.

위와 같은 페이지가 뜬다면 프로젝트 생성에 성공한 것이다.

 

컴포넌트 구성하기


그럼 이제 기본으로 연결되어있는 컴포넌트를 지우고 새롭게 위에서 구상한 네 개의 컴포넌트를 만들고 최종적인 App.vue 컴포넌트에 모두 종속시키도록 해보자.

src/components/HelloWorld.vue 파일을 지우고 대신에 다음의 파일들을 components폴더 아래에 만든다.

각 컴포넌트들을 기본양식들을 채워넣어준다.

<template>
  <div>TodoHeader</div>
</template>

<script>
export default {};
</script>

<style scoped>
</style>

div안에 TodoHeader의 내용들만 TodoInput, TodoList, TodoFooter로 바꿔줘서 모두 채워넣어주자.

이제 마지막으로 App.vue에서 컴포넌트들을 등록해주고 표현해주기만 하면 된다.

먼저 App.vue 의 template 부분을 등록한 컴포넌트들을 차례대로 넣어준다.

<template>
  <div id="app">
    <TodoHeader></TodoHeader>
    <TodoInput></TodoInput>
    <TodoList></TodoList>
    <TodoFooter></TodoFooter>
  </div>
</template>

App.vue 의 <script></script>부분은 모듈화된 컴포넌트들을 import시켜서 인스턴스로 등록을 시켜준다.

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 {
  name: "App",
  components: {
    TodoHeader,
    TodoInput,
    TodoList,
    TodoFooter
  }
};

style부분은 일단 그대로 두자.

App.vue는 style인데 컴포넌트들은 style scoped로 한 이유는 style은 컴포넌트 전역에 스타일을 넣어줄 수 있고 scoped는 말그대로 해당하는 컴포넌트에만 타겟팅을 두어서 스타일을 주기 위함이다.

이제 컨트롤+S를 누르면 자동적으로 뷰 프로젝트가 갱신되면서 div안에 적힌 내용 그대로 컴포넌트들이 출력되는 것을 확인할 수 있다.

 

Font Awesome 적용하기


font awesome은 UI에 적용하기 편하도록 라이브러리 형태로 제공되는 아이콘 모음이다. 이 프로젝트에서는 추가 버튼이나 삭제버튼, 목록 표시 등에 이 Font Awesome의 아이콘들을 적용해서 디자인을 하므로 적용해보도록 하자.

우선 yarn을 사용하기로 했으므로 yarn을 이용해 fontawesome 라이브러리를 설치한다. npm의 경우에는 yarn add를 npm i로만 바꿔서 설치하면 된다.

$ yarn add @fortawesome/fontawesome-svg-core
$ yarn add @fortawesome/free-solid-svg-icons
$ yarn add @fortawesome/vue-fontawesome

그 후에 vue-cli 프로젝트 설정을 다음과 같이 바꿔준다.

src/main.js

import Vue from "vue";
import App from "./App.vue";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";

library.add(faPlus, faCheck, faTrash);

Vue.component("font-awesome-icon", FontAwesomeIcon);

Vue.config.productionTip = false;

new Vue({
	render: (h) => h(App),
}).$mount("#app");

사용 방법은 위처럼 라이브러리로 등록해준 뒤에 사용하고 싶은 부분에 다음과 같은 코드를 입력하면 된다.

<font-awesome-icon icon="아이콘명" />

 

TodoHeader.vue


헤더부분은 사실 별 내용이 들어가지가 않는다. 이 앱의 이름을 표시해주기 위한 용도이기 때문이다.

<template>
  <header>
    <h1>TODO LIST</h1>
  </header>
</template>

template 부분을 자신이 원하는 내용에 맞춰서 디자인과 제목을 정해서 만들어주자.

<style scoped>
h1 {
  color: #2f3b52;
  font-weight: 900;
  margin: 2.5rem 0 1.5rem;
}
</style>

<script>부분은 <sciprt>태그까지 아예없애도 되고

export default {};

위처럼 처음에 우리가 넣어두었던 기본값 그대로 두어도 상관없다.

 

TodoInput.vue


<template>
  <div class="inputBox shadow">
    <input
      type="text"
      v-model="newTodoItem"
      placeholder="이곳에 해야할 일을 적어주세요"
      v-on:keyup.enter="addTodo"
    />
    <span class="addContainer" v-on:click="addTodo">
      <font-awesome-icon class="addBtn" icon="plus" aria-hidden="true" />
    </span>

    <modal v-if="showModal" @close="showModal = false">
      <h3 slot="header">경고</h3>
      <span slot="body">할 일을 입력해주세요.</span>
      <button slot="footer" @click="showModal = false">닫기</button>
    </modal>
  </div>
</template>

input박스와 함께 추가하는 버튼을 옆에 붙여준다. 여기에 나오는 v-로 시작하는 접두사들은 뷰의 디렉티브로서 HTML의 DOM을 조작할 수 있는 속성이다.

예를 들어 위의 v-on은 화면 요소의 이벤트를 감지하여 처리할 때 사용되며 이곳에서는 엔터키나 클릭을 감지해서 addTodo메소드를 실행하도록 설정되어 있고,

v-model은 주로 폼에서 사용되는데, 폼에 입력한 값을 뷰 인스턴스의 데이터와 동기화할 때 사용된다. v-model에 대해서는 이제부터 설명할 script코드들과 함께 보도록 하자.

v-if는 지정한 뷰 데이터 값이 참, 거짓 여부에 따라 해당 HTML 태그를 화면에 표시하거나 표시하지 않을 때 사용되는데 여기서는 showModal 변수가 true일때 모달창이 표시가 되고 닫힐 때false(거짓)로 showModal값을 바꿔주면서 창이 닫히도록 사용되었다.

모달창의 경우는 Vue.js의 공식홈페이지의 예제를 그대로 가져온 것이므로 아래 연결된 링크를 통해서 index.html의 소스는 <template>태그 안에 style.css의 소스는 <style scoped>태그 안에 넣어서 common의 폴더안에 Modal.vue라는 파일을 만들어 저장해주면 된다.

<template>
  <transition name="modal">
    <div class="modal-mask">
      <div class="modal-wrapper">
        <div class="modal-container">
          <div class="modal-header">
            <slot name="header"></slot>
          </div>

          <div class="modal-body">
            <slot name="body"></slot>
          </div>

          <div class="modal-footer">
            <slot name="footer">
              <button class="modal-default-button" @click="$emit('close')">OK</button>
            </slot>
          </div>
        </div>
      </div>
    </div>
  </transition>
</template>

<style scoped>
.modal-mask {
  position: fixed;
  z-index: 9998;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: table;
  transition: opacity 0.3s ease;
}

.modal-wrapper {
  display: table-cell;
  vertical-align: middle;
}

.modal-container {
  width: 300px;
  margin: 0px auto;
  padding: 20px 30px;
  background-color: #fff;
  border-radius: 2px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
  transition: all 0.3s ease;
  font-family: Helvetica, Arial, sans-serif;
}

.modal-header h3 {
  margin-top: 0;
  color: #42b983;
}

.modal-body {
  margin: 20px 0;
}

.modal-default-button {
  float: right;
}

/*
 * The following styles are auto-applied to elements with
 * transition="modal" when their visibility is toggled
 * by Vue.js.
 *
 * You can easily play with the modal transition by editing
 * these styles.
 */

.modal-enter {
  opacity: 0;
}

.modal-leave-active {
  opacity: 0;
}

.modal-enter .modal-container,
.modal-leave-active .modal-container {
  -webkit-transform: scale(1.1);
  transform: scale(1.1);
}
</style>

https://vuejs.org/v2/examples/modal.html

 

Modal Component — Vue.js

Vue.js - The Progressive JavaScript Framework

vuejs.org

import Modal from "./common/Modal.vue";

export default {
  data() {
    return {
      newTodoItem: "",
      showModal: false
    };
  },
  methods: {
    addTodo() {
      if (this.newTodoItem !== "") {
        let value = this.newTodoItem && this.newTodoItem.trim();
        this.$emit("addTodo", value);
        this.clearInput();
      } else {
        this.showModal = !this.showModal;
      }
    },
    clearInput() {
      this.newTodoItem = "";
    }
  },
  components: {
    Modal: Modal
  }
};

<script>부분부터 조금 복잡하고 햇갈릴 수 있는데 하나씩 천천히 살펴보자. 우선 모듈화해서 만들어 뒀던 컴포넌트인 Modal.vue 를 import해서 componets속성을 통해 지금 있는 TodoInput에 등록한다. slot에 해당하는 부분을 지정해주면 Modal.vue에 담긴 틀에 맞춰 내용만 바뀌게된다. 컴포넌트를 템플릿화 시킨다는 의미이다.

methods속성은 호출되었을 때 해당하는 로직이 실행되어두도록 미리 정의해두는 속성이다. 따라서 template에서 v-on 엔터나 클릭으로 실행되도록 설정해둔 곳을 통해서 addTodo()함수가 호출되게 되고, 만약 공백이 아니라면 input에 담긴 Model을 newTodoItem이라는 변수에 남아서 trim()함수로 앞뒤 여백을 잘라서 value라는 변수에 저장하게 된다.

그리고 $emit()을 통해 addTodo라는 이벤트 이름과 방금 전에 저장한 변수 value를 가지고 상위 컴포넌트로 보내게 된다.

emit은 발신이나 송신, 발산하다 정도의 의미로 이벤트를 발생시켜 하위 컴포넌트에서 상위 컴포넌트로 신호를 보낼 때 사용된다. 기본 형식은 $emit('이벤트 이름')이지만 $emit('이벤트 이름',인자1,인자2,인자3...) 이런식으로 여러개의 인자를 넣어 여러 특정 데이터들을 전달할 수 있다.

다만 전달받은 인자 값은 상위 컴포넌트에서 참고용으로만 활용하고 데이터 값은 변경하지 않도록 해야한다. 왜냐하면 컴포넌트는 각자 고유한 유효 범위를 갖고있기 때문에 상위 컴포넌트에서 전달받은 인자 값을 바꾸더라도 하위 컴포넌트에는 반영되지 않기 때문이다.

상위로 보낸 이유는 이 컴포넌트에서 처리해도 되지만 데이터의 유효 범위 때문에 다른 컴포넌트에서 이 newTodoItem의 값을 참조하지 못하기 때문에 모두 하나의 값을 통일되게 다루기 위해서 하나의 상위 컴포넌트에서 처리하도록 하기 위함이다.

물론 상하의 관계없이 컴포넌트 대 컴포넌트로 바로 데이터를 통신해서 처리할 수도 있지만 나중에 프로젝트가 커지게 되면 이 모든 데이터들이 어디서 어디로 흘러가고 주고받는지가 명확하지 않기 때문에 유지보수하기 힘들어질 수 있으므로 이 방법은 추천하지 않는다.

또 한 가지의 방법은 뷰엑스라는 리덕스와 같은 상태관리 라이브러리를 이용해서 한눈에 데이터를 관리하는 방법이다. 이 방법은 이 포스팅에서는 다루지 않으니 이런 것이 있다고만 알고 넘어가도록 하자.

데이터가 보내졌으면 미리 정의해둔 clearInput()을 호출해 채워져있던 newTodoItem을 다시 공백으로 초기화해준다.

만약 input창이 비어있지 않다면 showModal 변수를 true로 바꿔 모달창을 띄운다. 변수 앞에 !를 붙이면 그 값의 반대가 적용된다.

this.showModal = !this.showModal; 이 부분을 this.showModal = true 해도 결과는 같다.

지금까지의 결과들을 실행시켜보면 다음과 같다.

아무거나 입력하고 +버튼을 클릭하거나 enter키를 입력하면 공백으로 바뀌면서 아무런 일도 생기지 않는다. 상위 컴포넌트로 데이터를 보내기만 했을 뿐 아무런 데이터 생성 코드를 넣지 않았기 때문에 당연하다.

빈 공백상태에서 +버튼을 클릭하면 Modal창이 뜨며 경고를 띄우고 닫기 버튼을 눌러서 닫을 수 있다.

다음은 실질적으로 newToDoItem들을 표시해줄 TodoList 컴포넌트를 만들어보자.

2020/06/14 - [Programming/.JS] - Vue.js로 To Do List 애플리케이션 만들기(하)로 이어집니다.

 

Vue.js로 To Do List 애플리케이션 만들기(하)

2020/06/13 - [Programming/.JS] - Vue.js로 To Do List 애플리케이션 만들기(상) TodoList.vue TodoList는 추가한 값들이 로컬스토리지에 저장되고 그 저장된 것을 가져와서 차례대로 뿌려줄 컴포넌트이다. 세션..

blog.metafor.kr