컴포넌트
props와 emit으로 컴포넌트 사이에 데이터를 주고받는 방법을 배웁니다.
지금까지는 대부분의 코드를 App.vue 하나에 작성했습니다. 작은 예제에서는 문제가 없지만 프로그램이 커지면 하나의 파일 안에 여러 기능이 계속 추가됩니다.
사원등록 · 사원목록 · 사원수정 · 검색 · 메뉴 · 버튼 · 메시지 · ...
기능이 많아질수록 App.vue가 길어지고, 원하는 코드를 찾거나 수정하는 것도 어려워집니다.
Vue에서 화면과 기능을 여러 개의 작은 단위로 나누어 관리할 수 있는데, 이렇게 나눈 하나하나의 단위를 컴포넌트라고 합니다.
│
├─ 메뉴
├─ 사원 등록
├─ 사원 목록
└─ 사원 상세정보
각 영역은 필요에 따라 별도의 .vue 파일로 만들 수 있습니다.
컴포넌트로 프로그램을 나누면 다음과 같은 장점이 있습니다.
- 프로그램을 기능별로 나눌 수 있습니다.
- 하나의 파일이 지나치게 길어지는 것을 줄일 수 있습니다.
- 같은 화면이나 기능을 여러 곳에서 재사용할 수 있습니다.
- 수정해야 할 코드의 위치를 찾기 쉬워집니다.
- 여러 개발자가 기능을 나누어 작업하기 편리합니다.
예를 들어 사원관리 프로그램은 다음과 같이 구성할 수 있습니다.
하나의 거대한 App.vue를 만드는 것이 아니라 역할이 분명한 작은 컴포넌트들을 조립해 전체 화면을 만든다고 생각하면 이해하기 쉽습니다.
먼저 src/components 폴더에 Hello.vue 파일을 만들어 봅니다.
<template>
<h2>
안녕하세요. Vue 컴포넌트입니다.
</h2>
</template>
이제 Hello.vue를 App.vue에서 불러와 사용합니다.
<script setup> import Hello from './components/Hello.vue' </script> <template> <h1>Vue 프로그램</h1> <Hello /> </template>
안녕하세요. Vue 컴포넌트입니다.
↓
import
↓
template에서 사용
① .vue 파일 만들기 → ② import 하기 → ③ template에서 태그처럼 사용하기 순서로 기억하면 됩니다.
컴포넌트도 일반적인 .vue 파일이므로 script, template, style을 사용할 수 있습니다.
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increase() {
count.value++
}
</script>
<template>
<h2>카운터</h2>
<p>
현재 값 : {{ count }}
</p>
<button @click="increase">
증가
</button>
</template>
<script setup> import Counter from './components/Counter.vue' </script> <template> <h1>메인 화면</h1> <Counter /> </template>
Counter.vue 내부의 count 데이터와 increase() 함수는 해당 Counter 컴포넌트 안에서 독립적으로 관리됩니다.
한 번 만든 컴포넌트는 화면에서 여러 번 사용할 수 있습니다.
<template> <h1>카운터 프로그램</h1> <Counter /> <Counter /> <Counter /> </template>
현재 값 : 0
[증가]
카운터 2
현재 값 : 0
[증가]
카운터 3
현재 값 : 0
[증가]
세 개의 Counter 컴포넌트가 생성되며, 각 컴포넌트는 자신의 count 데이터를 따로 가지고 있습니다.
동일한 코드를 복사해서 세 번 작성하는 것이 아니라, 한 번 만든 컴포넌트를 필요한 곳에서 반복해서 사용할 수 있습니다.
Vue 컴포넌트는 서로 부모와 자식의 관계를 가질 수 있습니다.
│
└─ EmployeeInfo.vue
App.vue가 EmployeeInfo.vue를 사용하고 있다면 다음과 같이 부릅니다.
- App.vue → 부모 컴포넌트
- EmployeeInfo.vue → 자식 컴포넌트
실제 프로그램에서는 이런 관계가 여러 단계로 이어질 수 있습니다.
부모 컴포넌트가 가지고 있는 데이터를 자식 컴포넌트로 전달하려면 props를 사용합니다.
<script setup>
import EmployeeInfo from './components/EmployeeInfo.vue'
const employeeName = '홍길동'
</script>
<template>
<EmployeeInfo
:name="employeeName"
/>
</template>
<script setup>
const props = defineProps({
name: String
})
</script>
<template>
<h2>사원정보</h2>
<p>
이름 : {{ props.name }}
</p>
</template>
이름 : 홍길동
자식 컴포넌트는 부모가 전달한 값을 defineProps()를 이용해 받을 수 있습니다.
props는 기본적으로 부모에서 자식 방향으로 데이터를 전달합니다.
employeeName = "홍길동"
name = "홍길동"
employeeName = "홍길동"
│
│ props
↓
EmployeeInfo.vue
name = "홍길동"
│
↓
화면 출력
props를 처음 배울 때 가장 중요한 것은 부모 → 자식 방향으로 데이터가 전달된다는 점입니다.
하나의 값뿐 아니라 이름, 전화번호, 부서처럼 여러 값을 자식 컴포넌트로 전달할 수도 있습니다.
<script setup>
import EmployeeInfo from './components/EmployeeInfo.vue'
const employee = {
name: '홍길동',
phone: '010-1111-2222',
department: '개발팀'
}
</script>
<template>
<EmployeeInfo
:name="employee.name"
:phone="employee.phone"
:department="employee.department"
/>
</template>
<script setup>
const props = defineProps({
name: String,
phone: String,
department: String
})
</script>
<template>
<h2>사원정보</h2>
<p>이름 : {{ props.name }}</p>
<p>
전화번호 : {{ props.phone }}
</p>
<p>
부서 : {{ props.department }}
</p>
</template>
전달해야 할 값이 많아지면 각각의 속성을 따로 전달하는 대신 객체 자체를 props로 전달할 수 있습니다.
<script setup>
import EmployeeInfo from './components/EmployeeInfo.vue'
const employee = {
id: 1,
name: '홍길동',
phone: '010-1111-2222',
department: '개발팀'
}
</script>
<template>
<EmployeeInfo
:employee="employee"
/>
</template>
<script setup>
const props = defineProps({
employee: Object
})
</script>
<template>
<h2>사원정보</h2>
<p>
번호 : {{ props.employee.id }}
</p>
<p>
이름 : {{ props.employee.name }}
</p>
<p>
전화번호 :
{{ props.employee.phone }}
</p>
<p>
부서 :
{{ props.employee.department }}
</p>
</template>
사원처럼 여러 속성을 하나의 데이터로 다루는 경우에는 객체 자체를 props로 전달하는 형태를 활용할 수 있습니다.
props는 부모의 데이터를 자식에게 전달할 때 사용합니다. 반대로 자식 컴포넌트에서 어떤 일이 발생했음을 부모에게 알려야 할 때는 emit을 사용합니다.
emit : 자식 → 부모
예를 들어 자식 컴포넌트의 삭제 버튼을 클릭했을 때 부모에게 "이 사원을 삭제해 주세요"라고 알려야 하는 상황이 있습니다.
<script setup>
const props = defineProps({
name: String
})
const emit = defineEmits([
'remove'
])
function removeEmployee() {
emit('remove')
}
</script>
<template>
<p>
{{ props.name }}
<button @click="removeEmployee">
삭제
</button>
</p>
</template>
<script setup>
import EmployeeItem from './components/EmployeeItem.vue'
function remove() {
alert('사원을 삭제합니다.')
}
</script>
<template>
<EmployeeItem
name="홍길동"
@remove="remove"
/>
</template>
↓
emit('remove')
↓
부모에게 이벤트 전달
↓
부모의 remove() 실행
자식 컴포넌트에서 부모에게 전달할 이벤트를 정의할 때 defineEmits()를 사용합니다.
emit은 이벤트 이름뿐 아니라 부모에게 필요한 데이터도 함께 전달할 수 있습니다.
<script setup>
const props = defineProps({
id: Number,
name: String
})
const emit = defineEmits([
'remove'
])
function removeEmployee() {
emit(
'remove',
props.id
)
}
</script>
<template>
<p>
{{ props.id }}.
{{ props.name }}
<button @click="removeEmployee">
삭제
</button>
</p>
</template>
<script setup>
import EmployeeItem from './components/EmployeeItem.vue'
function remove(id) {
alert(
id + '번 사원을 삭제합니다.'
)
}
</script>
<template>
<EmployeeItem
:id="1"
name="홍길동"
@remove="remove"
/>
</template>
↓
부모의 remove(id)
↓
id = 1
<script setup>
import { ref } from 'vue'
import EmployeeList from './components/EmployeeList.vue'
const employees = ref([
{
id: 1,
name: '홍길동',
department: '개발팀'
},
{
id: 2,
name: '김철수',
department: '영업팀'
},
{
id: 3,
name: '이영희',
department: '관리팀'
}
])
function removeEmployee(id) {
employees.value =
employees.value.filter(
emp => emp.id !== id
)
}
</script>
<template>
<h1>사원관리 프로그램</h1>
<EmployeeList
:employees="employees"
@remove="removeEmployee"
/>
</template>
employees 데이터를 가지고 있으며, 사원 삭제가 필요할 때 실제 배열을 변경하는 역할을 담당합니다.
EmployeeList.vue는 부모가 전달한 employees를 props로 받아 화면에 목록을 출력합니다.
<script setup>
const props = defineProps({
employees: Array
})
const emit = defineEmits([
'remove'
])
function removeEmployee(id) {
emit('remove', id)
}
</script>
<template>
<h2>사원목록</h2>
<p>
전체 사원 :
{{ props.employees.length }}명
</p>
<table border="1">
<thead>
<tr>
<th>번호</th>
<th>이름</th>
<th>부서</th>
<th>관리</th>
</tr>
</thead>
<tbody>
<tr
v-for="emp in props.employees"
:key="emp.id"
>
<td>{{ emp.id }}</td>
<td>{{ emp.name }}</td>
<td>{{ emp.department }}</td>
<td>
<button
@click="removeEmployee(emp.id)"
>
삭제
</button>
</td>
</tr>
</tbody>
</table>
</template>
│
│ employees 전달
↓
EmployeeList.vue
│
│ props
↓
사원목록 출력
│
│ 삭제 버튼 클릭
↓
emit('remove', id)
│
↓
App.vue
│
↓
employees에서 삭제
│
↓
화면 자동 갱신
데이터는 부모가 관리하고, 자식은 받은 데이터를 출력하면서 필요한 이벤트를 부모에게 전달하는 구조입니다.
이번에는 사원 등록 화면도 별도의 컴포넌트로 분리합니다.
<script setup>
import { ref } from 'vue'
const name = ref('')
const department = ref('개발팀')
const emit = defineEmits([
'add'
])
function addEmployee() {
if (name.value === '') {
alert('이름을 입력하세요.')
return
}
emit('add', {
name: name.value,
department: department.value
})
name.value = ''
department.value = '개발팀'
}
</script>
<template>
<h2>사원 등록</h2>
<p>
이름 :
<input
v-model="name"
>
</p>
<p>
부서 :
<select
v-model="department"
>
<option>개발팀</option>
<option>영업팀</option>
<option>관리팀</option>
</select>
</p>
<button @click="addEmployee">
등록
</button>
</template>
↓
name / department
↓
등록 버튼
↓
emit('add', 사원정보)
↓
부모 컴포넌트
EmployeeForm.vue가 부모의 employees 배열을 직접 수정하는 것이 아니라, 새로 입력한 사원 정보를 add 이벤트와 함께 부모에게 전달합니다.
이제 App.vue에서 등록과 삭제 기능을 모두 처리합니다.
<script setup>
import { ref } from 'vue'
import EmployeeForm from './components/EmployeeForm.vue'
import EmployeeList from './components/EmployeeList.vue'
const employees = ref([
{
id: 1,
name: '홍길동',
department: '개발팀'
},
{
id: 2,
name: '김철수',
department: '영업팀'
}
])
let nextId = 3
function addEmployee(employee) {
employees.value.push({
id: nextId++,
name: employee.name,
department: employee.department
})
}
function removeEmployee(id) {
employees.value =
employees.value.filter(
emp => emp.id !== id
)
}
</script>
<template>
<h1>사원관리 프로그램</h1>
<EmployeeForm
@add="addEmployee"
/>
<hr>
<EmployeeList
:employees="employees"
@remove="removeEmployee"
/>
</template>
이제 사원관리 프로그램은 각 기능별로 역할이 분리되었습니다.
│
├─ employees 데이터 관리
│
├───────────────┐
EmployeeForm EmployeeList
│ │
사원 등록 사원 목록
│ │
emit(add) emit(remove)
│ │
└───────┬───────┘
↓
App.vue
↓
employees 변경
↓
화면 자동 갱신
- App.vue — 사원 배열과 등록·삭제 처리
- EmployeeForm.vue — 사원 입력과 add 이벤트 발생
- EmployeeList.vue — 사원 목록 출력과 remove 이벤트 발생
컴포넌트를 사용하지 않으면 하나의 App.vue 안에 다음 기능이 모두 들어갈 수 있습니다.
등록 입력창
등록 함수
목록 출력
삭제 함수
수정 함수
검색 기능
상세정보
메뉴
페이징
...
프로그램이 작을 때는 괜찮지만, 기능이 늘어날수록 코드의 위치를 찾고 수정하는 일이 어려워집니다.
컴포넌트를 사용하면 역할에 따라 다음처럼 분리할 수 있습니다.
화면과 기능을 역할별 컴포넌트로 나누면 프로그램이 커져도 구조를 비교적 명확하게 유지할 수 있습니다.
컴포넌트 파일명은 이름만 보아도 어떤 역할을 담당하는지 알 수 있도록 작성하는 것이 좋습니다.
| 기능 | 컴포넌트 이름 예 |
|---|---|
| 사원 | EmployeeForm.vue / EmployeeList.vue / EmployeeDetail.vue |
| 회원 | MemberForm.vue / MemberList.vue |
| 상품 | ProductForm.vue / ProductList.vue |
| 게시판 | BoardList.vue / BoardWrite.vue / BoardDetail.vue |
Vue 컴포넌트 파일명은 보통 각 단어의 첫 글자를 대문자로 작성하는 PascalCase 방식을 많이 사용합니다.
EmployeeList.vue ProductDetail.vue BoardWrite.vue
파일 이름만 보고도 어떤 기능인지 파악할 수 있도록 대상 + 역할 형태로 작성하면 관리하기 편리합니다.
- 컴포넌트는 화면과 기능을 작은 단위로 나누어 관리하는 Vue의 중요한 구조입니다.
- 컴포넌트를 사용하면 코드를 기능별로 분리하고 같은 기능을 여러 곳에서 재사용할 수 있습니다.
- 컴포넌트 파일은 보통 src/components 폴더에 만들고 App.vue 등에서 import하여 사용합니다.
- 같은 컴포넌트를 여러 번 사용해도 각 컴포넌트의 내부 데이터는 독립적으로 관리할 수 있습니다.
- 다른 컴포넌트를 사용하는 쪽을 부모 컴포넌트, 사용되는 쪽을 자식 컴포넌트라고 합니다.
- props는 부모 컴포넌트의 데이터를 자식 컴포넌트로 전달할 때 사용합니다.
- props의 기본 데이터 흐름은 부모 → 자식입니다.
- defineProps()를 이용하면 자식 컴포넌트가 부모에게 받은 값을 사용할 수 있습니다.
- emit은 자식 컴포넌트에서 발생한 일을 부모 컴포넌트에게 전달할 때 사용합니다.
- emit의 흐름은 기본적으로 자식 → 부모라고 생각하면 이해하기 쉽습니다.
- emit을 사용할 때 이벤트뿐 아니라 사원 번호와 같은 데이터도 함께 전달할 수 있습니다.
- 실제 프로그램에서는 부모가 주요 데이터를 관리하고, 자식 컴포넌트들이 화면과 입력 기능을 분담하는 구조를 사용할 수 있습니다.
- 컴포넌트 이름은 역할을 쉽게 알 수 있도록 작성하며 일반적으로 PascalCase 형식을 많이 사용합니다.