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

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

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

 


 

이번에는 Store를 통해 작업을 진행해보겠습니다.

기존에는 vuex를 사용해서 작업을 했었는데, vue3에서는 공식적으로 pinia 라이브러리를 사용하기를 권고하고 있습니다.

Pinia - Vuex를 대체할 새로운 Store!

 

사실 Pinia 는 Vue의 코어팀에서 활동하는 Eduardo 가
Vuex 5 의 RFC 에 따라 Vuex 5 의 구현체로 Pinia 를 작성한 것이다.
공식문서에 따르면 Vue 의 철학을 충실하게 따르면서 Pinia 를 개발중이며, 추후에 Pinia 와 Vuex 5 두 프로젝트를 하나로 합치거나 아주 쉽게 이동할 수 있도록 지원할 것이라 한다
- 출처: 사이트이동

 

 

Store 작업

pinia 설치

npm i pinia

 

 

store/index.ts 

파일 생성하고 아래와 같이 작성합니다.

import { createPinia } from "pinia";
const pinia = createPinia();

export default pinia;

 

 

main.ts

pinia를 등록해줍니다.

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

import BootstrapVue3 from "bootstrap-vue-3";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue-3/dist/bootstrap-vue-3.css";

import store from "@/store/index";

createApp(App).use(router).use(store).use(BootstrapVue3).mount("#app");

 

store/modules/todo.ts

파일 생성 후 뼈대를 작성합니다.

pinia는 vuex 처럼 따로 어딘가에 등록 할 필요가 없으며 mutatiaons이 없습니다.

actions에서 하거나 컴포넌트 내부에서 작업해도 간단합니다.

import { defineStore } from "pinia";

export const storeTodo = defineStore("todo", {
  state: () => ({}),
  getters: {},
  actions: {},
});

 

02편에서 작업했던 것들을 수정하면서 Store를 사용해보도록 하겠습니다.

 

 

App.vue 

todoList를 Store의 state로 옮겨줍니다.

그리고 싹 지워줍니다.

(interface, props, handler, data등... )

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

 

 

store/modules/todo.ts

App.vue에서 지웠던 todoList를 state에 등록하고

add, remove에 해당하는 이벤트 로직들을  actions에 등록합니다.

import { defineStore } from "pinia";
import { TodoItem } from "../index.interface";

export const useStoreTodo = defineStore("todo", {
  state: () => ({
    todoList: [
      {
        id: 0,
        title: "청소하기",
        status: "active",
      },
      {
        id: 1,
        title: "공과금 내기",
        status: "active",
      },
      {
        id: 2,
        title: "운동 30분하기",
        status: "clear",
      },
    ] as TodoItem[],
  }),
  getters: {},
  actions: {
    addTodoItem(item: TodoItem) {
      this.todoList.push(item);
    },
    removeTodoItem(id: number) {
      this.todoList.splice(id, 1);
    },
  },
});

 

 

store/index.interface.ts

여기저기 중복되고 있던 interface TodoItem을 모아줍니다.

다른 컴포넌트에서 사용하길 원한다면 import해서 사용하면됩니다.

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

 

 

views/item-list.vue

이제 interface파일에서 import해서 사용할 수 있게된 TodoItem interface는 지우고 store/index.interface에서 가져옵니다.

renderTodoList를 선언하여 ref로 감싸주어 해당 데이터의 변화를 감지하도록 합니다.

이때 Store에서 export하고 있는 인스턴스를 import해서 호출 한 뒤 바로 사용하면 됩니다.

<template>
  <todo-item
    v-for="item in renderTodoList"
    :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 { ref } from "vue";
import { useStoreTodo } from "@/store/modules/todo";
import TodoItem from "@/components/todo-item.vue";

const store = useStoreTodo();
const renderTodoList = ref(store.todoList);
</script>
<style lang="scss"></style>

 

 

components/todo-item.vue

(props의 interfece도 TodoItem을 바로 사용하고 싶으나, 저는 문제가 발생하여 못했습니다.이 부분은 할 수 있으면 개선해야겠습니다.)actions의 함수도 바로 사용할 수 있습니다.

<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 } from "vue";
import { useStoreTodo } from "@/store/modules/todo";
import { TodoItem } from "@/store/index.interface";

interface Props {
  id: TodoItem["id"];
  title: TodoItem["title"];
  status: TodoItem["status"];
}

const store = useStoreTodo();
const props = defineProps<Props>();

const handleRemoveItem = () => {
  store.removeTodoItem(props.id);
};
</script>

 

 

components/todo-input.vue

input도 마찬가지로 store를 사용하도록 합니다.

<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 } from "vue";
import { useStoreTodo } from "@/store/modules/todo";

const store = useStoreTodo();
const inputValue = ref("");

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

 

 

store/modules/todo.ts

이전에 구현하지 않았던 checked에 대한 로직도 추가하겠습니다.

Store에 actions를 추가해줍니다.

import { defineStore } from "pinia";
import { TodoItem } from "../index.interface";

export const useStoreTodo = defineStore("todo", {
  state: () => ({
    todoList: [
      {
        id: 0,
        title: "청소하기",
        status: "active",
      },
      {
        id: 1,
        title: "공과금 내기",
        status: "active",
      },
      {
        id: 2,
        title: "운동 30분하기",
        status: "clear",
      },
    ] as TodoItem[],
  }),
  getters: {},
  actions: {
    addTodoItem(item: TodoItem) {
      this.todoList.push(item);
    },
    removeTodoItem(id: number) {
      this.todoList.splice(id, 1);
    },
    changedStatus({
      id,
      status,
    }: {
      id: TodoItem["id"];
      status: TodoItem["status"];
    }) {
      this.todoList[id].status = status;
    },
  },
});

 

 

components/todo-item.vue

item의 체크를 할 경우, Store의 change 함수가 실행되어야 하므로 checkbox의 change이벤트에 핸들러를 바인딩해줍니다.

<template>
  <div class="input-group">
    <div class="input-group-text">
      <input
        class="form-check-input mt-0"
        type="checkbox"
        :checked="props.status === 'clear'"
        @change="handleChangeStatus"
      />
    </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 } from "vue";
import { useStoreTodo } from "@/store/modules/todo";
import { TodoItem } from "@/store/index.interface";

interface Props {
  id: TodoItem["id"];
  title: TodoItem["title"];
  status: TodoItem["status"];
}

const store = useStoreTodo();
const props = defineProps<Props>();

const handleRemoveItem = () => {
  store.removeTodoItem(props.id);
};
const handleChangeStatus = () => {
  const changeStatus = props.status === "active" ? "clear" : "active";
  store.changedStatus({
    id: props.id,
    status: changeStatus,
  });
};
</script>

 

 

(별도) Pinia에서 작업이 호출될 때, 콘솔로 찍어서 확인해보기

pinia 참고

 

 

store/index.ts

import { createPinia } from "pinia";
const pinia = createPinia();

pinia.use(({ store }) => {
  if (store) {
    store.$onAction(({ name, args, after, onError }) => {
      console.log(
        `%c 🍍 Event Name: ${name}`,
        "background: #222; color: #bada55"
      );
      const startTime = Date.now();
      console.log(
        `%c 🍍 Start: '${name}' with params [${args.join(", ")}].`,
        "background: #222; color: #bada55"
      );
      after((result) => {
        console.log(
          `%c 🍍 Finished: '${name}' after ${
            Date.now() - startTime
          }ms.\nResult: ${result}.`,
          "background: #222; color: #bada55"
        );
      });
      onError((error) => {
        console.log(
          `%c 🍍 Failed "${name}" after ${
            Date.now() - startTime
          }ms.\nError: ${error}.`,
          "background: red; color: white"
        );
      });
    });
  }
});

export default pinia;

귀여운 파인애플을 넣어서 작업해줍니다. 

store 이벤트가 발생할때 콘솔에 로그를 남깁니다.

 

삭제했을때 console.log

 

자 이렇게 하고나서 하나 더 해야겠죠???? 새로고침해도 데이터가 남아있을 수 있도록 localStorage에 데이터를 저장하는 작업을 하겠습니다.

 

 

localStorage에 데이터 저장하기

@vueuse/core 라이브러리 사용하여 어마어마하게 간단히 작업해보겠습니다.

 

설치

npm i @vueuse/core

 

 

store/modules/todo.ts

useStorage를 import해 온 뒤 저장하려는 state를 감싸줍니다. 

useStorage('이름', 데이터); 

크롬 개발자도구를 통해 localStorage에 저장된 것을 확인합니다.

import { defineStore } from "pinia";
import { useStorage } from "@vueuse/core";
import { TodoItem } from "../index.interface";

export const useStoreTodo = defineStore("todo", {
  state: () => ({
    todoList: useStorage("todoList", [
      {
        id: 0,
        title: "청소하기",
        status: "active",
      },
      {
        id: 1,
        title: "공과금 내기",
        status: "active",
      },
      {
        id: 2,
        title: "운동 30분하기",
        status: "clear",
      },
    ] as TodoItem[]),
  }),
  getters: {},
  actions: {
    addTodoItem(item: TodoItem) {
      this.todoList.push(item);
    },
    removeTodoItem(id: number) {
      this.todoList.splice(id, 1);
    },
    changedStatus({
      id,
      status,
    }: {
      id: TodoItem["id"];
      status: TodoItem["status"];
    }) {
      this.todoList[id].status = status;
    },
  },
});

localStorage

 

이제 더미로 넣었던 데이터를 비우고 정리 작업을 해줍니다.

그리고 remove에 splice시에 id를 index개념으로 하다보니 발생하는 버그도 해결하겠습니다.

 

 

store/modules/todo.ts

최초 데이터를 빈 배열로 바꿔줍니다.

그리고 removeTodoItem의 이벤트 내부 로직을 변경했습니다.

저는 for로 했으나 데이터가 크지 않기도 하고 직관적인 배열 메서드를 사용해도 좋습니다.

(이때, 기존 배열에서 수정하여 작업하고 있는데, 새로운 데이터를 담게 되면 오류가 발생하여서 이렇게 작업을 하였습니다. 이건 더 찾아봐야할 것 같습니다.)

import { defineStore } from "pinia";
import { useStorage } from "@vueuse/core";
import { TodoItem } from "../index.interface";

export const useStoreTodo = defineStore("todo", {
  state: () => ({
    todoList: useStorage("todoList", [] as TodoItem[]),
  }),
  getters: {},
  actions: {
    addTodoItem(item: TodoItem) {
      this.todoList.push(item);
    },
    removeTodoItem(id: number) {
      let index = null;
      for (let i = 0; this.todoList.length > i; i++) {
        const item = this.todoList[i];
        if (item.id === id) {
          console.log(item);
          index = i;
          break;
        }
      }

      if (index !== null) {
        this.todoList.splice(index, 1);
      }
    },
    changedStatus({
      id,
      status,
    }: {
      id: TodoItem["id"];
      status: TodoItem["status"];
    }) {
      this.todoList[id].status = status;
    },
  },
});

 

 

components/todo-input.vue

등록 시 id값도 마지막 데이터의 id를 찾아서 +1해줍니다.

<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 } from "vue";
import { useStoreTodo } from "@/store/modules/todo";

const store = useStoreTodo();
const inputValue = ref("");

const handleAddItem = (event: Event) => {
  const value = (event.target as HTMLInputElement).value;
  if (!value) return;
  const length = store.todoList.length;
  const createdId = length === 0 ? 0 : store.todoList[length - 1].id + 1;

  inputValue.value = value;
  store.addTodoItem({
    id: createdId,
    title: value,
    status: "active",
  });
  inputValue.value = "";
};
</script>

 

id 저장 동작 확인

 

 

라우터를 통해 리스트 구분해서 보기( All / Active / Clear ) 

views/item-list.vue

useRoute를 통해서 다이나믹라우터의 status로 넘겨준 값을 사용하고,

watch를 통해 값이 변경되면 (header의 all, active, clear를 클릭 시) renderList의 값을 변경해 줍니다.

<template>
  <todo-item
    v-for="item in renderTodoList"
    :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 { ref, watch } from "vue";
import { useRoute } from "vue-router";
import { useStoreTodo } from "@/store/modules/todo";
import TodoItem from "@/components/todo-item.vue";

const route = useRoute();
const store = useStoreTodo();
let renderTodoList = ref(store.todoList);

watch(
  () => route.params.status,
  (newVal) => {
    if (!newVal) {
      renderTodoList.value = store.todoList;
    } else if (newVal === "active" || newVal === "clear") {
      renderTodoList.value = [...store.todoList].filter((item: any) => {
        return item.status === newVal;
      });
    }
  },
  { deep: true }
);
</script>

 

이제 아이템을 등록해보고, 체크박스를 체크한 후 header의 링크를 눌러서 변화를 확인합니다.

 

All일 경우

all

Active일 경우

active

Clear일 경우

clear

 

 

 

 

최종적으로 todo-list 만들기 작업을 완료하였습니다.

저는 vue3의 compositionAPI 방식보다 script-setup 방식이 문법이 더 간결해서 마음에 듭니다.

바뀐 부분이 많이 있으니 공식문서를 확인해보시길 추천합니다.

 

 

pacakge.json 

{
  "name": "todo-list",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "@popperjs/core": "^2.11.2",
    "@vueuse/core": "^7.7.1",
    "bootstrap": "^5.1.3",
    "bootstrap-vue-3": "^0.1.7",
    "core-js": "^3.6.5",
    "pinia": "^2.0.11",
    "vue": "^3.0.0",
    "vue-router": "^4.0.0-0"
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^4.18.0",
    "@typescript-eslint/parser": "^4.18.0",
    "@vue/cli-plugin-babel": "~4.5.15",
    "@vue/cli-plugin-eslint": "~4.5.15",
    "@vue/cli-plugin-router": "~4.5.15",
    "@vue/cli-plugin-typescript": "~4.5.15",
    "@vue/cli-service": "~4.5.15",
    "@vue/compiler-sfc": "^3.0.0",
    "@vue/eslint-config-prettier": "^6.0.0",
    "@vue/eslint-config-typescript": "^7.0.0",
    "eslint": "^6.7.2",
    "eslint-plugin-prettier": "^3.3.1",
    "eslint-plugin-vue": "^7.0.0",
    "node-sass": "^4.12.0",
    "prettier": "^2.2.1",
    "sass-loader": "^8.0.2",
    "typescript": "~4.1.5"
  }
}

   할일 목록을 만들면서 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하고 있다. (즉, 실수해서 버그나기 딱 좋다)

 

 

다음편 보기

 

 

 

전체 적인 설정부분 node버전 16으로 변경하여 다시 진행해보도록하겠습니다. (시간이 날때 ㅠㅠ)

해당 글들에서 lint 내용 제외해서 보시는 것을 추천합니다.(린트 설정이 제대로 안된 듯 함)

 

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

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

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

 


 

Vue3와 Typescript를 사용해서 간단한 할일 목록을 만들어보겠습니다.

저도 학습하고나서 기억하기 위해 작성하는 포스트입니다.

그리고 해당 학습에서 중요하게 생각한 부분 외에는(lint, style 등) 거슬리지 않는한 신경쓰지 않고 진행하였고, 

UI는 bootstrap을 사용하였습니다.

 

기본 스펙 공유 (기존에 설치하고나서 업그레이드를 하지 않은 상태인데, 최신 안정 버전 사용해도 무방할 듯 합니다.)

- node 14.15.5 

- typescript 4.5.5

 

Node, Typescript 설치는 공식 사이트를 참고하세요.

https://nodejs.org/ko/

 

Node.js

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

nodejs.org

https://www.typescriptlang.org/download

 

How to set up TypeScript

Add TypeScript to your project, or install TypeScript globally

www.typescriptlang.org

 

 

vue-cli 설치

https://cli.vuejs.org/guide/installation.html

 

Installation | Vue CLI

Installation Warning regarding Previous Versions The package name changed from vue-cli to @vue/cli. If you have the previous vue-cli (1.x or 2.x) package installed globally, you need to uninstall it first with npm uninstall vue-cli -g or yarn global remove

cli.vuejs.org

저는 npm을 사용하므로 앞으로 명령어들은 npm으로 진행합니다.

npm install -g @vue/cli

 

 

프로젝트 생성

본인이 프로젝트를 설치할 경로 내에서 명령어를 통해 프로젝트를 생성해줍니다.

todo-list는 프로젝트 명입니다.

vue create todo-list

옵션은 아래와 같이 설정했습니다.

vue-cli 옵션 설정

간략하게 설명해보겠습니다.

- 수동으로 기능 선택

- 프로젝트에서 사용할 것들을 선택합니다. vue, babel, ts, router, css pre-processors, linter 

- vue 3.x 버전 선택

- class-style (vue-class-compoent 데코레이터를 사용하는 방식) 컴포넌트 사용 여부로, No

- TypeScript와 함께 Babel 사용

- history 모드 사용 (url에 #이 붙지 않도록)

- css pre-processor로 sass/scss 선택 (node-sass)

- eslint + prettier 선택

- 추가 린트 기능 선택: 린트 저장 시

- 각각 파일에서 설정할 수 있도록 선택

- 향후 프로젝트를 위해서 설정 저장 No

 

이렇게 설정하고 나면 아래와 같이 프로젝트 설치가 완료됩니다.

기본 구조

이제 todo-list 프로젝트로 이동하여 로컬 서버를 띄워서 정상적인지를 확인하고 넘어갑니다.

npm run serve

http:localhost:8080으로 접근하면 아래와 같은 화면을 확인할 수 있습니다.

기본화면

 

 

VSCode 인텔리센스 플러그인 설정

인텔리센스는 에디터에서 구문 강조 표시, Typescript 지원, 템플릿 표현식 및 구성 요소 소품에 대하여 도움을 주어 코딩을 편리하게 할 수 있는 기능의 집합을 말합니다.

지금 제가 개발하려는 vue3의 script setup 방식은 Vetur에서 동작이 되지 않기 때문에

vue3 공식 사이트에서 추천하는 Volar을 설치하도록 하겠습니다.(Volar은 많은기능과 함께 Vue SFC 내에서 TypeScript 지원을 제공하는 공식 VSCode 확장 플러그인입니다.)

 

자, 먼저 Vetur 플러그인과 충돌이 나므로, Vetur 플러그인을 비활성화 해줍니다.

다른 프로젝트에서 사용하고 있을 수 있으므로 해당 워크스페이스에서만 비활성화 해줍니다.

vetur 플러그인 비활성화

 

그 후에 Volar을 검색하여 아래 두 플러그인을 설치하고 해당 워크스페이스에서 활성화를 시켜줍니다.

- Vue Language Features (Volar)
- TypeScript Vue Plugin (Volar)

volar 플러그인 활성화

 

 

Bootstrap 설치

https://getbootstrap.com/

 

Bootstrap

The most popular HTML, CSS, and JS library in the world.

getbootstrap.com

모듈을 설치해줍니다. bootstrap docs에서 컴포넌트들을 확인 할 수 있습니다.

앞으로 UI에서 사용하게 됩니다.

npm i bootstrap bootstrap-vue-3

설치가 끝났다면 main.ts 파일에 global로 등록해줍니다.

필요한 파일들을 import해오고, bootstrap-vue-3 모듈은 use를 통해 등록해줍니다.

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

import BootstrapVue3 from "bootstrap-vue-3";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue-3/dist/bootstrap-vue-3.css";

createApp(App).use(router).use(BootstrapVue3).mount("#app");

 

 

Lint & VSCode Settings.json 설정

 

App.vue

기존에 작성되어있던 파일 내용을 다 지우고 난 후, 아래와 같이 수정합니다.

container, row 클래스에 대해 궁금하시다면 bootstrap의 grid를 확인하세요.

프로젝트 설정 시에 sass를 선택했기 때문에 다른 설정을 하지 않아도 scss를 사용할 수 있습니다.

<template>
  <div class="container">
    <div class="row">
      Header
    </div>
    <div class="row">
      input
    </div>
    <div class="row">
      <router-view />
    </div>
  </div>
</template>

<style lang="scss">
#app {
  padding: 100px;
  
  .row + .row {
    margin-top: 15px;
  }
}
</style>

위와 같이 작성하고 저장하고나면 에러가 나고 있는 것을 확인 할 수 있습니다.

@popperjs/core 설치하라

해당 안내에 따라 모듈을 추가적으로 설치해줍니다.

npm i @popperjs/core

에러를 해결하고 나니, 보기 싫은 노란색의 경고 줄이 보입니다.

린트 설정은 하지 않겠다고 하였으나 너무 보기 싫어서 eslint를 수정해줍니다.

lint 경고

 

 

 

.eslintrc.js

eslint 설정 파일에 들어가서 아래와 같이 속성을 추가합니다. 

module.exports = {
  root: true,
  env: {
    node: true,
  },
  extends: [
    "plugin:vue/vue3-essential",
    "eslint:recommended",
    "@vue/typescript/recommended",
    "@vue/prettier",
    "@vue/prettier/@typescript-eslint",
  ],
  parserOptions: {
    ecmaVersion: 2020,
  },
  rules: {
    "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
    "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
    "no-unused-vars": "off",
    "prettier/prettier": [
      "error",
      {
        endOfLine: "auto",
      },
    ],
  },
};

eslint 설정 추가

 

그리고나서 저장할 때마다 eslint가 수정될 수 있도록 VSCode 설정도 진행합니다.

ctrl + , 단축키를 눌러 settings에 접근한 후에 workspace를 선택하여 해당 워크스페이스의 설정만 수정하도록 합니다.

그 후에 setting를 검색하여 Edit in settings.json을 클릭하여 셋팅 파일을 엽니다.

setting 검색

Edit in settings.json을 누르는 순간 프로젝트 폴더를 보면 .vscode라는 폴더가 생기고 그 하위에 settings.json 파일이 생성된 것을 확인할 수 있습니다. (비어있습니다.)

.vscode/settings.json

 

.vscode/settings.json

아래의 설정을 추가합니다.

- editor.formatOnSave: 자동 save 사용안함

- editor.codeActionsOnsave > source.fixAll.eslint: save시 eslint 적용

- editor.tabSize & editor.detectIndentation: tab 시 스페이스 2칸, detectIndentation를 비활성화

- eslint.workingDirectories: 옵션을 사용하여 current working directory를 지정 ([{ "mode": "auto" }] : package.json, .eslintignore 그리고 .eslintrc* 파일이 위치한 폴더를 working directory로 지정한다. )

- eslint.validate: validate할 파일 (자신이 사용하는 언어가 있으면 추가하면 됨)

{
  "editor.formatOnSave": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true,
  },
  "editor.tabSize": 2,
  "editor.detectIndentation": false,
  "editor.formatOnType": true,
  "eslint.workingDirectories": [{ "mode": "auto" }],
  "eslint.validate": [
    "vue",
    "javascript",
    "typescript",
    "html"
  ],
}

 

수정을 마쳤다면 App.vue에서 저장을 해봅니다. 

eslint가 정상적으로 동작한다면 정리가 되면서 노란 경고 줄도 사라집니다.

 

App.vue

lint 적용 후 경고줄이 사라진 모습

 

 

UI 만들기

간단한 할일 목록을 만드는 일이기에 UI는 크게 신경쓰지 않았습니다.

bootstrap > docs에서 원하는 것을 검색하여 가져다 씁니다.

아까 위에서 말했던 container, row는 grid를 검색하여 확인 할 수 있습니다.

grid 시스템

 

 

사용하지 않는 컴포넌트 제거

App.vue를 제외하고 components 하위, views 하위 파일들을 죄다 지워줍니다.

그 후 router/index.ts에서 import 된 컴포넌트들도 제거해줍니다.

지우고 난 후는 아래와 같아야합니다.

// router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";

const routes: Array<RouteRecordRaw> = [];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
});

export default router;

 

 

Header 컴포넌트 만들기 (UI)

components 폴더에에 header.vue 파일을 생성하고, bootstrap 사이트에서 breadcrumb을 검색하여 마크업을 복사해옵니다.

bootstrap breadcrumb

 

components/header.vue

<template>
  <header>
    <h1>TODO LIST</h1>
    <nav aria-label="breadcrumb">
      <ol class="breadcrumb">
        <li class="breadcrumb-item"><a href="#">Home</a></li>
        <li class="breadcrumb-item"><a href="#">Library</a></li>
        <li class="breadcrumb-item active" aria-current="page">Data</li>
      </ol>
    </nav>
  </header>
</template>
<script></script>
<style lang="scss"></style>

 

 

App.vue

방금 생성한 header 컴포넌트를 App.vue 파일에서 등록해줍니다.

script setup 방식은 import한 후 component에 등록하지 않아도 template에서 바로 사용가능합니다.

<template>
  <div class="container">
    <div class="row">
      <base-header />
    </div>
    <div class="row">input</div>
    <div class="row">
      <router-view />
    </div>
  </div>
</template>
<script lang="ts" setup>
import BaseHeader from "@/components/header.vue";
</script>
<style lang="scss">
#app {
  padding: 50px 0;
  
  .row + .row {
    margin-top: 15px;
  }
}
</style>

lang을 ts로 적용해줘야 typescript가 인식할 수 있습니다.

setup은 script setup 방식을 의미합니다.

header 컴포넌트 등록

 

 

components/header.vue

a 태그로 되어있던 부분을 router-link로 바꿔줍니다. 나중에 이 router-link를 통해 모든 할일 목록(All), 해야하는 목록(Active), 진행 한 목록(Clear)을 분류하여 정렬할 것입니다.그 외 마음에 안드는 스타일 부분도 조금 손 봐줍니다.

<template>
  <header class="header">
    <h1 class="header__title">TODO LIST</h1>
    <nav aria-label="breadcrumb">
      <ol class="breadcrumb">
        <li class="breadcrumb-item">
          <router-link to="/">All</router-link>
        </li>
        <li class="breadcrumb-item">
          <router-link to="/active">Active</router-link>
        </li>
        <li class="breadcrumb-item">
          <router-link to="/clear">Clear</router-link>
        </li>
      </ol>
    </nav>
  </header>
</template>
<script lang="ts">
export default {
  name: "BaseHeader",
};
</script>
<style lang="scss">
.header {
  padding: 0 !important;

  .header__title {
    text-align: center;
    margin-bottom: 20px;
    font-weight: 700;
  }

  .breadcrumb {
    margin: 0;
    padding: 6px 12px;
    background: #e9ecef;
    border-radius: 5px;
    box-sizing: border-box;
  }
}
</style>

코드를 보면 script가 두번 들어가 있는것이 확인됩니다.

일반 script는 normal script라고 불리우는데, 이것은 script setup과 함께 사용할 수 있습니다.

normal script에서는 name이나 inheritAttrs 옵션등, 사용자 지정 옵션이 필요할 때 사용됩니다.

 

 

할일 목록 입력을 받을 input 컴포넌트를 만들기 위해 bootstrap에서 form text를 검색하여 UI의 마크업을 가져옵니다.

form text

 

components/todo-input.vue

<template>
  <input type="text" class="form-control" placeholder="할일을 입력해주세요." />
</template>
<script lang="ts">
export default {
  name: "TodoInput",
};
</script>
<script lang="ts" setup></script>
<style lang="scss">
.todo-input {
  font-size: 14px;

  &::placeholder {
    font-size: 14px;
  }
}
</style>

 

App.vue

방금 만든 todo-input 컴포넌트도 App.vue에 등록해줍니다.

<template>
  <div class="container">
    <div class="row">
      <base-header />
    </div>
    <div class="row">
      <todo-input />
    </div>
    <div class="row">
      <router-view />
    </div>
  </div>
</template>
<script lang="ts" setup>
import BaseHeader from "@/components/header.vue";
import TodoInput from "@/components/todo-input.vue";
</script>
<style lang="scss">
#app {
  padding: 50px 0;
  
  .row + .row {
    margin-top: 15px;
  }
}
</style>

아래와 같이 정상적으로 출력되는 것을 확인합니다.

todo-input 등록

 

 

등록된 할일 아이템 컴포넌트도 만들겠습니다.

이번에도 bootstrap에서 form을 검색하여 button과 input등의 UI 마크업을 가져옵니다.

 

components/todo-item.vue

<template>
  <div class="input-group">
    <div class="input-group-text">
      <input class="form-check-input mt-0" type="checkbox" />
    </div>
    <input type="text" class="form-control" value="밥 잘먹기" 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></script>
<style lang="scss">
.input-group {
  padding: 0 !important;

  input {
    font-size: 14px;

    &::placeholder {
      font-size: 14px;
    }
  }

  button {
    font-size: 14px;
  }
}
</style>

 

 

views/item-list.vue

이번에는 App.vue에 등록하지 않고 views 폴더 하위에 할일 목록 리스트 컴포넌트를 만들어서 이곳에 등록하겠습니다.

 

할일 목록 리스트 컴포넌트에서 나중에 header에서 말했던 all, active, clear에 대한 분류작업도 진행할 것입니다.

<template>
  <todo-item />
</template>
<script lang="ts">
export default {
  name: "ItemList",
};
</script>
<script lang="ts" setup>
import TodoItem from "@/components/todo-item.vue";
</script>
<style lang="scss"></style>

 

자 대중 UI를 등록하는 작업이 끝났다면, 라우터를 등록해주겠습니다.

페이지를 여러개 생성하지 않고 다우나믹 라우터를 통해 status를 받아서, 분류를 진행할 것입니다.

 

router/index.ts

path에서 /:status가 다우나믹 라우터의 핵심이며 물음표는 옵셔널을 뜻합니다.

옵셔널을 해줄 경우 '/'로 접근한 경우 item-list 컴포넌트가 라우팅됩니다. 

import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import itemList from "@/views/item-list.vue";

const routes: Array<RouteRecordRaw> = [
  {
    path: "/:status?",
    name: "item-list",
    component: itemList,
  },
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
});

export default router;

UI 결과

 

 

다음편 보기

 

Nuxtjs + Typescript 프로젝트를 진행하고 있는데, 초반 설정부터 많은 문제가 생겼다.

이전에 작업할 때는 내가 린트 설정한 적이 없어서 생기는 문제인지, (내 머리탓)

아니면 버전(현재 v2.15.8)이 달라져서 혹은 Typescript 설정으로 생기는 문제인지 모르겠으나.

결론은 골치아픈 린트 설정 문제였다.

 

그래서 이렇게 골치아픈 김에 이것저것 검색해보고 설정하는데 꽤나 시간을 들이고 있다. (eslint, prettier, stylelint....)

덕분에 설정에만 시간을 엄청 쏟고 있다.

뻘짓에 대한 기록글과 해결한 방법 (은 하단에!)

 

과정에 대한 글

.stylelintrc.js - 실패

이렇게 더러운걸 커밋했다고? ㅎㅎㅎㅋㅋㅋㅋ

module.exports = {
  customSyntax: "postcss-html",
  extends: "stylelint-config-standard",
  plugins: ["stylelint-scss", "stylelint-order"],
  rules: {
    "string-quotes": "single",
  },
  overrides: [
    {
      files: [
        '**/**/*.scss',
        '**/**/**/*.scss',
      ],
      customSyntax: "postcss-scss",
      extends: "stylelint-config-standard-scss",
      rules: {
        "no-eol-whitespace": true, // 줄 끝, 닫는 중괄호 뒤 공백 허용 여부
        "color-hex-length": "long", // 16진수 색상에 대해 표기법 지정
        "declaration-block-trailing-semicolon": "always", // 선언 블록 내 후행 세미콜론을 요구
        "color-no-invalid-hex": true, // 잘못된 16진수 색상을 허용 여부
        "font-family-no-duplicate-names": true, // 중복 폰트 선언 여부
        "declaration-block-no-duplicate-properties": true, // 같은 선언 내 중복 속성 선언 여부
        "declaration-block-no-duplicate-custom-properties": true, // 같은 선언 내 중복 사용자 속성 선언 여부
        "no-duplicate-at-import-rules": true, // 중복 @import 규칙을 허용 여부
        "media-query-list-comma-newline-after": "always", // 미디어 쿼리 목록 쉼표 뒤 줄 바꿈이나 공백

        // 개행
        "rule-empty-line-before": "always-multi-line", // 규칙 앞에 빈줄 속성
        "block-opening-brace-newline-before": "never-single-line", // 여는 중괄호 앞 (선택자와 괄호 사이)
        "block-opening-brace-newline-after": "always", // 여는 중괄호 다음 개행
        "block-closing-brace-newline-after": "always", // 닫는 중괄호 뒤
        "block-closing-brace-newline-before": "always", // 닫는 중괄호 앞
        "block-opening-brace-space-before": "always", // 여는 중괄호 앞
      },
    }
  ]
}

 

postCSS를 제거하고 SCSS로 작업하려고 했더니, 종속성때문에 에러가 났다.

그래서 그냥 삭제하지 않고 customSyntax 설정만 제거했는데 이때도 에러가 났다.

그래서 위와 같이 overrides를 통해 files에서 scss파일만 추가해서 작업했다.

이때 생기는 문제는 vue파일에서 상단 postcss-html이 적용되고 있어서 어쩔수 없이 룰에 string-quotes를 single를 추가해야했다. (이때 .vue 파일때문이 아닐까?? 생각했다. 생각해보니 검색법이 잘못된듯 postCSS를 사용한다는 전제로 검색했던 것인가 휴.)

폴더 구조

이렇게 작업을 하고 나니까 생각해보니 vue파일을 ignore 시키면 될 것 같아서 stylelint의 ignore 설정법을 찾아봤고,,,

https://stylelint.io/user-guide/ignore-code

 

Ignoring code | Stylelint

You can ignore:

stylelint.io

 

결론

.stylelintrc.js

module.exports = {
  extends: "stylelint-config-standard-scss",
  plugins: ["stylelint-scss", "stylelint-order"],
  rules: {
  	"order/properties-alphabetical-order": true, // 알파벳 정렬
    "no-eol-whitespace": true, // 줄 끝, 닫는 중괄호 뒤 공백 허용 여부
    "color-hex-length": "long", // 16진수 색상에 대해 표기법 지정
    "declaration-block-trailing-semicolon": "always", // 선언 블록 내 후행 세미콜론을 요구
    "color-no-invalid-hex": true, // 잘못된 16진수 색상을 허용 여부
    "font-family-no-duplicate-names": true, // 중복 폰트 선언 여부
    "declaration-block-no-duplicate-properties": true, // 같은 선언 내 중복 속성 선언 여부
    "declaration-block-no-duplicate-custom-properties": true, // 같은 선언 내 중복 사용자 속성 선언 여부
    "no-duplicate-at-import-rules": true, // 중복 @import 규칙을 허용 여부
    "media-query-list-comma-newline-after": "always", // 미디어 쿼리 목록 쉼표 뒤 줄 바꿈이나 공백

    // 개행
    "rule-empty-line-before": "always-multi-line", // 규칙 앞에 빈줄 속성
    "block-opening-brace-newline-before": "never-single-line", // 여는 중괄호 앞 (선택자와 괄호 사이)
    "block-opening-brace-newline-after": "always", // 여는 중괄호 다음 개행
    "block-closing-brace-newline-after": "always", // 닫는 중괄호 뒤
    "block-closing-brace-newline-before": "always", // 닫는 중괄호 앞
    "block-opening-brace-space-before": "always", // 여는 중괄호 앞
  },
}

 

.stylelintignore

루트에 파일 생성

*.vue

그렇다 걍 vue 파일을 ignore시키면 되는 것이었다. 

(postcss, postcss-html, postcss-scss 모두필요엄슴 uninstall)

 

stylelint 적용

 

+ Recent posts