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편 프로젝트를 이어서 쓴다. 이번에는 설치와 설정 파일 수정이 있으므로 개발 서버는 아직 켜지 않는다.
- VS Code를 실행하고 [File] → [Open Folder…] 로
vue_start폴더를 연다. - [터미널] → [새 터미널] 을 열고 아래를 입력한 뒤
Enter를 누른다.
cd vuetify-app
- 이전에 켜 둔 개발 서버가 돌고 있으면 그 터미널을 클릭하고
Ctrl+C를 눌러 끈다.
Note: 서버를 끄는 이유가 있다. 잠시 뒤 고치는
vite.config.js는 서버가 켜질 때 한 번만 읽는 설정 파일이라, 저장해도 돌던 서버에는 반영되지 않는다. 설치·설정을 끝내고 새로 켜는 편이 깔끔하다.
3.2 – Vuetify 3와 부속 패키지 설치
패키지 세 개를 설치한다. 본체, 아이콘 글꼴, 그리고 Vite용 연결 플러그인이다.
- 터미널에 아래 세 명령을 한 줄씩 입력하고, 줄마다
Enter를 누른다.
npm install vuetify@3
npm install @mdi/font
npm install -D vite-plugin-vuetify
- 각 명령이
added ... packages로 끝나는지 확인한다. - 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, 수정 후 전체 코드다.
- 탐색기에서
vite.config.js를 연다. - 아래와 같이 두 곳(위쪽 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, 수정 후 전체 코드다.
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, 전체 코드다.
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>
- 터미널에
npm run dev를 입력하고Enter를 누른다. - 브라우저에서
http://localhost:5173을 열어, 파란 알약 모양 버튼이 보이는지 확인한다. - 버튼을 클릭해 물결이 퍼지는 효과(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> 부분만 아래로 바꾼다.
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>
- 브라우저 위쪽에 남색 막대와 “나의 관리자” 제목이 붙었는지 확인한다.
v-app-bar 는 화면 맨 위에 고정되는 제목 막대이고, v-main 은 앱바·서랍 같은 고정 부품을 피해 본문 자리를 잡아 주는 틀이다. 본문을 v-main 안에 두면 앱바가 그만큼 자리를 비켜 주므로, “고정 부품은 v-app 바로 아래, 내용은 v-main 안”이 골격의 공식이다. class="pa-4" 는 안쪽 여백을 주는 Vuetify 유틸리티 클래스인데, 여백 체계는 다음 묶음(4.5)에서 제대로 다룬다.
3.7 – v-navigation-drawer와 햄버거 버튼 토글
관리자 화면의 상징인 왼쪽 서랍 메뉴를 달고, 앱바의 햄버거 버튼(≡)으로 여닫는다. 여닫는 상태는 1편에서 배운 ref 와 v-model 로 다룬다.
App.vue의<script setup>을 아래로 교체한다.
<script setup>
import { ref } from 'vue'
const drawer = ref(true)
</script>
<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>
<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>
- 브라우저에서 햄버거 버튼(≡)을 클릭해 서랍이 닫히고, 다시 클릭하면 열리는지 확인한다.
여닫는 원리를 짚는다. drawer 는 참/거짓을 담는 ref 상자이고, v-model="drawer" 가 서랍의 열림 상태와 이 상자를 양방향으로 묶는다. 햄버거의 @click="drawer = !drawer" 는 클릭할 때마다 값을 반대로 뒤집는 식이라, 결국 1편에서 익힌 @click 과 v-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로 마무리와 골격 완성본 대조
바닥에 푸터를 붙여 골격을 완성하고, 전체 코드를 대조한다.
<v-main>블록 아래(단,</v-app>위)에 한 줄을 추가하고Ctrl+S로 저장한다.
<v-footer app color="grey-lighten-3">© 2026 나의 관리자</v-footer>
- 여기까지의
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. 마지막에 앱바·서랍·푸터가 모두 제자리에 보이는지 확인 질문을 던진다.
{지금까지 한 단계}— 완료한 지점. 예:패키지 세 개 설치까지 마침.{막힌 지점}— 안 되는 증상. 예:v-btn이 스타일 없는 글자로만 나온다.{에러 메시지}— 터미널·Console의 빨간 글씨 전문. 없으면없음이라고 적는다.{내 코드}— 증상과 관련된 파일 전체를 붙인다. 스타일 문제면vite.config.js와main.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(칸을 차지하는 낱칸) 을 넣는다.
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>
- 브라우저에 흰 카드 세 장이 같은 폭으로 가로로 나란히 놓였는지 확인한다.
v-col 에 칸수를 안 적으면 형제끼리 12칸을 똑같이 나눠 갖는다. 셋이니 12 ÷ 3 = 4칸 씩이다. 안에 넣은 v-card 는 제목·본문이 달린 종이 카드 부품인데, 여기서는 그리드 크기를 눈으로 보는 내용물로만 쓰고 자세한 활용은 3편에서 다룬다.
4.2 – cols로 칸수 지정과 줄바꿈 규칙
칸수를 직접 정해 본다. 넓은 본문과 좁은 옆 칸 배치다.
<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>
- 왼쪽이 넓고 오른쪽이 좁은 2단 배치가 됐는지 확인한다.
8 + 4 = 12 정상. - 이번에는 두
cols값을 모두6으로 고치고,v-col블록 하나를 복사해 셋으로 만든 뒤 저장한다. - 카드 두 장이 첫 줄에, 세 번째 카드가 둘째 줄로 내려갔는지 확인한다.
내려간 이유는 계란판 규칙 그대로다. 한 줄의 정원은 12칸인데 6 + 6 + 6 = 18 로 넘치므로, 12를 채운 다음 칸부터 자동으로 다음 줄로 흘러 내려간다. 이 “넘치면 다음 줄” 규칙이 잠시 뒤 반응형의 바탕이 된다.
4.3 – breakpoint로 화면 폭마다 칸수 바꾸기
이제 반응형이다. Vuetify는 화면 폭을 구간(breakpoint, 중단점)으로 나누고, v-col 에 구간별 칸수를 따로 선언하게 한다.
| 이름 | 화면 폭 | 대표 기기 |
|---|---|---|
xs | 600px 미만 | 휴대폰 |
sm | 600 ~ 960px | 태블릿 세로 |
md | 960 ~ 1280px | 노트북 |
lg | 1280 ~ 1920px | 데스크톱 |
xl | 1920px 이상 | 큰 모니터 |
<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>
- 브라우저 창의 오른쪽 가장자리를 마우스로 잡고 천천히 좁혔다 넓혔다 하며, 카드가 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 – 개발자 도구 기기 모드로 반응형 검증
창 크기 조절은 눈대중이라, 정확한 픽셀 폭에서 확인하는 도구를 쓴다. 브라우저 개발자 도구의 기기 모드다.
- 브라우저에서
F12를 눌러 개발자 도구를 연다. Ctrl+Shift+M을 눌러 기기 모드로 바꾼다. 화면 위에 기기 선택 막대가 생긴다.- 막대 왼쪽 기기 목록에서 [Responsive] 를 고른다.
- 폭 입력칸을 클릭하고
500을 입력한 뒤Enter를 누른다. 카드가 1열인지 확인한다. - 같은 방법으로
700을 입력해 2열,1000을 입력해 3열인지 확인한다. - 확인이 끝나면
Ctrl+Shift+M으로 기기 모드를 끄고,F12로 개발자 도구를 닫는다.
500·700·1000이라는 수는 표의 경계값(600·960)을 하나씩 사이에 두고 고른 것이다. 세 구간에서 모두 의도한 열수가 나오면 breakpoint 선언이 맞다는 뜻이다.
Note: 기기 목록에서 [iPhone] 같은 실제 기기를 골라도 된다. 다만 학습 단계에서는 [Responsive]로 폭 숫자를 직접 치는 편이 “어느 경계에서 바뀌는지”를 또렷하게 드러낸다.
4.5 – 정렬과 여백 유틸리티
마지막으로 배치를 다듬는 두 도구를 익힌다. 줄 단위 정렬 속성과, 3.6에서 맛본 여백 클래스다.
<v-row>여는 태그를 아래로 고치고Ctrl+S로 저장한다.
<v-row justify="center">
- 브라우저 폭을 700px(2열 구간)로 두고, 둘째 줄에 홀로 남은 카드 3이 왼쪽 끝이 아니라 가운데에 오는지 확인한다.
- 각
v-card에 여백 클래스를 붙여 본다. 카드 1의 태그만 아래처럼 고치고 저장한다.
<v-card class="pa-4" title="카드 1" text="반응형 칸"></v-card>
- 카드 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-4 면 4 × 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에서 각각 몇 열인지 나에게 답하게 해 점검한다.
{만들려는 배치}— 응용 목표. 예:PC에서는 4열, 휴대폰에서는 2열인 사진 갤러리를 만들고 싶다.{내 코드}—<v-main>부터</v-main>까지 통째로 복사해 붙인다. 줄 하나만 붙이면 코치가 12칸 합계를 못 센다.{막힌 지점}— 증상을 구체적으로. 예:폭을 줄여도 카드가 계속 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 안의 그리드 예제만 걷어 내고 할 일 앱을 앉힌다.
- VS Code를 실행하고 [File] → [Open Folder…] 로
vue_start폴더를 연다. - [터미널] → [새 터미널] 을 열고 아래를 입력한 뒤
Enter를 누른다.
cd vuetify-app
npm run dev
- 브라우저에서
http://localhost:5173이 열리고, 단원 4에서 만든 카드 그리드 화면이 나오는지 확인한다.
5.2 – 할 일 데이터와 입력창 얹기
먼저 입력창부터 놓는다. 1편에서는 맨 <input> 에 v-model 을 걸었는데, Vuetify에서는 v-text-field 에 같은 v-model 을 건다. 부품만 바뀌고 문법은 그대로다.
src/App.vue의<script setup>을 아래로 교체한다. 할 일 문자열 하나(newTodo)와 할 일 배열(todos)을 둔다.
<script setup>
import { ref } from 'vue'
const drawer = ref(true)
const newTodo = ref('')
const todos = ref([])
</script>
<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>
- 브라우저에서 입력창에
우유 사기를 타이핑하고, 아래 “입력 중인 내용:” 뒤에 글자가 실시간으로 따라오는지 확인한다.
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에서 넣은 “입력 중인 내용” 문단은 입력이 잘 묶였는지 눈으로 보려는 확인용이었으므로, 이제 목록으로 바꾸며 지운다.
<script setup>에 등록 함수를 추가한다.todos선언 아래에 붙인다.
const addTodo = () => {
if (newTodo.value === '') return
todos.value.push(newTodo.value)
newTodo.value = ''
}
<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>
- 입력창에
우유 사기를 입력하고 [등록] 버튼을 클릭한다. 아래 목록에 항목이 하나 생기는지 확인한다. 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.enter 를 v-text-field 에 그대로 건다.
v-text-field태그를 아래로 고치고Ctrl+S로 저장한다.@keyup.enter한 조각만 추가된 것이다.
<v-text-field v-model="newTodo" @keyup.enter="addTodo" label="할 일"></v-text-field>
- 입력창에
택배 부치기를 입력하고 버튼 대신Enter를 누른다. 목록에 바로 추가되는지 확인한다.
@keyup.enter 는 “키보드에서 손을 뗄 때, 그 키가 Enter면 addTodo 를 실행하라”는 이벤트다. @click 과 마찬가지로 1편에서 익힌 @(v-on) 문법이라 새로 배울 것이 없다. 이제 버튼 클릭과 Enter 두 길 모두로 같은 함수를 부른다.
5.5 – 빈 목록 안내 문구
할 일이 하나도 없을 때 빈 화면 대신 안내 문구를 띄운다. 1편 2.7의 v-if·v-else 를 그대로 쓴다.
<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>
- 브라우저를
F5로 새로고침한다. 할 일을 등록하기 전에는 “등록된 할 일이 없다.” 가 보이는지 확인한다. - 할 일을 하나 등록하면 안내 문구가 사라지고 목록이 나오는지 확인한다.
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. 입력·등록·목록·빈 안내가 모두 동작하는지 마지막에 확인 질문을 던진다.
{지금까지 한 단계}— 완료한 지점. 예:v-text-field와 등록 버튼까지 놓음.{막힌 지점}— 안 되는 증상. 예:Enter를 눌러도 목록이 안 늘어난다.{내 코드}—<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: 코치가
computed나watch같은 새 문법을 꺼내면 “여섯 문법 안에서만 알려 달라”고 되묻는다. 코드를 붙일 때script setup과v-main을 둘 다 넣는다. 한쪽만 붙이는 것이 가장 흔한 실수다.
6 – 완료 표시와 삭제로 앱 완성하기
앞 묶음에서 할 일을 등록하고 목록에 쌓는 데까지 왔다. 이제 실제 할 일 앱답게 두 기능을 더한다. 항목을 눌러 완료로 표시하기, 그리고 다 한 항목을 지우기다. 두 기능 모두 새 Vue 문법 없이, 여섯 가지 안에서 만든다. 완료 여부를 담으려고 할 일을 문자열 대신 객체({ text, done })로 바꾸는 것이 이 묶음의 핵심 손질이다.
6.1 – 작업 환경 복원
앞 묶음 프로젝트를 그대로 쓴다.
- VS Code에서
vue_start폴더가 열려 있는지 확인한다. 닫혀 있으면 [File] → [Open Folder…] 로 연다. - 터미널에서 개발 서버가 꺼져 있으면 아래를 입력한다.
cd vuetify-app
npm run dev
- 브라우저에서 단원 5까지 만든 할 일 목록 화면이 나오는지 확인한다.
6.2 – 할 일을 객체로 바꾸기
완료 여부를 저장하려면 할 일 하나에 두 정보가 필요하다. 내용(text)과 완료 여부(done)다. 여기에 항목마다 서로 겹치지 않는 이름표 id 를 하나 더 붙인다. 이 이름표는 잠시 뒤 삭제에서 “어느 항목을 지울지”를 순번이 아니라 이름표로 가리키려고 미리 넣어 두는 것이다. 그래서 배열에 문자열 대신 이 세 값을 담은 객체를 넣도록 addTodo 를 손본다.
<script setup>의addTodo를 아래로 교체한다.push에 넣는 값이 문자열에서 객체로 바뀐 것이다.
const addTodo = () => {
if (newTodo.value === '') return
todos.value.push({ id: Date.now(), text: newTodo.value, done: false })
newTodo.value = ''
}
- 목록의
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>
- 브라우저를
F5로 새로고침하고, 할 일 두어 개를 등록해 이전과 똑같이 목록에 나오는지 확인한다.
이제 배열 한 칸에는 { id: 1717000000000, text: '우유 사기', done: false } 처럼 객체가 들어간다. text 는 내용, done 은 완료 여부이며 처음엔 false(안 함)로 시작한다. id 는 Date.now() 로 만든 값인데, 이 함수는 지금 시각을 밀리초 숫자로 돌려주므로 항목마다 서로 다른 값이 붙어 이름표 노릇을 한다.
v-for 의 :key 를 todo.id 로 바꾼 것이 중요하다. 순번 i 를 이름표로 쓰면, 중간 항목을 지웠을 때 아래 항목들의 순번이 한 칸씩 당겨지면서 Vue가 화면 조각을 엉뚱한 항목에 붙일 수 있다. id 는 항목이 사라지든 순서가 바뀌든 그대로라, Vue가 각 항목을 정확히 알아본다. 목록에서 내용을 꺼낼 때 todo 만으로는 객체 전체라 글자로 못 쓰므로, todo.text 로 내용만 꺼낸다.
6.3 – 클릭으로 완료 표시하기
항목을 클릭하면 완료 여부를 뒤집는다. 1편 2.5에서 카운터를 @click 으로 뒤집던 것과 같은 방식이다.
<script setup>에 완료 토글 함수를 추가한다.addTodo아래에 붙인다.
const toggleDone = (todo) => {
todo.done = !todo.done
}
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>
- 목록의 아무 항목이나 클릭한다. 오른쪽에 체크 아이콘이 붙는지 확인한다.
- 같은 항목을 다시 클릭한다. 체크 아이콘이 사라지는지 확인한다.
toggleDone 은 넘겨받은 할 일 객체의 done 값을 반대로 뒤집는다. 클릭할 때마다 false → true → false 로 오간다. @click="toggleDone(todo)" 는 지금 그린 항목의 객체를 그대로 함수에 넘긴다. :append-icon 은 항목 오른쪽 끝에 아이콘을 붙이는 자리인데, 값 자리에 삼항 연산(todo.done ? 'mdi-check' : '')을 넣어 “완료면 체크 아이콘, 아니면 빈 값”으로 정한다. 이 삼항은 자바스크립트 표현이라 새 Vue 문법이 아니다.
Note: 항목 글자에 취소선을 긋고 싶을 수 있다. 다만 이 교안은 CSS 장식을 더하지 않으므로, 완료 표시는 체크 아이콘 하나로만 나타낸다. 취소선·색 변화 같은 꾸밈은 앞으로 CSS나 Vuetify 클래스를 익힌 뒤에 얹으면 된다.
6.4 – 삭제 버튼 달기
다 한 할 일을 목록에서 지운다. 배열에서 한 칸을 빼는 일인데, 순번 대신 항목의 이름표(id)로 “이 이름표가 아닌 것만 남겨라”라고 걸러 새 배열로 바꾼다.
<script setup>에 삭제 함수를 추가한다.toggleDone아래에 붙인다.
const removeTodo = (id) => {
todos.value = todos.value.filter((todo) => todo.id !== id)
}
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>
- 항목 오른쪽의 휴지통 버튼을 클릭한다. 그 항목만 목록에서 사라지는지 확인한다.
- 항목 세 개 중 가운데 것을 지워, 위아래 두 개만 남는지 확인한다.
3 - 1 = 2개 정상. - 이번에는 항목 세 개 중 첫째·셋째에 완료 체크를 켠 다음 가운데 것을 지운다. 남은 두 항목의 체크가 그대로 붙어 있는지 확인한다.
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 – 완성본 대조와 남은 개수 표시
마지막으로 아직 안 한 할 일이 몇 개인지 위에 보인다. 그리고 앱 전체 코드를 대조한다.
<v-container>맨 위,v-text-field위에 남은 개수 줄을 추가하고Ctrl+S로 저장한다.
<p class="pa-2">전체 {{ todos.length }}개</p>
- 여기까지의
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. 내가 고쳐 본 결과를 확인한 뒤 다음 단계로 넘어간다.
{만들려는 기능}— 응용 목표. 예:완료한 할 일만 한꺼번에 지우는 버튼을 만들고 싶다.{내 코드}—<script setup>과v-list블록을 둘 다 붙인다. 삭제·완료 문제는 두 곳이 얽혀 있다.{막힌 지점}— 증상을 구체적으로. 예:삭제를 누르면 완료 체크도 같이 바뀐다.
변수를 채운 완성 예시는 다음과 같다.
너는 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: 코치가
computed나filter로 목록을 새로 만들어 해결하려 하면 “여섯 문법 안에서, 지금 배열을 그대로 쓰는 방법으로 알려 달라”고 되묻는다. 삭제·완료가 겹치는 증상이면@click.stop을 아직 안 붙였는지부터 본다.