2. Vue

Vuetify로 할 일 앱 완성하기

이 문서는 Vue + Vuetify 실습 시리즈의 완결 편이다. 1편에서 Vite 개발 환경과 Vue 핵심 문법(ref·{{ }}·v-bind·@click·v-model·v-if·v-for) 여섯 가지를 익혔다는 전제로 쓰였고, 본문 번호는 1편에서 이어져 3부터 시작한다. 이 한 편에서 Vuetify 3 설치와 앱 골격 레이아웃, 반응형 그리드를 익힌 뒤, 그 화면 안에 할 일 앱을 얹어 등록·완료 표시·삭제까지 완성한다. 새로 배우는 것은 Vuetify 부품이고, 앱을 움직이는 Vue 문법은 1편의 여섯 가지를 넘지 않는다. 다 따라 하면 할 일을 입력해 목록에 쌓고, 눌러서 완료로 표시하고, 다 한 것을 지우는 실제 할 일 앱 한 벌이 손에 남는다.

3 – Vuetify 설치와 앱 골격 레이아웃

Vue가 집의 골조라면 Vuetify는 문·창틀·가구를 파는 완제품 매장이다. 버튼·카드·메뉴 같은 부품이 구글 Material Design(머티리얼 디자인, 구글이 정리한 화면 디자인 규격) 모양으로 미리 만들어져 있어, CSS를 한 줄도 쓰지 않고 짜임새 있는 화면을 조립한다. 이 묶음에서는 매장(Vuetify)을 프로젝트에 들이고, 첫 가구로 앱 골격을 세운다.

3.1 – 작업 환경 복원

1편 프로젝트를 이어서 쓴다. 이번에는 설치와 설정 파일 수정이 있으므로 개발 서버는 아직 켜지 않는다.

  1. VS Code를 실행하고 [File] → [Open Folder…] 로 vue_start 폴더를 연다.
  2. [터미널] → [새 터미널] 을 열고 아래를 입력한 뒤 Enter 를 누른다.
cd vuetify-app
  1. 이전에 켜 둔 개발 서버가 돌고 있으면 그 터미널을 클릭하고 Ctrl+C 를 눌러 끈다.

Note: 서버를 끄는 이유가 있다. 잠시 뒤 고치는 vite.config.js 는 서버가 켜질 때 한 번만 읽는 설정 파일이라, 저장해도 돌던 서버에는 반영되지 않는다. 설치·설정을 끝내고 새로 켜는 편이 깔끔하다.

3.2 – Vuetify 3와 부속 패키지 설치

패키지 세 개를 설치한다. 본체, 아이콘 글꼴, 그리고 Vite용 연결 플러그인이다.

  1. 터미널에 아래 세 명령을 한 줄씩 입력하고, 줄마다 Enter 를 누른다.
npm install vuetify@3
npm install @mdi/font
npm install -D vite-plugin-vuetify
  1. 각 명령이 added ... packages 로 끝나는지 확인한다.
  2. VS Code 탐색기에서 package.json 을 열어 dependencies"vuetify": "^3....." 처럼 3으로 시작하는 버전이 적혔는지 확인한다.

세 패키지의 역할을 갈라 두면 이렇다. vuetify@3 은 부품 본체인데, 그냥 vuetify 로 설치하면 최신 메이저인 4가 깔릴 수 있어 @3 을 붙여 이 시리즈 기준인 3.x 최신판으로 고정한다. @mdi/font 는 Material Design Icons(머티리얼 디자인 아이콘) 글꼴로, 잠시 뒤 메뉴에 붙일 아이콘이 여기서 나온다. vite-plugin-vuetify 는 Vite와 Vuetify를 잇는 다리이며, 개발할 때만 쓰는 도구라 -D(개발 의존성) 표시를 붙여 설치한다.

3.3 – vite.config.js에 플러그인 등록

설치한 다리(플러그인)를 Vite 설정에 꽂는다. 파일명 vite.config.js, 수정 후 전체 코드다.

  1. 탐색기에서 vite.config.js 를 연다.
  2. 아래와 같이 두 곳(위쪽 import 한 줄, plugins 배열 안 한 항목)을 추가하고 Ctrl+S 로 저장한다.
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vuetify from 'vite-plugin-vuetify'

export default defineConfig({
  plugins: [vue(), vuetify({ autoImport: true })],
})

autoImport: true 는 “템플릿에 쓴 Vuetify 부품을 알아서 찾아 등록하라”는 옵션이다. 이 덕분에 앞으로 v-btn 이든 v-card 든 import 문 없이 태그만 적으면 되고, 빌드할 때는 실제로 쓴 부품만 담겨 결과물 용량도 준다.

3.4 – main.js에 Vuetify 연결

이제 시동 파일에서 Vuetify를 만들어 Vue 앱에 꽂는다. 파일명 main.js, 수정 후 전체 코드다.

  1. src/main.js 를 열고 내용 전체를 아래로 바꾼 뒤 Ctrl+S 로 저장한다.
import { createApp } from 'vue'
import App from './App.vue'

import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'
import { createVuetify } from 'vuetify'

const vuetify = createVuetify()

createApp(App).use(vuetify).mount('#app')

윗부분 import 세 줄이 아이콘 글꼴과 Vuetify 전체 스타일, 그리고 Vuetify 생성 함수를 들여온다. createVuetify() 로 Vuetify 한 벌을 만들고, .use(vuetify) 가 그것을 Vue 앱에 플러그인으로 꽂는 연결점이다. 1편에서 지운 import './style.css' 는 없는 상태 그대로 둔다. 꾸밈은 이제 Vuetify가 맡는다.

3.5 – v-app으로 동작 확인

연결이 됐는지 버튼 하나로 확인한다. 파일명 App.vue, 전체 코드다.

  1. src/App.vue 를 열고 내용 전체를 아래로 바꾼 뒤 Ctrl+S 로 저장한다.
<script setup>
</script>

<template>
  <v-app>
    <v-main>
      <v-btn color="primary">Vuetify 동작 확인</v-btn>
    </v-main>
  </v-app>
</template>
  1. 터미널에 npm run dev 를 입력하고 Enter 를 누른다.
  2. 브라우저에서 http://localhost:5173 을 열어, 파란 알약 모양 버튼이 보이는지 확인한다.
  3. 버튼을 클릭해 물결이 퍼지는 효과(ripple)가 나오는지 확인한다.

태그 구조가 1편의 SFC와 똑같고, 다른 점은 태그 이름이 v- 로 시작한다는 것뿐이다. <v-app> 은 모든 Vuetify 부품을 감싸는 최상위 틀로, 테마·배치 계산이 전부 이 안에서 이뤄진다. 그래서 어떤 화면을 만들든 템플릿의 맨 바깥은 항상 <v-app> 이다.

Warning: 버튼이 스타일 없는 민짜 글자로 나오면 → vite.config.js 의 plugins 등록(3.3)을 빠뜨렸거나, 설정 수정 전에 켠 서버가 그대로 도는 경우다. 터미널에서 Ctrl+C 로 끄고 npm run dev 로 다시 켜면 된다. 화면이 아예 하얗면 → 브라우저에서 F12 를 눌러 Console 탭의 빨간 에러를 읽고, main.js 의 import 철자를 대조한다.

3.6 – v-app-bar와 v-main으로 골격 세우기

동작이 확인됐으니 관리자 화면의 뼈대를 올린다. 먼저 상단 앱바와 본문 자리다. 파일명 App.vue, <template> 부분만 아래로 바꾼다.

  1. App.vue<template> 을 아래로 교체하고 Ctrl+S 로 저장한다.
<template>
  <v-app>
    <v-app-bar color="indigo">
      <v-app-bar-title>나의 관리자</v-app-bar-title>
    </v-app-bar>

    <v-main>
      <p class="pa-4">본문 영역이다.</p>
    </v-main>
  </v-app>
</template>
  1. 브라우저 위쪽에 남색 막대와 “나의 관리자” 제목이 붙었는지 확인한다.

v-app-bar 는 화면 맨 위에 고정되는 제목 막대이고, v-main 은 앱바·서랍 같은 고정 부품을 피해 본문 자리를 잡아 주는 틀이다. 본문을 v-main 안에 두면 앱바가 그만큼 자리를 비켜 주므로, “고정 부품은 v-app 바로 아래, 내용은 v-main 안”이 골격의 공식이다. class="pa-4" 는 안쪽 여백을 주는 Vuetify 유틸리티 클래스인데, 여백 체계는 다음 묶음(4.5)에서 제대로 다룬다.

3.7 – v-navigation-drawer와 햄버거 버튼 토글

관리자 화면의 상징인 왼쪽 서랍 메뉴를 달고, 앱바의 햄버거 버튼(≡)으로 여닫는다. 여닫는 상태는 1편에서 배운 refv-model 로 다룬다.

  1. App.vue<script setup> 을 아래로 교체한다.
<script setup>
import { ref } from 'vue'

const drawer = ref(true)
</script>
  1. <template><v-app-bar> 블록을 아래로 교체한다. 햄버거 버튼 한 줄이 추가된 것이다.
<v-app-bar color="indigo">
  <v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
  <v-app-bar-title>나의 관리자</v-app-bar-title>
</v-app-bar>
  1. <v-app-bar> 블록과 <v-main> 사이에 서랍 블록을 추가하고 Ctrl+S 로 저장한다.
<v-navigation-drawer v-model="drawer">
  <v-list>
    <v-list-item prepend-icon="mdi-format-list-checks" title="할 일"></v-list-item>
    <v-list-item prepend-icon="mdi-check-circle" title="완료함"></v-list-item>
    <v-list-item prepend-icon="mdi-cog" title="설정"></v-list-item>
  </v-list>
</v-navigation-drawer>
  1. 브라우저에서 햄버거 버튼(≡)을 클릭해 서랍이 닫히고, 다시 클릭하면 열리는지 확인한다.

여닫는 원리를 짚는다. drawer 는 참/거짓을 담는 ref 상자이고, v-model="drawer" 가 서랍의 열림 상태와 이 상자를 양방향으로 묶는다. 햄버거의 @click="drawer = !drawer" 는 클릭할 때마다 값을 반대로 뒤집는 식이라, 결국 1편에서 익힌 @clickv-model 이 부품에도 그대로 통한다. prepend-icon="mdi-check-circle"mdi- 이름은 3.2에서 설치한 아이콘 글꼴에서 온다.

Tip: 다른 아이콘을 쓰고 싶으면 Chrome 주소창에 https://pictogrammers.com/library/mdi/ 를 입력해 연다. 검색창에 home 처럼 영어 단어를 입력하고 마음에 드는 아이콘을 클릭하면 이름이 나온다. 그 이름 앞에 mdi- 를 붙여 prepend-icon="mdi-home" 처럼 쓴다.

3.8 – v-footer로 마무리와 골격 완성본 대조

바닥에 푸터를 붙여 골격을 완성하고, 전체 코드를 대조한다.

  1. <v-main> 블록 아래(단, </v-app> 위)에 한 줄을 추가하고 Ctrl+S 로 저장한다.
<v-footer app color="grey-lighten-3">© 2026 나의 관리자</v-footer>
  1. 여기까지의 App.vue 전체 완성본은 아래와 같다. 내 파일과 대조해 빠진 블록이 없는지 확인한다.
<script setup>
import { ref } from 'vue'

const drawer = ref(true)
</script>

<template>
  <v-app>
    <v-app-bar color="indigo">
      <v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
      <v-app-bar-title>나의 관리자</v-app-bar-title>
    </v-app-bar>

    <v-navigation-drawer v-model="drawer">
      <v-list>
        <v-list-item prepend-icon="mdi-format-list-checks" title="할 일"></v-list-item>
        <v-list-item prepend-icon="mdi-check-circle" title="완료함"></v-list-item>
        <v-list-item prepend-icon="mdi-cog" title="설정"></v-list-item>
      </v-list>
    </v-navigation-drawer>

    <v-main>
      <p class="pa-4">본문 영역이다.</p>
    </v-main>

    <v-footer app color="grey-lighten-3">© 2026 나의 관리자</v-footer>
  </v-app>
</template>

app 속성은 “이 푸터를 v-app 배치 계산에 넣어 바닥에 고정하라”는 표시다. 이 덕분에 v-main 이 푸터 높이만큼 본문 자리를 알아서 줄인다.

Info: 골격이 잘 섰는지 다섯 가지로 가린다. (1) 남색 앱바가 위에 고정된다 (2) 햄버거 클릭으로 서랍이 여닫힌다 (3) 메뉴 세 항목에 아이콘이 보인다 (4) 본문이 앱바 아래에서 온전히 보인다 (5) 회색 푸터가 바닥에 붙는다. 3개 이상 맞으면 합격이고, 안 맞는 항목은 해당 절로 돌아가 대조한다.

실습 코칭 프롬프트

이 묶음을 혼자 다시 해 보다 막히면, 아래 프롬프트의 { } 를 채워 대화형 AI(ChatGPT·Claude·Gemini)에 붙여 넣는다. AI가 답을 통째로 주는 대신 한 단계씩 이끌어 준다.

너는 Vuetify 3 설치와 레이아웃 실습 코치다.
나는 Vite로 만든 Vue 3 프로젝트에 Vuetify 3를 설치하고,
v-app-bar / v-navigation-drawer / v-main / v-footer로 관리자 골격을 세우는 실습을 하고 있다.
설치 구성: vuetify@3, @mdi/font, vite-plugin-vuetify(autoImport 사용).

내 상황:
- 지금까지 한 것: {지금까지 한 단계}
- 막힌 지점 또는 화면 증상: {막힌 지점}
- 터미널이나 브라우저 Console(F12)에 뜬 메시지(있는 그대로): {에러 메시지}
- 관련 파일 코드(vite.config.js / main.js / App.vue 중 해당분): {내 코드}

코칭 규칙:
1. 해결책을 한꺼번에 다 주지 말고, 다음 한 단계만 알려 준다.
2. 단계를 주기 전에, 원인이 설치·설정 등록·서버 재시작·코드 중 어디인지 먼저 짚는다.
3. 내가 그 단계를 해 본 결과를 물은 뒤에 다음 단계로 넘어간다.
4. 마지막에 앱바·서랍·푸터가 모두 제자리에 보이는지 확인 질문을 던진다.
  1. {지금까지 한 단계} — 완료한 지점. 예: 패키지 세 개 설치까지 마침.
  2. {막힌 지점} — 안 되는 증상. 예: v-btn이 스타일 없는 글자로만 나온다.
  3. {에러 메시지} — 터미널·Console의 빨간 글씨 전문. 없으면 없음 이라고 적는다.
  4. {내 코드} — 증상과 관련된 파일 전체를 붙인다. 스타일 문제면 vite.config.jsmain.js 둘 다 붙인다.

변수를 채운 완성 예시는 다음과 같다.

너는 Vuetify 3 설치와 레이아웃 실습 코치다.
나는 Vite로 만든 Vue 3 프로젝트에 Vuetify 3를 설치하고,
v-app-bar / v-navigation-drawer / v-main / v-footer로 관리자 골격을 세우는 실습을 하고 있다.
설치 구성: vuetify@3, @mdi/font, vite-plugin-vuetify(autoImport 사용).

내 상황:
- 지금까지 한 것: 패키지 세 개 설치와 main.js 수정까지 마침
- 막힌 지점 또는 화면 증상: v-btn이 스타일 없는 글자로만 나온다
- 터미널이나 브라우저 Console(F12)에 뜬 메시지(있는 그대로): 없음
- 관련 파일 코드(vite.config.js / main.js / App.vue 중 해당분): (vite.config.js와 main.js 전문 붙여 넣음)

코칭 규칙:
1. 해결책을 한꺼번에 다 주지 말고, 다음 한 단계만 알려 준다.
2. 단계를 주기 전에, 원인이 설치·설정 등록·서버 재시작·코드 중 어디인지 먼저 짚는다.
3. 내가 그 단계를 해 본 결과를 물은 뒤에 다음 단계로 넘어간다.
4. 마지막에 앱바·서랍·푸터가 모두 제자리에 보이는지 확인 질문을 던진다.

Tip: 코치가 해결 명령 대여섯 개를 한꺼번에 쏟으면 “규칙 1대로 다음 한 단계만 달라”고 되묻는다. 화면 증상만 적고 에러 메시지를 빠뜨리는 것이 가장 흔한 실수이니, 없으면 “없음”이라도 적어 넣는다.

4 – 그리드 시스템과 반응형 디자인

골격은 섰으니 이제 v-main 안을 채운다. Vuetify의 그리드는 가로 한 줄을 12칸으로 나눈 계란판이다. 각 내용물(v-col)이 “나는 4칸 차지”라고 선언하면 되고, 화면 폭에 따라 차지할 칸수를 다르게 선언하면 그것이 곧 반응형 디자인이다. 이 묶음이 끝나면 PC에서 3열, 태블릿에서 2열, 휴대폰에서 1열로 스스로 재배치되는 카드 화면이 손에 남는다.

4.1 – v-container·v-row·v-col 기본 구조

그리드는 상자 3종 세트로 만든다. v-container(내용 폭을 잡는 바깥 상자) 안에 v-row(12칸짜리 한 줄) 를 두고, 그 안에 v-col(칸을 차지하는 낱칸) 을 넣는다.

  1. App.vue<v-main> 블록을 아래로 교체하고 Ctrl+S 로 저장한다.
<v-main>
  <v-container>
    <v-row>
      <v-col>
        <v-card title="첫째 칸" text="v-col 하나"></v-card>
      </v-col>
      <v-col>
        <v-card title="둘째 칸" text="v-col 둘"></v-card>
      </v-col>
      <v-col>
        <v-card title="셋째 칸" text="v-col 셋"></v-card>
      </v-col>
    </v-row>
  </v-container>
</v-main>
  1. 브라우저에 흰 카드 세 장이 같은 폭으로 가로로 나란히 놓였는지 확인한다.

v-col 에 칸수를 안 적으면 형제끼리 12칸을 똑같이 나눠 갖는다. 셋이니 12 ÷ 3 = 4칸 씩이다. 안에 넣은 v-card 는 제목·본문이 달린 종이 카드 부품인데, 여기서는 그리드 크기를 눈으로 보는 내용물로만 쓰고 자세한 활용은 3편에서 다룬다.

4.2 – cols로 칸수 지정과 줄바꿈 규칙

칸수를 직접 정해 본다. 넓은 본문과 좁은 옆 칸 배치다.

  1. <v-row> 블록을 아래로 교체하고 Ctrl+S 로 저장한다.
<v-row>
  <v-col cols="8">
    <v-card title="본문" text="8칸을 차지한다"></v-card>
  </v-col>
  <v-col cols="4">
    <v-card title="옆 칸" text="4칸을 차지한다"></v-card>
  </v-col>
</v-row>
  1. 왼쪽이 넓고 오른쪽이 좁은 2단 배치가 됐는지 확인한다. 8 + 4 = 12 정상.
  2. 이번에는 두 cols 값을 모두 6 으로 고치고, v-col 블록 하나를 복사해 셋으로 만든 뒤 저장한다.
  3. 카드 두 장이 첫 줄에, 세 번째 카드가 둘째 줄로 내려갔는지 확인한다.

내려간 이유는 계란판 규칙 그대로다. 한 줄의 정원은 12칸인데 6 + 6 + 6 = 18 로 넘치므로, 12를 채운 다음 칸부터 자동으로 다음 줄로 흘러 내려간다. 이 “넘치면 다음 줄” 규칙이 잠시 뒤 반응형의 바탕이 된다.

4.3 – breakpoint로 화면 폭마다 칸수 바꾸기

이제 반응형이다. Vuetify는 화면 폭을 구간(breakpoint, 중단점)으로 나누고, v-col 에 구간별 칸수를 따로 선언하게 한다.

이름화면 폭대표 기기
xs600px 미만휴대폰
sm600 ~ 960px태블릿 세로
md960 ~ 1280px노트북
lg1280 ~ 1920px데스크톱
xl1920px 이상큰 모니터
  1. <v-row> 블록을 아래로 교체하고 Ctrl+S 로 저장한다.
<v-row>
  <v-col cols="12" sm="6" md="4">
    <v-card title="카드 1" text="반응형 칸"></v-card>
  </v-col>
  <v-col cols="12" sm="6" md="4">
    <v-card title="카드 2" text="반응형 칸"></v-card>
  </v-col>
  <v-col cols="12" sm="6" md="4">
    <v-card title="카드 3" text="반응형 칸"></v-card>
  </v-col>
</v-row>
  1. 브라우저 창의 오른쪽 가장자리를 마우스로 잡고 천천히 좁혔다 넓혔다 하며, 카드가 3열 → 2열 → 1열로 바뀌는지 확인한다.

선언을 읽는 법은 “작은 폭 기본값부터, 커지면 덮어쓰기”다. cols="12" 가 기본이라 휴대폰에서는 한 장이 12칸(한 줄 독차지, 1열)이고, 600px를 넘으면 sm="6" 이 덮어써 6칸(12 ÷ 6 = 2열), 960px를 넘으면 md="4" 가 덮어써 4칸(12 ÷ 4 = 3열)이 된다. CSS 미디어 쿼리를 직접 쓰지 않고 속성 세 개로 반응형이 끝나는 것이 Vuetify 그리드의 값어치다.

4.4 – 개발자 도구 기기 모드로 반응형 검증

창 크기 조절은 눈대중이라, 정확한 픽셀 폭에서 확인하는 도구를 쓴다. 브라우저 개발자 도구의 기기 모드다.

  1. 브라우저에서 F12 를 눌러 개발자 도구를 연다.
  2. Ctrl+Shift+M 을 눌러 기기 모드로 바꾼다. 화면 위에 기기 선택 막대가 생긴다.
  3. 막대 왼쪽 기기 목록에서 [Responsive] 를 고른다.
  4. 폭 입력칸을 클릭하고 500 을 입력한 뒤 Enter 를 누른다. 카드가 1열인지 확인한다.
  5. 같은 방법으로 700 을 입력해 2열, 1000 을 입력해 3열인지 확인한다.
  6. 확인이 끝나면 Ctrl+Shift+M 으로 기기 모드를 끄고, F12 로 개발자 도구를 닫는다.

500·700·1000이라는 수는 표의 경계값(600·960)을 하나씩 사이에 두고 고른 것이다. 세 구간에서 모두 의도한 열수가 나오면 breakpoint 선언이 맞다는 뜻이다.

Note: 기기 목록에서 [iPhone] 같은 실제 기기를 골라도 된다. 다만 학습 단계에서는 [Responsive]로 폭 숫자를 직접 치는 편이 “어느 경계에서 바뀌는지”를 또렷하게 드러낸다.

4.5 – 정렬과 여백 유틸리티

마지막으로 배치를 다듬는 두 도구를 익힌다. 줄 단위 정렬 속성과, 3.6에서 맛본 여백 클래스다.

  1. <v-row> 여는 태그를 아래로 고치고 Ctrl+S 로 저장한다.
<v-row justify="center">
  1. 브라우저 폭을 700px(2열 구간)로 두고, 둘째 줄에 홀로 남은 카드 3이 왼쪽 끝이 아니라 가운데에 오는지 확인한다.
  2. v-card 에 여백 클래스를 붙여 본다. 카드 1의 태그만 아래처럼 고치고 저장한다.
<v-card class="pa-4" title="카드 1" text="반응형 칸"></v-card>
  1. 카드 1의 테두리와 글자 사이가 다른 카드보다 넓어졌는지 확인한다.

justify 는 줄(v-row) 안에서 칸들을 가로로 어디에 모을지 정한다. center(가운데)·start(앞)·end(뒤)·space-between(양 끝 벌림)을 주로 쓴다. 여백 클래스는 p(padding, 안쪽 여백)·m(margin, 바깥 여백)에 방향과 크기를 붙이는 조립식 이름이다.

클래스
pa-4안쪽 여백을 네 방향(all) 모두 4단계
pt-2안쪽 여백을 위(top)만 2단계
ma-2바깥 여백을 네 방향 모두 2단계
mb-4바깥 여백을 아래(bottom)만 4단계

크기 숫자는 0~16단계이고 1단계가 4px이다. pa-44 × 4 = 16px 여백 이 된다. 이 유틸리티 클래스 덕분에 이 시리즈 내내 CSS 파일을 열 일이 없다.

Info: 이 묶음의 합격 기준 다섯 가지다. (1) 카드 세 장이 그리드 안에 놓인다 (2) 폭 500px에서 1열이다 (3) 폭 700px에서 2열이다 (4) 폭 1000px에서 3열이다 (5) pa-4 를 붙인 카드의 안쪽 여백이 눈에 띄게 넓다. 3개 이상 맞으면 합격이고, 안 맞는 항목은 해당 절로 돌아가 대조한다.

실습 코칭 프롬프트

그리드를 내 화면 구상에 응용해 보다 막히면, 아래 프롬프트를 채워 대화형 AI에 붙여 넣는다.

너는 Vuetify 3 그리드·반응형 실습 코치다.
나는 v-container / v-row / v-col과 breakpoint 속성(cols·sm·md), justify, 여백 클래스(pa·ma)로
화면 폭에 따라 재배치되는 카드 배치를 만들고 있다.

내 상황:
- 만들려는 배치: {만들려는 배치}
- 지금 내 코드(App.vue의 v-main 블록 전체): {내 코드}
- 막힌 지점 또는 화면 증상: {막힌 지점}

코칭 규칙:
1. 완성 코드를 통째로 주지 말고, 다음 한 단계만 알려 준다.
2. 먼저 내 코드에서 원인이 되는 줄을 짚고, 12칸 규칙과 breakpoint 중 무엇이 어긋났는지 한 줄로 설명한다.
3. 고칠 속성의 이름(cols, sm, md, justify 등)만 힌트로 주고 값은 내가 정하게 한다.
4. 내가 고쳐 본 결과를 확인한 뒤 다음 단계로 넘어간다.
5. 배치가 완성되면, 폭 500 / 700 / 1000px에서 각각 몇 열인지 나에게 답하게 해 점검한다.
  1. {만들려는 배치} — 응용 목표. 예: PC에서는 4열, 휴대폰에서는 2열인 사진 갤러리를 만들고 싶다.
  2. {내 코드}<v-main> 부터 </v-main> 까지 통째로 복사해 붙인다. 줄 하나만 붙이면 코치가 12칸 합계를 못 센다.
  3. {막힌 지점} — 증상을 구체적으로. 예: 폭을 줄여도 카드가 계속 3열이다.

변수를 채운 완성 예시는 다음과 같다.

너는 Vuetify 3 그리드·반응형 실습 코치다.
나는 v-container / v-row / v-col과 breakpoint 속성(cols·sm·md), justify, 여백 클래스(pa·ma)로
화면 폭에 따라 재배치되는 카드 배치를 만들고 있다.

내 상황:
- 만들려는 배치: PC에서는 4열, 휴대폰에서는 2열인 사진 갤러리를 만들고 싶다
- 지금 내 코드(App.vue의 v-main 블록 전체): (v-main 블록 전문 붙여 넣음)
- 막힌 지점 또는 화면 증상: 폭을 줄여도 카드가 계속 3열이다

코칭 규칙:
1. 완성 코드를 통째로 주지 말고, 다음 한 단계만 알려 준다.
2. 먼저 내 코드에서 원인이 되는 줄을 짚고, 12칸 규칙과 breakpoint 중 무엇이 어긋났는지 한 줄로 설명한다.
3. 고칠 속성의 이름(cols, sm, md, justify 등)만 힌트로 주고 값은 내가 정하게 한다.
4. 내가 고쳐 본 결과를 확인한 뒤 다음 단계로 넘어간다.
5. 배치가 완성되면, 폭 500 / 700 / 1000px에서 각각 몇 열인지 나에게 답하게 해 점검한다.

Tip: 코치가 완성 코드를 바로 내밀면 “규칙 1과 3대로 속성 이름 힌트만 달라”고 되묻는다. 코드를 붙일 때 v-row 여는 태그를 빠뜨리는 실수가 잦은데, justify 문제는 그 줄에 있으므로 반드시 포함한다.

5 – Vuetify 부품으로 할 일 앱 만들기

지금까지 세운 골격(앱바·서랍·푸터)은 그대로 두고, v-main 안을 할 일 앱으로 채운다. 1편 2.7에서 만든 미니 할 일 목록을 이번에는 Vuetify 부품으로 옮긴다. 맨 HTML <input>·<button>·<ul> 대신 v-text-field·v-btn·v-list를 쓰면, 같은 동작에 Material Design 모양과 여백·정렬이 얹힌다. 이 묶음에서 쓰는 Vue 문법은 1편에서 익힌 여섯 가지(ref, {{ }}, v-bind(:), @click, v-model, v-if·v-for)를 넘지 않는다. 새 문법은 들이지 않는다.

5.1 – 작업 환경 복원

단원 4까지 이어 온 프로젝트를 그대로 쓴다. v-main 안의 그리드 예제만 걷어 내고 할 일 앱을 앉힌다.

  1. VS Code를 실행하고 [File] → [Open Folder…] 로 vue_start 폴더를 연다.
  2. [터미널] → [새 터미널] 을 열고 아래를 입력한 뒤 Enter 를 누른다.
cd vuetify-app
npm run dev
  1. 브라우저에서 http://localhost:5173 이 열리고, 단원 4에서 만든 카드 그리드 화면이 나오는지 확인한다.

5.2 – 할 일 데이터와 입력창 얹기

먼저 입력창부터 놓는다. 1편에서는 맨 <input>v-model 을 걸었는데, Vuetify에서는 v-text-field 에 같은 v-model 을 건다. 부품만 바뀌고 문법은 그대로다.

  1. src/App.vue<script setup> 을 아래로 교체한다. 할 일 문자열 하나(newTodo)와 할 일 배열(todos)을 둔다.
<script setup>
import { ref } from 'vue'

const drawer = ref(true)

const newTodo = ref('')
const todos = ref([])
</script>
  1. <v-main> 블록 전체를 아래로 교체하고 Ctrl+S 로 저장한다.
<v-main>
  <v-container>
    <v-text-field v-model="newTodo" label="할 일"></v-text-field>
    <p>입력 중인 내용: {{ newTodo }}</p>
  </v-container>
</v-main>
  1. 브라우저에서 입력창에 우유 사기 를 타이핑하고, 아래 “입력 중인 내용:” 뒤에 글자가 실시간으로 따라오는지 확인한다.

v-text-field 는 1편의 <input> 자리를 대신하는 Vuetify 입력 부품이다. v-model="newTodo" 가 입력창과 newTodo 상자를 양방향으로 묶는 원리는 1편 2.6과 똑같다. 다른 점은 label="할 일" 인데, 이 라벨은 입력창이 비면 안쪽에 안내로 떠 있다가, 글자를 치기 시작하면 위로 올라가 붙는다. 맨 <input>placeholder 와 달리 입력 중에도 무엇을 적는 칸인지 남는다.

5.3 – 등록 버튼과 목록 그리기

입력한 할 일을 배열에 넣고, v-list 로 목록을 그린다. 배열에 넣는 함수(addTodo)와 v-for 는 1편 2.7에서 쓴 것을 그대로 옮긴다. 5.2에서 넣은 “입력 중인 내용” 문단은 입력이 잘 묶였는지 눈으로 보려는 확인용이었으므로, 이제 목록으로 바꾸며 지운다.

  1. <script setup> 에 등록 함수를 추가한다. todos 선언 아래에 붙인다.
const addTodo = () => {
  if (newTodo.value === '') return
  todos.value.push(newTodo.value)
  newTodo.value = ''
}
  1. <v-container> 안을 아래로 교체하고 Ctrl+S 로 저장한다. 입력창 옆에 버튼을 두고, 그 아래 목록을 그린다.
<v-container>
  <v-text-field v-model="newTodo" label="할 일"></v-text-field>
  <v-btn color="primary" @click="addTodo">등록</v-btn>

  <v-list>
    <v-list-item v-for="(todo, i) in todos" :key="i" :title="todo"></v-list-item>
  </v-list>
</v-container>
  1. 입력창에 우유 사기 를 입력하고 [등록] 버튼을 클릭한다. 아래 목록에 항목이 하나 생기는지 확인한다.
  2. Vue 복습, Vuetify 예습 을 이어서 등록해 세 줄 목록이 되는지 확인한다. 1 + 2 = 3개 정상.

동작 흐름은 1편과 같다. [등록] 클릭 → addTodo 실행 → todos 배열에 값 추가 → 배열이 바뀐 것을 Vue가 감지 → v-for 가 늘어난 개수만큼 v-list-item 을 다시 그린다. 바뀐 것은 그리는 부품뿐이다. <ul><li> 대신 v-list·v-list-item 을 쓰면 목록에 여백·구분·클릭 반응이 얹힌다. :title="todo" 의 앞 콜론은 1편 2.4에서 익힌 v-bind 로, 따옴표 안을 글자가 아니라 변수 todo 로 읽으라는 표시다.

Warning: [등록]을 눌러도 목록이 안 늘면 addTodo 안의 todos.value.push 를 확인한다. .value 를 빼고 todos.push 로 쓰면 상자를 열지 않고 상자째 다루려는 셈이라 동작하지 않는다 → todos.value.push(...) 로 고치면 된다. 이 .value 규칙은 1편 2.5에서 다룬 그대로다.

5.4 – Enter 키로 등록하기

버튼을 누르지 않고 Enter 로도 등록되게 한다. 1편 2.7에서 쓴 @keyup.enterv-text-field 에 그대로 건다.

  1. v-text-field 태그를 아래로 고치고 Ctrl+S 로 저장한다. @keyup.enter 한 조각만 추가된 것이다.
<v-text-field v-model="newTodo" @keyup.enter="addTodo" label="할 일"></v-text-field>
  1. 입력창에 택배 부치기 를 입력하고 버튼 대신 Enter 를 누른다. 목록에 바로 추가되는지 확인한다.

@keyup.enter 는 “키보드에서 손을 뗄 때, 그 키가 Enter면 addTodo 를 실행하라”는 이벤트다. @click 과 마찬가지로 1편에서 익힌 @(v-on) 문법이라 새로 배울 것이 없다. 이제 버튼 클릭과 Enter 두 길 모두로 같은 함수를 부른다.

5.5 – 빈 목록 안내 문구

할 일이 하나도 없을 때 빈 화면 대신 안내 문구를 띄운다. 1편 2.7의 v-if·v-else 를 그대로 쓴다.

  1. <v-list> 블록 위에 안내 문구 한 줄을 추가하고, <v-list> 여는 태그에 v-else 를 붙인다. 아래처럼 고치고 Ctrl+S 로 저장한다.
<p v-if="todos.length === 0" class="pa-4">등록된 할 일이 없다.</p>
<v-list v-else>
  <v-list-item v-for="(todo, i) in todos" :key="i" :title="todo"></v-list-item>
</v-list>
  1. 브라우저를 F5 로 새로고침한다. 할 일을 등록하기 전에는 “등록된 할 일이 없다.” 가 보이는지 확인한다.
  2. 할 일을 하나 등록하면 안내 문구가 사라지고 목록이 나오는지 확인한다.

v-if="todos.length === 0" 은 배열 길이가 0일 때만 안내 문구를 보인다. 할 일을 등록해 배열이 비지 않으면 이 조건이 거짓이 돼, v-else 를 단 목록 쪽으로 화면이 넘어간다. 두 태그는 짝이라, 한쪽이 보이면 다른 쪽은 숨는다. class="pa-4" 는 단원 4.5에서 익힌 여백 클래스다.

Info: 여기까지 잘됐는지 다섯 가지로 가린다. (1) 입력창에 라벨이 뜬다 (2) [등록] 버튼으로 항목이 추가된다 (3) Enter 로도 추가된다 (4) 목록이 비면 안내 문구가 보인다 (5) 항목을 넣으면 안내 문구가 사라진다. 3개 이상 맞으면 합격이고, 안 맞는 항목은 해당 절로 돌아가 대조한다.

실습 코칭 프롬프트

이 묶음을 혼자 다시 해 보다 막히면, 아래 프롬프트의 { } 를 채워 대화형 AI(ChatGPT·Claude·Gemini)에 붙여 넣는다. AI가 답을 통째로 주는 대신 한 단계씩 이끌어 준다.

너는 Vuetify 3 할 일 앱 실습 코치다.
나는 Vite로 만든 Vue 3 프로젝트에서 v-text-field / v-btn / v-list로 할 일 입력·목록을 만들고 있다.
쓰는 Vue 문법은 여섯 가지(ref, {{ }}, v-bind(:), @click, v-model, v-if·v-for)를 넘지 않는다.
새 문법(computed, watch 등)은 쓰지 않는다.

내 상황:
- 지금까지 한 것: {지금까지 한 단계}
- 막힌 지점 또는 화면 증상: {막힌 지점}
- 지금 내 코드(App.vue의 script setup과 v-main 블록): {내 코드}

코칭 규칙:
1. 완성 코드를 통째로 주지 말고, 다음 한 단계만 알려 준다.
2. 먼저 내 코드에서 원인이 되는 줄을 짚고, 여섯 문법 중 무엇이 어긋났는지 한 줄로 설명한다.
3. 여섯 문법 밖의 새 문법으로 해결하려 하지 말고, 익힌 여섯 가지 안에서 힌트를 준다.
4. 내가 고쳐 본 결과를 확인한 뒤 다음 단계로 넘어간다.
5. 입력·등록·목록·빈 안내가 모두 동작하는지 마지막에 확인 질문을 던진다.
  1. {지금까지 한 단계} — 완료한 지점. 예: v-text-field와 등록 버튼까지 놓음.
  2. {막힌 지점} — 안 되는 증상. 예: Enter를 눌러도 목록이 안 늘어난다.
  3. {내 코드}<script setup><v-main> 을 둘 다 복사해 붙인다. 한쪽만 붙이면 코치가 원인을 못 찾는다.

변수를 채운 완성 예시는 다음과 같다.

너는 Vuetify 3 할 일 앱 실습 코치다.
나는 Vite로 만든 Vue 3 프로젝트에서 v-text-field / v-btn / v-list로 할 일 입력·목록을 만들고 있다.
쓰는 Vue 문법은 여섯 가지(ref, {{ }}, v-bind(:), @click, v-model, v-if·v-for)를 넘지 않는다.
새 문법(computed, watch 등)은 쓰지 않는다.

내 상황:
- 지금까지 한 것: v-text-field와 등록 버튼까지 놓음
- 막힌 지점 또는 화면 증상: Enter를 눌러도 목록이 안 늘어난다
- 지금 내 코드(App.vue의 script setup과 v-main 블록): (script setup과 v-main 전문 붙여 넣음)

코칭 규칙:
1. 완성 코드를 통째로 주지 말고, 다음 한 단계만 알려 준다.
2. 먼저 내 코드에서 원인이 되는 줄을 짚고, 여섯 문법 중 무엇이 어긋났는지 한 줄로 설명한다.
3. 여섯 문법 밖의 새 문법으로 해결하려 하지 말고, 익힌 여섯 가지 안에서 힌트를 준다.
4. 내가 고쳐 본 결과를 확인한 뒤 다음 단계로 넘어간다.
5. 입력·등록·목록·빈 안내가 모두 동작하는지 마지막에 확인 질문을 던진다.

Tip: 코치가 computedwatch 같은 새 문법을 꺼내면 “여섯 문법 안에서만 알려 달라”고 되묻는다. 코드를 붙일 때 script setupv-main 을 둘 다 넣는다. 한쪽만 붙이는 것이 가장 흔한 실수다.

6 – 완료 표시와 삭제로 앱 완성하기

앞 묶음에서 할 일을 등록하고 목록에 쌓는 데까지 왔다. 이제 실제 할 일 앱답게 두 기능을 더한다. 항목을 눌러 완료로 표시하기, 그리고 다 한 항목을 지우기다. 두 기능 모두 새 Vue 문법 없이, 여섯 가지 안에서 만든다. 완료 여부를 담으려고 할 일을 문자열 대신 객체({ text, done })로 바꾸는 것이 이 묶음의 핵심 손질이다.

6.1 – 작업 환경 복원

앞 묶음 프로젝트를 그대로 쓴다.

  1. VS Code에서 vue_start 폴더가 열려 있는지 확인한다. 닫혀 있으면 [File] → [Open Folder…] 로 연다.
  2. 터미널에서 개발 서버가 꺼져 있으면 아래를 입력한다.
cd vuetify-app
npm run dev
  1. 브라우저에서 단원 5까지 만든 할 일 목록 화면이 나오는지 확인한다.

6.2 – 할 일을 객체로 바꾸기

완료 여부를 저장하려면 할 일 하나에 두 정보가 필요하다. 내용(text)과 완료 여부(done)다. 여기에 항목마다 서로 겹치지 않는 이름표 id 를 하나 더 붙인다. 이 이름표는 잠시 뒤 삭제에서 “어느 항목을 지울지”를 순번이 아니라 이름표로 가리키려고 미리 넣어 두는 것이다. 그래서 배열에 문자열 대신 이 세 값을 담은 객체를 넣도록 addTodo 를 손본다.

  1. <script setup>addTodo 를 아래로 교체한다. push 에 넣는 값이 문자열에서 객체로 바뀐 것이다.
const addTodo = () => {
  if (newTodo.value === '') return
  todos.value.push({ id: Date.now(), text: newTodo.value, done: false })
  newTodo.value = ''
}
  1. 목록의 v-list-item 을 객체에 맞춰 고친다. 내용은 :title="todo.text" 로 꺼내고, v-for 의 이름표(:key)도 순번 i 대신 todo.id 로 바꾼다. 아래로 고치고 Ctrl+S 로 저장한다.
<v-list-item v-for="todo in todos" :key="todo.id" :title="todo.text"></v-list-item>
  1. 브라우저를 F5 로 새로고침하고, 할 일 두어 개를 등록해 이전과 똑같이 목록에 나오는지 확인한다.

이제 배열 한 칸에는 { id: 1717000000000, text: '우유 사기', done: false } 처럼 객체가 들어간다. text 는 내용, done 은 완료 여부이며 처음엔 false(안 함)로 시작한다. idDate.now() 로 만든 값인데, 이 함수는 지금 시각을 밀리초 숫자로 돌려주므로 항목마다 서로 다른 값이 붙어 이름표 노릇을 한다.

v-for:keytodo.id 로 바꾼 것이 중요하다. 순번 i 를 이름표로 쓰면, 중간 항목을 지웠을 때 아래 항목들의 순번이 한 칸씩 당겨지면서 Vue가 화면 조각을 엉뚱한 항목에 붙일 수 있다. id 는 항목이 사라지든 순서가 바뀌든 그대로라, Vue가 각 항목을 정확히 알아본다. 목록에서 내용을 꺼낼 때 todo 만으로는 객체 전체라 글자로 못 쓰므로, todo.text 로 내용만 꺼낸다.

6.3 – 클릭으로 완료 표시하기

항목을 클릭하면 완료 여부를 뒤집는다. 1편 2.5에서 카운터를 @click 으로 뒤집던 것과 같은 방식이다.

  1. <script setup> 에 완료 토글 함수를 추가한다. addTodo 아래에 붙인다.
const toggleDone = (todo) => {
  todo.done = !todo.done
}
  1. v-list-item@click 을 걸고, 완료된 항목에 체크 아이콘을 붙인다. 아래로 고치고 Ctrl+S 로 저장한다.
<v-list-item
  v-for="todo in todos"
  :key="todo.id"
  :title="todo.text"
  @click="toggleDone(todo)"
  :append-icon="todo.done ? 'mdi-check' : ''"
></v-list-item>
  1. 목록의 아무 항목이나 클릭한다. 오른쪽에 체크 아이콘이 붙는지 확인한다.
  2. 같은 항목을 다시 클릭한다. 체크 아이콘이 사라지는지 확인한다.

toggleDone 은 넘겨받은 할 일 객체의 done 값을 반대로 뒤집는다. 클릭할 때마다 false → true → false 로 오간다. @click="toggleDone(todo)" 는 지금 그린 항목의 객체를 그대로 함수에 넘긴다. :append-icon 은 항목 오른쪽 끝에 아이콘을 붙이는 자리인데, 값 자리에 삼항 연산(todo.done ? 'mdi-check' : '')을 넣어 “완료면 체크 아이콘, 아니면 빈 값”으로 정한다. 이 삼항은 자바스크립트 표현이라 새 Vue 문법이 아니다.

Note: 항목 글자에 취소선을 긋고 싶을 수 있다. 다만 이 교안은 CSS 장식을 더하지 않으므로, 완료 표시는 체크 아이콘 하나로만 나타낸다. 취소선·색 변화 같은 꾸밈은 앞으로 CSS나 Vuetify 클래스를 익힌 뒤에 얹으면 된다.

6.4 – 삭제 버튼 달기

다 한 할 일을 목록에서 지운다. 배열에서 한 칸을 빼는 일인데, 순번 대신 항목의 이름표(id)로 “이 이름표가 아닌 것만 남겨라”라고 걸러 새 배열로 바꾼다.

  1. <script setup> 에 삭제 함수를 추가한다. toggleDone 아래에 붙인다.
const removeTodo = (id) => {
  todos.value = todos.value.filter((todo) => todo.id !== id)
}
  1. v-list-item 안에 삭제 버튼을 넣는다. 항목 태그를 열고 닫는 형태로 바꾼 뒤, 그 안에 버튼을 둔다. 아래로 고치고 Ctrl+S 로 저장한다.
<v-list-item
  v-for="todo in todos"
  :key="todo.id"
  :title="todo.text"
  @click="toggleDone(todo)"
  :append-icon="todo.done ? 'mdi-check' : ''"
>
  <template #append>
    <v-btn icon="mdi-delete" size="small" variant="text" @click.stop="removeTodo(todo.id)"></v-btn>
  </template>
</v-list-item>
  1. 항목 오른쪽의 휴지통 버튼을 클릭한다. 그 항목만 목록에서 사라지는지 확인한다.
  2. 항목 세 개 중 가운데 것을 지워, 위아래 두 개만 남는지 확인한다. 3 - 1 = 2개 정상.
  3. 이번에는 항목 세 개 중 첫째·셋째에 완료 체크를 켠 다음 가운데 것을 지운다. 남은 두 항목의 체크가 그대로 붙어 있는지 확인한다.

removeTodo(id) 는 배열에서 넘겨받은 이름표(id)와 같은 항목만 빼고 나머지로 배열을 다시 채운다. filter 는 “조건에 맞는 것만 골라 새 배열을 만든다”는 뜻이고, 조건 todo.id !== id 는 “이름표가 지울 대상과 다른 것”이므로 그 하나만 빠진다. 삭제 버튼은 항목의 #append(오른쪽 끝 자리)에 넣었는데, 앞서 붙인 체크 아이콘도 같은 오른쪽 자리라 아이콘은 삭제 버튼 왼쪽에 나란히 놓인다.

순번(i)이 아니라 이름표(id)로 지우는 이유가 5번 확인에서 드러난다. 순번으로 지우면 가운데를 뺐을 때 아래 항목들의 순번이 당겨지면서, Vue가 완료 체크를 엉뚱한 항목에 옮겨 붙일 수 있다. 이름표는 항목마다 고정이라, 어느 자리를 지워도 남은 항목의 체크가 제자리를 지킨다.

여기서 @click.stop.stop 이 중요하다. 삭제 버튼은 완료 토글이 걸린 항목 안에 있어, 버튼만 누르려 해도 항목 클릭까지 같이 일어난다. .stop 은 “이 클릭을 항목까지 번지지 않게 여기서 멈춰라”는 표시라, 삭제만 일어나고 완료 토글은 일어나지 않는다. @click 뒤에 붙이는 조각이라 1편의 @ 문법 안에 있다.

Warning: 삭제 버튼을 눌렀는데 완료 체크까지 같이 바뀌면 @click.stop.stop 이 빠진 것이다 → 버튼 태그를 @click.stop="removeTodo(todo.id)" 로 고치면 항목 클릭이 번지지 않는다.

6.5 – 완성본 대조와 남은 개수 표시

마지막으로 아직 안 한 할 일이 몇 개인지 위에 보인다. 그리고 앱 전체 코드를 대조한다.

  1. <v-container> 맨 위, v-text-field 위에 남은 개수 줄을 추가하고 Ctrl+S 로 저장한다.
<p class="pa-2">전체 {{ todos.length }}개</p>
  1. 여기까지의 App.vue 전체 완성본은 아래와 같다. 내 파일과 대조해 빠진 블록이 없는지 확인한다.
<script setup>
import { ref } from 'vue'

const drawer = ref(true)

const newTodo = ref('')
const todos = ref([])

const addTodo = () => {
  if (newTodo.value === '') return
  todos.value.push({ id: Date.now(), text: newTodo.value, done: false })
  newTodo.value = ''
}

const toggleDone = (todo) => {
  todo.done = !todo.done
}

const removeTodo = (id) => {
  todos.value = todos.value.filter((todo) => todo.id !== id)
}
</script>

<template>
  <v-app>
    <v-app-bar color="indigo">
      <v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
      <v-app-bar-title>할 일 앱</v-app-bar-title>
    </v-app-bar>

    <v-navigation-drawer v-model="drawer">
      <v-list>
        <v-list-item prepend-icon="mdi-format-list-checks" title="할 일"></v-list-item>
        <v-list-item prepend-icon="mdi-check-circle" title="완료함"></v-list-item>
        <v-list-item prepend-icon="mdi-cog" title="설정"></v-list-item>
      </v-list>
    </v-navigation-drawer>

    <v-main>
      <v-container>
        <p class="pa-2">전체 {{ todos.length }}개</p>

        <v-text-field
          v-model="newTodo"
          @keyup.enter="addTodo"
          label="할 일"
        ></v-text-field>
        <v-btn color="primary" @click="addTodo">등록</v-btn>

        <p v-if="todos.length === 0" class="pa-4">등록된 할 일이 없다.</p>
        <v-list v-else>
          <v-list-item
            v-for="todo in todos"
            :key="todo.id"
            :title="todo.text"
            @click="toggleDone(todo)"
            :append-icon="todo.done ? 'mdi-check' : ''"
          >
            <template #append>
              <v-btn
                icon="mdi-delete"
                size="small"
                variant="text"
                @click.stop="removeTodo(todo.id)"
              ></v-btn>
            </template>
          </v-list-item>
        </v-list>
      </v-container>
    </v-main>

    <v-footer app color="grey-lighten-3">© 2026 할 일 앱</v-footer>
  </v-app>
</template>

{{ todos.length }} 는 1편 2.3에서 익힌 출력 구멍으로, 배열 길이를 그대로 찍는다. 항목을 등록하거나 지울 때마다 배열 길이가 바뀌므로 이 숫자도 따라 바뀐다. 앱바 제목과 서랍 메뉴 문구는 할 일 앱에 맞춰 손봤다.

Info: 앱이 완성됐는지 다섯 가지로 가린다. (1) 입력·[등록]·Enter 로 할 일이 쌓인다 (2) 항목 클릭에 체크 아이콘이 붙고 다시 클릭하면 사라진다 (3) 휴지통 버튼으로 그 항목만 지워진다 (4) 삭제 버튼을 눌러도 완료 체크는 바뀌지 않는다 (5) 위쪽 “전체 N개” 숫자가 등록·삭제에 따라 바뀐다. 3개 이상 맞으면 합격이고, 안 맞는 항목은 해당 절로 돌아가 대조한다.

실습 코칭 프롬프트

완료·삭제 기능을 내 방식으로 손봐 보다 막히면, 아래 프롬프트를 채워 대화형 AI에 붙여 넣는다.

너는 Vuetify 3 할 일 앱 실습 코치다.
나는 할 일을 객체({ text, done })로 배열에 담고,
항목 클릭으로 완료를 토글하고 휴지통 버튼으로 삭제하는 기능을 만들고 있다.
쓰는 Vue 문법은 여섯 가지(ref, {{ }}, v-bind(:), @click, v-model, v-if·v-for)를 넘지 않는다.
새 문법(computed, watch 등)은 쓰지 않는다.

내 상황:
- 만들려는 기능: {만들려는 기능}
- 지금 내 코드(App.vue의 script setup과 v-list 블록): {내 코드}
- 막힌 지점 또는 화면 증상: {막힌 지점}

코칭 규칙:
1. 완성 코드를 통째로 주지 말고, 다음 한 단계만 알려 준다.
2. 먼저 내 코드에서 원인이 되는 줄을 짚고, 여섯 문법 중 무엇이 어긋났는지 한 줄로 설명한다.
3. 여섯 문법 밖의 새 문법으로 해결하려 하지 말고, 익힌 여섯 가지 안에서 힌트를 준다.
4. 클릭이 항목과 버튼에 겹치는 문제면 @click.stop을 떠올리게 힌트만 준다.
5. 내가 고쳐 본 결과를 확인한 뒤 다음 단계로 넘어간다.
  1. {만들려는 기능} — 응용 목표. 예: 완료한 할 일만 한꺼번에 지우는 버튼을 만들고 싶다.
  2. {내 코드}<script setup>v-list 블록을 둘 다 붙인다. 삭제·완료 문제는 두 곳이 얽혀 있다.
  3. {막힌 지점} — 증상을 구체적으로. 예: 삭제를 누르면 완료 체크도 같이 바뀐다.

변수를 채운 완성 예시는 다음과 같다.

너는 Vuetify 3 할 일 앱 실습 코치다.
나는 할 일을 객체({ text, done })로 배열에 담고,
항목 클릭으로 완료를 토글하고 휴지통 버튼으로 삭제하는 기능을 만들고 있다.
쓰는 Vue 문법은 여섯 가지(ref, {{ }}, v-bind(:), @click, v-model, v-if·v-for)를 넘지 않는다.
새 문법(computed, watch 등)은 쓰지 않는다.

내 상황:
- 만들려는 기능: 완료한 할 일만 한꺼번에 지우는 버튼을 만들고 싶다
- 지금 내 코드(App.vue의 script setup과 v-list 블록): (script setup과 v-list 전문 붙여 넣음)
- 막힌 지점 또는 화면 증상: 삭제를 누르면 완료 체크도 같이 바뀐다

코칭 규칙:
1. 완성 코드를 통째로 주지 말고, 다음 한 단계만 알려 준다.
2. 먼저 내 코드에서 원인이 되는 줄을 짚고, 여섯 문법 중 무엇이 어긋났는지 한 줄로 설명한다.
3. 여섯 문법 밖의 새 문법으로 해결하려 하지 말고, 익힌 여섯 가지 안에서 힌트를 준다.
4. 클릭이 항목과 버튼에 겹치는 문제면 @click.stop을 떠올리게 힌트만 준다.
5. 내가 고쳐 본 결과를 확인한 뒤 다음 단계로 넘어간다.

Tip: 코치가 computedfilter 로 목록을 새로 만들어 해결하려 하면 “여섯 문법 안에서, 지금 배열을 그대로 쓰는 방법으로 알려 달라”고 되묻는다. 삭제·완료가 겹치는 증상이면 @click.stop 을 아직 안 붙였는지부터 본다.

댓글 남기기