[Vue] Props/Emit/Slot/Provide/Inject

배창민·2025년 11월 20일
post-thumbnail

Vue 3 개발 환경 정리

Vite + SFC + 컴포넌트 패턴 한 번에 훑기


1. Vite로 Vue 3 프로젝트 시작하기

1-1. Vite란

Vite는 Vue 3 공식 권장 개발 환경이다.

  • 개발 서버 속도가 매우 빠른 번들러 겸 빌드 도구

  • 개발 시:

    • 변경된 파일만 ESBuild로 바로 변환
    • 브라우저의 ES 모듈 기능 활용 → 전체 번들 없이 동작
  • 빌드 시:

    • Rollup 기반 번들링 → 최적화된 결과물 생성

Vue CLI 대비

  • 설정이 더 단순
  • Vue 팀이 공식 Vite 템플릿을 제공

1-2. Node 환경

  • Vue 3 + Vite 권장 버전: Node 18 이상

1-3. 프로젝트 생성

npm create vue@latest
  • 학습용이라면 옵션은 대체로 전부 No 로 두고 시작해도 충분

1-4. 개발 서버 실행

cd 프로젝트명
npm install
npm run dev
  • 기본 주소: http://localhost:5173

1-5. 기본 디렉토리 구조

src/
  assets/       # 이미지, CSS
  components/   # 공용 컴포넌트
  App.vue       # 루트 컴포넌트
  main.js       # 앱 엔트리
index.html      # 앱 틀
  • index.html

    • #app 영역에 Vue 앱이 마운트되는 기본 HTML
  • main.js

    • createApp(App).mount('#app') 같은 코드가 위치
  • App.vue

    • 최상위 컴포넌트
  • components/

    • 재사용 가능한 컴포넌트 모음

2. SFC(Single File Component) 구조

Vue에서는 .vue 파일 하나에 템플릿 + 로직 + 스타일을 모아 관리한다.

2-1. 기본 골격

<template>
  <!-- UI -->
</template>

<script setup>
// 로직 (상태, 함수, props, emit 등)
</script>

<style scoped>
/* 스타일 (해당 컴포넌트에만 적용) */
</style>
  • Vue가 내부적으로 이 파일을 하나의 컴포넌트로 컴파일한다
  • <style scoped> 를 사용하면 해당 컴포넌트에만 스타일이 적용된다

2-2. <script setup> 의 장점

Vue 3 Composition API 를 쓰는 가장 권장되는 방식.

장점 정리:

장점설명
코드 간결export default, setup() 선언 필요 없음
자동 importdefineProps, defineEmits 자동 인식
성능 최적화컴파일 단계에서 더 효율적인 코드로 변환
자연스러운 문법Composition API 를 기본 문법처럼 사용 가능

예시:

<script setup>
import { ref } from 'vue';

const count = ref(0);

const increase = () => {
  count.value++;
};
</script>

3. 컴포넌트 설계와 디렉토리 구조

3-1. 네이밍 규칙

  • Vue 컴포넌트 파일 이름은 PascalCase 권장

    • MyButton.vue, UserCard.vue
  • HTML 기본 태그와 구분하기 위한 컨벤션


3-2. 설계 기본 원칙

  • 화면에 반복적으로 쓰이는 UI 조각은 components/로 분리
  • 하나의 컴포넌트는 가능하면 한 가지 역할만 담당
  • 데이터 흐름은 기본적으로 단방향이 원칙
부모 → (props) → 자식
자식 → (emit)  → 부모

이 흐름을 지키면 구조가 단순해지고 유지보수가 쉬워진다.


4. Props – 부모 → 자식 데이터 전달

4-1. Props란

  • 부모가 자식에게 값을 전달하는 공식적인 통로

  • 특징

    • 단방향 바인딩 (부모 → 자식)
    • 자식은 props 를 직접 수정하지 않는다 (필요하면 emit 으로 부모에 알림)

4-2. Props 정의 방법

<script setup>
const props = defineProps({
  message: String,
  count: Number
});
</script>

<template>
  <div>
    <p>{{ message }}</p>
    <p>{{ count }}</p>
  </div>
</template>
  • 타입 지정, 기본값, required 여부도 설정 가능

예시(옵션 확장):

const props = defineProps({
  message: {
    type: String,
    default: '기본 메시지'
  },
  count: {
    type: Number,
    required: true
  }
});

5. Emit – 자식 → 부모 이벤트 전달

5-1. Emit이 필요한 이유

  • 자식에서 버튼 클릭, 입력 완료 등 이벤트가 발생했을 때
    부모가 그 결과를 처리해야 하는 경우가 많다
  • 이때 자식이 부모의 함수를 직접 호출하는 대신 이벤트를 쏘는 방식을 사용

5-2. Emit 정의

<script setup>
const emit = defineEmits(['add']);

const onClick = () => {
  emit('add', 10); // 'add' 이벤트 + payload 전달
};
</script>

<template>
  <button @click="onClick">추가</button>
</template>

부모 쪽 사용 예:

<MyButton @add="handleAdd" />
const handleAdd = value => {
  console.log('자식이 넘긴 값:', value);
};

5-3. 데이터 흐름 요약

부모 → (props) → 자식  : 값 전달
자식 → (emit)  → 부모  : 이벤트 전달

이 패턴을 지키면 데이터 방향이 명확해지고 버그를 줄일 수 있다.


6. 커스텀 v-model – 안전한 양방향 바인딩

컴포넌트에서도 v-model 을 지원하게 만들 수 있다.

6-1. 동작 원리

  • 자식 컴포넌트

    • modelValue라는 props로 현재 값 받기
    • 값이 바뀔 때 update:modelValue 이벤트 emit
  • 부모 컴포넌트

    • <MyInput v-model="username" /> 처럼 사용

6-2. 자식 컴포넌트 구현 예시

<script setup>
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);

const onInput = event => {
  emit('update:modelValue', event.target.value);
};
</script>

<template>
  <input :value="props.modelValue" @input="onInput" />
</template>

부모에서는 그냥:

<MyInput v-model="username" />

이렇게 쓰면 된다.


7. Slot – 부모가 자식 내부 내용을 채우는 방법

슬롯은 부모가 자식 컴포넌트 안의 특정 영역을 채우는 기능이다.

7-1. 기본 Slot

자식:

<template>
  <div class="card">
    <slot></slot>
  </div>
</template>

부모:

<MyCard>
  <p>이 내용이 slot 안으로 들어간다</p>
</MyCard>

7-2. Named Slot

자식:

<template>
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</template>

부모:

<MyLayout>
  <template #header>
    <h1>헤더</h1>
  </template>

  <p>본문 내용</p>

  <template #footer>
    <p>푸터</p>
  </template>
</MyLayout>

7-3. Slot 활용 패턴

  • Modal 컴포넌트

    • header, body, footer 를 slot 으로 공개
  • Layout 컴포넌트

    • left, right 영역을 slot 으로 제공해서 다양한 레이아웃 구성

8. Scoped Slot – 자식 데이터로 부모 UI 그리기

Slot 은 기본적으로 “부모 → 자식 콘텐츠 전달” 구조지만,
Scoped Slot 을 사용하면 자식 데이터를 부모에게도 넘길 수 있다.

8-1. 자식: slot 에 props 바인딩

<template>
  <div>
    <slot :user="user"></slot>
  </div>
</template>

<script setup>
const user = {
  name: '홍길동',
  age: 20
};
</script>

8-2. 부모: slot props 구조 분해

<UserCard>
  <template #default="{ user }">
    <p>{{ user.name }} ({{ user.age }})</p>
  </template>
</UserCard>
  • 자식이 가진 데이터를 기반으로 부모가 다양한 UI를 만들 수 있다
  • 리스트 렌더링, 테이블 레이아웃 커스터마이징 등에서 자주 쓰이는 패턴

9. Provide / Inject – 깊은 트리에서 공통 상태 공유

Props는 항상 한 단계씩 내려 보내야 한다.
Provide/Inject 는 중간 단계를 건너뛰고 필요한 자식에게 바로 데이터를 내려준다.

9-1. Provide – 상위에서 값 제공

import { provide, ref } from 'vue';

const theme = ref('dark');
provide('theme', theme);

보통 루트 컴포넌트나 상위 레이아웃에서 제공한다.


9-2. Inject – 하위에서 값 사용

import { inject } from 'vue';

const theme = inject('theme');

어디서든 같은 키로 inject 하면 같은 ref/값을 공유한다.


9-3. 사용 예

  • 다국어(i18n) 설정
  • 다크 모드 / 라이트 모드 같은 테마 상태
  • Form → FormItem 구조에서 form 상태 공유
  • 깊게 중첩된 컴포넌트들 간의 공통 설정 공유

단, 남용하면 구조 파악이 어려워지므로

  • 전역 상태 관리: Pinia 같은 스토어 사용
  • 트리 내부 공유: Provide/Inject 사용

이 정도로 역할을 나누는 편이 좋다.


마무리

이 글에서 정리한 것

  • Vite 로 Vue 3 프로젝트 생성
  • SFC 구조 (<template>, <script setup>, <style scoped>)
  • 컴포넌트 설계 원칙과 네이밍
  • Props / Emit 으로 단방향 데이터 흐름 유지
  • 컴포넌트 커스텀 v-model 구현 패턴
  • Slot / Named Slot / Scoped Slot 으로 유연한 컴포넌트 설계
  • Provide / Inject 로 깊은 단계 상태 공유

여기까지 이해하면, Vite + SFC 기반 Vue 3 프로젝트에서
컴포넌트 구조를 어떻게 나누고 데이터를 어떻게 주고받을지 큰 그림을 잡을 수 있다.

profile
개발자 희망자

0개의 댓글