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">가 해당 컴포넌트를 렌더링합니다. –> 이 방식으로 버튼으로 뷰를 트랜지션 가능할 것 같음. 이거 통해서 나는 메인 뷰 세컨드 페이지로 넘어갈 생각임.
동작 원리
:is에 바인딩된 값이 변경되면 Vue는 해당 컴포넌트를 동적으로 로드하고 DOM에 반영합니다.- 컴포넌트는 등록된 이름(예:
'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>
위 코드에서:
- 사용자가
ComponentA의 입력란에 “안녕”을 입력합니다. ComponentB로 전환했다가 다시ComponentA로 돌아와도 입력값 “안녕”이 유지됩니다.<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')
);
이후 :is에 AsyncComponent를 바인딩하면 해당 컴포넌트가 필요할 때 비동기적으로 로드됩니다.
결론
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 단계에 해당하며, 정확히는 beforeDestroy와 destroyed 훅이 실행되는 시점을 가리킵니다. 좀 더 쉽게 풀어서 설명해볼게요.
컴포넌트의 라이프사이클
Vue 컴포넌트는 생성부터 소멸까지 여러 단계를 거칩니다:
- 생성 (Creation):
beforeCreate,created- 컴포넌트가 초기화되고 데이터가 설정됩니다.
- 마운트 (Mounting):
beforeMount,mounted- 컴포넌트가 DOM에 추가되어 화면에 나타납니다.
- 업데이트 (Updating):
beforeUpdate,updated- 데이터가 변경되어 DOM이 다시 렌더링됩니다.
- 파괴 (Destruction):
beforeDestroy,destroyed- 컴포넌트가 제거되고 정리됩니다.
“파괴”는 이 마지막 단계인 Destruction을 뜻합니다.
컴포넌트가 파괴되는 상황
컴포넌트가 파괴되는 경우는 보통 다음과 같습니다:
- 조건부 렌더링 해제:
<div v-if="showComponent">에서showComponent가true에서false로 바뀌면 해당 컴포넌트가 DOM에서 제거되고 파괴됩니다.
- 동적 컴포넌트 전환:
<component :is="currentComponent">에서currentComponent가 다른 컴포넌트로 바뀌면 이전 컴포넌트가 파괴됩니다(단,<keep-alive>를 사용하면 파괴되지 않고 캐싱됨).
- 부모 컴포넌트의 제거:
- 부모 컴포넌트가 파괴되면 자식 컴포넌트도 함께 파괴됩니다.
- 페이지 이동:
- Vue Router로 다른 경로로 이동하면 현재 페이지의 컴포넌트가 파괴될 수 있습니다.
파괴 시 무슨 일이 일어나는가?
컴포넌트가 파괴되면 Vue가 다음 작업을 수행합니다:
- 이벤트 리스너 제거:
- 컴포넌트가 추가한 이벤트 리스너(예:
window.addEventListener)가 자동으로 정리됩니다(단, 수동으로 추가한 경우 직접 제거해야 함).
- 컴포넌트가 추가한 이벤트 리스너(예:
- 데이터와 반응성 해제:
data,computed,watch등 반응형 속성의 연결이 끊어집니다.
- DOM에서 제거:
- 컴포넌트의 템플릿이 DOM에서 삭제됩니다.
- 자식 컴포넌트 파괴:
- 해당 컴포넌트에 포함된 자식 컴포넌트도 재귀적으로 파괴됩니다.
beforeDestroy와 destroyed 훅
파괴 과정에서 실행되는 두 가지 훅이 있습니다:
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가 호출됩니다.
왜 파괴가 중요한가?
- 메모리 관리:
- 파괴되지 않은 컴포넌트가 메모리에 남아 있으면 메모리 누수(memory leak)가 발생할 수 있습니다.
- 리소스 정리:
- 이벤트 리스너, 타이머(
setInterval), 외부 구독 등이 정리되지 않으면 앱 성능에 영향을 줄 수 있습니다.
- 이벤트 리스너, 타이머(
- 상태 초기화:
- 더 이상 필요 없는 컴포넌트의 상태를 정리해 혼란을 방지합니다.
<keep-alive>와의 관계
<keep-alive>를 사용하면 컴포넌트가 DOM에서 제거되더라도 파괴되지 않고 캐싱됩니다. 이 경우 beforeDestroy와 destroyed 훅이 호출되지 않고, 나중에 다시 렌더링될 때 기존 상태를 유지합니다.
결론
“컴포넌트가 파괴된다”는 건 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>
provide와 inject는 컴포넌트 간 공용 데이터를 전달합니다.
“provide/inject”는 Vue.js에서 컴포넌트 간 데이터를 전달하는 방법 중 하나로, 주로 부모 컴포넌트에서 자식 컴포넌트(또는 더 깊은 하위 컴포넌트)로 데이터를 제공할 때 사용됩니다. 이 기능은 props를 일일이 전달하지 않고도 컴포넌트 트리 깊숙이 데이터를 주입할 수 있게 해줍니다. 주로 상태 관리나 전역적으로 공유해야 하는 데이터를 다룰 때 유용합니다.
기본 개념
- provide: 부모 컴포넌트에서 데이터를 “제공”하는 역할을 합니다. 제공된 데이터는 자식 컴포넌트에서 접근할 수 있습니다.
- inject: 자식 컴포넌트에서 부모가 제공한 데이터를 “주입”받아 사용하는 역할을 합니다.
사용법
Vue 3에서 provide와 inject는 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에서는 provide와 inject를 setup() 함수 내에서 사용할 수 있습니다.
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로 반응형 데이터를 전달하려면 ref나 reactive를 사용하면 됩니다.
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가 변경되면 자식 컴포넌트에서도 반영됩니다.
주의점
- 범위:
provide는 해당 컴포넌트의 자식 트리에만 영향을 미칩니다. 다른 트리에는 전달되지 않습니다. - 이름 충돌:
inject로 주입받는 키가 부모에서 제공되지 않으면undefined가 됩니다. 기본값을 설정하려면inject('key', defaultValue)형식으로 작성하세요. - Props와의 차이: Props는 명시적이고 단방향 데이터 흐름을 제공하지만,
provide/inject는 암묵적이고 깊은 컴포넌트 간 통신에 적합합니다.
언제 사용하나요?
- 컴포넌트 계층이 깊을 때 props drilling(계속해서 props를 전달하는 번거로움)을 피하고 싶을 때.
- 전역 상태 관리 라이브러리(Vuex, Pinia 등)를 사용하지 않고 간단히 데이터를 공유하고 싶을 때.
궁금한 점이 더 있으면 말씀해주세요!
Swift에서는 Vue.js의 provide/inject와 정확히 동일한 개념은 없지만, 비슷한 목적으로 컴포넌트 간 데이터를 전달하거나 의존성을 주입하는 방법이 존재합니다. Swift는 주로 iOS/macOS 앱 개발에 사용되며, UI 프레임워크로는 UIKit이나 SwiftUI를 활용합니다. 여기서는 SwiftUI를 기준으로 provide/inject와 유사한 역할을 하는 메커니즘을 설명하겠습니다.
SwiftUI에서 이와 비슷한 개념은 Environment와 Dependency 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:
showprop으로 로딩 컴포넌트의 표시 여부를 제어합니다. - 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 개선을 위해 필수적인 상황에서.
궁금한 점이 더 있으면 말씀해주세요!