ksundev 님의 블로그

[TODO App] Modal 기능 구현 - Slot과 내장 Transition 활용 본문

[개발] Vue.js/중급

[TODO App] Modal 기능 구현 - Slot과 내장 Transition 활용

ksundev 2025. 7. 9. 17:31

이번 포스트에서는 Vue.js에서 Modal 기능을 구현하는 방법을 알아보겠습니다. 특히 Vue의 강력한 Slot 시스템과 내장 Transition 컴포넌트를 활용한 Modal 구현에 대해 중점적으로 다뤄보겠습니다.

1. 현재 상황과 해결해야 할 문제

컴포넌트 구조 개선: UI와 비즈니스 로직 분리

이전 포스트들과 마찬가지로, 이번에도 컴포넌트의 책임을 명확히 분리했습니다:

  • TodoInput 컴포넌트: UI 구현만 담당 (프레젠테이션 컴포넌트)
  • App 컴포넌트: 비즈니스 로직과 데이터 관리 담당 (컨테이너 컴포넌트)
  • Modal 컴포넌트: 재사용 가능한 UI 컴포넌트

해결해야 할 문제

  • 빈 입력 시 사용자에게 알림 표시
  • 재사용 가능한 Modal 컴포넌트 구현
  • Vue의 Slot 시스템을 활용한 유연한 Modal 설계

2. Vue의 내장 기능들

Transition 컴포넌트

Vue는 애니메이션을 위한 내장 Transition 컴포넌트를 제공합니다:

<template>
  <Transition name="modal">
    <div v-if="show" class="modal-mask">
      <!-- Modal 내용 -->
    </div>
  </Transition>
</template>

<style>
.modal-enter-from {
  opacity: 0;
}
.modal-leave-to {
  opacity: 0;
}
.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
  transform: scale(1.1);
}
</style>

이벤트 시스템

Vue의 강력한 이벤트 시스템으로 컴포넌트 간 통신이 간단합니다:

<!-- 자식 컴포넌트에서 이벤트 발생 -->
<button @click="$emit('close')">확인</button>

<!-- 부모 컴포넌트에서 이벤트 수신 -->
<Modal @close="closeModal" />

3. Slot 시스템의 강력함

Slot이란?

Slot은 Vue의 핵심 기능 중 하나로, 컴포넌트의 내용을 동적으로 전달할 수 있게 해줍니다.

기본 Slot vs Named Slots

<!-- 기본 Slot -->
<template>
  <div>
    <slot>기본 내용</slot>
  </div>
</template>

<!-- Named Slots -->
<template>
  <div>
    <header>
      <slot name="header">기본 헤더</slot>
    </header>
    <main>
      <slot name="body">기본 내용</slot>
    </main>
    <footer>
      <slot name="footer">기본 푸터</slot>
    </footer>
  </div>
</template>

Modal에서 Slot 활용

<!-- Modal.vue -->
<template>
  <Transition name="modal">
    <div v-if="show" class="modal-mask" @click="$emit('close')">
      <div class="modal-container" @click.stop>
        <div class="modal-header">
          <slot name="header">default header</slot>
        </div>
        <div class="modal-body">
          <slot name="body">default body</slot>
        </div>
        <div class="modal-footer">
          <slot name="footer">
            <button class="modal-default-button" @click="$emit('close')">
              OK
            </button>
          </slot>
        </div>
      </div>
    </div>
  </Transition>
</template>

Slot 사용의 장점

  1. 재사용성: 하나의 Modal 컴포넌트로 다양한 용도 사용 가능
  2. 유연성: 부모 컴포넌트에서 내용을 자유롭게 커스터마이징
  3. 일관성: 모든 Modal이 동일한 스타일과 동작을 가짐

4. TodoInput 컴포넌트 구현

TodoInput.vue 구조

<template>
  <div class="inputBox shadow">
    <input
      type="text"
      placeholder="할 일을 입력하세요"
      v-model="newTodo"
      @keyup.enter="addTodo"
    />
    <span class="addContainer" @click="addTodo">
      <i class="fa-solid fa-plus addBtn"></i>
    </span>
  </div>
</template>

<script>
export default {
  data() {
    return {
      newTodo: "",
    };
  },
  methods: {
    addTodo() {
      if (this.newTodo !== "") {
        this.$emit("addTodo", this.newTodo);
        this.clearInput();
      } else {
        this.$emit("showModal");
      }
    },
    clearInput() {
      this.newTodo = "";
    },
  },
};
</script>

프레젠테이션 컴포넌트로서의 역할

TodoInput은 다음과 같은 UI 관련 책임만 가집니다:

  1. 입력 필드 렌더링: 할 일 입력을 위한 텍스트 필드
  2. 이벤트 처리: Enter 키와 클릭 이벤트 처리
  3. 유효성 검사: 빈 입력 시 Modal 표시 이벤트 발생

5. App 컴포넌트에서 Modal 관리

App.vue에서 Modal 구현

<template>
  <div id="app">
    <TodoHeader />
    <TodoInput @addTodo="addOneItem" @showModal="handleShowModal" />
    <TodoList
      :todoItems="todoItems"
      @removeItem="removeOneItem"
      @toggleComplete="toggleOneItem"
    />
    <TodoFooter @clearTodo="clearAllItems" />
    <Modal :show="showModal" @close="closeModal">
      <template #header>
        <h3>알림</h3>
      </template>
      <template #body>
        <p>아무것도 입력하지 않았습니다.</p>
      </template>
    </Modal>
  </div>
</template>

컨테이너 컴포넌트로서의 역할

App 컴포넌트는 다음과 같은 비즈니스 로직을 담당합니다:

  1. Modal 상태 관리: showModal 상태로 Modal 표시/숨김 제어
  2. 이벤트 처리: 자식 컴포넌트로부터 받은 이벤트 처리
  3. Slot 내용 정의: Modal의 헤더와 바디 내용 정의

6. 이벤트 기반 통신 패턴

부모-자식 컴포넌트 통신 원리

자식 컴포넌트 (TodoInput) - UI 담당
    ↓ $emit("showModal")
부모 컴포넌트 (App) - 비즈니스 로직 담당
    ↓ @showModal="handleShowModal"
Modal 표시 및 상태 관리

Props Down, Events Up 패턴

// Props Down: 부모에서 자식으로 데이터 전달
<Modal :show="showModal" />

// Events Up: 자식에서 부모로 이벤트 발생
<TodoInput @showModal="handleShowModal" />

7. 실제 동작 과정

Modal 표시 플로우

1. 사용자 액션: 빈 입력으로 Enter 또는 클릭
   ↓
2. 유효성 검사: TodoInput에서 빈 입력 확인
   ↓
3. 이벤트 발생: showModal 이벤트 발생
   ↓
4. 상태 변경: App에서 showModal = true
   ↓
5. Modal 렌더링: Transition과 함께 Modal 표시
   ↓
6. Slot 내용 표시: 헤더와 바디 내용 렌더링

Modal 닫기 플로우

1. 사용자 액션: OK 버튼 클릭 또는 배경 클릭
   ↓
2. 이벤트 발생: Modal에서 close 이벤트 발생
   ↓
3. 상태 변경: App에서 showModal = false
   ↓
4. Modal 숨김: Transition과 함께 Modal 사라짐

8. Vue의 내장 기능 활용

Transition 컴포넌트

<Transition name="modal">
  <div v-if="show" class="modal-mask">
    <!-- Modal 내용 -->
  </div>
</Transition>

이벤트 수식어

<!-- 배경 클릭 시 닫기 -->
<div class="modal-mask" @click="$emit('close')">
  <!-- 내용 클릭 시 이벤트 전파 방지 -->
  <div class="modal-container" @click.stop>
    <!-- Modal 내용 -->
  </div>
</div>

9. Vue의 장점

내장 기능의 편리함

  1. Transition 컴포넌트: 애니메이션을 위한 전용 컴포넌트 제공
  2. 자동 클래스 관리: Vue가 enter/leave 클래스를 자동으로 적용
  3. 간단한 설정: name 속성만으로 애니메이션 설정
  4. 성능 최적화: Vue 엔진에서 최적화된 애니메이션 처리

Slot 시스템의 유연성

  1. 재사용성: 하나의 Modal 컴포넌트로 다양한 용도 사용
  2. 커스터마이징: 부모 컴포넌트에서 내용을 자유롭게 변경
  3. 일관성: 모든 Modal이 동일한 스타일과 동작 유지

10. 결론

이번 시간에는 Modal을 간단하지만 확실하게 구현해보았습니다. slot을 통해 재사용성도 높였습니다.
다음 포스트에서는 transition-group을 활용하여 리스트 쪽에서 부드러운 애니메이션을 구현해보겠습니다.