Post

8장 컴포넌트 심화

8장 컴포넌트 심화

8장 컴포넌트 심화

8.1 단일 파일 컴포넌트에서의 스타일

8.1.1 범위 CSS

1
2
3
4
5
6
7
<!-- MyComponent.vue -->
<template>
  <p class="text">Hello</p>
</template>
<style scoped>
.text { color: blue; }
</style>

scoped 속성은 CSS를 해당 컴포넌트에만 적용되도록 제한합니다. 공통으로 적용할 것은 main.css 파일에 적용하고 아니면 scope 내에 적용하는 것이 낫다고

8.1.2 CSS 모듈

1
2
3
4
5
6
7
<!-- MyComponent.vue -->
<template>
  <p :class="$style.text">Hello</p>
</template>
<style module>
.text { color: red; }
</style>

CSS 모듈은 스타일을 모듈화해 클래스 이름 충돌을 방지합니다.

8.2 슬롯

8.2.1 슬롯 사용 전의 컴포넌트

1
2
3
4
<!-- StaticComponent.vue -->
<template>
  <div>고정된 내용</div>
</template>

슬롯 없이 컴포넌트는 고정된 내용만 표시합니다.

8.2.2 슬롯의 기본 사용법

1
2
3
4
5
6
<!-- SlotComponent.vue -->
<template>
  <div><slot></slot></div>
</template>
<!-- 사용 -->
<SlotComponent>Hello World!</SlotComponent>

슬롯은 부모에서 자식 컴포넌트로 콘텐츠를 주입합니다.

8.2.3 명명된 슬롯

1
2
3
4
5
6
<!-- NamedSlot.vue -->
<template>
  <div><slot name="header"></slot></div>
</template>
<!-- 사용 -->
<NamedSlot><template #header>Header</template></NamedSlot>

명명된 슬롯은 특정 위치에 콘텐츠를 삽입할 수 있게 합니다.

8.2.4 범위 슬롯

1
2
3
4
5
6
7
8
9
10
11
<!-- ScopedSlot.vue -->
<template>
  <slot :data="item"></slot>
</template>
<script>
export default {
  data: () => ({ item: 'Scoped' })
}
</script>
<!-- 사용 -->
<ScopedSlot v-slot="{ data }"></ScopedSlot>

범위 슬롯은 자식 데이터를 부모에서 활용할 수 있게 합니다.

8.3 동적 컴포넌트

1
2
3
4
5
6
7
8
9
10
<!-- App.vue -->
<template>
  <component :is="currentComponent"></component>
</template>
<script>
import CompA from './CompA.vue'
export default {
  data: () => ({ currentComponent: CompA })
}
</script>

동적 컴포넌트는 :is로 런타임에 컴포넌트를 전환합니다. 바인딩된 값에 따라 어떤 컴포넌트를 렌더링 할지 결정 이에 대한 설명 –> Vue.js에서 동적 컴포넌트(dynamic components)는 런타임에 표시할 컴포넌트를 동적으로 전환할 수 있게 해주는 기능입니다. 이를 통해 특정 조건이나 사용자 입력에 따라 서로 다른 컴포넌트를 렌더링할 수 있습니다. Vue에서는 <component>라는 특별한 요소와 :is 속성을 사용해 동적 컴포넌트를 구현합니다.

동적 컴포넌트의 기본 개념

<component> 태그는 :is 속성에 바인딩된 값에 따라 어떤 컴포넌트를 렌더링할지 결정합니다. :is에 지정된 값은 컴포넌트 이름(문자열), 컴포넌트 정의 객체, 또는 등록된 컴포넌트일 수 있습니다.

예제

다음은 동적 컴포넌트의 간단한 사용 예제입니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
  <div>
    <button @click="currentComponent = 'ComponentA'">컴포넌트 A</button>
    <button @click="currentComponent = 'ComponentB'">컴포넌트 B</button>
    
    <!-- 동적 컴포넌트 렌더링 -->
    <component :is="currentComponent"></component>
  </div>
</template>

<script>
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';

export default {
  data() {
    return {
      currentComponent: 'ComponentA' // 기본값
    };
  },
  components: {
    ComponentA,
    ComponentB
  }
};
</script>

위 코드에서:

  • currentComponent는 현재 렌더링할 컴포넌트를 나타내는 데이터 속성입니다.
  • 버튼 클릭 시 currentComponent 값이 변경되며, <component :is="currentComponent">가 해당 컴포넌트를 렌더링합니다. –> 이 방식으로 버튼으로 뷰를 트랜지션 가능할 것 같음. 이거 통해서 나는 메인 뷰 세컨드 페이지로 넘어갈 생각임.

동작 원리

  1. :is에 바인딩된 값이 변경되면 Vue는 해당 컴포넌트를 동적으로 로드하고 DOM에 반영합니다.
  2. 컴포넌트는 등록된 이름(예: 'ComponentA') 또는 컴포넌트 정의 자체를 참조할 수 있습니다.

주의사항

  • 컴포넌트 등록: :is에 사용되는 컴포넌트는 사전에 components 옵션에 등록되어 있어야 합니다.
  • 성능 최적화: 컴포넌트가 자주 전환될 경우, 불필요한 재렌더링을 방지하기 위해 <keep-alive>를 사용할 수 있습니다.

여기서 상태유지가 뭐냐

Vue에서 “상태 유지”라는 개념은 주로 컴포넌트가 DOM에서 제거되거나 교체될 때, 그 컴포넌트가 가지고 있던 데이터나 UI 상태(예: 입력값, 스크롤 위치 등)를 잃지 않고 보존하는 것을 의미합니다. 특히 동적 컴포넌트와 관련해서는 <keep-alive>를 사용할 때 자주 언급됩니다. 좀 더 자세히 설명해볼게요.

일반적인 컴포넌트 전환의 문제

동적 컴포넌트를 사용할 때 <component :is="...">로 컴포넌트를 전환하면, 기존 컴포넌트는 DOM에서 제거되고 새 컴포넌트가 새로 생성됩니다. 이 과정에서:

  • 기존 컴포넌트의 데이터(예: data 속성에 정의된 값)가 초기화됩니다.
  • 사용자가 입력한 값(예: 텍스트 필드의 내용)이나 UI 상태(예: 체크박스 선택 여부)가 사라집니다.

예를 들어, 사용자가 컴포넌트 A에서 텍스트 입력란에 “안녕”이라고 입력한 뒤 컴포넌트 B로 전환하고 다시 A로 돌아오면, 입력했던 “안녕”이 사라지고 초기 상태로 돌아갑니다.

<keep-alive>로 상태 유지하기

<keep-alive>는 이런 문제를 해결하기 위해 Vue에서 제공하는 빌트인 컴포넌트입니다. <keep-alive>로 동적 컴포넌트를 감싸면, 전환되는 컴포넌트가 DOM에서 제거되지 않고 메모리에 캐싱됩니다. 결과적으로:

  • 컴포넌트의 데이터와 상태가 유지됩니다.
  • 사용자가 입력한 값이나 UI 상태가 보존됩니다.

예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
  <div>
    <button @click="currentComponent = 'ComponentA'">컴포넌트 A</button>
    <button @click="currentComponent = 'ComponentB'">컴포넌트 B</button>
    
    <keep-alive>
      <component :is="currentComponent"></component>
    </keep-alive>
  </div>
</template>

<script>
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';

export default {
  data() {
    return {
      currentComponent: 'ComponentA'
    };
  },
  components: {
    ComponentA,
    ComponentB
  }
};
</script>

그리고 ComponentA.vue가 이렇게 생겼다고 가정해봅시다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
  <div>
    <input v-model="text" placeholder="여기에 입력" />
    <p>입력값: </p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      text: ''
    };
  }
};
</script>

위 코드에서:

  1. 사용자가 ComponentA의 입력란에 “안녕”을 입력합니다.
  2. ComponentB로 전환했다가 다시 ComponentA로 돌아와도 입력값 “안녕”이 유지됩니다.
  3. <keep-alive>가 없으면 “안녕”이 사라지고 입력란이 초기화됩니다.

상태 유지의 장점

  • 사용자 경험 향상: 사용자가 입력한 데이터를 잃지 않으므로 더 자연스러운 UI 전환을 제공합니다.
  • 성능 최적화: 컴포넌트를 새로 생성하지 않고 캐싱된 인스턴스를 재사용하므로 렌더링 비용이 줄어듭니다.

추가 설정

<keep-alive>는 몇 가지 속성을 통해 더 세밀하게 제어할 수 있습니다:

  • include: 상태를 유지할 컴포넌트 이름(문자열, 정규식, 배열)을 지정.
  • exclude: 상태 유지에서 제외할 컴포넌트 지정.
  • max: 캐싱할 최대 컴포넌트 수를 제한.

예:

1
2
3
<keep-alive include="ComponentA,ComponentB" exclude="ComponentC">
  <component :is="currentComponent"></component>
</keep-alive>

결론

상태 유지란 컴포넌트가 전환되더라도 그 내부 데이터와 UI 상태를 잃지 않도록 보존하는 것을 뜻합니다. Vue에서는 <keep-alive>를 통해 이를 쉽게 구현할 수 있고, 특히 동적 컴포넌트와 함께 사용할 때 유용합니다. 이해가 더 필요하거나 예제가 더 필요하면 말씀해주세요!

<keep-alive>와 함께 사용

동적 컴포넌트가 전환될 때 상태를 유지하고 싶다면 <keep-alive> 태그로 감싸면 됩니다. 이렇게 하면 컴포넌트가 DOM에서 제거되지 않고 캐싱되어 상태가 보존됩니다.

1
2
3
4
5
6
7
8
9
10
<template>
  <div>
    <button @click="currentComponent = 'ComponentA'">컴포넌트 A</button>
    <button @click="currentComponent = 'ComponentB'">컴포넌트 B</button>
    
    <keep-alive>
      <component :is="currentComponent"></component>
    </keep-alive>
  </div>
</template>

활용 사례

  • 탭 인터페이스: 각 탭에 서로 다른 컴포넌트를 표시.
  • 모달 창: 상황에 따라 다른 모달 콘텐츠를 동적으로 로드.
  • 설정에 따른 UI 변경: 사용자 선택에 따라 다른 UI 컴포넌트를 렌더링.

고급 사용: 비동기 컴포넌트

동적 컴포넌트는 비동기적으로 로드되는 컴포넌트와도 결합할 수 있습니다. 예를 들어, defineAsyncComponent를 사용하면 필요할 때만 컴포넌트를 로드할 수 있습니다:

1
2
3
4
5
import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() =>
  import('./AsyncComponent.vue')
);

이후 :isAsyncComponent를 바인딩하면 해당 컴포넌트가 필요할 때 비동기적으로 로드됩니다.

결론

Vue의 동적 컴포넌트는 유연성과 재사용성을 높여주는 강력한 기능입니다. <component :is><keep-alive>를 적절히 활용하면 복잡한 UI 요구사항도 효율적으로 처리할 수 있습니다. 추가 질문이 있다면 언제든 물어보세요!

그러면 여기서 왜 굳이 이렇게 복잡하게 사용해야 하나 싶은데

좋은 질문이에요! 슬롯(slots)과 동적 컴포넌트는 Vue에서 서로 다른 목적을 가지고 사용되는 도구들이라서, “슬롯을 사용하면 되는데 왜 굳이 동적 컴포넌트를 쓰느냐”는 질문은 이 둘의 사용 사례와 장점을 비교해보면 명확해집니다. 각각의 특징과 언제 어떤 걸 써야 하는지 설명해볼게요.


슬롯(Slots)이란?

슬롯은 부모 컴포넌트에서 자식 컴포넌트로 콘텐츠를 전달하는 방법입니다. 자식 컴포넌트의 특정 부분을 부모가 커스터마이징할 수 있게 해줍니다. 주로 컴포넌트의 구조는 고정되어 있고, 일부 콘텐츠만 동적으로 채우고 싶을 때 사용됩니다.

슬롯 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 부모 컴포넌트 -->
<template>
  <ChildComponent>
    <p>이 부분은 슬롯으로 전달됩니다!</p>
  </ChildComponent>
</template>

<!-- 자식 컴포넌트 (ChildComponent.vue) -->
<template>
  <div>
    <h1>고정된 제목</h1>
    <slot></slot> <!-- 부모에서 전달된 콘텐츠가 여기 렌더링 -->
  </div>
</template>

슬롯의 장점

  • 간단하고 직관적: 부모에서 자식으로 콘텐츠를 주입하는 방식이 명확합니다.
  • 재사용성: 동일한 구조를 가진 컴포넌트를 여러 곳에서 다른 콘텐츠로 채워 사용할 수 있습니다.
  • 정적인 구조: 컴포넌트의 기본 틀은 유지하면서 일부만 변경 가능.

슬롯의 한계

  • 구조적 제약: 슬롯은 자식 컴포넌트의 템플릿에 미리 정의된 위치(<slot>)에만 콘텐츠를 넣을 수 있습니다. 완전히 다른 컴포넌트로 교체하는 건 불가능합니다.
  • 유연성 부족: 슬롯은 콘텐츠를 전달하는 데 초점이 맞춰져 있어, 로직이나 컴포넌트 자체를 동적으로 바꾸는 데는 적합하지 않습니다.

동적 컴포넌트(Dynamic Components)란?

동적 컴포넌트는 <component :is="...">를 통해 런타임에 완전히 다른 컴포넌트를 렌더링할 수 있게 해줍니다. 즉, 컴포넌트 자체를 조건에 따라 교체할 수 있는 더 높은 수준의 유연성을 제공합니다.

동적 컴포넌트 예제

1
2
3
4
5
6
7
<template>
  <div>
    <button @click="currentComponent = 'ComponentA'">컴포넌트 A</button>
    <button @click="currentComponent = 'ComponentB'">컴포넌트 B</button>
    <component :is="currentComponent"></component>
  </div>
</template>

동적 컴포넌트의 장점

  • 완전한 교체 가능: 단순히 콘텐츠를 채우는 게 아니라, 서로 다른 템플릿, 로직, 스타일을 가진 컴포넌트를 동적으로 로드할 수 있습니다.
  • 조건적 렌더링: 사용자 입력, 데이터 상태 등에 따라 완전히 다른 UI와 기능을 제공할 수 있습니다.
  • 상태 유지 가능: <keep-alive>를 사용하면 전환된 컴포넌트의 상태도 보존할 수 있습니다.

동적 컴포넌트의 복잡성

  • 설정이 상대적으로 복잡: 컴포넌트를 등록하고, 전환 로직을 관리해야 합니다.
  • 상태 관리 필요: <keep-alive> 없이는 상태가 초기화될 수 있어 추가 고려가 필요합니다.

슬롯 vs 동적 컴포넌트: 언제 무엇을 사용할까?

슬롯을 사용하는 경우

  • 컴포넌트 구조는 고정이고, 일부 콘텐츠만 부모에서 주입하고 싶을 때.
  • 예: 모달 창의 본문 콘텐츠를 바꾸거나, 카드 컴포넌트의 내용을 동적으로 채울 때.
  • 특징: 컴포넌트 자체는 하나이고, 그 안의 일부만 변경.

동적 컴포넌트를 사용하는 경우

  • 완전히 다른 컴포넌트를 렌더링해야 할 때.
  • 예: 탭 UI에서 각 탭이 서로 다른 컴포넌트(예: 설정 탭, 프로필 탭, 대시보드 탭)를 표시하거나, 사용자 역할에 따라 다른 대시보드를 로드할 때.
  • 특징: 컴포넌트 전체를 교체하며, 각 컴포넌트가 독립적인 로직과 UI를 가짐.

질문에 대한 답변: 왜 동적 컴포넌트를 쓰는가?

슬롯은 “하나의 컴포넌트 안에서 콘텐츠를 유연하게 채우는” 데 적합하지만, 완전히 다른 컴포넌트를 런타임에 전환하려면 동적 컴포넌트가 필요합니다. 예를 들어:

  • 슬롯으로는 “설정 페이지”와 “대시보드 페이지”처럼 구조와 로직이 완전히 다른 두 컴포넌트를 하나의 자리에서 전환할 수 없습니다.
  • 동적 컴포넌트는 이런 경우에 적합하며, 더 복잡한 시나리오(예: 비동기 로딩, 상태 유지 등)를 다룰 수 있습니다.

결론

  • 슬롯: 간단하고 고정된 컴포넌트의 일부를 커스터마이징할 때.
  • 동적 컴포넌트: 복잡하고 조건에 따라 완전히 다른 컴포넌트를 렌더링할 때.

상황에 따라 다르지만, “복잡하게” 보이는 동적 컴포넌트는 사실 더 큰 유연성과 제어력을 제공하는 도구예요. 슬롯으로 해결할 수 없는 문제를 동적 컴포넌트가 해결해준다고 생각하면 됩니다. 더 궁금한 점 있으면 물어보세요!

여기서 또 컴포넌트가 파괴된다는게 무엇일까 해서 찾아본 내용

“컴포넌트가 파괴된다”는 말은 Vue.js에서 컴포넌트 인스턴스가 더 이상 필요 없어져서 메모리에서 제거되고, 관련 리소스가 정리되는 과정을 의미합니다. Vue의 라이프사이클에서 이는 destroyed 단계에 해당하며, 정확히는 beforeDestroydestroyed 훅이 실행되는 시점을 가리킵니다. 좀 더 쉽게 풀어서 설명해볼게요.


컴포넌트의 라이프사이클

Vue 컴포넌트는 생성부터 소멸까지 여러 단계를 거칩니다:

  1. 생성 (Creation): beforeCreate, created
    • 컴포넌트가 초기화되고 데이터가 설정됩니다.
  2. 마운트 (Mounting): beforeMount, mounted
    • 컴포넌트가 DOM에 추가되어 화면에 나타납니다.
  3. 업데이트 (Updating): beforeUpdate, updated
    • 데이터가 변경되어 DOM이 다시 렌더링됩니다.
  4. 파괴 (Destruction): beforeDestroy, destroyed
    • 컴포넌트가 제거되고 정리됩니다.

“파괴”는 이 마지막 단계인 Destruction을 뜻합니다.


컴포넌트가 파괴되는 상황

컴포넌트가 파괴되는 경우는 보통 다음과 같습니다:

  1. 조건부 렌더링 해제:
    • <div v-if="showComponent">에서 showComponenttrue에서 false로 바뀌면 해당 컴포넌트가 DOM에서 제거되고 파괴됩니다.
  2. 동적 컴포넌트 전환:
    • <component :is="currentComponent">에서 currentComponent가 다른 컴포넌트로 바뀌면 이전 컴포넌트가 파괴됩니다(단, <keep-alive>를 사용하면 파괴되지 않고 캐싱됨).
  3. 부모 컴포넌트의 제거:
    • 부모 컴포넌트가 파괴되면 자식 컴포넌트도 함께 파괴됩니다.
  4. 페이지 이동:
    • Vue Router로 다른 경로로 이동하면 현재 페이지의 컴포넌트가 파괴될 수 있습니다.

파괴 시 무슨 일이 일어나는가?

컴포넌트가 파괴되면 Vue가 다음 작업을 수행합니다:

  1. 이벤트 리스너 제거:
    • 컴포넌트가 추가한 이벤트 리스너(예: window.addEventListener)가 자동으로 정리됩니다(단, 수동으로 추가한 경우 직접 제거해야 함).
  2. 데이터와 반응성 해제:
    • data, computed, watch 등 반응형 속성의 연결이 끊어집니다.
  3. DOM에서 제거:
    • 컴포넌트의 템플릿이 DOM에서 삭제됩니다.
  4. 자식 컴포넌트 파괴:
    • 해당 컴포넌트에 포함된 자식 컴포넌트도 재귀적으로 파괴됩니다.

beforeDestroydestroyed

파괴 과정에서 실행되는 두 가지 훅이 있습니다:

  • beforeDestroy:
    • 컴포넌트가 파괴되기 직전에 호출됩니다.
    • 이 시점에서 아직 DOM과 데이터에 접근할 수 있으므로 정리 작업(예: 이벤트 리스너 제거)을 할 수 있습니다.
  • destroyed:
    • 파괴가 완료된 후 호출됩니다.
    • 모든 리소스가 정리된 상태라 추가 작업은 제한적입니다.

예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
export default {
  mounted() {
    window.addEventListener('scroll', this.handleScroll);
  },
  beforeDestroy() {
    // 수동으로 추가한 이벤트 리스너 제거
    window.removeEventListener('scroll', this.handleScroll);
    console.log('컴포넌트가 곧 파괴됩니다!');
  },
  destroyed() {
    console.log('컴포넌트가 파괴되었습니다!');
  },
  methods: {
    handleScroll() {
      console.log('스크롤 중...');
    }
  }
};
</script>

위 코드에서:

  • mounted에서 스크롤 리스너를 추가합니다.
  • beforeDestroy에서 리스너를 제거해 메모리 누수를 방지합니다.
  • 컴포넌트가 제거되면 destroyed가 호출됩니다.

왜 파괴가 중요한가?

  1. 메모리 관리:
    • 파괴되지 않은 컴포넌트가 메모리에 남아 있으면 메모리 누수(memory leak)가 발생할 수 있습니다.
  2. 리소스 정리:
    • 이벤트 리스너, 타이머(setInterval), 외부 구독 등이 정리되지 않으면 앱 성능에 영향을 줄 수 있습니다.
  3. 상태 초기화:
    • 더 이상 필요 없는 컴포넌트의 상태를 정리해 혼란을 방지합니다.

<keep-alive>와의 관계

<keep-alive>를 사용하면 컴포넌트가 DOM에서 제거되더라도 파괴되지 않고 캐싱됩니다. 이 경우 beforeDestroydestroyed 훅이 호출되지 않고, 나중에 다시 렌더링될 때 기존 상태를 유지합니다.


결론

“컴포넌트가 파괴된다”는 건 Vue가 컴포넌트를 더 이상 사용하지 않기로 결정하고, 그와 관련된 모든 리소스를 정리하며 메모리에서 제거하는 과정이에요. 특히 동적 컴포넌트나 조건부 렌더링에서 자주 발생하며, 개발자는 beforeDestroy를 활용해 필요한 정리 작업을 해야 합니다. 이해가 더 필요하면 질문 주세요!

8.4 컴포넌트에서의 v-model 디렉티브

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- InputComponent.vue -->
<template>
  <input v-model="inputValue" @input="$emit('update:modelValue', $event.target.value)">
</template>
<script>
export default {
  props: ['modelValue'],
  computed: {
    inputValue: {
      get() { return this.modelValue },
      set(value) { this.$emit('update:modelValue', value) }
    }
  }
}
</script>

v-model은 컴포넌트에서 양방향 데이터 바인딩을 지원합니다.

8.5 provide, inject를 이용한 공용데이터 사용

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Parent.vue -->
<script>
export default {
  provide: { sharedData: '공유 데이터' }
}
</script>
<!-- Child.vue -->
<script>
export default {
  inject: ['sharedData']
}
</script>

provideinject는 컴포넌트 간 공용 데이터를 전달합니다.

“provide/inject”는 Vue.js에서 컴포넌트 간 데이터를 전달하는 방법 중 하나로, 주로 부모 컴포넌트에서 자식 컴포넌트(또는 더 깊은 하위 컴포넌트)로 데이터를 제공할 때 사용됩니다. 이 기능은 props를 일일이 전달하지 않고도 컴포넌트 트리 깊숙이 데이터를 주입할 수 있게 해줍니다. 주로 상태 관리나 전역적으로 공유해야 하는 데이터를 다룰 때 유용합니다.

기본 개념

  • provide: 부모 컴포넌트에서 데이터를 “제공”하는 역할을 합니다. 제공된 데이터는 자식 컴포넌트에서 접근할 수 있습니다.
  • inject: 자식 컴포넌트에서 부모가 제공한 데이터를 “주입”받아 사용하는 역할을 합니다.

사용법

Vue 3에서 provideinject는 Composition API와 함께 사용할 수 있으며, Options API에서도 지원됩니다. 아래에서 두 가지 방식으로 설명하겠습니다.

1. Options API에서의 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
// 부모 컴포넌트
Vue.component('ParentComponent', {
  provide: {
    message: '안녕, Vue!'
  },
  template: '<child-component></child-component>'
});

// 자식 컴포넌트
Vue.component('ChildComponent', {
  inject: ['message'],
  template: '<div></div>'
});
  • 부모 컴포넌트에서 provide 속성을 통해 message라는 데이터를 제공합니다.
  • 자식 컴포넌트에서 inject를 사용해 message를 주입받아 템플릿에서 출력합니다.

2. Composition API에서의 사용

Vue 3에서는 provideinjectsetup() 함수 내에서 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 부모 컴포넌트
import { provide } from 'vue';

export default {
  setup() {
    provide('message', '안녕, Vue 3!');
  },
  template: '<child-component></child-component>'
};

// 자식 컴포넌트
import { inject } from 'vue';

export default {
  setup() {
    const message = inject('message');
    return { message };
  },
  template: '<div></div>'
};
  • provide(key, value): 부모에서 데이터를 제공합니다.
  • inject(key): 자식에서 해당 데이터를 주입받습니다.

반응형 데이터 제공

provide로 반응형 데이터를 전달하려면 refreactive를 사용하면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 부모 컴포넌트
import { provide, ref } from 'vue';

export default {
  setup() {
    const count = ref(0);
    provide('count', count); // 반응형 데이터 제공
    return { count };
  },
  template: '<child-component></child-component>'
};

// 자식 컴포넌트
import { inject } from 'vue';

export default {
  setup() {
    const count = inject('count');
    return { count };
  },
  template: '<div>9</div>'
};
  • 부모에서 count가 변경되면 자식 컴포넌트에서도 반영됩니다.

주의점

  1. 범위: provide는 해당 컴포넌트의 자식 트리에만 영향을 미칩니다. 다른 트리에는 전달되지 않습니다.
  2. 이름 충돌: inject로 주입받는 키가 부모에서 제공되지 않으면 undefined가 됩니다. 기본값을 설정하려면 inject('key', defaultValue) 형식으로 작성하세요.
  3. Props와의 차이: Props는 명시적이고 단방향 데이터 흐름을 제공하지만, provide/inject는 암묵적이고 깊은 컴포넌트 간 통신에 적합합니다.

언제 사용하나요?

  • 컴포넌트 계층이 깊을 때 props drilling(계속해서 props를 전달하는 번거로움)을 피하고 싶을 때.
  • 전역 상태 관리 라이브러리(Vuex, Pinia 등)를 사용하지 않고 간단히 데이터를 공유하고 싶을 때.

궁금한 점이 더 있으면 말씀해주세요!

Swift에서는 Vue.js의 provide/inject와 정확히 동일한 개념은 없지만, 비슷한 목적으로 컴포넌트 간 데이터를 전달하거나 의존성을 주입하는 방법이 존재합니다. Swift는 주로 iOS/macOS 앱 개발에 사용되며, UI 프레임워크로는 UIKit이나 SwiftUI를 활용합니다. 여기서는 SwiftUI를 기준으로 provide/inject와 유사한 역할을 하는 메커니즘을 설명하겠습니다.

SwiftUI에서 이와 비슷한 개념은 EnvironmentDependency Injection(DI)입니다. 아래에서 각각 살펴보겠습니다.


1. Environment (SwiftUI)

SwiftUI에서는 Environment를 통해 부모 뷰에서 하위 뷰로 데이터를 전달할 수 있습니다. 이는 Vue의 provide/inject와 가장 유사한 기능으로, 뷰 계층 구조 깊숙이 데이터를 주입할 수 있습니다.

사용법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import SwiftUI

// 부모 뷰
struct ParentView: View {
    @State private var message = "안녕, SwiftUI!"

    var body: some View {
        ChildView()
            .environment(\.message, message) // Environment로 데이터 제공
    }
}

// EnvironmentKey 정의
private struct MessageKey: EnvironmentKey {
    static let defaultValue: String = "기본 메시지"
}

extension EnvironmentValues {
    var message: String {
        get { self[MessageKey.self] }
        set { self[MessageKey.self] = newValue }
    }
}

// 자식 뷰
struct ChildView: View {
    @Environment(\.message) var message // Environment에서 데이터 주입

    var body: some View {
        Text(message)
    }
}

#Preview {
    ParentView()
}
  • 부모 뷰: .environment(\.key, value)를 사용해 message를 제공합니다.
  • 자식 뷰: @Environment(\.key)를 사용해 제공된 데이터를 주입받습니다.
  • EnvironmentKey: 사용자 정의 데이터를 전달하려면 EnvironmentKey를 만들어야 합니다.

특징

  • Vue의 provide/inject처럼 계층 구조를 통해 암묵적으로 데이터를 전달합니다.
  • 반응형으로 동작하며, @State@Binding과 결합하면 값이 변경될 때 UI가 자동으로 업데이트됩니다.

언제 사용?

  • 앱의 테마(예: 다크 모드), 설정값, 사용자 정보 등 하위 뷰에서 공통적으로 필요한 데이터를 전달할 때.

2. Dependency Injection (DI)

Swift에서는 의존성 주입(DI)을 통해 객체 간 데이터를 전달하는 패턴이 자주 사용됩니다. 이는 Vue의 provide/inject와 목적은 비슷하지만, 더 일반적인 프로그래밍 패턴에 가깝습니다. SwiftUI에서도 DI를 활용할 수 있습니다.

사용법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import SwiftUI

// 데이터 제공 객체
class AppData: ObservableObject {
    @Published var message: String = "안녕, DI!"
}

// 부모 뷰
struct ParentView: View {
    @StateObject private var appData = AppData()

    var body: some View {
        ChildView()
            .environmentObject(appData) // DI로 객체 제공
    }
}

// 자식 뷰
struct ChildView: View {
    @EnvironmentObject var appData: AppData // DI로 객체 주입

    var body: some View {
        Text(appData.message)
    }
}

#Preview {
    ParentView()
}
  • 부모 뷰: .environmentObject()를 사용해 AppData 인스턴스를 제공합니다.
  • 자식 뷰: @EnvironmentObject를 사용해 제공된 객체를 주입받습니다.
  • ObservableObject: @Published 속성을 통해 데이터가 변경되면 뷰가 자동으로 업데이트됩니다.

특징

  • Vue의 provide/inject와 달리 객체 단위로 데이터를 전달합니다.
  • 상태 관리에 더 적합하며, 복잡한 앱에서 전역 상태를 공유할 때 유용합니다.

언제 사용?

  • 앱 전역에서 공유해야 하는 상태(예: 사용자 인증 정보, 설정 등)를 관리할 때.

Vue의 provide/inject와의 비교

| 특징 | Vue (provide/inject) | SwiftUI (Environment) | SwiftUI (DI with EnvironmentObject) | |———————|———————————-|———————————-|——————————————| | 데이터 전달 방식 | 키-값 쌍으로 전달 | EnvironmentKey로 전달 | 객체 단위로 전달 | | 반응형 여부 | ref/reactive로 반응형 가능 | @State 등으로 반응형 가능 | @Published로 반응형 가능 | | 사용 범위 | 컴포넌트 트리 내 | 뷰 트리 내 | 뷰 트리 내 | | 주요 용도 | 간단한 데이터 공유 | 설정값, 테마 전달 | 복잡한 상태 관리 |


결론

SwiftUI에서 Vue의 provide/inject와 가장 가까운 개념은 Environment입니다. 간단한 값 전달이 필요할 때는 Environment를, 더 복잡한 상태 관리가 필요할 때는 EnvironmentObject를 활용한 DI를 사용하는 것이 일반적입니다. Swift는 Vue와 달리 UI와 로직이 더 밀접하게 통합되어 있어, 상황에 따라 적절한 방법을 선택하면 됩니다.

추가로 궁금한 점이 있으면 말씀해주세요!

8.6 텔레포트

1
2
3
4
5
6
<!-- TeleportComponent.vue -->
<template>
  <teleport to="body">
    <div>모달</div>
  </teleport>
</template>

텔레포트는 DOM의 다른 위치로 컴포넌트를 이동시킵니다. 로딩화면 이건가?

8.7 비동기 컴포넌트

1
2
3
4
5
6
7
8
9
<!-- App.vue -->
<script>
import { defineAsyncComponent } from 'vue'
export default {
  components: {
    AsyncComp: defineAsyncComponent(() => import('./AsyncComp.vue'))
  }
}
</script>

비동기 컴포넌트는 필요할 때만 로드해 성능을 최적화합니다.

Vue.js에서 로딩 컴포넌트를 구현하는 방법에 대해 더 자세히 설명하겠습니다. 로딩 컴포넌트는 데이터 fetching이나 비동기 작업 중에 사용자에게 진행 상태를 보여주는 역할을 하며, Vue의 반응형 데이터와 조건부 렌더링을 활용해 쉽게 만들 수 있습니다. 아래에서 기본 구현부터 재사용 가능한 컴포넌트 설계까지 다뤄보겠습니다.


1. 기본 로딩 컴포넌트 구현

가장 간단한 형태로, 단일 컴포넌트 내에서 로딩 상태를 관리하는 예제입니다.

예제 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<template>
  <div>
    <!-- 로딩 중일 때 -->
    <div v-if="isLoading" class="loading">
      <p>로딩 중...</p>
      <div class="spinner"></div>
    </div>
    <!-- 데이터가 로드된 후 -->
    <div v-else-if="data" class="content">
      <p></p>
    </div>
    <!-- 데이터가 없는 초기 상태 -->
    <div v-else>
      <p>데이터를 불러오세요</p>
      <button @click="fetchData">가져오기</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isLoading: false,
      data: null,
    };
  },
  methods: {
    async fetchData() {
      this.isLoading = true;
      try {
        const response = await fetch('https://api.example.com/data');
        this.data = await response.json();
      } catch (error) {
        console.error('에러 발생:', error);
      } finally {
        this.isLoading = false;
      }
    },
  },
};
</script>

<style scoped>
.loading {
  text-align: center;
  padding: 20px;
}
.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin: 0 auto;
}
@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
.content {
  padding: 20px;
}
</style>

동작 원리

  • isLoading: 로딩 상태를 관리하는 반응형 데이터입니다.
  • v-if/v-else-if/v-else: 조건에 따라 로딩 UI, 데이터, 초기 상태를 렌더링합니다.
  • 비동기 작업: fetchData 메서드에서 API 호출을 시뮬레이션하며, try/catch/finally로 에러와 로딩 종료를 처리합니다.
  • CSS 애니메이션: 스피너를 회전시키는 애니메이션으로 시각적 피드백을 제공합니다.

2. 재사용 가능한 로딩 컴포넌트

여러 곳에서 로딩 UI를 재사용하려면 별도의 로딩 컴포넌트를 만들어 props로 상태를 전달하는 방식이 효율적입니다.

로딩 컴포넌트 (Loading.vue)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
  <div class="loading" v-if="show">
    <slot>
      <p>로딩 중...</p>
      <div class="spinner"></div>
    </slot>
  </div>
</template>

<script>
export default {
  props: {
    show: {
      type: Boolean,
      default: false,
    },
  },
};
</script>

<style scoped>
.loading {
  text-align: center;
  padding: 20px;
}
.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin: 0 auto;
}
@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
</style>

사용 예제 (Parent.vue)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
  <div>
    <Loading :show="isLoading" />
    <div v-if="!isLoading && data">
      <p></p>
    </div>
    <button v-if="!isLoading && !data" @click="fetchData">데이터 가져오기</button>
  </div>
</template>

<script>
import Loading from './Loading.vue';

export default {
  components: { Loading },
  data() {
    return {
      isLoading: false,
      data: null,
    };
  },
  methods: {
    async fetchData() {
      this.isLoading = true;
      try {
        const response = await fetch('https://api.example.com/data');
        this.data = await response.json();
      } catch (error) {
        console.error('에러:', error);
      } finally {
        this.isLoading = false;
      }
    },
  },
};
</script>

동작 원리

  • Props: show prop으로 로딩 컴포넌트의 표시 여부를 제어합니다.
  • Slot: 기본 UI(텍스트와 스피너)를 제공하되, 필요하면 커스텀 콘텐츠를 삽입할 수 있게 합니다.
  • 재사용성: <Loading> 컴포넌트를 여러 컴포넌트에서 호출해 일관된 로딩 UI를 유지합니다.

3. Composition API로 구현

Vue 3의 Composition API를 사용하면 로딩 로직을 더 모듈화할 수 있습니다.

예제 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<template>
  <div>
    <div v-if="isLoading" class="loading">
      <p>로딩 중...</p>
      <div class="spinner"></div>
    </div>
    <div v-else-if="data">
      <p></p>
    </div>
    <button v-else @click="fetchData">데이터 가져오기</button>
  </div>
</template>

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

export default {
  setup() {
    const isLoading = ref(false);
    const data = ref(null);

    const fetchData = async () => {
      isLoading.value = true;
      try {
        const response = await fetch('https://api.example.com/data');
        data.value = await response.json();
      } catch (error) {
        console.error('에러:', error);
      } finally {
        isLoading.value = false;
      }
    };

    return { isLoading, data, fetchData };
  },
};
</script>

<style scoped>
.loading {
  text-align: center;
  padding: 20px;
}
.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin: 0 auto;
}
@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
</style>

동작 원리

  • ref: 반응형 상태를 관리합니다.
  • setup: 로직을 한 곳에 모아 깔끔하게 구성합니다.

4. 추가 팁

  • 에러 처리: 로딩 실패 시 사용자에게 알리기 위해 error 상태를 추가할 수 있습니다.
    1
    
    <div v-else-if="error" class="error">에러 발생: </div>
    
  • 로딩 스타일: 로딩 스피너 대신 Lottie 애니메이션이나 Skeleton UI를 사용해 더 세련되게 만들 수 있습니다.
  • Suspense: Vue 3의 <Suspense> 컴포넌트를 사용하면 비동기 컴포넌트 로딩을 간소화할 수 있습니다.
    1
    2
    3
    4
    5
    6
    7
    8
    
    <Suspense>
      <template #default>
        <AsyncComponent />
      </template>
      <template #fallback>
        <Loading :show="true" />
      </template>
    </Suspense>
    

언제 사용하나요?

  • API 호출, 파일 업로드, 페이지 전환 등 비동기 작업 중 사용자 대기 시간을 시각적으로 표현할 때.
  • UX 개선을 위해 필수적인 상황에서.

궁금한 점이 더 있으면 말씀해주세요!

This post is licensed under CC BY 4.0 by the author.