본 글은 Vue.js 문서에서 컴포넌트 - Props 부분을 정리하고 예시를 추가한 글 입니다.
Vue.js - 컴포넌트(Props)
모든 컴포넌트 인스턴스에는 각자의 자체 격리된 범위(scope)가 있습니다. 이 때문에 하위 컴포넌트의 템플릿에서는 상위 데이터를 직접 참조할 수 없는데, props
를 사용하면 하위 컴포넌트로 데이터를 전달할 수 있습니다.
props
는 상위 컴포넌트의 데이터를 전달하기 위한 사용자 지정 특성 입니다. 하위 컴포넌트에서는 props
옵션을 사용해 수신할 것으로 기대되는 props
를 명시적으로 선언해야 합니다.
# 하위 컴포넌트 js
Vue.component('child', {
// props 정의
props: ['message'],
// 데이터와 마찬가지로 prop은 템플릿 내부에서 사용할 수 있으며
// vm의 this.message로 사용할 수 있습니다.
template: '<span>{{ message }}</span>'
})
그런 다음 일반 문자열을 다음과 같이 전달할 수 있습니다. (리터럴 방식)
# 상위 컴포넌트 html template
<child message="안녕하세요!"></child>
이렇게 하면 상위 컴포넌트에서 message
에 담은 안녕하세요!
라는 문자열을 하위 컴포넌트의 message
로 보내줄 수 있습니다.
하지만 이 방식은 동적 바인딩은 아니라서, 상위 컴포넌트의 데이터가 변해도 하위 컴포넌트에는 반영이 되지 않습니다.
v-bind
를 사용하면 부모(상위 컴포넌트)의 데이터를 동적으로 바인딩해 자식(하위 컴포넌트)에게 전달할 수 있습니다. 데이터가 상위에서 업데이트 될 때마다 하위 컴포넌트로도 전달됩니다.
# 상위 컴포넌트 html template
<div>
<input v-model="parentMsg">
<br>
<child v-bind:my-message="parentMsg"></child> # 동적 바인딩
</div>
v-bind
는 아래와 같이 :
을 활용해 단축 구문으로 사용하는 경우가 더 많습니다.
<child :my-message="parentMsg"></child>
전달하고 싶은 데이터가 객체라면, 인자 없이 v-bind
를 써도 전달 가능합니다. (v-bind:prop-name
대신 v-bind:객체명
)
예를 들어 todo
라는 객체가 있다면,
# 상위 컴포넌트 js
todo: {
text: 'Learn Vue',
isComplete: false
}
아래의 두 가지 작성 방식은 같은 동작을 합니다. 각자의 props를 바인딩 하는 것과 객체로 바인딩하는 방식이 가져오는 효과는 같습니다.
# 상위 컴포넌트 html template
1)
<todo-item v-bind="todo"></todo-item>
2)
<todo-item
v-bind:text="todo.text"
v-bind:is-complete="todo.isComplete"
></todo-item>
초보자가 흔히 범하는 실수는 리터럴 구문을 사용하여 숫자를 전달하려고 하는 것 입니다.
# 상위 컴포넌트 html template
<!-- 이것은 일반 문자열 "1"을 전달합니다. -->
<comp some-prop="1"></comp>
하지만 위의 리터럴 방식은 실제 숫자가 아닌 일반 문자열 "1"
로 데이터를 전달합니다. 실제 숫자를 전달하려면 값이 Javascript 표현식으로 평가 되도록 v-bind
를 사용해야합니다.
# 상위 컴포넌트 html template
<!-- 이것은 실제 숫자로 전달합니다. -->
<comp v-bind:some-prop="1"></comp>
하위 컴포넌트가 상위 컴포넌트로부터 받는 Props
에 대한 요구사항을 지정할 수도 있습니다. 요구사항이 충족되지 않으면 Vue에서 경고를 내보냅니다. 이 기능은 다른 사용자가 사용할 컴포넌트를 제작할 때 특히 유용합니다.
Props
를 문자열 배열로 정의하는 대신, 유효성 검사 요구사항을 담은 객체를 사용할 수 있습니다.
# 하위 컴포넌트 js
Vue.component('example', {
props: {
// 기본 타입 확인 (`null` 은 어떤 타입이든 가능하다는 뜻입니다)
// propA 는 숫자 타입이어야 함
propA: Number,
// 여러개의 가능한 타입
// propB는 문자열이나 숫자 타입이어야 함
propB: [String, Number],
// propC는 문자열이며, 필수 값임
propC: {
type: String,
required: true
},
//propD는 숫자 타입이며, 100의 기본 값을 가짐
propD: {
type: Number,
default: 100
},
// propE의 타입은 객체/배열이며 기본값은 팩토리 함수에서 반환 되어야 함
propE: {
type: Object,
default: function () {
return { message: 'hello' }
}
},
// 사용자 정의 유효성 검사 가능
propF: {
validator: function (value) {
return value > 10
}
}
}
})
type
은 다음 네이티브 생성자 중 하나를 사용할 수 있습니다.
props
검증이 실패하면 Vue는 콘솔에서 경고를 출력합니다.(개발 빌드를 사용하는 경우) Props
는 컴포넌트 인스턴스가 생성되기 전에 검증되기 때문에 default 또는 validator 함수 내에서 data, computed 또는 methods와 같은 인스턴스 속성을 사용할 수 없습니다.
팩토리 함수란 클래스나 생성자를 반환하지 않는 모든 함수를 칭함
A factory function is any function which is not a class or constructor that returns a (presumably new) object. In JavaScript, any function can return an object. When it does so without the new keyword, it’s a factory function.
JavaScript Factory Functions with ES6+ 내용 발췌
Vue JS Crash Course 유튜브를 통해 공부한 내용을 사례로 제시 합니다. 설명에 사용되지 않는 코드는 삭제했습니다.
살펴 볼 투두리스트의 컴포넌트 구조는 아래와 같습니다. Home
이라는 컴포넌트 안에 Todos
가 있고, 그 아래에 각각의 TodoItem
이 배치되어 있습니다.
# Home.vue
<template>
<div id="app">
// 3) api의 데이터가 저장된 todos를 v-bind를 통해 todos라는 이름으로 'Todos' 하위 컴포넌트로 전달해줌
<Todos v-bind:todos="todos" v-on:del-todo="deleteTodo"/>
</div>
</template>
<script>
import Todos from '../components/Todos';
import axios from 'axios';
export default {
name: 'Home',
components: {
Todos
},
// 1) todos를 빈배열로 선언
data() {
return {
todos: []
}
},
// 2) api를 통해 todo 리스트 데이터를 받아오고, 이 데이터를 1)에서 선언한 todos에 넣어줌
created() {
axios.get('https://jsonplaceholder.typicode.com/todos?_limit=5 ')
.then(res => this.todos = res.data)
.catch(err => console.log(err));
}
}
</script>
<style>
...
</style>
# Todos.vue
<template>
<div>
// 2) 상위 컴포넌트에서 받아 온 todos를 v-for 문을 돌려 TodoItem 컴포넌트를 적용시킵니다.
<div v-bind:key="todo.id" v-for="todo in todos">
// 3) for 문을 통해 생긴 todo 데이터를 todo로 v-bind 하여 하위 컴포넌트로 전달합니다.
<TodoItem v-bind:todo="todo" v-on:del-todo="$emit('del-todo', todo.id)" />
</div>
</div>
</template>
<script>
import TodoItem from './TodoItem.vue';
export default {
name: "Todos",
components: {
TodoItem
},
// 1) 상위 컴포넌트인 Home.vue에서 받은 'todos' 데이터를 props를 정의해 받아옵니다.
props: ["todos"]
}
</script>
<style scoped>
</style>
# TodoItem.vue
<template>
<div class="todo-item" v-bind:class="{'is-complete':todo.completed}">
<p>
<input type="checkbox" v-on:change="markComplete">
// 2) todo 데이터를 이용해 데이터를 렌더링합니다.
{{todo.title}}
<button @click="$emit('del-todo', todo.id)" class="del">x</button>
</p>
</div>
</template>
<script>
export default {
name: "TodoItem",
// 1) Todos.vue 에서 전달 받은 todo를 props로 정의합니다.
props: ["todo"],
methods: {
markComplete() {
this.todo.completed = !this.todo.completed;
}
}
}
</script>
<style scoped>
...
</style>