[Day 36-2] 컴포넌트 등록, props, 커스텀 이벤트
컴포넌트 등록
컴포넌트는 전역 등록, 지역 등록이 가능하다.
컴포넌트로 등록을 하기 위해서는 컴포넌트의 이름을 지정해야 하는데 파스칼 케이스 혹은 케밥 케이스(= 대쉬 케이스)로 작성이 가능하다.
전역 등록
전역으로 컴포넌트를 등록하면 컴포넌트 어디에서든지 import를 하지 않고 등록한 컴포넌트를 사용 가능하다.
main.js에서 생성한 app에 component 메소드를 통해 등록한다.
// main.js
import {createApp} from 'vue'
import App from '~/App'
import Btn from '~/components/Btn'
const app = createApp(App)
app.component('Btn', Btn) // 전역 등록
app.mount('#app')
<!-- App.vue -->
<template>
<h1>Hello Vue!</h1>
<!-- import 없이 바로 사용 가능 -->
<Btn />
</template>
지역 등록
지역적으로 컴포넌트를 등록하기 위해서는 components
옵션 안에 정의해야 한다.
<!-- App.vue -->
<template>
<h1>Hello Vue!</h1>
<Hello />
</template>
<script>
import Hello from '~/components/Hello'
export default {
components: {
Hello
}
}
</script>
컴포넌트 Props
단방향 데이터 흐름
상위 컴포넌트에서 전달하는 props의 데이터는 하위 컴포넌트에서는 수정할 수 없다.
<!-- App.vue -->
<template>
<button @click="reverseMsg">click!</button>
<Hello :message="msg" />
</template>
<script>
import Hello from '~/components/Hello'
export default {
components: {
Hello,
},
data() {
return {
msg: 'Hello Vue!',
}
},
methods: {
reverseMsg() {
this.msg = this.msg.split('').reverse().join('')
}
}
}
</script>
<!-- Hello.vue -->
<template>
<h1>{{ message }}</h1>
</template>
<script>
export default {
props: ['message']
}
</script>
따라서 props를 받은 데이터를 컴포넌트 내부에서 데이터를 따로 담아서 해당 데이터를 변경하는 방식으로 해결할 수 있다.
<!-- Hello.vue -->
<template>
<h1 @click="updateMessage">
{{ message }}
</h1>
</template>
<script>
export default {
props: ['message'],
data() {
return {
newMessage: this.message // props 데이터를 새로운 변수에 담음
}
},
methods: {
updateMessage() {
this.newMessage = 'Good!'
}
}
}
</script>
Prop 타입
props는 데이터 타입을 명시해줄 수 있으며, 기본값을 지정할 수도 있다.
props: {
title: String, // { type: String, default: 기본값 }
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise
}
객체나 배열과 같은 참조형 데이터의 default값은 항상 함수로부터 반환이 되어야 한다. (= 팩토리 함수)
props:{
id: Number,
title: [String, Number],
email:{
type: String,
default: '1111@abc.com'
}
data:{
type: Object,
// 객체나 배열의 기본 값은 항상 팩토리 함수로부터 반환
default: function() {
return { message: 'hello' }
}
},
},
아래와 같이 커스텀 유효성 검사 함수를 작성할 수도 있다.
props: {
prop: {
validator: function(value) {
// 값이 꼭 아래 세 문자열 중 하나와 일치해야 함
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
HTML 속성명은 대소문자를 구분하지 않기 때문에 props를 통해 전달할 데이터 이름은 html상에서 케밥 케이스로 작성해야 한다.
props: ['postTitle']
<Post post-title="hello~" />
객체의 속성 전달
객체의 모든 속성을 props로 전달하려면 v-bind
를 사용할 수 있다. (:prop-name
대신 v-bind
사용)
<!-- App.vue -->
<template>
<Hello v-bind="post" />
<!-- 위 코드는 아래 코드와 동일 -->
<!-- <Hello :id="post.id" :title="post.title" :email="post.email"/> -->
</template>
<script>
import Hello from '~/components/Hello'
export default {
components: {
Hello,
},
data() {
return {
post: {
id: 1,
title: 'Hello!',
email: 'hello@gmail.com'
},
}
},
}
</script>
<!-- Hello.vue -->
<template>
<h1>{{ id }} / {{ title }} / {{ email }}</h1>
</template>
<script>
export default {
props: {
id: Number,
title: String,
email: {
type: String,
default: 'leon@abc.com'
},
},
}
</script>
컴포넌트 Non-Prop 속성
Non-prop 속성은 컴포넌트에 전달되지만 props나 emits에 정의된 특성을 지니고 있지 않은 속성(class, style, id) 또는 이벤트 리스너를 의미한다.
이러한 속성들은 $attrs
프로퍼티를 통해 접근할 수 있다.
속성 상속
상위 컴포넌트에서 하위 컴포넌트에 속성을 전달할 때 하위 컴포넌트 내부에 최상위 요소가 하나일 경우 속성이나 이벤트가 어디에 적용될 것인가가 명확하기 때문에 잘 적용이 되는 반면, 최상위 요소가 여러 개일 경우 어디에 적용해야 하는지 알 수 없기 때문에 적용이 되지 않는다.
<!-- App.vue -->
<template>
<h1>{{ msg }}</h1>
<Hello
class="hello"
style="font-size: 100px;"
@click="msg += '!'" />
</template>
<script>
import Hello from '~/components/Hello'
export default {
components: {
Hello,
},
data() {
return {
msg: 'Hello'
}
},
}
</script>
<!-- Hello.vue -->
<template>
<h1>Hello</h1>
<h2>Haha</h2>
</template>
이렇게 최상위 요소가 여러 개일 경우 $attrs
를 사용하여 전달된 속성을 나누어 줄 수 있다.
$attrs
는 attributes 객체로 컴포넌트가 가지고 있는 여러가지 속성들을 담고 있는 객체이다.
주의할 점은 props로 명시 된 속성들은 $attrs에 들어가지 않는다는 사실이다.
$attrs
는 props
를 통해 지정되지 않은 나머지 속성들을 다룬다.
<!-- Hello.vue -->
<template>
<h1
:class="$attrs.class"
:style="$attrs.style">
Hello
</h1>
<h2 @click="$attrs.onClick">
Haha
</h2>
</template>
v-bind
를 통해 $attrs
에 담긴 속성들을 요소에 전부 적용할 수도 있다.
<!-- Hello.vue -->
<template>
<h1 v-bind="$attrs">
Hello
</h1>
</template>
만일 최상위 요소가 하나일 경우 속성들이 적용되는 것을 원하지 않는다면 inheritAttrs: false
를 통해 자동 상속을 방지한다.
<!-- Hello.vue -->
<template>
<h1>Hello</h1>
</template>
<script>
export default {
inheritAttrs: false,
mounted() {
console.log(this.$attrs)
}
}
</script>
컴포넌트 커스텀 이벤트
v-on
(@) 디렉티브와 emit
을 사용해서 커스텀 이벤트를 정의하고 사용할 수 있다.
<!-- App.vue -->
<template>
<h1>{{ msg }}</h1>
<Hello
:message="msg"
@please="reverseMsg" />
</template>
<script>
import Hello from '~/components/Hello'
export default {
components: {
Hello,
},
data() {
return {
msg: 'Hello Vue!'
}
},
methods: {
reverseMsg() {
this.msg = this.msg.split('').reverse().join('')
}
}
}
</script>
<!-- Hello.vue -->
<template>
<h1 @click="$emit('please')">
{{ message }}
</h1>
</template>
<script>
export default {
props: {
message: {
type: String,
default: ''
}
},
emits: ['please']
}
</script>
emits
옵션을 통해 커스텀 이벤트를 정의해줘야 한다.
정의하지 않아도 동작은 하지만 해당 컴포넌트에서 어떤 커스텀 이벤트를 쓸 수 있는지 판단하기 위해서 모든 이벤트를 정의하는 것이 권장된다.
다만 네이티브 이벤트(e.g., click)를 주의해야 한다.
<!-- App.vue -->
<template>
<h1>{{ msg }}</h1>
<Hello
@click="msg += '?!'" />
</template>
<!-- Hello.vue -->
<template>
<div>
<h1>A</h1>
<h1>B</h1>
</div>
</template>
<script>
export default {
emits: ['click']
}
</script>
만약 emits 옵션을 이용해 네이티브 이벤트가 재정의 된 경우, 컴포넌트에 정의된 커스텀 이벤트가 네이티브 이벤트를 덮어쓴다.
커스텀 이벤트 검사
<!-- App.vue -->
<template>
<h1>{{ msg }}</h1>
<Hello
@click="msg += '?!'"
@please="msg += '~~'" />
</template>
<!-- Hello.vue -->
<template>
<div>
<h1 @click="$emit('please', 6)">please</h1>
<h1 @click="$emit('click')">click</h1>
</div>
</template>
<script>
export default {
emits: {
click: null, // 값이 null이면 검사 실행 x
please: (number) => {
if (number > 10) {
return true
} else {
console.error('숫자가 10보다 크지 않음')
return false
}
}
}
}
</script>
양방향 데이터 바인딩
아래 예제의 방식을 통해 양방향 데이터 바인딩을 구현할 수 있지만 복잡해보인다.
<!-- App.vue -->
<template>
<h1>{{ msg }}</h1>
<Hello
:message="msg"
@update="msg = $event" />
</template>
<!-- Hello.vue -->
<template>
<label>
<input
:value="message"
@input="$emit('update', $event.target.value)" />
</label>
</template>
<script>
export default {
props: {
message: {
type: String,
default: ''
}
}
}
</script>
이럴 때 사용할 수 있는 것이 modelValue
props이다.
modelValue
props를 사용하면 자동으로 v-model 디렉티브와 연결된다.
<!-- App.vue -->
<template>
<h1>{{ msg }}</h1>
<Hello v-model="msg" />
</template>
<!-- Hello.vue -->
<template>
<label>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)" />
</label>
</template>
<script>
export default {
props: {
modelValue: {
type: String,
default: ''
}
}
}
</script>
만일 modelValue
라는 props 명을 변경하고 싶다면 v-model에 전달인자를 넘겨줌으로써 이름을 변경할 수 있다
<!-- App.vue -->
<template>
<h1>{{ msg }}</h1>
<Hello v-model:message="msg" />
</template>
<!-- Hello.vue -->
<template>
<label>
<input
:value="message"
@input="$emit('update:message', $event.target.value)" />
</label>
</template>
<script>
export default {
props: {
message: {
type: String,
default: ''
}
}
}
</script>
아래는 여러개의 양방향 데이터 바인딩 예시이다.
<!-- App.vue -->
<template>
<h1>{{ msg }}</h1>
<h1>{{ name }}</h1>
<Hello
v-model:message="msg"
v-model:name="name" />
</template>
<!-- Hello.vue -->
<template>
<label>
<input
:value="message"
@input="$emit('update:message', $event.target.value)" />
</label>
<label>
<input
:value="name"
@input="$emit('update:name', $event.target.value)" />
</label>
</template>
<script>
export default {
props: {
message: {
type: String,
default: ''
},
name: {
type: String,
default: ''
}
}
}
</script>
출처: 프로그래머스 프론트엔드 데브코스
[Day 36] Vue (4)