- MVVM패턴
- SFC
- createApp
- 에러 처리
- 전역 컴포넌트 등록
- 동일 페이지 내 앱 인스턴스 여러 개 가능
- 페이지 일부 변경
- 텍스트 보간법
- HTML 출력 : v-html
- 속성 바인딩 : v-bind
- 동적인 인자 []
- 수식어 (.)
- JavaScript 표현식 사용
- 반응형 기초
- 깊은 반응형
- reactive() 제한 사항
- ref()
- 언래핑
- 계산된 속성 computed()
- 클래스와 스타일 바인딩
- 조건부 렌더링 v-if, v-show
- 리스트 렌더링 v-for
- 이벤트 핸들링 v-on
- Form 입력 바인딩
- 동적 바인딩
- 생명주기 훅
- 감시자 watch()
- 깊은 감시자 deep, 열성적인 감시자 watchEffect()
- 템플릿 참조
- 컴포넌트 기초 (Props 전달)
- emit (이벤트 내보내기)
● MVVM패턴
<template> : html. 화면에 보이는 부분이 View
<script setup> 에서
- const count = ref(0) 처럼 데이터를 나타내는 게 Model
- onMounted(() => { }), function () {} 처럼 사용자의 동작을 처리하고 (이벤트 핸들러) 생명 주기를 다루는 게 ViewModel
● SFC
: Single File Component
: 컴포넌트를 하나의 vue파일에 작성하는 것
● 모든 Vue 앱은 createApp함수를 사용하여 새로운 앱 인스턴스를 생성하는 것으로 시작한다.
import { createApp } from 'vue'
const app = createApp({
// 직접 컴포넌트를 코드에 작성해서 루트 컴포넌트로 사용하는 방식
})
const app = createApp({})의 예시
import { createApp, ref } from 'vue'
const App = {
setup() {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
},
template: `<button @click="increment">Count: {{ count }}</button>`
}
const app = createApp(App)
app.mount('#app')
: 컴포넌트를 따로 만들지 않고 내용까지 직접 createApp() 안에 쓴 것. 컴포넌트를 만드는 동시에 앱도 만듦
● 최상위 컴포넌트
createApp 에 전달하는 객체는 컴포넌트이다.
모든 앱에는다른 컴포넌트를 자식으로 포함할 수 있는 최상위 컴포넌트가 필요하다.
SFC를 사용할 때 일반적으로 다른 파일에서 루트 컴포넌트를 가져온다.
import { createApp } from 'vue'
// 싱글 파일 컴포넌트에서 최상위 컴포넌트 앱을 가져온다.
import App from './App.vue'
const app = createApp(App)
: .vue파일로 만든 컴포넌트인 App.vue를 루트 컴포넌트로 사용하겠다는 의미
const app = createApp(App)의 예시
const A = {
template: `<h1>안녕</h1>`
}
// A라는 컴포넌트를 생성
const B = createApp(A)
// A를 createApp에 넣고 B에 저장
B.mount('#app')
// B를 #app에 마운트
: 내가 만든 A라는 컴포넌트(화면에 보여줄 내용)을 기반으로 B라는 Vue 앱을 만든다.
: B를 #app 에 마운트하는 것은 Vue에서 앱을 HTML화면에 연결한다는 의미
: A컴포넌트의 템플릿이 html문서에서 id가 app인 공간에 들어간다.
main.html
<body>
<div id="app"></div>
</body>
HTML파일이 이렇게 되어있고
Vue파일에서 컴포넌트 생성과 마운트 해서 연결하는 것
이 때 최상위 컴포넌트에 template 옵션이 없으면 Vue는 자동으로 컨테이너의 innerHTML을 템플릿으로 사용한다.
앱 인스턴스(이벤트 핸들러나 function)는 .mount() 메서드가 호출될 때까지 아무 것도 렌더링하지 않는다.
● 에러 처리
app.config.errorHandler = (err) => {
/* 에러 처리 */
}
Vue앱에서 어떤 컴포넌트든 에러 났을 때 이 코드가 대신 처리해준다.
console.log 찍거나 서버로 에러 보내거나 등등 가능
● 전역 컴포넌트 등록
app.component('TodoDeleteButton', TodoDeleteButton)
TodoDeleteButton 이라는 컴포넌트를 앱 전체 어디서나 쓸 수 있도록 등록하는 것
<TodoDeletebutton /> 태그를 쓸 수 있다.
● 앱 인스턴스는 동일 페이지 내 여러 개 사용 가능하다.
const app1 = createApp({
})
app1.mount('#container-1')
const app2 = createApp({
})
app2.mount('#container-2')
● 페이지 일부만 변경할 경우
createApp(App).mount('body')
이렇게 body 영역 전체를 Vue가 제어하면 무거워지므로 비효율적
createApp(LikeButton).mount('#like-button')
createApp(CommentBox).mount('#comment-box')
각각 필요한 부분만 Vue 앱 인스턴스로 붙이는 게 낫다.
● 텍스트 보간법
데이터 바인딩의 가장 기본적인 형태 Mustache (이중 중괄호) 문법을 사용한 텍스트 보간법
<span>메세지 : {{msg}} </span>
이중 중괄호 내 msg는 해당 컴포넌트 인스턴스의 msg 속성 값으로 대체되며, msg 속성이 변경될 때마다 업데이트 된다.
● HTML 출력 : v-html
{{}} 이중 중괄호는 데이터를 HTML이 아닌 일반 텍스트로 해석하기 때문에 (=텍스트 그대로 보여줌)
HTML을 출력하기 위해선 v-html 디렉티브를 사용해야 한다.
script setup 에서 const rawHtml = <span ~ /span> 으로 정의했을 때
<span style="color: red">이것은 빨간색이어야 합니다.</span>
<p>텍스트 보간법 사용: {{ rawHtml }}</p>
<p>v-html 디렉티브 사용: <span v-html="rawHtml"></span></p>
출력

● 속성 바인딩 : v-bind
<div v-bind:id="dynamicId"></div>
id 속성을 컴포넌트의 dynamicId 속성과 동기화된 상태로 유지하도록 Vue에 지시
v-bind 는 생략 가능
<div :id="dynamicId"></div>
<a v-bind:href="url"> ... </a>
<!-- 단축 문법 -->
<a :href="url"> ... </a>
':' 뒤에 인자 사용 가능
● 동적인 인자
디렉티브의 인자를 [] 대괄호로 감싸면 동적인 인자
<a v-bind:[attributeName]="url"> ... </a>
<!-- 단축 문법 -->
<a :[attributeName]="url"> ... </a>
컴포넌트 인스턴스의 데이터에 attributeName 속성값이 "href"인 경우 이 바인딩은 v-bind:href 와 같다.
<a v-on:[eventName]="doSomething"> ... </a>
<!-- 단축 문법 -->
<a @[eventName]="doSomething">
eventname의 값이 focus인 경우 v-on:focus 와 같다.
** [] 내부 따옴표 불가하며
브라우저가 속성 이름을 강제 소문자로 변환하기 때문에 컴포넌트에서 소문자 속성 사용해야 하지만 SFC는 제약 조건 비해당
● 수식어
: 점(.)으로 시작하는 특수한 접미사. 디렉티브가 특별하게 바인딩될 때 사용
<form @submit.prevent="onSubmit">...</form>
: .prevent 는 트리거된 이벤트에서 event.preventDefault() 를 호출하도록 v-on에 지시
● Boolaen 속성
<button :disabled="isButtonDisabled">버튼</button>
만약 여러 개 속성을 동적으로 바인딩하는 JavaScript 객체가 있는 경우 그냥 v-bind로 단일 엘리먼트에 바인딩할 수 있다.
data() {
return {
objectOfAttrs: {
id: 'container',
class: 'wrapper'
}
}
}
<div v-bind="objectOfAttrs"></div>

● JavaScript 표현식 사용
{{ number + 1 }}
{{ ok ? '예' : '아니오' }}
{{ message.split('').reverse().join('') }}
<div :id="`list-${id}`"></div>
이중중괄호 내부
v-로 시작하는 모든 Vue 디렉티브 속성 내부
에서 사용할 수 있다.
이중 중괄호 내에서는 하나의 표현식만 사용할 수 있다.
{{ var a = 1 }} 처럼 선언 가능
{{ if (ok) { return message } }} 이런 건 불가능. 삼항연산자 써야 한다.
{{ formatDate(data) }} 함수 호출 가능
v- 접두사 있는 특수한 속성을 디렉티브라고 한다.
v-on 은 @ 로 단축 가능
● 반응형 기초
: 반응형 객체는 JavaScript Proxy이며, 일반 객체처럼 작동
: 차이점은 Vue가 속성에 접근 및 반응형 객체의 변경사항을 감지할 수 있다.
reactive()
ref()
import { reactive } from 'vue'
export default {
setup() {
const state = reactive({ count: 0 })
// 상태를 템플릿에 노출
return {
state
}
}
}
* setup() 은 CompositionAPI에서 사용되는 컴포지션 훅
<div>{{ state.count }}</div>
이렇게 사용 가능하다.
또한 반응형 상태를 변경하는 함수를 같은 범위에서 선언하고 상태와 함께 메서드로 노출할 수 있다.
// vue3에서는 해당 예문처럼 export default 없음!! 그냥 알고나 있을 내용
import { reactive } from 'vue'
export default {
setup() {
const state = reactive({ count: 0 })
function increment() {
state.count++
}
// 함수를 꼭 반환해야 한다.
return {
state,
increment
}
}
}
** setup() 함수에서 준비한 변수와 함수들을 꼭 return 해주어야 <template> 에서 사용할 수 있다.
<button @click="increment">
{{ state.count }}
</button>
그러나 SFC에서는 setup()훅이 아닌 <script setup>을 사용할 수도 있다.
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0 })
function increment() {
state.count++
}
</script>
<template>
<button @click="increment">
{{ state.count }}
</button>
</template>
<script setup>에서 import 또는 최상위 레벨로 선언된 변수나 함수는 해당 템플릿에서 바로 사용 가능
● 깊은 반응형
: Vue는 반응형 상태 내부를 깊숙이 추적하므로 중첩된 객체나 배열을 변경할 때에도 변경 사항이 감지된다.
import { reactive } from 'vue'
const obj = reactive({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
obj.nested.count++
obj.arr.push('baz')
}
* reactive()의 반환 값은 원본 객체와 같지 않고, 원본 객체를 재정의한 Proxy 라는 점을 유의
const obj = { count: 0 }
const state = reactive(obj)
겉보기엔 비슷하지만, state 가 obj를 그대로 사용하는 게 아닌 특수한 감시장치 Proxy를 붙인다고 이해할 것
state.count++ 하면 Vue가 값 변경된 걸 알아차리지만
obj.count++ 하면 Vue가 모른다.
Proxy 는 wrapper다.. 껍데기..
const proxy = reactive({})
const raw = {}
proxy.nested = raw
console.log(proxy.nested === raw) // false
● reactive() API 제한 사항
1. 객체, 배열, Map, Set등 컬렉션 유형에만 작동. string, number, boolean 등 기본 유형에 사용할 수 없다.
컬렉션 유형 : 여러 개의 값을 한 번에 담을 수 있는 자료형 (Map, Set, Array 등)
2. 항상 반응형 객체에 대한 동일한 참조를 유지해야 한다.
let state = reactive({ count: 0 })
// 위에서 참조한 ({ count: 0 })는 더 이상 추적되지 않음 (반응형 연결이 끊어짐)
state = reactive({ count: 1 })
** 또한 반응형 객체의 속성을 로컬 변수에 할당하거나, 분해 할당, 함수에 전달할 때 반응형 연결이 끊어진다.
const state = reactive({ count: 0 })
let n = state.count
// n은 state.count에서 연결이 끊긴 로컬 변수
// n에 state.count 값을 복사한 것
n++
// 원본의 상태(state.count)에 영향을 미치지 않음
let { count } = state
// 구조 분해 할당으로 값만 꺼낸 것. count는 그냥 숫자. 반응성 없음
// 로컬 변수 count는 state.count로부터 연결이 끊김
count++
// 원본의 상태(state.count)에 영향을 미치지 않음
callSomeFunction(state.count)
// 함수는 일반적인 숫자를 수신 (값만 넘긴 것)
// state.count의 변경 사항을 감지할 수 없다
- ref()
: reactive()의 제한 사항을 해결하기 위해 어떤 유형의 데이터라도 반응형으로 재정의할 수 있는 함수
: 받은 인자를 .value 속성을 포함하는 ref 객체에 래핑한 후 반환한다.
ref()는 모든 값에 대한 참조를 만들어 반응성 잃지 않고 전달할 수 있다.
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
const obj = {
foo: ref(1),
bar: ref(2)
}
// 함수가 ref를 전달받음
// .value를 통해 값에 접근해야 하지만
// 반응형 연결 상태가 유지된다.
callSomeFunction(obj.foo)
// 분해 할당했지만, 반응형 상태가 유지
const { foo, bar } = obj
● 언래핑 (unwrapping)
ref값이 자동으로 풀리는 조건
const object = { foo: ref(1) }
foo는 그냥 1이 아닌 { value: 1} 과 같은 객체
<template>에서 {{ foo }} 쓰면 Vue가 자동으로 .value를 꺼내서 1로 보여준다. => 이게 언래핑 (.value없이 자동으로 꺼내줌)
const object = { foo: ref(1) }
그런데, object.foo 는 최상위가 아니다. {{ object.foo +1 }} 하면 .value를 꺼내주지 않고 "[object Object]1" 이 된다.
** 최상위 : setup()에서 return된 객체 안에서 직접 접근되는 변수 이름
const { foo } = object
이렇게 구조분해를 해야 foo 가 최상위가 되면서 언래핑 가능
foo만 따로 빼서 최상위로 올리는 것이고 object도 여전히 최상위
const object = { foo: ref(1) }
이런식으로 쓰는 게 객체 리터럴에서 프로퍼티 만들면서 값 넣는 문법
키가 foo 고 값이 ref(1) = object라는 객체에 foo라는 이름으로 ref값을 넣은 것
object.foo 가 ref(1) 이 된다는 의미
import { ref } from 'vue'
const object = {
foo: ref(1)
}
* ref가 반응형 객체의 속성으로 접근하거나 변경되면 자동으로 언래핑되어 일반 속성처럼 작동한다.
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
다만, 깊은 반응형 객체 내부에 중첩된 경우에만 ref 언래핑 발생
** 배열이나 Map 안에 들어가면 자동으로 .value를 꺼내주지 않는다.
books[0] 또는 map.get('count') 에서는 직접 .value를 써야 한다.
const books = reactive([ref('Vue 3 Guide')])
// .value 필요
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// .value 필요
console.log(map.get('count').value)
● 계산된 속성 computed(() => {})
<p>책을 가지고 있다:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>
이런 식으로 삼항연산자를 쓰는 방법도 있겠지만
계산된 속성을 사용할 경우 간단해진다.
<script setup>
import { reactive, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// 계산된 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>
<template>
<p>책을 가지고 있다:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
computed() 함수에는 getter로 사용될 함수가 전달돼야 하고 반환 값은 computed ref 다.
- 계산된 캐싱 vs 메서드
: 표현식에서 메서드를 호출하여 동일한 결과 얻을 수 있다.
<p>{{ calculateBooksMessage() }}</p>
function calculateBooksMessage() {
return author.books.length > 0 ? 'Yes' : 'No'
}
const now = computed(() => Date.now())
.now 는 현재시각을 계산해서 담는 computed 변수이지만 Date.now() 가 반응형이 아니다.
computed()는 값이 바뀔 때만 자동으로 계산해주기 때문에 주의
const now = ref(Date.now()) 로 해주어야 함
const now = ref(Date.now())
setInterval(() => {
now.value = Date.now()
}, 1000)
계산된 속성은 값이 변경되지 않으면 getter 함수를 다시 실행하지 않고 이전에 계산된 결과를 즉시 반환
메서드 호출은 리렌더링 발생할 때마다 항상 함수를 실행
● 클래스와 스타일 바인딩
<div :class="{ active: isActive }"></div>
class 나 style 모두 속성이므로 v-bind를 이용해 문자열 값을 동적으로 할당할 수 있지만 연결된 문자열을 이용할 떄 오류 가능성
- HTML 클래스 바인딩
객체를 :class 에 전달할 수 있다.
<div :class="{ active: isActive }"></div>
isActive 데이터 속성의 truthiness에 의해 active 클래스의 존재 여부가 결정됨을 의미 (isActive가 true면 active속성 o)
const isActive = ref(true)
const hasError = ref(false)
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }"
></div>
이럴 경우 렌더링은
<div class="static active"></div>
isActive 또는 hasError가 변경되면 그에 따라 클래스 목록이 업데이트된다.
hasError가 true가 되면 클래스 목록은 "static active text-danger"가 된다.
이렇게 써도 동일한 결과 ( 바인딩된 객체는 인라인일 필요 없음)
const classObject = reactive({
active: true,
'text-danger': false
})
<div :class="classObject"></div>
* 인라인 객체
: {}를 직접 적은 것
ex)
<div :class="{ active: true, 'text-danger': false }"></div>
- 배열로 바인딩
const activeClass = ref('active')
const errorClass = ref('text-danger')
<div :class="[activeClass, errorClass]"></div>
// 렌더링 결과
<div class="active text-danger"></div>
- 삼항 표현식 사용하여 목록 내 클래스를 토글할 수도 있다.
<div :class="[isActive ? activeClass : '', errorClass]"></div>
- 컴포넌트 바인딩
<!-- 자식 컴포넌트의 템플릿 -->
<p class="foo bar">안녕!</p>
<!-- 컴포넌트가 사용될 때 -->
<MyComponent class="baz boo" />
<!-- 렌더링 결과 -->
<p class="foo bar baz boo">안녕!</p>
- 클래스 바인딩
<!-- 자식 컴포넌트의 템플릿 -->
<p class="foo bar">안녕!</p>
<!-- 클래스 바인딩도 마찬가지 -->
<MyComponent :class="{ active: isActive }" />
<!-- 렌더링 결과 -->
<p class="foo bar active">안녕!</p>
- 여러 개의 최상위 엘리먼트로 컴포넌트가 구성되어있는 경우, 클래스를 적용할 엘리먼트들을 정의해야 한다.
: $attrs 컴포넌트 속성 사용하여 가능
<!-- MyComponent 템플릿에서 $attrs 속성을 사용 -->
<p :class="$attrs.class">안녕!</p>
<span>반가워!</span>
<MyComponent class="baz" />
<!-- 렌더링 결과 -->
<p class="baz">Hi!</p>
<span>반가워!</span>
MyComponent 안에는 <p>와 <span> 이 있는데 어디로 class를 적용할지 ? -> $attrs.붙은 쪽
- 인라인 스타일 바인딩
const activeColor = ref('red')
const fontSize = ref(30)
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<!-- :style 에 사용되는 CSS 속성에 해당하는 키 문자열은 보통 camelCase 권장
하지만 kebab-cased 도 지원 -->
<div :style="{ 'font-size': fontSize + 'px' }"></div>
<!-- 직접 바인딩하는 방법-->
const styleObject = reactive({
color: 'red',
fontSize: '13px'
})
<div :style="styleObject"></div>
* 스타일 속성에 다중값을 배열로 제공할 수 있다.
다만 브라우저가 지원하는 배열 내 마지막 값을 렌더링한다. flex랑 -webkit-box 지원하면 -webkit-box만 렌더링
<div :style="{ display: ['flex', '-webkit-box', '-ms-flexbox'] }"></div>
● 조건부 렌더링
v-if
<h1 v-if="awesome">Vue는 정말 멋지죠!</h1>
v-else
<button @click="awesome = !awesome">전환</button>
<h1 v-if="awesome">Vue는 정말 멋지죠!</h1>
<h1 v-else>아닌가요?</h1>
v-else-if
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
A/B/C 아님
</div>
* v-if 는 디렉티브이므로 단일 엘리먼트에 연결해야 하는데, 둘 이상의 엘리먼트를 전환하려면 ?
보이지 않는 래퍼 역할을 하는 <template> 엘리먼트에 v-if 사용 가능
최종 렌더링된 결과에 <template> 엘리먼트 포함되지 않음 (하나의 컴포넌트에 여러 개 <template v-if> 가능
<template v-if="ok">
<h1>제목</h1>
<p>단락 1</p>
<p>단락 2</p>
</template>
v-show
<h1 v-show="ok">안녕!</h1>
<template> 엘리먼트를 지원하지 않고 v-else와 상호작용하지 않는다.
v-if 랑 비슷한데, 차이점은
: v-show의 경우 엘리먼트가 항상 렌더링되고 DOM에 남아있다. (v-if는 false면 DOM에서 제거됨)
: display CSS 속성만 전환 (display: none로 숨겨짐)
● 리스트 렌더링
v-for
<!-- 여기서 items는 배열, item은 배열 내 반복되는 앨리먼트의 별칭Alias -->
const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
<li v-for="item in items">
{{ item.message }}
</li>
* v-for 범위 내 템플릿 표현식은 모든 상위 범위 속성에 접근할 수 있다. (컴포넌트 전체 데이터에 자유롭게 접근 가능)
또한 현재 item의 index를 가리키는 선택적 두 번째 별칭도 지원한다.
const parentMessage = ref('Parent')
const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
<li v-for="(item, index) in items">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
<!-- 출력 결과 -->
Parent - 0 - Foo
Parent - 1 - Bar
items 에 a속성 b속성 message속성이 있을 때 message만 구조 분해 할당해서 꺼내 사용하기
const items = [
{ message: '안녕', a: 1, b: 2 },
{ message: '반가워', a: 3, b: 4 }
]
<li v-for="{ message } in items">
{{ message }}
</li>
<!-- index 별칭도 사용 -->
<li v-for="({ message }, index) in items">
{{ message }} {{ index }}
</li>
중첩된 v-for
<li v-for="item in items">
<span v-for="childItem in item.children">
{{ item.message }} {{ childItem }}
</span>
</li>
in 대신 of 를 구분 기호로 사용 가능
<div v-for="item of items"></div>
객체에 v-for 사용
const myObject = reactive({
title: 'Vue에서 목록을 작성하는 방법',
author: '홍길동',
publishedAt: '2016-04-10'
})
<ul>
<li v-for="value in myObject">
{{ value }}
</li>
</ul>
<!-- 속성명을 가리치는 두 번째 별칭 사용 가능 -->
<li v-for="(value, key) in myObject">
{{ key }}: {{ value }}
</li>
<!-- 인덱스를 가리키는 세 번째 별칭 사용 가능 -->
<li v-for="(value, key, index) in myObject">
{{ index }}. {{ key }}: {{ value }}
</li>
숫자에 v-for 사용
<span v-for="n in 10">{{ n }}</span>
<!-- n은 0이 아닌 1부터 시작 -->
<template>에서 v-for 사용
<ul>
<template v-for="item in items">
<li>{{ item.msg }}</li>
<li class="divider" role="presentation"></li>
</template>
</ul>
key를 통한 상태 유지
v-for를 사용할 때는 기존 DOM을 재사용하려고 하기 때문에 순서만 바꾸면 Vue는 내용만 바꾼다.
= in-place patch 전략
만약 입력칸이나 체크박스처럼 상태가 있는 요소일 때 문제생길 수 있기 때문에 (Vue가 헷갈려함)
:key="item.id" 처럼 고유한 key를 준다.
key는 문자열, 심볼, 숫자만 가능!
<template v-for="todo in todos" :key="todo.name">
<li>{{ todo.name }}</li>
</template>
<div v-for="item in items" :key="item.id">
<!-- 내용 -->
</div>
** 결론은 그냥 v-for 쓸 때 :key 같이 쓰란 얘기
컴포넌트에 v-for 사용
<MyComponent v-for="item in items" :key="item.id" />
<MyComponent
v-for="(item, index) in items"
:item="item"
:index="index"
:key="item.id"
/>
item 데이터는 자동으로 넘어가지 않으므로 :item = "item" 처럼 props 로 직접 전달해야 함
index도 쓰고 싶으면 :index = "index"
만약 이걸 자동으로 처리해버릴 경우 v-for 없이 컴포넌트를 쓸 수 없어서 그렇다고 함 .. 명시적으로 전달 필요
** 결론은 v-for로 컴포넌트 반복 시 props도 꼭 직접! 넘기자 !
배열 변경 감지
: Vue는 반응형 배열의 변경 메소드가 호출되는 것을 감지한다.
| 지원하는 변경 메소드 종류 |
| push() pop() shift() unshift() splice() sort() reverse() |
** filter(), concat(), slice() 는 원래 배열을 수정하지 않고 항상 새 배열을 반환한다.
// `items`는 값이 있는 배열의 ref라고 가정된 경우
items.value = items.value.filter((item) => item.message.match(/Foo/))
: items는 ref()로 만든 반응형 배열
: filter()는 조건에 맞는 것만 골라서 새 배열 만드는 메서드
: item.massage에 'Foo"라는 글자가 들어간 항목만 남기기
또한 이럴 때 Vue는 똑똑하기 때문에 기존 DOM 버리고 전체 리스트를 재렌더링하지 않고 중복 객체를 효율적으로 처리
스마트 휴리스틱을 구현한다.
계산된 속성에서 reverse()와 sort() 사용 주의해야 한다.
computed(() => myArray.reverse())
reverse() 나 sort() 는 원본 배열 자체를 바꿔버리기 때문에
computed()에서 쓰면 버그 생길 수 있음
computed(() => [...myArray].reverse())
이렇게 복사해서 쓸 것
** [...] 는 배열 복사
● 이벤트 핸들링
v-on
: 단축 문법으로 @ 사용
: DOM 이벤트를 수신하고 트리거될 때 사전에 정의해둔 JavaScript 코드를 실행할 수 있다.
v-on:click="handler" 또는 @:click="handler"
핸들러 값은 인라인핸들러나 메서드 핸들러
- 인라인 핸들러
const count = ref(0)
<button @click="count++">1 추가</button>
<p>숫자 값은: {{ count }}</p>
- 메서드 핸들러
const name = ref('Vue.js')
function greet(event) {
alert(`안녕 ${name.value}!`)
// 'event'는 네이티브 DOM 이벤트 객체입니다.
if (event) {
alert(event.target.tagName)
}
}
<!-- `greet`는 위에서 정의한 메서드의 이름입니다. -->
<button @click="greet">환영하기</button>
**
v-on에 들어간 값이 단순 이름, 객체 속성 경로면 메서드 핸들러로 인식
- foo
- foo.bar
- foo['bar']
함수 호출이나 연산이 포함되면 인라인 핸들러로 인식
- foo()
- count++
<!-- 헷갈릴 수 있는데, foo()랑 foo랑 비슷하게 동작하긴 하지만 차이가 있음 -->
@click="foo" : 함수 이름 기억했다가 클릭 시 실행
@click="foo()" : 버튼 클릭하면 Vue가 바로 실행할 코드 foo()를 실행
<button @click="foo(); count++">클릭</button>
이런식으로 뒤에 다른 코드 같이 쓸 수 있다.
foo 는 함수 참조라 단독으로 쓰고
foo() 는 실행 코드라 여러 동작 가능 !!
● Form 입력 바인딩
값 바인딩을 수동으로 연결하고 이벤트 리스너를 변경하는 것은 번거로울 수 있으므로
<input
:value="text"
@input="event => text = event.target.value">
<!-- input 박스에 text값을 보여주고
사용자가 입력할 때마다 값을 text변수에 저장
즉, 입력 박스와 text 변수를 연결하는 코드 -->
v-model 을 쓰면 단순화 가능
<input v-model="text">
또한 <input> 뿐만아니라 다른 유형의 입력인 <textarea> 와 <select> 엘리먼트에 사용할 수 있다.
<input>, <textarea> 의 경우 value 속성, input 이벤트 사용
<input type="checkbox"> 와 <input type="radio"> 의 경우 checked 속성과 change 이벤트 사용
<select> 는 value 속성 사용, change 이벤트 사용
예시)
<p>메세지: {{ message }}</p>
<input v-model="message" placeholder="메세지 입력하기" />
<!-- 여러 줄 텍스트 -->
<span>여러 줄 메세지:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<textarea v-model="message" placeholder="여러 줄을 추가해보세요"></textarea>


<textarea> 내부에 이중 중괄호 문법은 작동하지 않는다. v-model 써야 함
<!-- 잘못된 사례 -->
<textarea>{{ text }}</textarea>
<!-- 올바른 사례 -->
<textarea v-model="text"></textarea>
단일 체크박스는 boolean 사용
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>
배열, Set에 여러 체크박스 값 바인딩 가능
const checkedNames = ref([])
<div>체크된 이름: {{ checkedNames }}</div>
<input type="checkbox" id="jack" value="젝" v-model="checkedNames">
<label for="jack">젝</label>
<input type="checkbox" id="john" value="존" v-model="checkedNames">
<label for="john">존</label>
<input type="checkbox" id="mike" value="마이크" v-model="checkedNames">
<label for="mike">마이크</label>

HTML 에서 name 쓰듯이 v-model쓰면 되는 건데
name 속성은 폼 데이터를 서버로 보낼 때 이름 붙여서 보내는 거고
v-model 은 입력 값을 Vue 변수랑 연결할 때 (양방향 바인딩) 사용하는 것
라디오
<div>선택한 것: {{ picked }}</div>
<input type="radio" id="one" value="하나" v-model="picked" />
<label for="one">하나</label>
<input type="radio" id="two" value="둘" v-model="picked" />
<label for="two">둘</label>

셀렉트
<div>선택됨: {{ selected }}</div>
<select v-model="selected">
<option disabled value="">다음 중 하나를 선택하세요</option>
<option>가</option>
<option>나</option>
<option>다</option>
</select>

다중 선택 (배열로 바인딩 된다)
<div>선택됨: {{ selected }}</div>
<select v-model="selected" multiple>
<option>가</option>
<option>나</option>
<option>다</option>
</select>

select 는 v-for 로 동적으로 렌더링할 수 있다.
const selected = ref('1')
const options = ref([
{ text: '하나', value: '1' },
{ text: '둘', value: '2' },
{ text: '셋', value: '3' }
])
<select v-model="selected">
<option v-for="option in options" :value="option.value">
{{ option.text }}
</option>
</select>
<div>선택됨: {{ selected }}</div>
●값 바인딩
라디오, 체크박스, 셀렉트 옵션의 경우 v-model 에 바인딩된 값은 일반적으로 정적 문자열이다.
<!-- `picked`는 선택 시 문자열 "가" -->
<input type="radio" v-model="picked" value="가" />
<!-- `toggle`은 true 또는 false -->
<input type="checkbox" v-model="toggle" />
<!-- `selected`는 첫 번째 옵션이 선택될 때 문자열 "한글" -->
<select v-model="selected">
<option value="한글">한글</option>
</select>
동적 속성에 바인딩하고 싶으면 v-bind 사용
<input
type="checkbox"
v-model="toggle"
true-value="네"
false-value="아니오" />
<!-- 이건 그냥 문자열 네, 아니오 출력
true-value, false-value 는 v-model 에서만 작동하는 Vue전용 속성 -->
<input
type="checkbox"
v-model="toggle"
:true-value="dynamicTrueValue"
:false-value="dynamicFalseValue" />
<!--
이렇게도 사용 가능
:가 붙으면 동적바인딩
JS 변수 값이 들어가는 것 -->
const dynamicTrueValue = 1
const dynamicFalseValue = 0
<!-- 이 경우 체크되면 v-model 에 1, 해제되면 0 -->
라디오 동적 바인딩
<input type="radio" v-model="pick" :value="first" />
<input type="radio" v-model="pick" :value="second" />
<!-- pick은 n번째 라디오 입력이 확인되면 그 값으로 설정 -->
셀렉트 동적 바인딩
<select v-model="selected">
<!-- 인라인 객체 리터럴 -->
<option :value="{ number: 123 }">123</option>
</select>
selected 는 { number: 123 } 객체 값으로 설정된다.
사용자가 123을 선택할 경우 -> 123 문자열이 들어가는 게 아니라 객체 { number: 123 } 이 들어간다는 뜻
● 수식어
- .lazy
<!-- "input" 대신 "change" 이벤트 후에 동기화됨 -->
<input v-model.lazy="msg" />
v-model 은 input 이벤트 후에 데이터와 입력을 동기화하지만
change 이벤트 이후에 동기화할 수 있다. 이때 .lazy 수식어 사용
**원래는 글자가 입력될 때마다 msg에 실시간으로 반영된다.
하지만 .lazy를 붙이면 입력 다 하고 포커스를 벗어나거나 Enter누를 때 msg에 반영된다.
- .number
<input v-model.number="age" />
값을 parseFloat() 로 파싱할 수 없으면 원래 값이 사용된다.
input 에 type="number" 가 있으면 .number 수식어가 자동으로 적용
** 사용자가 입력한 값은 원래 문자열이다. "123"
그러나 .number를 쓰면 Vue가자동으로 parseFloat() 써서 숫자로 바꿔준다는 뜻
(abc처럼 숫자로 못 바꾸면 원래 NaN 되지만 Vue가 원래 값 abc유지)
- .trim
<input v-model.trim="msg" />
사용자 입력의 공백이 자동으로 트리밍되도록 하려면 .trim 추가하면됨
● 생명주기 훅
: 각 Vue 컴포넌트 인스턴스는 생성될 때 일련의 초기화 단계를 거친다.
이때 생명 주기 훅(lifecycle hooks) 함수도 실행해서 개발자가 의도하는 로직을 실행할 수 있다.
onMounted, onUpdated, onUnmounted 등이 있다.
onMounted: 컴포넌트가 처음 화면에 나타났을 때 실행
onUpdated: 반응형 상태가 변경되어 컴포넌트가 다시 렌더링될 때 실행
onUnmounted: 컴포넌트가 화면에서 사라질 때(제거될 때) 실행
- onMounted
컴포넌트 초기 렌더링 및 DOM 노드 생성이 완료된 후 코드를 실행
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
console.log(`컴포넌트가 마운트 됐습니다.`)
})
</script>
이렇게 하면 안됨
setTimeout(() => {
onMounted(() => {
// 작동하지 않습니다.
})
}, 100)
<!-- onMounted 는 setup 내에서 시작될 떄만 외부 함수 실행하는 방식으로 사용 가능 -->
또한 setup() 내에서 this. 로 컴포넌트 인스턴스에 접근 불가
왜냐면 컴포넌트 인스턴스가 만들어지기 전이라서...!!!
onMounted() 안에서도 setup 내부이기 때문에 마찬가지로 this 없음
근데 mounted() 는 ?
OptionsAPI 방식이라, 이 시점에는 이미 컴포넌트 인스턴스가 만들어진 거라 사용이 가능하다 ...
생명 주기 표


● 감시자
(어려워서 감시자랑 깊은 감시자랑 아래 따로 설명 있음)
watch() 함수를 사용해서 반응형 속성이 변경될 떄마다 함수를 실행할 수 있다.
인자에 ref, 함수 사용 가능
**
기본 구조
watch (감시할 값, 콜백 함수(새 값, 이전 값))
콜백 함수는 "(새로운 값) => { }" 이 부분
또는 값 여러 개 중 인자가 ref, 함수일 경우
watch([x, () => y.value], (새로운 값) => { })
x는 ref라 그냥 써도 되고 y는 함수라 직접 감시가 안되어서 y.value 를 감시해야 하기 때문에.
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('질문에는 일반적으로 물음표가 포함됩니다.')
// watch는 ref에서 직접 작동합니다
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.indexOf('?') > -1) {
answer.value = '생각 중...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer === 'yes' ? '네' : '아니오'
} catch (error) {
answer.value = '에러! API에 연결할 수 없습니다. ' + error
}
}
})
</script>
<template>
<p>
예/아니오 질문:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</template>
question 변수 생성하고 입력값 저장
-> answer 변수에 답변 문구 담아서
-> question이 바뀔 때마다 watch 실행
-> 질문에 ? 가 있으면 answer를 "생각 중..." 으로 변경
-> 외부 API에서 'yes'나 'no'받아와서 answer에 저장
-> 실패 시 에러 메시지
감시자가 어려워서 한 줄 한 줄 파헤쳐보자.
const x = ref(0)
const y = ref(0)
<!-- 단일 ref -->
watch(x, (newX) => {
console.log(`x값: ${newX}`)
})
// getter
watch(
() => x.value + y.value,
(sum) => {
console.log(`x + y: ${sum}`)
}
)
<!-- 여러 소스의 배열 -->
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x는 ${newX}이고, y는 ${newY} 입니다.`)
})
일단 x랑 y 변수를 만들어 반응형 변수고 0으로 시작하겠지
-> 단일 ref : 감시자 watch 지정이야 x 값이 바뀌면 바뀐 값을 console 에 찍어
-> getter : x랑 y랑 더한 값이 바뀌면 console에 합계를 찍어
-> 여러 소스의 배열 : x나 y 둘 중에 하나 바뀌면 둘 다 새 값으로 console 찍기
const obj = reactive({ count: 0 })
<!-- 반응형 객체의 속성을 감시할 수는 없다.
이것은 watch()에 숫자를 전달하기 때문에 작동하지 않는다. -->
watch(obj.count, (count) => {
console.log(`count 값: ${count}`)
})
<!-- 대신 getter 사용해야 한다. -->
watch(
() => obj.count,
(count) => {
console.log(`count 값: ${count}`)
}
)
(count) => { ... } : count라는 새 값을 받아서 ... 을 하는 함수
● 깊은 감시자
(어려워서 아래 설명 따로 추가함)
반응형 객체에서 watch()를 직접 호출하면 암시적으로 심층 감시자가 생성된다.
콜백은 중첩된 모든 변경에서 트리거된다.
const someObject = reactive({ count: 0 })
watch(someObject, (newValue, oldValue) => {
<!-- 중첩된 속성 변경사항이 있을 경우 실행
`newValue`와 `oldValue`는 같다.
둘 다 동일한 객체를 참고하고 있기 때문
(someObject가 반응형 객체이고 내부 값이 바뀌어도 같은 객체 주소를 유지하기 때문) -->
})
someObject.count++
반응형 객체를 반환하는 게터와 구분해야 한다.
아래의 경우 콜백은 게터가 다른 객체를 반환하는 경우에만 실행된다.
const state = reactive({
someObject: { count: 0 }
})
watch(
() => state.someObject,
() => {
// state.someObject가 교체될 때만 실행됩니다.
}
)
deep 옵션을 명시적으로 사용하여 깊은 감시자로 강제할 수 있다.
watch(
() => state.someObject,
(newValue, oldValue) => {
// 참고:
// state.someObject가 교체되지 않는 한 여기에서
// `newValue`와 `oldValue`는 같습니다.
},
{ deep: true }
)
너무 어려우니까 다시 정리
const someObject = reactive({ count: 0 })
watch(someObject, (newValue, oldValue) => {
console.log('중첩된 값이 바뀜!')
})
쉽게 말하면 reactive( {count: 0}) 에서, reactive 객체는 내부 값이 바뀌어도 감지된다.
someObject.count++ 도 감지된다.
= 이게 깊은 감시자. 안쪽 속성까지 전부 지켜봄
모든 것을 다 감시하니까 데이터가 크면 느릴 수 있고
newValue === oldValue 인 경우가 많다. 같은 객체라서..
(someObject가 반응형 객체이고 내부 값이 바뀌어도 같은 객체 주소를 유지하기 때문)
근데 객체 안 객체는 직접 감시가 안된다.
const state = reactive({
someObject: { count: 0 }
})
watch(() => state.someObject, () => {
console.log('someObject 통째로 바뀌었을 때만 실행됨')
})
someObject.count++ 가 감지 안 된다.
someObject 객체 자체가 새 객체로 바뀌어야 실행된다.
그래서 deep: true 옵션을 추가하면 된다.
watch(() => state.someObject, () => {
console.log('이제 안쪽까지 감시함')
}, { deep: true })
또는
immediate: true 를 넣으면 처음부터 바로 실행 (데이터 불러오기 같은 경우 유용)
● 열성적인 감시자 watchEffect()
watch()는 게을러서 감시 소스가 변경되기 전까지 콜백이 호출되지 않는다.
하지만 동일한 콜백 로직이 열성적으로 실행되기를 원할 수도 있기 때문에 ...?
const url = ref('https://...')
const data = ref(null)
async function fetchData() {
const response = await fetch(url.value)
data.value = await response.json()
}
// 즉시 데이터 가져오기
fetchData()
// ...그런다음 url 변경을 감시하도록 watch를 실행합니다.
watch(url, fetchData)
이 예제를 watchEffect()로 단순화할 수 있다.
const url = ref('https://...')
const data = ref(null)
watchEffect(async () => {
const response = await fetch(url.value)
data.value = await response.json()
})
이렇게 할 경우 콜백이 최초에 즉시 한 번 실행된다.
실행되는 동안 자동으로 의존성인 url.value를 추적하고, url.value가 변경될 때마다 콜백이 재실행된다.
결론
watch()는 명시적으로 감시된 소스만 추적
watchEffect()는 의존성 추적과 사이드 이펙트를 하나의 단계로 결합.
동기적(sync) 실행 중에 조회되는 모든 반응형 속성을 자동 추적한다.
내부에서 사용되는 모든 반응형 값을 알아서 자동 감시한다.
- 콜백 실행 타이밍
기본적으로 개발자가 생성한 감시자 콜백은 Vue 컴포넌트가 업데이트되기 전에 실행
따라서 감시자 콜백 내에서 DOM에 접근하면, Vue에 의해 DOM이 업데이트 되기 전의 상태
Vue에 의해 업데이트된 후의 DOM을 감시자 콜백에서 접근하려면
flush: 'post' 옵션 지정해야 한다.
flush: 'pre' | 'post' | 'sync'
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
watchPostEffect()
: watchEffect + flush: 'post'
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* Vue가 업데이트 된 후 실행됩니다 */
})
flush: sync 는 여러 번 상태 변경 시 동기적으로 콜백 호출할 때 쓰는데, 비효율적일 수 있음
const count = ref(0)
const callback = (val, preVal) => console.log('변경이 감지됨!', val, preVal)
const options = { flush: 'sync' }
watch(count, callback, options)
count.value++
// 이어서 callback이 실행됨
count.value++
// 역시 callback이 실행됨
count.value++
// 또 callback이 실행됨
● 감시자 중지
<script setup>
import { watchEffect } from 'vue'
<!-- 이 감시자는 컴포넌트가 마운트 해제되면 자동으로 중지 -->
watchEffect(() => {})
<!-- ...하지만 이것은 자동으로 중지되지 않음 -->
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
<!-- 수동 중지할 때 반환된 함수 사용 -->
const unwatch = watchEffect(() => {})
<!-- ...나중에 감시자가 더 이상 필요하지 않을 때: -->
unwatch()
비동기식으로 감시자를 생성할 수도 있긴한데, 가능하면 동기식 생성이 좋다.
비동기 데이터를 기다려야 할 때 이렇게 쓸 수도 있음
// 비동기적으로 로드할 데이터
const data = ref(null)
watchEffect(() => {
if (data.value) {
// 데이터가 로드될 때 실행될 로직
}
})
● 템플릿 참조 ref 속성
<input ref="input">
: key 속성과 유사한 특수 속성
: 마운트된 특정 DOM 엘리먼트 또는 자식 컴포넌트 인스턴스에 직접적인 참조를 얻을 수 있음
- ref로 접근하기
<script setup>
import { ref, onMounted } from 'vue'
// 엘리먼트 참조를 위해 ref를 선언하십시오.
// 이름은 템플릿 ref 값과 일치해야 합니다.
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>
** 컴포넌트가 마운트 된 후에만 ref에 접근할 수 있다.
따라서 input 이 template 표현식에 있으면 첫 렌더링 시 null이다.
(첫 렌더링 끝날 때까지 엘리먼트가 존재하지 않기 때문)
그~래~서 ~! 템플릿 ref의 변경 사항을 감시하려는 경우 ref에 null값이 있는 경우를 고려해야 한다.
watchEffect(() => {
if (input.value) {
input.value.focus()
} else {
// v-if에 의해 아직 마운트 되지 않았거나, 마운트 해제된 경우
}
})
● v-for 내부에서 ref 사용하기
ref 가 v-for 내부에서 사용되면, 해당 ref는 마운트 이후 엘리먼트로 채워지므로 배열 값이어야 한다.
(여러 개의 DOM 요소가 생기니까 ref가 하나가 아닌 배열이 되어야 한다는 의미
= v-for 는 반복문이라 같은 태그를 여러 개 만드니까. ref하나에 여러 엘리먼트가 들어감)
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([
/* ... */
])
const itemRefs = ref([])
onMounted(() => console.log(itemRefs.value))
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
● 함수로 참조하기
: 문자열 키 대신 ref속성을 함수에 바인딩
<input :ref="(el) => {
/* el을 속성이나 ref에 할당 */
}">
원래 <input ref=:myRef">이렇게 문자열로 ref를 주는데,
대신 ref를 함수로 사용해서 그 안에서 직접 DOM을 변수에 저장
이 함수는 컴포넌트가 마운트되거나 갱신될 때마다 자동 호출된다.
el은 실제 <input>같은 DOM 요소를 의미하며 엘리먼트가 사라질 때는 el 이 null로 돌아온다.
const myRef = ref(null)
<input :ref="el => myRef.value = el">
이렇게 하면 myRef.value 에 <input> DOM 이 들어간다.
● 컴포넌트에 ref 사용하기
: ref는 자식 컴포넌트에 사용할 수도 있음. 이 경우 ref는 컴포넌트 인스턴스를 참조한다.
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const child = ref(null)
onMounted(() => {
// child.value는 <Child />의 인스턴스를 가짐
})
</script>
<template>
<Child ref="child" />
</template>
ref = "child" 하면 child.value 에 자식 컴포넌트 인스턴스가 들어간다.
자식 컴포넌트 안의 데이터, 메서드를 부모에서 쓸 수 있다. 이렇게 쓰면 부모 자식이 너무 강한 연결이 되니까 필요할 때만 쓰기
근데 <script setup> 으로 만든 자식 컴포넌트는 기본적으로 비공개라 부모가 내부 속성을 못 본다.
공개하려면 이렇게 써야
<script setup>
import { ref, defineExpose } from 'vue'
const a = 1
const b = ref(2)
defineExpose({ a, b }) // 부모한테 이 두 값만 보여줄게
</script>
이제 부모는 child.value.a, child.value.b 처럼 접근 가능
● 컴포넌트 기초

자식 컴포넌트를 사용하려면 부모 컴포넌트에서 가져와야 한다.
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>
<template>
<h1>아래에 자식 컴포넌트가 있습니다.</h1>
<ButtonCounter />
</template>
ButtonCounter 라는 카운터 컴포넌트가 있을 때 <script setup>에서 import해서 갖다 쓸 수 있다.
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />
그리고 이렇게 template 작성할 경우 Button이 세 개 생기며 각각 따로 작동한다.
ButtonCounter 컴포넌트를 사용할 때마다 해당 컴포넌트의 새 인스턴스가 생성된다.
각 컴포넌트는 자기만의 setup() 을 실행하고, 자기만의 count를 가지고. 서로 독립적
그리고 이때 Native HTML 엘리먼트와 구분하기 위해 자식 컴포넌트에는 PascalCase를 사용하는 게 좋다.
: 단어의 첫 글자를 모두 대문자로 쓰는 것
| HTML 태그처럼 Kebab-case | button-counter |
| JS 변수처럼 camelCase | buttonCounter |
| Vue 컴포넌트처럼 : PascalCase | ButtonCounter |
| 자바에서는 상속받으면 부모 클래스는 자식 클래스의 것을 쓸 수 없다. extends로 상속받은 자식이 부모의 변수, 메서드를 쓸 수 있다. |
전역으로 등록하면 import 없이 지정된 앱의 모든 곳에서 컴포넌트를 사용할 수 있다.
* 전역으로 등록 (app.component() 이용)
import MyComponent from './App.vue'
app.component('MyComponent', MyComponent)
부모-자식은 정해진 건 없고 누가 누구 쓰냐에 따라 다름
가져다가 쓰는 애가 parent... 부모가 자식 child 것을 가져다 쓰는 것이다 ..
● Props 전달하기
: 컴포넌트에 등록할 수 있는 사용자 정의 속성
: 부모 -> 자식으로 값을 보내는 방법
<BlogPost title="오늘 날씨 최고" />
<!-- 부모가 자식 컴포넌트인 BolgPost에게 "title"값을 전달 -->
자식 컴포넌트에서 title값을 받으려면
<!-- BlogPost.vue -->
<script setup>
defineProps(['title']) // 'title'이라는 이름의 값을 받겠다
</script>
<template>
<h4>{{ title }}</h4>
</template>
<BlogPost title="제목" content="내용입니다" />
<!-- 여러 개 보내고 -->
defineProps(['title', 'content'])
<!-- 자식이 받고 -->
블로그 내 특정 게시물의 제목이나 컨텐츠 같은 데이터들을 전달할 때 유용하다.
블로그 제목을 컴포넌트에 전달하려면 defineProps 매크로를 사용한다.
defineProps 는 <script setup> 내에서만 사용할 수 있는 컴파일 타임 매크로
템플릿에 선언된 props는 자동으로 노출된다.
컴포넌트에 전달된 모든 props를 객체로 반환하기 때문에 JS에서 접근 가능하다.
import { defineProps } from 'vue'
const props = defineProps(['title'])
console.log(props.title)
컴포넌트는 여러 개의 props를 가질 수 있다.
<BlogPost title="Vue와 함께한 나의 여행" />
<BlogPost title="Vue로 블로깅하기" />
<BlogPost title="Vue가 재미있는 이유" />
const posts = ref([
{ id: 1, title: 'Vue와 함께한 나의 여행' },
{ id: 2, title: 'Vue로 블로깅하기' },
{ id: 3, title: 'Vue가 재미있는 이유' }
])
부모 컴포넌트에 이렇게 배열이 있을 때
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
v-for를 사용하여 각각을 컴포넌트로 렌더링할 수 있다.
const posts = ref([
{ id: 1, title: '1번 제목' },
{ id: 2, title: '2번 제목' },
])
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
v-for로 컴포넌트를 여러 개 뿌리면 각각의 제목을 넘길 수 있다.
● 이벤트 청취하기 emit
부모 컴포넌트는 v-on 또는 @ 사용해서 자식 컴포넌트 인스턴스의 모든 이벤트를 수신할 수 있다.
<BlogPost @enlarge-text="postFontSize += 0.1" />
: @enlarge-text 는 'enlarge-text' 라는 이벤트가 오면 실행할 코드
(자식이 이벤트 보내면 부모가 postFontSize를 키움)
그리고 자식 컴포넌트 BlogPost.vue는 빌트인 $emit 메서드를 호출하고
이벤트 이름을 전달해서 자체적으로 이벤트 생성할 수 있다.
BlogPost.vue
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlarge-text')">텍스트 확대</button>
</div>
</template>
버튼 클릭 시 enlarge-text 이벤트를 부모에게 보냄
**
$emit : 자식 컴포넌트가 부모 컴포넌트에게 이벤트 발생시키는 것
: <script setup> 에서는 emit 으로 쓰고, 외부에서는 $emit 가능 (템플릿 기능)
ex)
$emit('hello') 는 자식 컴포넌트가 hello라는 이름의 이벤트를 부모에게 보내는 것이다.
<script setup>
defineProps(['title']) // 부모가 넘겨주는 데이터 받기
defineEmits(['enlarge-text']) // 부모한테 보낼 이벤트 등록하기
</script>
이건 컴포넌트가 내보내는 모든 이벤트를 문서화하고 선택적으로 유효성 검사를 한다.
defineEmits 도 <script setup>에서만 사용 가능하고 import할 필요 없다.
$emit 메서드와 동일한 emit 함수 반환 -> 이벤트 내보내기 가능
<script setup>
const emit = defineEmits(['enlarge-text'])
emit('enlarge-text') // 부모 컴포넌트에게 이벤트 보내기
</script>
● 슬롯이 있는 컨텐츠 배포
<AlertBox>
나쁜 일이 일어났습니다.
</AlertBox>

이런 식으로 컴포넌트에 컨텐츠를 전달할 수 있다.
컨텐츠를 이동하려는 자리 표시자로 <slot>을 사용한다.
Vue 사용자의 <slot> 엘리먼트를 사용하여 만들 수 있다.
<template>
<div class="alert-box">
<strong>이것은 데모용 에러입니다.</strong>
<slot />
</div>
</template>
<style scoped>
.alert-box {
/* ... */
}
</style>
** AlertBox.vue 의 내용이 <slot /> 자리에 들어가는 것
쉽게 설명
A.vue에서 B.vue를 사용할 때
자식이 부모꺼 갖다 쓰는 거라 A.vue가 자식, B.vue가 부모고
부모가 자식의 컴포넌트를 import해서 부모 <template>안에서 <자식컴포넌트이름>으로 사용
A.vue
<template>
abc
<slot />
</template>
<script setup>
import B from './B.vue' // A.vue가 B.vue를 import해서 사용
</script>
B.vue
<template>
<A>
가나다 // slot에 넣을 내용
</A>
</template>
<script setup>
import A from './A.vue'
</script>
출력
abc
가나다
● 동적 컴포넌트
<!-- currentTab이 변경되면 컴포넌트가 변경됩니다 -->
<component :is="tabs[currentTab]"></component>
:is 에 전달된 값은
등록된 컴포넌트의 이름 문자열 또는 실제 가져온 컴포넌트 객체 중 하나를 포함할 수 있다.
** :is= "컴포넌트 이름이나 객체"
화면에서 어떤 컴포넌트를 보여줄지 실시간으로 변경 가능
ex)
탭1 누르면 A컴포넌트, 탭2 누르면 B컴포넌트 보여줄 때 활용 가능
A.vue
<template>
<div>A 컴포넌트 내용이에요</div>
</template>
B.vue
<template>
<div>B 컴포넌트 내용이에요</div>
</template>
<template>
<button @click="currentTab = 'A'">A 보여줘</button>
<button @click="currentTab = 'B'">B 보여줘</button>
<!-- currentTab이 바뀌면 보여주는 컴포넌트도 바뀜 -->
<component :is="currentTabComponent" />
</template>
<script setup>
import A from './A.vue'
import B from './B.vue'
import { ref, computed } from 'vue'
// 어떤 탭이 선택되었는지
const currentTab = ref('A')
// 탭 이름을 실제 컴포넌트로 연결
const components = {
A,
B
}
// 선택된 컴포넌트를 리턴하는 computed
const currentTabComponent = computed(() => components[currentTab.value])
</script>
● DOM 템플릿 파싱 주의 사항
HTML 태그와 속성의 이름은 대소문자 구분하지 않아서 브라우저가 모두 소문자로 해석한다.
따라서 HTML 템플릿 안에서 Vue컴포넌트를 사용할 때는 kebab-case를 써야 한다. (소문자+하이픈)
Vue가 자동으로 camelCase <-> kebabCase 변환해서 연결하기 때문에
JS파일이나 Vue파일 등 선언할 때는 PascalCase, camelCase 를 쓰고
HTML (템플릿) 에서 쓸 때는 kebab-case 쓴다
<!-- MyComponent.vue -->
<script setup>
defineProps({
someProp: String
})
</script>
<!-- 사용할 때는 -->
<my-component some-prop="값" />
● 엘리먼트 배치 제한
<ul>, <ol>, <table>, <select> 같은 일부 HTML 엘리먼트 내부에 표시할 수 있는 엘리먼트에 대한 제한이 있다.
<li>, <tr>, <option> 같은 일부 엘리먼트는 특정 엘리먼트 내부에서만 사용할 수 있다.
<table>
<blog-post-row></blog-post-row>
</table>
불가능
:is 속성 써서 가능하게 할 수 있다.
<table>
<tr is="vue:blog-post-row"></tr>
</table>
'웹 개발 > Vue' 카테고리의 다른 글
| [Vue3] v-slot (0) | 2025.05.28 |
|---|---|
| [Vue3] data() -> <script setup> 문법 (0) | 2025.05.28 |
| [Vue] Webpack, CDN (0) | 2025.05.22 |
| [Vue3] Router (0) | 2025.05.20 |
| [Vue3] change, key 이벤트 (v-on:change, v-on:key) (0) | 2025.05.19 |