# Tutorial - Todo App

์ด๋ฒˆ ํŠœํ† ๋ฆฌ์–ผ์—์„œ๋Š” ๊ฐ„๋‹จํ•œ Todo App์„ ๋งŒ๋“ค๋ฉด์„œ ๊ฐ ์ปดํฌ๋„ŒํŠธ์˜ ์œ ๋‹› ํ…Œ์ŠคํŠธ(Unit Test)๋ฅผ ์ž‘์„ฑํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ์ˆœ์„œ๋กœ ์ž‘์—…ํ•ฉ๋‹ˆ๋‹ค. ์ด ํŠœํ† ๋ฆฌ์–ผ์€ ํ•™์Šตํ•˜์‹œ๋Š” ๋ถ„๋“ค์„ ์œ„ํ•ด ๋‹จ๊ณ„๋ณ„๋กœ ์ฝ”๋“œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ตœ๋Œ€ํ•œ ๋จผ์ € ๋”ฐ๋ผ ํ•ด๋ณด์‹œ๊ณ  ์•ˆ๋˜๋Š” ๋ถ€๋ถ„๋งŒ ์ฐธ๊ณ ํ•˜๋Š” ์šฉ๋„๋กœ ์‚ฌ์šฉํ•ด ์ฃผ์„ธ์š”.

# ํ”„๋กœ์ ํŠธ ์…‹์—…

ํ”„๋กœ์ ํŠธ ์…‹์—… ์ˆœ์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์œผ๋ฉฐ ์ตœ์ข… ์ฝ”๋“œ๋Š” ์—ฌ๊ธฐ์„œ (opens new window) ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. vue cli ์ตœ์‹  ๋ฒ„์ „ ์„ค์น˜
npm install -g @vue/cli

  1. ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
vue create todo-app-test

ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ์‹œ ๋งค๋‰ด์–ผ ์„ ํƒ์€ ๋งํฌ (opens new window)๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”.

  1. eslint์˜ env ์˜ต์…˜ ์†์„ฑ์— jest: true ์ถ”๊ฐ€
module.exports = {
  root: true,
  env: {
    node: true,
    jest: true, // jest api๋“ค์„ ์‚ฌ์šฉํ•  ๋•Œ ์—๋Ÿฌ ํ‘œ์‹œ๊ฐ€ ๋‚˜์ง€ ์•Š๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.
  },
  //...
}

  1. @types/jest ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜

@types/jest ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” jest api๋“ค์˜ ์ž๋™์™„์„ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

npm install @types/jest -D

  1. jest.config.js์— testMatch ์„ค์ • ์ถ”๊ฐ€

ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์™€ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ํ•œ ํด๋” ๋‚ด์— ์กด์žฌํ•˜๋ฉด ์ฐพ์„ ๋•Œ ํŽธ๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฏ€๋กœ @vue/cli-plugin-unit-jest (opens new window)์˜ testMatch ์„ค์ •๊ฐ’์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์ธ jest ์„ค์ •๋“ค์˜ ์—ญํ• ๋“ค์€ ์—ฌ๊ธฐ (opens new window)๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”.

module.exports = {
  preset: "@vue/cli-plugin-unit-jest",
  testMatch: ["**/src/**/*.(test|spec).js"], // src ํด๋” ๋‚ด์˜ ํŒŒ์ผ ์ด๋ฆ„์— spec์ด๋‚˜ test๊ฐ€ ํฌํ•จ๋ผ ์žˆ๋‹ค๋ฉด ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
};

  1. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์‹คํ–‰ํ•ด๋ณด๊ธฐ

App ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ œ์™ธํ•œ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ญ์ œํ•ด ์ฃผ์„ธ์š”. ๊ทธ๋ฆฌ๊ณ  ์•„๋ž˜์™€ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด ์ค๋‹ˆ๋‹ค.

<!-- src/App.vue -->
<template>
  <div>
    <h1>Todo App</h1>
  </div>
</template>
// src/App.test.js
import { shallowMount } from "@vue/test-utils";

import App from "./App.vue";

describe("App", () => {
  it("renders title", () => {
    const wrapper = shallowMount(App);

    expect(wrapper.find("h1").text()).toMatch("Todo App");
  });
});

์ด์ œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•ด ์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

npm run test:unit

ํ„ฐ๋ฏธ๋„ ์ฐฝ์— ์•„๋ž˜์™€ ๊ฐ™์ด ์ถœ๋ ฅ ๋œ๋‹ค๋ฉด ํ”„๋กœ์ ํŠธ ์…‹์—…์ด ์ •์ƒ์ ์œผ๋กœ ์™„๋ฃŒ๋œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

> vue-cli-service test:unit

PASS  src/App.test.js
  App
    โœ“ renders title (21ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.403s
Ran all test suites.

TIP

// package.json
{
  //...
  "scripts": {
    // ...
    "test:unit": "vue-cli-service test:unit --watchAll"
  }
}

package.json์— test:unit ์Šคํฌ๋ฆฝํŠธ์— --watchAll ์˜ต์…˜์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”. ํ…Œ์ŠคํŠธ๊ฐ€ ์ถ”๊ฐ€๋˜๊ฑฐ๋‚˜ ์ˆ˜์ •๋˜๋ฉด ์ž๋™์œผ๋กœ ๋‹ค์‹œ ์‹คํ–‰์‹œ์ผœ์ค๋‹ˆ๋‹ค.

# ํ”„๋กœ์ ํŠธ ์‹œ์ž‘

ํ”„๋กœ์ ํŠธ ์ค€๋น„๊ฐ€ ๋๋‚ฌ์œผ๋‹ˆ Todo App์„ ๊ตฌํ˜„ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๊ฐ€ ๊ตฌํ˜„ํ•ด์•ผ ๋˜๋Š” ๊ธฐ๋Šฅ๋“ค์€ ๋‹ค์Œ๊ณผ ๊ฐ™์œผ๋ฉฐ ์ฐจ๋ก€๋Œ€๋กœ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

  • ํ•  ์ผ ์ถ”๊ฐ€ํ•˜๊ธฐ
  • ํ•  ์ผ ์ฒดํฌํ•˜๊ธฐ
  • ํ•  ์ผ ์‚ญ์ œํ•˜๊ธฐ

# ํ•  ์ผ ์ถ”๊ฐ€ํ•˜๊ธฐ

ํ•  ์ผ ์ถ”๊ฐ€ํ•˜๊ธฐ์˜ ๊ตฌํ˜„ ์ˆœ์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์œผ๋ฉฐ ์ตœ์ข… ์ฝ”๋“œ๋Š” ์—ฌ๊ธฐ์„œ (opens new window) ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. UI ๊ตฌํ˜„

์•„๋ž˜์— ๋นจ๊ฐ„ ๋ฐ•์Šค๋กœ ํ‘œ์‹œ๋œ ๋ถ€๋ถ„์˜ UI๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2021-08-23 แ„‹แ…ฉแ„’แ…ฎ 7 24 47

<!-- src/App.vue -->
<template>
  <div>
    <h1>Todo App</h1>
    <div>
      <label for="todo-control">ํ•  ์ผ ์ž‘์„ฑ</label>
      <div>
        <input
          id="todo-control"
          type="text"
          placeholder="ํ•  ์ผ์„ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”"
        />
        <button type="button">์ถ”๊ฐ€ํ•˜๊ธฐ</button>
      </div>
    </div>
  </div>
</template>

  1. UI ๊ตฌํ˜„ - ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ

UI ๊ตฌํ˜„ ์ฝ”๋“œ์—์„œ ์ž‘์„ฑํ•ด์•ผ ๋˜๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • "ํ•  ์ผ ์ž‘์„ฑ"์ด๋ผ๋Š” ํ…์ŠคํŠธ๊ฐ€ ํ™”๋ฉด์— ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค.
  • ํ•  ์ผ์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ์ธํ’‹ ํƒœ๊ทธ๊ฐ€ ํ™”๋ฉด์— ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค.
  • "์ถ”๊ฐ€ํ•˜๊ธฐ"๋ผ๋Š” ๋ฒ„ํŠผ์ด ํ™”๋ฉด์— ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค.

ํ•œ ๊ฐ€์ง€ ์•Œ์•„๋‘์…”์•ผ ํ•  ๊ฒƒ์€ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ์ž‘์„ฑํ•˜๋Š” ์‚ฌ๋žŒ์— ๋”ฐ๋ผ ์–ผ๋งˆ๋“ ์ง€ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์–ผ๋งˆ๋‚˜ ์„ธ์„ธํ•˜๊ฒŒ ์ž‘์„ฑํ•˜๋ƒ์— ๋”ฐ๋ผ์„œ ์ฝ”๋“œ์˜ ์•ˆ์ „์„ฑ์ด ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค. ์ฆ‰, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค๊ณ  ํ•ด์„œ ๋ชจ๋“  ์—๋Ÿฌ๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒŒ ์•„๋‹™๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฏ€๋กœ ํ•™์Šตํ•˜์‹ค ๋•Œ๋Š” ์กฐ๊ธˆ์ด๋ผ๋„ ๋” ์„ธ์„ธํ•˜๊ฒŒ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์‹œ๋Š” ๊ฑธ ๊ถŒ์žฅํ•ด ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

์ด์ œ ๊ด€๋ จ ์žˆ๋Š” ํ…Œ์ŠคํŠธ๋ผ๋ฆฌ ๋ฌถ์–ด์„œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

// src/App.test.vue
import { shallowMount } from "@vue/test-utils";

import App from "./App.vue";

describe("App", () => {
  it("renders title", () => {
    const wrapper = shallowMount(App);

    expect(wrapper.find("h1").text()).toMatch("Todo App");
  });

  it("renders label, input", () => {
    const wrapper = shallowMount(App);

    // 'ํ•  ์ผ ์ž‘์„ฑ'์ด๋ผ๋Š” ํ…์ŠคํŠธ๊ฐ€ ํ™”๋ฉด์— ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค.
    expect(wrapper.find("label").text()).toMatch("ํ•  ์ผ ์ž‘์„ฑ");

    // ํ•  ์ผ์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋Š” 'control'์ฐฝ ์ด ํ™”๋ฉด์— ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค.
    expect(wrapper.find("input").attributes("placeholder")).toMatch(
      "ํ•  ์ผ์„ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”"
    );
  });

  it("renders button", () => {
    const wrapper = shallowMount(App);

    // '์ถ”๊ฐ€ํ•˜๊ธฐ'๋ผ๋Š” ๋ฒ„ํŠผ์ด ํ™”๋ฉด์— ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค.
    expect(wrapper.find("button").text()).toMatch("์ถ”๊ฐ€ํ•˜๊ธฐ");
  });
});

๋ง‰์ƒ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑ์„ ํ•˜๊ณ  ๋ณด๋‹ˆ ํ•œ ๊ฐ€์ง€ ์•„์‰ฌ์šด ๊ฒŒ ์žˆ์Šต๋‹ˆ๋‹ค. ์ธํ’‹ ํƒœ๊ทธ์™€ ๋ ˆ์ด๋ธ”์ด ์—ฐ๊ฒฐ๋œ ์ง€๋„ ํ™•์ธํ•ด๋ณด๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋‚˜ ๋” ์ถ”๊ฐ€ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

it("connects label and input", () => {
  const wrapper = shallowMount(App);
  const TODO_CONTROL = 'todo-control'

  expect(wrapper.find("label").attributes("for")).toMatch(TODO_CONTROL);
  expect(wrapper.find("input").attributes("id")).toMatch(TODO_CONTROL);
});

์ด๋Ÿฐ ์‹์œผ๋กœ ํ•™์Šตํ•˜์‹œ๋ฉด์„œ ์•„์‰ฌ์šด ๋ถ€๋ถ„๋“ค์ด ์ƒ๊ธด๋‹ค๋ฉด ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด์„ธ์š”.

๊ทธ๋ฆฌ๊ณ  ์œ„์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ describe, it์˜ ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜๋ฅผ ํ•ฉ์ณ๋ณด๋ฉด "App renders label, input", "App renders button"์œผ๋กœ ๋ฌธ์žฅ์ด ๋งŒ๋“ค์–ด์ง€๋Š” ๊ฒƒ์„ ๋ณด์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. "render"๋Š” "App"์ด ๋‹จ์ˆ˜์ด๊ธฐ ๋•Œ๋ฌธ์— ๋’ค์— s๋ฅผ ๋ถ™์—ฌ์„œ "renders"๋กœ ์ž‘์„ฑํ•˜๋Š” ๊ฒ๋‹ˆ๋‹ค. ์ด๋Ÿฐ ์‹์œผ๋กœ ๋ฌธ๋ฒ•์— ๋งž๊ฒŒ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”. ๊ทธ๋ฆฌ๊ณ  ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์‹ค ๋•Œ๋Š” ํ•ญ์ƒ ๋ง์ด ๋˜๊ฒŒ ์ž‘์„ฑํ•˜์‹œ๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ด ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•˜์‹œ๊ฒŒ ๋˜๋ฉด ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ–ˆ์„ ๋•Œ๋„ ์•„๋ž˜์™€ ๊ฐ™์ด ์ˆ˜์›”ํ•˜๊ฒŒ ์ฝ์œผ์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

PASS  src/App.test.js
App
  โœ“ renders title (21ms)
  โœ“ renders label, input (5ms)
  โœ“ connects label and input (3ms)

๊ทธ๋ฆฌ๊ณ  ๋‚˜์ค‘์— ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ์˜ ์—ญํ• ์„ ํŒŒ์•…ํ•˜๊ณ  ์‹ถ์„ ๋•Œ ๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ(separation of concerns, SoC) (opens new window)๋ฅผ ์ž˜ํ•ด๋†“์•˜๋‹ค๋ฉด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋งŒ์œผ๋กœ๋„ ํŒŒ์•…์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ถ€๋ถ„์€ ํŠœํ† ๋ฆฌ์–ผ ๋งˆ์ง€๋ง‰ ๋ถ€๋ถ„์—์„œ ๋‹ค๋ค„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

  1. ๊ธฐ๋Šฅ ๊ตฌํ˜„ - ์ธํ’‹ ํƒœ๊ทธ์— ํ•  ์ผ ์ž‘์„ฑ ์‹œ data์— ํ•  ์ผ ํ…์ŠคํŠธ ๊ฐ’ ๋„ฃ๊ธฐ










ย 
ย 








ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

<!-- src/App.vue -->
<template>
  <div>
    <h1>Todo App</h1>
    <div>
      <label for="todo-control">ํ•  ์ผ ์ž‘์„ฑ</label>
      <div>
        <input
          id="todo-control"
          type="text"
          placeholder="ํ•  ์ผ์„ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”"
          :value="text"
          @input="handleInput"
        />
        <button type="button">์ถ”๊ฐ€ํ•˜๊ธฐ</button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      text: "",
    };
  },
  methods: {
    handleInput(event) {
      this.text = event.target.value;
    },
  },
};
</script>

v-model์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์€ ์ด์œ ๋Š” ํ˜„์žฌ ์‹œ์ ์—์„œ๋Š” IME ์ž…๋ ฅ(ํ•œ๊ตญ์–ด, ์ผ๋ณธ์–ด, ์ค‘๊ตญ์–ด)์— ๋Œ€ํ•ด์„œ ํ•œ๊ณ„์ ์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์ด ๋งํฌ (opens new window)๋ฅผ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”.

  1. ๊ธฐ๋Šฅ ๊ตฌํ˜„ - ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ
// src/App.test.js
  it("changes input value when listens input event", async () => {
    const wrapper = shallowMount(App);

    // setValue๋Š” ์•„๋ž˜ ๋‘ ์ฝ”๋“œ์˜ ์ถ•์•ฝ api ์ž…๋‹ˆ๋‹ค.
    await wrapper.find("input").setValue("์•„๋ฌด๊ฒƒ๋„ ์•ˆํ•˜๊ธฐ");
    // wrapper.find("input").element.value = "์•„๋ฌด๊ฒƒ๋„ ์•ˆํ•˜๊ธฐ";
    // wrapper.find("input").trigger("input");

    expect(wrapper.vm.text).toMatch("์•„๋ฌด๊ฒƒ๋„ ์•ˆํ•˜๊ธฐ");
  });

vue-test-utils ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„  input์ด๋ฒคํŠธ๋ฅผ trigger์‹œ event.target.value๋ฅผ ์ง์ ‘์ ์œผ๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ input์˜ value๊ฐ’์„ ๋ณ€๊ฒฝํ•œ ๋’ค input์ด๋ฒคํŠธ๋ฅผ triggerํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. input์ด๋ฒคํŠธ๋ฅผ triggerํ•˜๋ฉด handleInputํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๊ณ  data์˜ text๊ฐ’์ด ๋ณ€๊ฒฝ๋๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด๋ฒคํŠธ ํŠธ๋ฆฌ๊ฑฐ๋Š” ๋น„๋™๊ธฐ๋กœ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— async, await๋ฌธ๋ฒ•์„ ์‚ฌ์šฉํ•˜์—ฌ ์‹คํ–‰ ์ˆœ์„œ๋ฅผ ๋ณด์žฅํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  1. ๊ธฐ๋Šฅ ๊ตฌํ˜„ - "์ถ”๊ฐ€ํ•˜๊ธฐ" ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ํ•  ์ผ ์ถ”๊ฐ€














ย 


ย 
ย 
ย 
ย 
ย 








ย 
ย 






ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 



<!-- src/App.vue -->
<template>
  <div>
    <h1>Todo App</h1>
    <div>
      <label for="todo-control">ํ•  ์ผ ์ž‘์„ฑ</label>
      <div>
        <input
          id="todo-control"
          type="text"
          placeholder="ํ•  ์ผ์„ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”"
          :value="text"
          @input="handleInput"
        />
        <button type="button" @click="handleClickAddTodo">์ถ”๊ฐ€ํ•˜๊ธฐ</button>
      </div>
    </div>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        {{ todo.text }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      text: "",
      newId: 0,
      todos: [],
    };
  },
  methods: {
    handleInput(event) {
      this.text = event.target.value;
    },
    handleClickAddTodo() {
      // ํ•  ์ผ ์ถ”๊ฐ€
      this.todos.push({
        id: this.newId,
        text: this.text,
      });
      this.newId += 1;

      // ์ธํ’‹ ๊ฐ’ ์ดˆ๊ธฐํ™”
      this.text = "";
    },
  },
};
</script>

  1. ๊ธฐ๋Šฅ ๊ตฌํ˜„ - ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ
// src/App.test.js
it("adds todo when listens '์ถ”๊ฐ€ํ•˜๊ธฐ' click event", async () => {
  const wrapper = shallowMount(App);

  wrapper.find("input").setValue("์•„๋ฌด๊ฒƒ๋„ ์•ˆํ•˜๊ธฐ");
  await wrapper.find("button").trigger("click");

  expect(wrapper.find("li").text()).toContain("์•„๋ฌด๊ฒƒ๋„ ์•ˆํ•˜๊ธฐ");
});

์ธํ’‹ ํƒœ๊ทธ์— ํ•  ์ผ์„ ํƒ€์ดํ•‘ํ•˜๊ณ  "์ถ”๊ฐ€ํ•˜๊ธฐ" ๋ฒ„ํŠผ์ด ํด๋ฆญ ๋์„ ๋•Œ ํƒ€์ดํ•‘ํ•œ ํ•  ์ผ์ด ํ™”๋ฉด์— ์ถœ๋ ฅ ๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค.