Vue는 화면을 여러 개의 컴포넌트로 나누어 구성한다.
예를 들어 하나의 페이지가 다음과 같이 구성되어 있다고 해보자.
ParentComponent
├─ HeaderComponent
├─ SearchBar
├─ WeatherCard
└─ FooterComponent
각 컴포넌트는 서로 독립적으로 동작하지만, 실제 애플리케이션에서는 부모와 자식이 데이터를 주고받아야 한다.
Vue에서는 이때 기본적으로 다음 두 가지 방식을 사용한다.
Props
→ 부모 → 자식
Emits
→ 자식 → 부모
즉,
데이터는 부모에서 자식으로 내려가고, 이벤트는 자식에서 부모로 올라간다.
이 구조를 보통 Props Down, Events Up이라고 한다.
Props란?
Props는 부모 컴포넌트가 자식 컴포넌트에게 데이터를 전달하는 방법이다.
예를 들어 부모 컴포넌트가 도시 이름을 가지고 있다고 해보자.
<script setup>
import WeatherCard from './WeatherCard.vue'
const cityName = '서울'
</script>
<template>
<WeatherCard :city-name="cityName" />
</template>
여기서
:city-name="cityName"
은 부모의 cityName 데이터를 자식 컴포넌트에 전달한다는 의미다.
자식 컴포넌트에서는 defineProps()를 이용해 받을 데이터를 정의한다.
<script setup>
defineProps({
cityName: String
})
</script>
<template>
<p>{{ cityName }}</p>
</template>
전체 흐름은 다음과 같다.
Parent Component
│
│ Props
▼
Child Component
cityName = "서울"
즉 Props는 부모가 가진 데이터를 자식에게 내려주는 통로다.
defineProps()
Props를 받을 때 사용하는 함수가 defineProps()다.
가장 간단하게는 다음처럼 작성할 수 있다.
defineProps({
cityName: String,
temperature: Number
})
조금 더 세밀하게 지정할 수도 있다.
defineProps({
cityName: {
type: String,
required: true
},
temperature: {
type: Number,
default: 20
}
})
여기서는 각각 다음 의미를 가진다.
cityName
→ String
→ 반드시 전달 필요
temperature
→ Number
→ 전달되지 않으면 기본값 20
Props는 단순히 데이터를 받는 것뿐 아니라, 어떤 타입의 값을 받을지 규격을 정할 수도 있다.
Props는 읽기 전용이다
부모에게 받은 Props는 자식 컴포넌트에서 직접 수정하면 안 된다.
예를 들어 다음과 같은 코드는 잘못된 방식이다.
const props = defineProps({
count: Number
})
props.count = 100
Props는 부모가 소유한 데이터이기 때문이다.
Parent
│
│ owns data
▼
Child
자식 컴포넌트는 전달받은 값을 읽을 수는 있지만, 부모의 원본 상태를 직접 바꾸는 역할을 하지 않는다.
따라서 Props는 다음과 같이 기억하면 된다.
Props
→ 부모 → 자식
→ 데이터 전달
→ 읽기 전용
자식에서 부모 상태를 변경하고 싶다면 직접 수정하는 대신 이벤트를 발생시켜 부모에게 변경을 요청해야 한다.
이때 사용하는 것이 Emits다.
Emits란?
Emits는 자식 컴포넌트에서 발생한 일을 부모 컴포넌트에게 전달하는 방식이다.
예를 들어 WeatherCard를 클릭했을 때 선택된 도시 이름을 부모에게 전달한다고 해보자.
자식 컴포넌트에서는 먼저 사용할 이벤트를 정의한다.
<script setup>
const emit = defineEmits(['select-city'])
</script>
그리고 실제 이벤트를 발생시킨다.
emit('select-city', '서울')
여기서
select-city
는 이벤트 이름이고,
서울
은 부모에게 함께 전달할 데이터다.
자식 컴포넌트를 전체로 보면 다음과 같다.
<script setup>
const emit = defineEmits(['select-city'])
const handleClick = () => {
emit('select-city', '서울')
}
</script>
<template>
<button @click="handleClick">
도시 선택
</button>
</template>
부모 컴포넌트에서는 @를 사용해 이벤트를 받는다.
<script setup>
import WeatherCard from './WeatherCard.vue'
const handleSelectCity = (city) => {
console.log(city)
}
</script>
<template>
<WeatherCard @select-city="handleSelectCity" />
</template>
흐름은 다음과 같다.
Child Component
│
│ emit
▼
Parent Component
select-city
+
"서울"
즉 Emits는 자식이 부모에게 이벤트를 발생시키고, 필요한 데이터를 함께 전달하는 통로다.
defineEmits()
defineEmits()는 자식 컴포넌트가 어떤 이벤트를 발생시킬지 정의하는 함수다.
const emit = defineEmits([
'select-city',
'delete-item'
])
이후 이벤트를 발생시킬 때는 다음과 같이 사용한다.
emit('select-city', cityName)
구조를 보면 다음과 같다.
defineEmits()
→ 사용할 이벤트 정의
emit()
→ 실제 이벤트 발생
Props와 Emits를 함께 사용하기
Props와 Emits는 따로 떨어진 기능이라기보다 부모와 자식 컴포넌트 사이의 하나의 통신 구조로 이해하는 것이 좋다.
예를 들어 부모가 자식에게 도시 정보를 전달하고, 자식이 클릭되면 다시 부모에게 선택된 도시를 알려준다고 해보자.
부모 컴포넌트:
<script setup>
import { ref } from 'vue'
import WeatherCard from './WeatherCard.vue'
const selectedCity = ref('선택된 도시 없음')
const handleSelectCity = (cityName) => {
selectedCity.value = cityName
}
</script>
<template>
<p>선택된 도시: {{ selectedCity }}</p>
<WeatherCard
city-name="서울"
status="맑음"
@select-city="handleSelectCity"
/>
</template>
자식 컴포넌트:
<script setup>
const props = defineProps({
cityName: String,
status: String
})
const emit = defineEmits(['select-city'])
const handleClick = () => {
emit('select-city', props.cityName)
}
</script>
<template>
<div @click="handleClick">
<h3>{{ cityName }}</h3>
<p>{{ status }}</p>
</div>
</template>
전체 흐름은 다음과 같다.
Parent
│
│ Props
│
├─ cityName = 서울
└─ status = 맑음
│
▼
WeatherCard
│
│ 사용자 클릭
│
│ emit('select-city', '서울')
▼
Parent
│
└─ selectedCity = 서울
부모가 데이터를 내려주고,
Parent
↓
Props
↓
Child
자식은 발생한 이벤트를 다시 부모에게 알린다.
Child
↓
Emits
↓
Parent
이 두 방향을 합치면 Vue의 기본적인 부모·자식 통신 구조가 완성된다.
:와 @의 의미
부모 컴포넌트에서 자식 컴포넌트를 사용할 때 Props와 Emits는 보통 다음 형태로 함께 나타난다.
<WeatherCard
:city-name="cityName"
@select-city="handleSelectCity"
/>
여기서 :는 v-bind의 축약형이다.
:
→ v-bind
→ 자식에게 데이터 전달
→ Props
@는 v-on의 축약형이다.
@
→ v-on
→ 자식 이벤트 수신
→ Emits
따라서 다음 코드는
<ChildComponent
:data="value"
@update="handleUpdate"
/>
이렇게 해석할 수 있다.
:data="value"
→ 부모의 value를 자식에게 전달
@update="handleUpdate"
→ 자식이 update 이벤트를 발생시키면
부모의 handleUpdate 실행
Payload
emit()을 사용할 때 이벤트와 함께 부모에게 데이터를 전달할 수 있다.
이 데이터를 Payload라고 한다.
예를 들어 다음과 같다.
emit('select-city', '서울')
여기서 '서울'이 Payload다.
부모에서는 이벤트 핸들러의 매개변수로 받을 수 있다.
const handleSelectCity = (cityName) => {
console.log(cityName)
}
여러 데이터를 객체로 묶어 전달하는 것도 가능하다.
emit('select-city', {
id: 1,
name: '서울',
temperature: 28
})
부모에서는 다음처럼 처리한다.
const handleSelectCity = (city) => {
console.log(city.id)
console.log(city.name)
console.log(city.temperature)
}
즉 Emits는 단순한 신호뿐 아니라 필요한 데이터까지 함께 전달할 수 있다.
왜 자식이 Props를 직접 수정하면 안 될까?
Props를 읽기 전용으로 두는 이유는 데이터의 흐름을 명확하게 만들기 위해서다.
만약 부모와 자식이 같은 데이터를 마음대로 변경할 수 있다면 상태가 어디에서 바뀌었는지 추적하기 어려워진다.
예를 들어 다음과 같은 구조라면
Parent
↓
Child A
↓
Child B
여러 컴포넌트가 같은 상태를 마음대로 수정하면 데이터 흐름을 파악하기 어려워질 수 있다.
Vue에서는 이를 다음과 같이 단순화한다.
상태를 가진 곳
→ Parent
데이터 전달
→ Props
변경 요청
→ Emits
실제 상태 변경
→ Parent
즉 데이터의 소유권은 부모에게 두고, 자식은 변경이 필요하다는 이벤트만 전달하는 방식이다.
이러한 단방향 흐름 덕분에 상태 변화를 추적하기 쉬워진다.
컴포넌트 통신 전체 구조
Props와 Emits를 하나의 그림으로 정리하면 다음과 같다.
Parent Component
│
│ Props
│
▼
Child Component
│
│ Emits
│
▼
Parent Component
좀 더 구체적으로 표현하면 다음과 같다.
Parent State
cityName = 서울
│
│ :city-name
▼
┌─────────────┐
│ WeatherCard │
└─────────────┘
│
│ 사용자 클릭
│
│ emit
▼
@select-city
│
▼
handleSelectCity()
│
▼
Parent State 변경
이 구조가 Vue 컴포넌트 통신의 가장 기본적인 형태다.
정리
Props와 Emits는 Vue에서 부모와 자식 컴포넌트가 통신하는 기본 방식이다.
| 구분 | Props | Emits |
| 방향 | 부모 → 자식 | 자식 → 부모 |
| 목적 | 데이터 전달 | 이벤트 전달 |
| 정의 | defineProps() | defineEmits() |
| 부모 Template | :prop="value" | @event="handler" |
| 데이터 변경 | 자식 직접 수정 X | 부모에게 변경 요청 |
| 전달 데이터 | 상태 값 | Payload 가능 |
가장 핵심인 구조는 다음과 같다.
Props Down
Events Up
즉,
부모
│
│ Props
▼
자식
│
│ Emits
▼
부모
Vue에서는 부모가 상태를 관리하고 자식에게 필요한 데이터를 Props로 전달한다.
자식 컴포넌트에서 상태 변경이 필요하면 부모의 데이터를 직접 수정하는 대신 Emits를 통해 이벤트를 발생시킨다.
결국 Props와 Emits는 컴포넌트 사이의 데이터 흐름을 한 방향으로 명확하게 유지하기 위한 Vue의 기본 통신 구조라고 정리할 수 있다.