![[웹][Vue.js] Vue3에서 컴포넌트 간 데이터 전송 방법](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FqxiGE%2FbtsO2j25Ylc%2FAAAAAAAAAAAAAAAAAAAAAErUNS2OyL726II9W_aVKpw0Ok3xIIKyuddwXYnSD1GM%2Fimg.gif%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1761922799%26allow_ip%3D%26allow_referer%3D%26signature%3DNIrhKioUws5qpm9dHO%252BP98XGq6o%253D)
이 글은 Vue3에서 컴포넌트 간 데이터를 전송하는 방법(props & emit | mitt | Vuex | pinia)에 대해 정리한 글입니다.
📖 props와 emit
📌 props
- 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 방법 (부모 ➡️ 자식)
- 자식 컴포넌트는 props로 전달받은 데이터를 읽기 전용(read-only)으로만 사용 가능
- 단방향 데이터 흐름(one-way data flow)을 유지하는 것이 Vue의 기본 철학 중 하나
⚡ props 구조 예시
<!-- 부모 컴포넌트 -->
<template>
<ChildComponent :message="parentMsg" />
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const parentMsg = ref('안녕하세요, 자식 컴포넌트!')
</script>
<!-- 자식 컴포넌트 (ChildComponent.vue) -->
<template>
<div>{{ message }}</div>
</template>
<script setup>
defineProps({
message: String
})
</script>
- 부모 컴포넌트에서
:message="parentMsg"
로 자식에게 값을 전달 - 자식 컴포넌트에서는
defineProps
를 사용하여 props를 정의하고, 해당 값을 받아와서 사용
⚠️ props의 장점과 단점
- 장점
- 구조가 간단하고 명확하다
- 유지보수가 쉽다
- 단점
- 계층이 깊어질수록(props를 여러 단계로 넘겨야 하는 경우) 코드가 복잡해질 수 있다.
- 형제 컴포넌트 간에는 직접 전달이 불가능하다.
📌 emit
- 자식 컴포넌트에서 부모 컴포넌트로 이벤트와 데이터를 전달하는 방법 (자식 ➡️ 부모)
- 부모 컴포넌트는 자식에서 발생한 이벤트를 감지하여 필요한 작업을 수행
- 실제 동작은 이벤트를 발생시키는 것(event emitting)
⚡emit 구조 예시
<!-- 자식 컴포넌트 -->
<template>
<button @click="sendData">클릭해서 데이터 전송</button>
</template>
<script setup>
const emit = defineEmits(['custom-event'])
function sendData() {
emit('custom-event', '자식에서 보낸 데이터')
}
</script>
<!-- 부모 컴포넌트 -->
<template>
<ChildComponent @custom-event="handleCustomEvent" />
</template>
<script setup>
import ChildComponent from './ChildComponent.vue'
function handleCustomEvent(data) {
console.log('자식 컴포넌트에서 받은 데이터:', data)
}
</script>
- 자식 컴포넌트에서
emit('custom-event', data)
로 이벤트와 데이터를 발생 - 부모 컴포넌트에서는
<ChildCoponents @custom-event="handleCustomEvent" />
로 해당 이벤트를 감지하고 처리
⚠️ emit의 장점과 단점
- 장점
- props와 함께 사용하면 부모와 자식 컴포넌트 간의 양방향 소통이 가능하다.
- 데이터 변경, 사용자 인터렉션 등 다양한 이벤트 핸들링이 유연하다.
- 단점
- 컴포넌트 트리가 깊어지면 관리가 복잡해질 수 있다.
- 형제 컴포넌트 간 데이터 전달은 직접적으로 어렵고, 부모 컴포넌트를 거쳐야만 전달이 가능하다.
📌 props와 emit의 구조적 흐름
- 직접적으로는 부모 - 자식 간 1:1 전송만 지원
- 형제 컴포넌트 간 데이터 전송은 부모 컴포넌트를 거쳐(emit) 다른 자식 컴포넌트로 전달(props)하는 과정이 필요
📖 mitt
- 매우 가볍고 심플한 이벤트 버스(event bus) 라이브러리
- mitt과 같은 외부 이벤트 버스 라이브러리를 도입하여 컴포넌트 간(특히, 형제 또는 멀리 떨어진 컴포넌트 간) 데이터를 주고 받을 수 있음
- TypeScript 지원이 잘 되어 있으며, 설치와 사용이 매우 간단
- 내부적으로 단순히 이벤트 이름과 리스너(콜백 함수)를 매핑해서 관리
- 주로 형제 컴포넌트 간 데이터 교환, 멀리 떨어진 컴포넌트(조상/손자 관계 X) 간 통신, 그리고 글로벌 이벤트 처리(ex. 전역적으로 알림, 브로드캐스트 등)가 필요한 경우 사용
⚙️ mitt 사용 방법
mitt 설치
mitt은 외부 라이브러리이기 때문에 사용 전 설치가 필요하다.
npm install mitt
mitt 인스턴스 생성
프로젝트 전체에서 공용 이벤트 버스로 사용할 인스턴스를 생성해야 한다.
// src/utils/eventBus.js
import mitt from 'mitt'
const emitter = mitt()
export default emitter
이벤트 발생 (emit)
특정 컴포넌트에서 이벤트를 발생(emit) 시킬 수 있다.
// 어떤 컴포넌트에서
import emitter from '@/utils/eventBus'
emitter.emit('이벤트이름', 데이터)
이벤트 수신 (on)
다른 컴포넌트에서 이벤트를 수신(on)할 수 있다.
// 또 다른 컴포넌트에서
import emitter from '@/utils/eventBus'
emitter.on('이벤트이름', (데이터) => {
// 데이터 처리 로직
})
이벤트 해제 (off)
컴포넌트가 인마운트될 때 불필요한 이벤트 리스너를 해제해주어야 메모리 누수를 막을 수 있다.
import { onUnmounted } from 'vue'
import emitter from '@/utils/eventBus'
function handler(데이터) {
// 처리 로직
}
emitter.on('이벤트이름', handler)
onUnmounted(() => {
emitter.off('이벤트이름', handler)
})
⚡mitt 사용 예시
- A 컴포넌트에서 버튼을 클릭 ➡️ B 컴포넌트에 값을 전달하는 사용 예시
// A.vue
import emitter from '@/utils/eventBus'
function selectO() {
emitter.emit('select-value', 'O')
}
// B.vue
import { ref, onMounted, onUnmounted } from 'vue'
import emitter from '@/utils/eventBus'
const value = ref('')
function handler(data) {
value.value = data
}
onMounted(() => {
emitter.on('select-value', handler)
})
onUnmounted(() => {
emitter.off('select-value', handler)
})
⚠️ mitt의 장점과 단점
➡️ Vue3에서 간단하게 컴포넌트 간 데이터 전달이 필요한 상황에 적합한 라이브러리이다. 글로벌 이벤트 버스로서 빠르고 쉽게 도입할 수 있지만, 데이터의 상태를 보존하거나 복잡한 데이터 흐름을 관리해야 하는 상황에서는 적합하지 않다.
- 장점
- 매우 가볍고 심플하다. (의존성 최소)
- Vue와 강하게 결합되어 있지 않아 다른 프레임워크에서도 사용이 가능하다.
- 전역/범위 제한 없이 이벤트 통신이 가능하다.
- 단점
- 상태 추적/관리 기능이 없다. (단순 이벤트 송수신만 가능)
- 대규모 프로젝트에서는 이벤트 관리가 복잡해질 수 있다.
- 이벤트 명의 충돌 위험이 있어서 네이밍 관리가 중요하다.
📖 Vuex
➡️ 애플리케이션 규모가 커질수록 여러 컴포넌트에서 동일한 데이터를 공유하거나 복잡한 데이터 흐름을 제어해야 하는 경우가 많다. 이런 경우, 상태를 한 곳에 모아서 관리하고, 컴포넌트들이 필요한 데이터를 공유, 변경, 감시할 수 있도록 하면 유지보수와 확장성 측면에서 매우 유리하다.
- Vue.js 애플리케이션을 위한 공식 상태 관리 라이브러리 (Vue3에서는 Pinia가 공식 권장으로 바뀜)
- Vuex는 많은 프로젝트에서 사용되고 있으며 개념적으로 중요한 위치를 차지하고 있음
- 애플리케이션의 상태(state)를 중앙 집중식으로 관리하여, 컴포넌트 간 데이터 전달을 명확하고 예측 가능하게 만드는 것이 목적
⚙️ Vuex의 구조와 개념
➡️ Vuex는 State, Getters, Mutations, Actions 4가지 주요 개념으로 구성된다.
- State
- 실제로 저장되는 데이터 (중앙 저장소)
- 예시: 사용자 정보, 장바구니 목록 등
- Getters
- State를 기반으로 계산된 값(derived state)을 반환
- 예시: 로그인 여부, 상품 종합 등
- Mutations
- State를 실제 변경하는 동기 함수
- 직접 State를 수정하는 유일한 방법
- Actions
- 비동기 로직(ajax, setTimeout 등) 또는 여러 mutation을 한 번에 실행할 때 사용
- Action 내부에서 mutation을 commit해서 state를 변경
⚡ Vuex 사용 예시
📌 Vuex 구조 예시
// store/index.js
import { createStore } from 'vuex'
const store = createStore({
state() {
return {
count: 0,
message: ''
}
},
getters: {
doubleCount(state) {
return state.count * 2
}
},
mutations: {
increment(state) {
state.count++
},
setMessage(state, payload) {
state.message = payload
}
},
actions: {
asyncIncrement({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
})
export default store
📌 컴포넌트에서 데이터 읽기/변경 사용 예시
// main.js (store 등록)
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
createApp(App).use(store).mount('#app')
<!-- A.vue -->
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">+1</button>
<button @click="asyncIncrement">1초 후 +1</button>
</div>
</template>
<script setup>
import { useStore } from 'vuex'
import { computed } from 'vue'
const store = useStore()
const count = computed(() => store.state.count)
const increment = () => store.commit('increment')
const asyncIncrement = () => store.dispatch('asyncIncrement')
</script>
📌 Vuex 데이터 흐름
- 컴포넌트에서 Action 호출 (비동기 / 로직) ➡️ Action에서 Mutation 호출
- Mutation이 State를 직접 변경
- 컴포넌트는 State를 구독(reactive)하여 값이 자동으로 갱신
⚠️ Vuex의 장점과 단점
- 장점
- 중앙 집중식 관리로 복잡한 상태를 일관되게 유지 가능
- Vue의 반응형 시스템과 자연스럽게 통합
- 공식 플러그인으로 Vue 생태계와 호환성이 뛰어남
- 단점
- 구현이 다소 복잡할 수 있음 (작은 프로젝트에서는 오버엔지니어링)
- 보일러플레이트(반복 코드)가 많음 ➡️ Vue3에서 Pinia로 넘어가는 원인
➡️ 실제로 Vuex는 대규모 SPA, 여러 컴포넌트가 동일 상태를 공유/수정해야 하는 경우, 여러 계층의 컴포넌트에서 중복되는 데이터 전달이 필요한 경우, 인증, 사용자 세션, 글로벌 알림 등 글로벌 상태 관리가 필요한 경우 사용한다.
➡️ Vue3 기준으로 Pinia가 공식 상태관리 라이브러리로 자리잡으며, Vuex의 단점을 개선하여 더 간단하고 직관적으로 사용할 수 있게 되었다. Pinia도 Vuex와 마찬가지로 중앙 저장소 관리라는 기본 철학과 개념은 동일하다.
🍍 Pinia
➡️ Vuex와 마찬가지로 대규모 프로젝트에서 여러 컴포넌트가 상태를 공유해야 하는 경우, 글로벌 상태(인증, 알림, 환경설정 등) 관리가 필요한 경우, 그리고 복잡한 데이터 플로우가 필요한 경우 사용한다.
- Vue3를 위한 공식 상태 관리 라이브러리 (2022년부터 Vuex의 후속 라이브러리로 Pinia를 공식 채택)
- Vuex보다 사용법이 간단하며, 코드가 훨씬 짧아짐
- 중복 코드(보일러플레이트)가 거의 없음
- 타입스크립트 지원이 강력함
- Vuex와 같은 중앙 집중식 상태 관리를 제공하지만, 더 간단하고 가벼우며 직관적인 API를 제공
- 모듈 기반(store별 분할 관리), 자동 완성, 반응형(reactive), SSR 지원 등 다양한 현대적 기능을 제공
⚙️ Pinia의 핵심 구조와 개념
➡️ Pinia의 핵심은 store로, 각 store에는 state(상태), getters(계산된 값), actions(함수/비동기 로직)를 가질 수 있다.
// src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
message: ''
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
asyncIncrement() {
setTimeout(() => {
this.count++
}, 1000)
},
setMessage(msg) {
this.message = msg
}
}
})
🛠️ Pinia 설치 및 기본 세팅
Pinia 설치
npm install pinia
main.js
(or main.ts
)에 등록
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
store 생성
// src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
message: ''
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
asyncIncrement() {
setTimeout(() => {
this.count++
}, 1000)
},
setMessage(msg) {
this.message = msg
}
}
})
컴포넌트에서 사용
<!-- CounterComponent.vue -->
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">+1</button>
<button @click="asyncIncrement">1초 후 +1</button>
<p>{{ doubleCount }}</p>
<input v-model="message" placeholder="메시지 입력" />
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
const count = computed(() => counter.count)
const doubleCount = computed(() => counter.doubleCount)
const message = computed({
get: () => counter.message,
set: (val) => counter.setMessage(val)
})
const increment = counter.increment
const asyncIncrement = counter.asyncIncrement
</script>
📌 Pinia의 데이터 흐름
- state : 중앙 저장소의 데이터, 반응형으로 관리
- getters : state를 기반으로 계산된 값 (캐싱 / 의존성 자동 추적)
- actions : 동기/비동기 로직 및 state 변경 (actions 내에서 this를 통해 state, getters 모두 사용 가능)
⚠️ Pinia의 장점과 단점
- 장점
- 쉽고 빠른 사용법
- 강력한 타입 지원
- 비동기 로직(actions)과 동기 로즉(state 변경)이 자연스럽게 통합
- Composition API와 잘 어울림
- 단점
- 지나치게 작은 프로젝트에서는 굳이 필요하지 않을 수 있음
'🖥️ Dev > WEB' 카테고리의 다른 글
[WEB][DB] MySQL DBMS 설치 (0) | 2025.03.13 |
---|---|
[WEB] 웹 서버(Web Server)와 REST API 개념 정리 (0) | 2025.03.07 |
[WEB][Node.js] Node.js 설치 및 NVM을 이용한 버전 관리 (feat. nvm-windows) (0) | 2025.03.06 |
[WEB][JS] JavaScript 기초 - DOM 제어 (0) | 2025.02.10 |
[WEB][JS] JavaScript 기초 - JavaScript 기본 문법 (0) | 2025.02.09 |
since 2025.01.27. ~ 개발자를 향해....🔥