02. Vue 기초 문법 점검: JavaScript, 템플릿
Vue 학습에 필요한 JavaScript 문법과 ref, 디렉티브, 이벤트, 컴포넌트 통신의 기초를 알아보자.
Vue에서 반드시 알아야 하는 JavaScript
Vue를 시작하기 전에 진짜~ 기본적인 JavaScript를 알아보고 가자.
전부 외울 필요는 없고, 아래 문법을 읽고 사용할 수 있으면 된다.
- 객체와 배열
- 함수와 화살표 함수
- 구조 분해와 스프레드 문법
map,filter,find,reduceimport와exportasync와await
변수, 객체, 배열
const name = 'Lee'
let count = 0
const user = {
id: 1,
name: 'Lee',
active: true
}
console.log(user.name)
console.log(user.active)
const users = [
{ id: 1, name: 'Lee', active: true },
{ id: 2, name: 'Kim', active: false }
]
함수와 구조 분해
const add = (a, b) => {
return a + b
}
const { name, age } = {
name: 'Lee',
age: 25
}
배열 함수와 자주 쓰는 문법
const names = users.map((user) => user.name)
const activeUsers = users.filter((user) => user.active)
const firstUser = users.find((user) => user.id === 1)
const newUser = {
...users[0],
active: false
}
const message = isLoggedIn ? '환영합니다' : '로그인하세요'
map은 배열을 변환하고,
filter는 조건에 맞는 항목만 남기며,
find는 조건에 맞는 첫 번째 항목을 찾는다.
상품 총합처럼 여러 값을 하나로 합칠 때는 reduce를 사용한다.
비동기 처리
const loadUsers = async () => {
const response = await fetch('/api/users')
const data = await response.json()
return data
}
Vue 코드를 읽는 방법
Vue 코드는 먼저 상태를 찾고, 그 상태를 바꾸는 함수와 템플릿의 연결을 확인하면 된다.
<script setup>
import { computed, ref } from 'vue'
// ref로 반응형 상태 선언
const products = ref([
{ id: 1, name: '키보드', price: 50000 },
{ id: 2, name: '마우스', price: 30000 }
])
const selectedProduct = ref(null)
const totalPrice = computed(() => {
return products.value.reduce((sum, product) => {
return sum + product.price
}, 0)
})
const selectProduct = (product) => {
selectedProduct.value = product
}
</script>
<template>
<div v-for="product in products" :key="product.id">
<button @click="selectProduct(product)">
{{ product.name }}
</button>
</div>
<p>총가격: {{ totalPrice }}원</p>
<p v-if="selectedProduct">
선택 상품: {{ selectedProduct.name }}
</p>
</template>
읽는 순서는 다음과 같다.
- 어떤 반응형 상태가 있는가? →
products,selectedProduct - 어떤 계산된 값이 있는가? →
totalPrice - 어떤 함수가 상태를 바꾸는가? →
selectProduct - 템플릿에서 무엇을 반복하는가? →
products - 어떤 이벤트가 함수를 호출하는가? →
@click - 어떤 조건에서 화면이 나타나는가? →
v-if="selectedProduct"
결국 Vue 코드는 상태 → 함수 → 템플릿 연결 순서로 분석하면 된다!
아주 쉽죠?
기본 개념
1. 일반 변수와 ref
일반 변수도 값을 변경할 수 있지만, Vue가 그 변경을 반응형 상태로 추적하지는 않는다.
let count = 0
count++
Vue가 값을 추적하고 화면까지 갱신하게 하려면 ref를 사용한다.
(Vue 쓰고 있으면 제발 이걸로 선언해라!)
import { ref } from 'vue'
const count = ref(0)
count.value++
정리하면 다음과 같다.
| 구분 | 값 변경 | Vue의 변경 추적 | 화면 갱신 |
|---|---|---|---|
| 일반 변수 | 가능 | 하지 않음 | 보장하지 않음 |
ref | 가능 | 함 | 반응형으로 갱신 |
2. JavaScript에서는 .value, 템플릿에서는 생략
처음 써봐서 그런지 좀 헷갈린다.
ref(0)은 내부적으로 값을 감싸는 반응형 객체를 만든다.
const count = ref(0)
// JavaScript에서는
console.log(count.value)
count.value++
템플릿에서는 Vue가 .value를 자동으로 처리하므로 생략할 수 있다.
<p>{{ count }}</p>
이것을 자동 ref 언래핑이라고 한다.
템플릿에서는 .value 로 객체로 접근할 필요가 없다.
3. v-bind:src와 :src
둘은 같은 문법이다. :는 v-bind:의 줄임말이다.
<img v-bind:src="imageUrl" alt="프로필" />
<img :src="imageUrl" alt="프로필" />
4. @click="increase"와 @click="increase()"
인자가 없는 단순한 이벤트라면 함수 이름을 바로 전달하는 방식이 읽기 좋다.
<button @click="increase">증가</button>
const increase = () => {
count.value++
}
인자를 전달해야 할 때는 괄호를 사용한다.
<button @click="selectCity(city)">선택</button>
함수 이름만 전달하면 Vue가 이벤트 객체를 함수에 전달할 수도 있다.
<button @click="handleClick">클릭</button>
const handleClick = (event) => {
console.log(event)
}
5. v-if와 v-show
둘 다 조건에 따라 화면을 보이거나 숨기지만 동작이 다르다.
<p v-if="isVisible">내용</p>
<p v-show="isVisible">내용</p>
v-if: 조건에 따라 DOM 요소를 생성하거나 제거한다.v-show: DOM 요소를 유지하고 CSS의display로 보이거나 숨긴다.
v-if는 조건 변경 비용이 있지만 처음부터 렌더링하지 않을 수 있다. v-show는 자주 보였다 숨겨지는 요소에 적합하다.
6. v-for와 key
v-for가 만든 반복 변수는 해당 요소와 자식 요소 안에서만 사용할 수 있다.
<div v-for="city in cities" :key="city.id">
<span>{{ city.name }}: {{ city.temp }}℃</span>
<button @click="selectCity(city)">선택</button>
</div>
city는 반복문 밖에서는 존재하지 않는다.
<div v-for="city in cities" :key="city.id">
{{ city.name }}
</div>
<!-- 오류: city는 v-for 바깥에서 사용할 수 없음 -->
<button @click="selectCity(city)">선택</button>
key는 각 항목의 고유한 정체성을 Vue에 알려준다. 목록의 순서가 바뀌거나 항목이 삭제될 때 Vue가 어떤 DOM을 재사용하고 제거할지 정확하게 판단할 수 있다.
<div v-for="user in users" :key="user.id">
{{ user.name }}
</div>
7. computed와 일반 함수
Vue에서는
상태에서 새로운 값을 계산하면 computed를 사용하고,
사용자 행동을 처리하려면 일반 함수를 사용한다.
const totalPrice = computed(() => {
return products.value.reduce((sum, product) => {
return sum + product.price
}, 0)
})
const save = () => {
alert('저장되었습니다.')
}
<p>총가격: {{ totalPrice }}원</p>
<button @click="save">저장</button>
computed는 계산된 값이므로 템플릿에서 함수처럼 호출하지 않는다.
<!-- 잘못된 사용 -->
<button @click="totalPrice(products)">총가격</button>
자! 다시 한 번 외워보자! 외울거 없다면서
- 증가, 삭제, 선택, 저장 → 일반 함수
- 총합, 개수, 평균, 필터 결과 →
computed
8. v-model은 입력 요소에 사용
v-model은 사용자가 값을 입력하거나 선택하는 요소에 연결한다.
<input v-model="name" />
<textarea v-model="content"></textarea>
<select v-model="selectedCity">
<option value="서울">서울</option>
</select>
p는 값을 입력받는 요소가 아니라 결과를 보여주는 요소다.
<!-- 잘못된 사용 -->
<p input v-model="name">{{ name }}</p>
<!-- 올바른 사용 -->
<input v-model="name" />
<p>{{ name }}</p>
입력과 출력의 흐름은 다음과 같다.
사용자가 input에 입력
↓
v-model이 name 상태를 변경
↓
실제로는 name.value가 변경
↓
{{ name }}을 사용하는 화면이 갱신
9. 부모와 자식 컴포넌트 통신
여기는 중요한 내용이므로 집중하자.
지금부터 Vue에서 컴포넌트 간 데이터 전달 방식에 대해 설명하겠다.
Vue에서는 컴포넌트 간에 데이터를 주고받을 수 있다.
부모에서 자식으로는 props, 자식에서 부모로는 emit을 사용한다.
<!-- 부모 컴포넌트 -->
<UserCard :name="userName" @select="handleSelect" />
<!-- 자식 컴포넌트 -->
<script setup>
defineProps({
name: String
})
const emit = defineEmits(['select'])
const handleClick = () => {
emit('select', 10)
}
</script>
<template>
<p>{{ name }}</p>
<button @click="handleClick">선택</button>
</template>
흐름은 다음과 같다.
부모 → 자식: props
자식 → 부모: emit
10. 버튼 클릭부터 화면 변경까지
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increase = () => {
count.value++
}
</script>
<template>
<p>{{ count }}</p>
<button @click="increase">증가</button>
</template>
사용자가 버튼을 클릭
↓
@click이 increase 실행
↓
count.value 변경
↓
Vue가 변경을 감지
↓
{{ count }}를 사용하는 화면 갱신
코드 따라치기: 작은 관리 화면 만들기
앞에서 배운 ref, computed, v-model, v-if, v-for, 이벤트를 한 번에 연습해보자.
<script setup>
import { computed, ref } from 'vue'
// 1. 카운터
const count = ref(0)
const increase = () => {
count.value++
}
const decrease = () => {
count.value--
}
const reset = () => {
count.value = 0
}
// 2. 이름 입력
const name = ref('')
// 3. 도시 목록
const cities = ref([
{ id: 1, name: '서울', temp: 28 },
{ id: 2, name: '수원', temp: 27 },
{ id: 3, name: '부산', temp: 26 }
])
const selectedCity = ref(null)
const selectCity = (city) => {
selectedCity.value = city
}
// 4. 상품 목록
const products = ref([
{ id: 1, name: '키보드', price: 50000 },
{ id: 2, name: '마우스', price: 30000 },
{ id: 3, name: '모니터', price: 250000 }
])
const totalPrice = computed(() => {
return products.value.reduce((sum, product) => {
return sum + product.price
}, 0)
})
// 5. 할 일 목록
const newTodo = ref('')
const todos = ref([
{ id: 1, text: 'Vue 공부하기' }
])
const addTodo = () => {
const text = newTodo.value.trim()
if (text === '') {
return
}
todos.value.push({
id: Date.now(),
text
})
newTodo.value = ''
}
const deleteTodo = (todoId) => {
todos.value = todos.value.filter((todo) => {
return todo.id !== todoId
})
}
const todoCount = computed(() => {
return todos.value.length
})
</script>
<template>
<main>
<section>
<h2>1. 카운터</h2>
<p>현재 숫자: {{ count }}</p>
<button @click="increase">증가</button>
<button @click="decrease">감소</button>
<button @click="reset">초기화</button>
<p v-if="count > 0">양수입니다.</p>
<p v-else-if="count === 0">0입니다.</p>
<p v-else>음수입니다.</p>
</section>
<section>
<h2>2. 이름 입력</h2>
<input v-model="name" placeholder="이름을 입력하세요" />
<p v-if="name.trim() === ''">이름을 입력하세요.</p>
<p v-else>안녕하세요, {{ name }}님!</p>
</section>
<section>
<h2>3. 도시 목록</h2>
<div v-for="city in cities" :key="city.id">
<span>{{ city.name }}: {{ city.temp }}℃</span>
<button @click="selectCity(city)">선택</button>
</div>
<p v-if="selectedCity">
선택된 도시: {{ selectedCity.name }}
</p>
<p v-else>선택된 도시가 없습니다.</p>
</section>
<section>
<h2>4. 상품 목록</h2>
<div v-for="product in products" :key="product.id">
{{ product.name }}: {{ product.price }}원
</div>
<p>총가격: {{ totalPrice }}원</p>
</section>
<section>
<h2>5. 할 일 목록</h2>
<input
v-model="newTodo"
placeholder="할 일을 입력하세요"
@keyup.enter="addTodo"
/>
<button @click="addTodo">추가</button>
<p>현재 할 일: {{ todoCount }}개</p>
<p v-if="todos.length === 0">등록된 할 일이 없습니다.</p>
<ul v-else>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
<button @click="deleteTodo(todo.id)">삭제</button>
</li>
</ul>
</section>
</main>
</template>
자주 발생하는 문법 오류
1. ref 값을 .value 없이 변경
const count = ref(0)
// 잘못된 사용
const increase = () => {
count++
}
// 올바른 사용
const increase = () => {
count.value++
}
2. 화살표 함수 문법 오류
// 잘못된 사용
const handleClick() = () => {
alert('클릭')
}
// 올바른 사용 1
const handleClick = () => {
alert('클릭')
}
// 올바른 사용 2
function handleClick() {
alert('클릭')
}
화살표 함수와 일반 함수 문법을 섞으면 안 된다.
3. 배열 객체 사이의 쉼표 누락
// 잘못된 사용
const cities = ref([
{ id: 1, name: '서울' }
{ id: 2, name: '부산' }
])
// 올바른 사용
const cities = ref([
{ id: 1, name: '서울' },
{ id: 2, name: '부산' }
])
4. v-for 변수의 범위 착각
<div v-for="city in cities" :key="city.id">
{{ city.name }}
</div>
<!-- city는 이 위치에 없음 -->
<button @click="selectCity(city)">선택</button>
반복 중인 항목을 사용하려면 버튼도 반복 범위 안에 넣어야 한다.
<div v-for="city in cities" :key="city.id">
{{ city.name }}
<button @click="selectCity(city)">선택</button>
</div>
5. ref import와 .value 누락
<script setup>
import { ref } from 'vue'
const selectedCity = ref(null)
const selectCity = (city) => {
selectedCity.value = city
}
</script>
ref를 사용하면 import가 필요하고, JavaScript에서 값을 변경할 때는 .value를 사용한다.
6. HTML 부모·자식 구조 오류
목록 전체를 버튼 안에 넣기보다, 입력, 버튼, 목록을 자연스러운 형제로 구성한다.
<!-- 잘못된 구조 -->
<button @click="addTodo">
<ul>
<li>Vue 공부</li>
</ul>
</button>
<!-- 올바른 구조 -->
<section>
<input v-model="newTodo" />
<button @click="addTodo">추가</button>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
<button @click="deleteTodo(todo.id)">삭제</button>
</li>
</ul>
</section>
최종 점검
아래 질문에 답할 수 있으면 Vue 기초 흐름은 잘 잡힌 것이다.
ref값은 JavaScript에서 어떻게 읽고 변경하는가? →.value- 템플릿에서
ref의.value를 생략할 수 있는 이유는? → 자동 ref 언래핑 - 이벤트가 함수를 실행하고 함수가 상태를 변경하는 흐름을 설명할 수 있는가?
v-for로 만든 변수는 어느 범위에서 사용할 수 있는가? → 반복 요소와 자식 요소 안computed와 일반 함수의 차이는 무엇인가? → 계산된 값과 사용자 행동 처리v-model은 어떤 요소에 사용하는가? → 값을 입력하거나 선택하는 요소- 부모에서 자식으로, 자식에서 부모로 데이터를 전달할 때 무엇을 사용하는가? →
props,emit
ref 선언
↓
템플릿과 v-model, 이벤트 연결
↓
함수가 상태 변경
↓
computed가 필요한 값을 계산
↓
Vue가 화면을 갱신
여기까지 이해했으면 이제부터는 문법 자체보다, 어떤 상태를 만들고 어떤 흐름으로 화면에 연결할지 생각하면 될 듯하다.
잘 이해했지?
COMMENTS
GitHub 계정으로 로그인하여 댓글을 남길 수 있습니다. 댓글은 GitHub Discussions에 공개 저장되며, 작성 내용과 GitHub 프로필 정보가 다른 방문자에게 보일 수 있습니다.