자바스크립트 인터프리터가 함수의 선언, 할당, 실행을 나눠해서 모든 선언이 코드의 선두로 끌어올려진 것처럼 동작하는 현상의 개념

즉, 자바스크립트 엔진이 실행컨텍스트가 활성화 될 때 변수정보를 수집하는데 

이때 실제 끌어올려지지는 않았지만 수집과정에서 엔진이 수집된 정보를 토대로 변수들을 알고 있기 때문에 끌어올려진 것으로 간주하는 개념 

 

설명보기

Q. var, let 둘 다 호이스팅이 된다? 

- 정답은 둘 다 호이스팅되나 동작때문에 var만 실행 시 에러가 나지 않는다. 이때문에 var만 호이스팅된다고 착각할 수 있다.

 

설명

var는 선언과 할당을 동시에 실행하므로 호이스팅되었을때, undefined가 메모리에 저장된다.

let은 선언과 할당을 별도로 실행한다.

따라서 호이스팅되었을때, 선언은 되었지만 할당은 되지 않아서 메모리에 저장되지 않으며 TDZ(Temporary Dead Zome)에 들어간다. 따라서 호출 시 초기화 전에는 엑세스할 수 없다는 에러가 뜨는데, 이것때문에 호이스팅 되지 않았다고 착각할 수 있다.

 

 

 

 

 

실행 컨텍스트는 실행한 코드에 제공 할 환경 정보들을 모아놓는 객체라고 생각하자.

콜 스택에 쌓아서 전체 코드를 관리하고 있어서 환경과 순서를 보장한다.

동작

실행 컨텍스트는 크게 3가지 동작을 한다.

  1. 내부 환경 정보 기록: 실행컨텍스트가 활성화 되는 시점(실행)에 선언된 변수를 수집한다. 
  2. 외부 환경 정보를 구성한다.  
  3. this 값을 설정하는 동작 구성

 

수집 정보

- 자바스크립트 엔진이 활용할 목적으로 생성할 뿐, 개발자가 코드에 접근 할 수 없다.

Variable Environment

  • 선언 시점의 Lexical Environment의 스냅샷이므로 변경사항이 반영되지 않는다.
  • Variable Environment를 생성하여 정보를 담고 복사하여 Lexical Environment를 만든다.

Lexical Environment

처음엔 Variable Environment와 같으나 변경사항이 실시간으로 반영된다. (이후에는 Lexical을 사용하는 이유)

  1. Environment Record (환경 기록)
    • 현재 컨텍스트내의 식별자(변수명, 함수명, 매개변수명....) 들에 대한 정보를 순서대로 저장한다.
    • 이때 정보의 수집을 마치게되면 실행컨텍스트가 관여할 코드들은 실행전이지만, 자바스크립트 엔진은 이미 해당 환경에 속해 있는 정보(변수명..)들을 알고 있기 때문에 '호이스팅'이 발생 할 수 있다.
  2. Outer-Environment Reference (외부 환경 참조)
    • 현재 호출된 함수가 선언된 당시 상위의 Lexical Environment를 참조한다.
    • 스코프 체인이 가능하게 하는 수집자료 (중첩된 자바스크립트 코드에서 스코프 탐색을 하기위해 사용)

This Binding

  • this 식별자가 바라봐야 할 대상 객체
  • 실행 컨텍스트 활성화 당시에 this로 지정된 객체가 저장된다. (this는 함수호출 방식 및 전역 여부에 따라 결정)



 

사용자 동작을 나열하며 간략히 공부해보자.

 

>>> 사용자 동작 시작

1. https://okayoon.tistory.com/ 를 검색한다.

 

2. DNS(Domain Name System)에서 주소를 검색, 해당 ip 주소를 찾아서 사용자가 입력한 URL과 함께 전달한다.

- 이때 캐싱된 DNS 기록이 있는지 먼저 확인한다.

 

3. 전달받은 ip 주소를 이용하여 웹 브라우저는 웹 서버에게 해당 웹 사이트의 리소스를 요청하고 해상 웹 서버는 리소스를 전달한다.

 

>>>> 전달받은 리소스를 통해 브라우저 렌더링 과정 시작

Main flow 예시 (출처: http://taligarsiel.com/Projects/howbrowserswork1.htm)

4. DOM Tree 구축을 위해 HTML을 파싱한다.

- DOM Tree는 문서 객체 모델로 모든 요소, 속성, 텍스트 등 (문서노드, 요소노드, 속성노드, 텍스트노드)을 트리 구조로 표현한 것.

- 중간에 외부 스타일 로드를 만나게되면 DOM Tree 생성을 중단하고 CSS 파싱을 시작한다.

- Style Sheets를 통해 CSSOM Tree를 생성한다.

- CSSOM Tree는 스타일 노드들을 트리 구조로 표현한 것. 

 

5. Render Tree 생성 

- DOM Tree와 CSSOM Tree를 통해 Render Tree를 생성한다.

- Render Tree는 최종적으로 실제 브라우저에 표현되어야하는 노드들로만 구성하여 트리 구조로 표현한 것.

(ex: <head> 태그나 display: none;와 같이 브라우저에 그려지지 않는 것들은 포함되지 않는다.)

- 만약 과정 중에 JS 코드를 만나게 된다면 JS 파싱을 위해 렌더링이 잠시 중단되고, JS 파싱이 끝난 후 재개된다.)

 

6. Layout (레이아웃)

- Render Tree와 viewport를 통해 노드들의 화면상 위치와 크기를 계산한다. 

(ex: 1em -> 16px)

 

7. Paint (그리기)

- Layout 단계에서 계산한 레이아웃을 각 레이어에 px 단위로 그린다. (이 단계에서 Layer 생성)

- Layout에 포함안된 속성들도 그린다. (ex: 색상, 그림자효과 등)

- Update Layer Tree: 렌더링에 사용될 최종 레이어들을 계산하여 생성하며 생성조건은 root object, css filter, position, transform, overflow, canvas, video....

- 사용자 경험을 위해 렌더링 엔진은 모든 파싱을 기다리지 않고 동시에 일부를 그리기 시작한다.

 

8. Composite (합성)

- Update Layer Tree에서 생성한 레이어들을 합성하여 한장의 비트맵으로 만들어 실제 화면에 나타낸다.

- 별도의 합성 스레드를 두어 합성 과정만 별도로 분리해서 진행한다.

 

Q. 만약 Re-rendering이 될 경우에는?

Reflow (리플로우)

- Layout 단계의 변화가 있을때 발생한다.

- Layout 계산부터 다시한다 (Layout -> Paint -> Composite)

Repaint (리페인트)

- Paint 단계의 변화가 있을때 발생한다.

- 재 결합된 Render Tree를 기반으로 다시 그린다. 

- Repaint는 Reflow가 발생하지 않고도 발생할 수 있다. (Paint -> Composite)

 

Q. <scrpt> 태그를 만나서 HTML 파싱이 중단되는 문제를 해결할 수는 없나?

script 태그에 async와 defer 속성을 사용할 수 있다. (둘 다 사용 시 우선순위는 async)

- async: 비동기

- defer: html 파싱이 끝나고 진행하는 속성

 

 


더 깊은 내용에 대해 알고 싶거나, 참고한 글을 확인하고 싶다면!

https://d2.naver.com/helloworld/59361

http://taligarsiel.com/Projects/howbrowserswork1.htm

 

브라우저 구조에 대해 간략히 공부해보자.

 

이미지 출처:  http://taligarsiel.com/Projects/howbrowserswork1.htm

 

1. User Interface (사용자 인터페이스)

사용자가 접근할 수 있는 모든 영역을 말한다.

즉, 페이지를 보여주는 창(사이트)을 제외한 나머지 모든 부분을 말한다.

ex: 주소표시줄, 이전이나 다음 혹은 새로고침 버튼등. 

user interface 예시 (네모박스를 제외한 모든 영역)

 

 

2. Browser Engine (브라우저 엔진)

User Interface와 Rendering Engine 두 사이의 동작을 제어한다. (연결 부분이 된다.)

자료 저장소(Data Storage)를 참조하고 있어서 로컬에 데이터 저장 및 읽기 등의 작업을 할 수 있다.

 

 

3. Rendering Engine (렌더링 엔진)

웹 서버로 부터 받은 자원을 브라우저 상에 표시하기 위한 작업을 한다.

즉, *HTML과 CSS을 파싱하는 동작을 한다. (중요!)

 

- 크롬은 각 탭마다 별도의 렌더링 엔진 인스턴스를 유지한다고 한다. (= 독립된 프로세스)

(크롬은 컴퓨터에서 사용하는 모든 탭, 플러그인 및 확장 프로그램에 대해 개별 프로세스를 생성하도록 설계되었다.)

별도의 프로세스

 

 

4. Networking (통신)

HTTP 요청과 같은 각종 네트워크 요청을 수행하는 부분이다.

 

 

5. Javascript Interpreter (or Javascript Engine ?) (자바스크립트 해석기) 

JS를 읽고 해석하고 실행하는 역할을 한다. (Chrome 같은 경우 V8엔진을 사용한다.)

 

 

6. UI Backend (or Display Backend) (UI 백엔드)

브라우저의 기본적인 위젯을 그리는 인터페이스이다.

ex: input, select, 

ui backend 예시

 

 

7. Data Persistance (자료 저장소)

브라우저 메모리(보조 기억 장치)를 사용하여 데이터를 저장하는 부분이다.

ex: LocalStorage, SessionStorage, Cookie...

 

 

 


더 깊은 내용에 대해 알고 싶거나, 참고한 글을 확인하고 싶다면!

https://d2.naver.com/helloworld/59361

http://taligarsiel.com/Projects/howbrowserswork1.htm

기존 작업자가 작업해 둔 코드가 있었고, 요구 조건에 따라 이슈가 전달되어왔다.

 

기존 코드

function addComma(value) {
	return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

 

addComma 결과 값

 

요구 조건은 아래와 같았고.. 

1. 숫자 천 단위마다 콤마 추가

2. 소수점에서는 콤마 추가하지 않아야 함

 

기존 코드에서는 소수점에서도 콤마가 추가되고 있었다.

function addComma(value) {
	return value.toString().replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ',');
}

수정한 addComma 결과 값

 

잘되는 듯 했지만, 크로스브라우징 이슈가 있었다. 

safari에서 흰페이지로 뜨는 것이었다. 두둥.. 

그리고 SyntaxError: Invalid regular expression: invalid group specifier name 에러 메시지가 출력되고 있었다.

 

왜???

검토해보니, 정규식에 사용된 lookbehind 패턴 때문이었다.

 

정규식에는 두가지 알고리즘이 존재한다고 한다.

  • Deterministic Finite Automaton (DFA): 문자열의 문자를 한번만 확인한다.
  • Nondeterministic Finite Automaton (NFA): 최적의 일치를 찾을 때 까지 여러번 확인한다.

여기에서 자바스크립트는 NFA 알고리즘을 사용하고 있고 이 알고리즘의 동작으로 인해 Catastrophic Backtracking 가 일어날 수 있다고 한다.

 

regexp-catastrophic-backtracking 란?

더보기

오랜 시간 실행됨에 있어서 js 엔진이 중단되는 현상이라고 한다.

일반적인 증상으로 특정 문자열의 경우 CPU를 100% 사용하여 중단된다고 한다. 

그리고 중단되기 때문에 화면에 아무것또 뜨지 않았던 것이다.(;;)

해결법으로는..

1. 조합의 수를 줄여서 순서대로 찾아 나가는 방식

2. 역 추적을 방지하는 lookahead 패턴으로 개발

두번째 방식을 권장한다고 한다.

 

정규식의 전방 탐색(Lookahead)과 후방 탐색(Lookbehind) 패턴에 대해 간단히 알아보면...

전방 탐색은 작성한 패턴에 일치하는 영역이 존재하여도 그 값이 제외되어서 나오는 패턴이고

후방탐색은 전방 탐색이 앞에 있는 문자열을 탐색하는 거라면 후방탐색은 뒤에 있는 문자열을 탐색하는 것이라고 한다.

이해가 잘 안되면 이 블로그 글을 읽어보자! (저는 이해에 도움이 많이되었습니다 -> https://blog.hexabrain.net/205)

 

즉, 처음에 수정한 정규식에서 (?<!\.\d*) 이 부분이 lookbehind에 해당하는 것이었다.

(1234.1234) <- 뒤의 문자열을 탐색

해결법을 찾기 위해 검색에 검색을 해봤고~!

정리가 잘되어있는 블로그 글을 발견했다!!! ㄳㄳ -> Javascript에서 천 단위 구분 기호로 쉼표가 있는 숫자를 인쇄하는 방법

 

그리고 선택한 방법들을 작성해보았다.!

1. split, join

function addComma(value) {
	let splitVal = value.toString().split('.');   
	splitVal[0] = splitVal[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); 
    return splitVal.join("."); 
}

split-join 방식

 

2. replace function

function addComma(value){ 
	return value
    	.toString()
        .replace(/(\..*)$|(\d)(?=(\d{3})+(?!\d))/g, (digit, fract) => fract || digit + ',');
}

replace function을 방식

 

3. toLocaleString('ko-KR')

이때 maximumFractionDigits를 따로 수정하지 않으면 소수점 자릿수는 3으로 기본 값으로 출력된다.

(예시로 5로 바꾸어봤다)

function addComma(value) {
	return value.toLocaleString('ko-KR', { maximumFractionDigits: 5 });
}

toLocaleString 방식

 

여러 방식을 알아보면서 정규식에 대해서 정말정말 공부하면 도움이 많이 되겠다고 생각한 하루였다.

인프런에 정규식 강의가 있던데,.ㅎㅎ..

 

 


참고!! (감사감사! 합니다.)

toLocaleString()

lookbehind 문법은 사파리와 익스플로러에서 쓸 수 없다.

Javascript에서 천 단위 구분 기호로 쉼표가 있는 숫자를 인쇄하는 방법

내장 메소드를 사용하여 숫자 천단위마다 콤마 찍기

   할일 목록을 만들면서 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"
  }
}

+ Recent posts