ch8. 컴포넌트 간 연계
SFC(Single File Component)
- .vue 확장자로 이루어지는 파일로 HTML + CSS + JS 세트 (.jsx와 비슷)
scoped CSS 메커니즘
1) 컴포넌트별 hash 문자열이 생성되고, 해당 컴포넌트가 렌더링되면 HTML data attributes를 통해 엘리먼트에 이름이 붙여짐
예) v-eee87bea 라는 has 문자열 data attributes 생성
<section>
<h2>v-model을 포함한 컴포넌트</h2>
<section data-v-eee87bea="">
<p data-v-eee87bea="">이름 없음 님이네요!</p>
<input type="text" data-v-eee87bea="">
</section>
<section data-v-eee87bea="">
<p data-v-eee87bea="">이름 없음 님이네요!</p>
<input type="text" data-v-eee87bea="">
</section>
</section>
2) CSS에서 해당 attributes에 스타일링
section[data-v-eee87bea] {
border: orange 1px dashed;
margin: 10px;
}
전역적으로 적용하고 싶은 CSS는 App.vue에 스타일링하고, 이외에는 각 개별 컴포넌트에 scoped 형식으로 작성한다
Provide와 Inject: props 드릴링을 방지하며 부모 -> 자식 데이터 전달
- provide(데이터 이름, 반응형 값) 형태로 부모 컴포넌트에서 선언하면
- Inject(데이터 이름) 형태로 변수에 저장하여 사용 가능
- inject로 받은 데이터를 v-model 을 통해 바꾸면, 자식 -> 부모 데이터 전송 가능 (값이 바뀜)
- provide 에서 일반 값을 전달해야 한다면, reactive 를 씌워 전달하면 된다
- inject를 통해 provide 된 데이터를 사용할 때는 타입 지정을 해줘야 함 (안해주면 기본 unknown 상태)
예시: app.vue 에서 provide 된 반응형 값을 자식 컴포넌트들에서 inject 하고 변경할 수 있는 양방향 데이터 바인딩
- 부모 컴포넌트 (app.vue)
<template>
<BaseSection />
</template>
<script setup lang="ts">
import type { Member } from "~/interfaces";
import BaseSection from "~/components/BaseSection.vue";
const memberListRef = ref({
33456: {
id: 33456,
name: "소희",
email: "abc@sample.com",
points: 35,
note: "신규 가입 특전",
},
47783: {
id: 47783,
name: "채영",
email: "def@sample.com",
points: 53,
},
});
provide("memberListRef", memberListRef as Ref<{ [key: number]: Member }>);
</script>
- 자식1 컴포넌트 (BaseSection.vue)
<template>
<section>
<h2>회원 리스트</h2>
<p>모든 회원의 보유 포인트 합계: {{ totalPoints }}</p>
<OneMember v-for="id in Object.keys(memberListRef)" :id="+id" :key="id" />
</section>
</template>
<script setup lang="ts">
import type { Member } from "~/interfaces";
const memberListRef = inject<Ref<{ [key: number]: Member }>>("memberListRef", ref({}));
const totalPoints = computed(() => {
let total = 0;
// Map 객체의 값만 배열 형태로 순회하기 위해 values() 사용
for (const member of Object.values(memberListRef.value)) {
total += member.points;
}
return total;
});
</script>
<style scoped>
section {
border: orange 1px dashed;
margin: 10px;
}
</style>
- 자식1-1 컴포넌트 (OneMember.vue)
- v-model에 연결하여 변경하면 변경된 member가 memberListRef 에 반영됨(양방향 데이터 바인딩)
<template>
<section>
<h4>{{ member.name }} 님의 정보</h4>
<dl>
<dt>ID</dt>
<dd>{{ id }}</dd>
<dt>메일 주소</dt>
<dd>{{ member.email }}</dd>
<dt>보유 포인트</dt>
<dd>
<input v-model.number="member.points" type="number" />
</dd>
<dt>비고</dt>
<dd>{{ localNote }}</dd>
</dl>
</section>
</template>
<script setup lang="ts">
import type { Member } from "~/interfaces";
interface Props {
id: number;
}
const props = defineProps<Props>();
const memberListRef = inject<Ref<{ [key: number]: Member }>>("memberListRef", ref({}));
const member = computed(() => memberListRef.value[props.id]);
const localNote = computed(() => {
let localNote = member.value.note;
if (localNote === undefined) {
localNote = "--";
}
return localNote;
});
</script>
ch9. 자식 컴포넌트 활용
slot의 fallback
- < slot > 내부에 작성한 내용은 slot의 fallback이 된다
예: slot 내부에 대체 컨텐츠 작성
- 이 컴포넌트를 사용할 때 내부에 어떤 내용도 전달하지 않으면 slot 내부에 작성한 내용이 렌더링된다
<template>
<section class="box">
<h1>{{ name }} 님의 상황</h1>
<slot>
<p>문제 없습니다</p>
</slot>
</section>
</template>
<script setup lang="ts">
interface Props {
name: string;
}
defineProps<Props>();
</script>
<style>
.box {
border: green 1px solid;
margin: 10px;
}
</style>
명명된 slot
- 기본적으로 < slot > 을 여러개 사용할 수도 있다 -> 동일한 내용이 렌더링된다
<template>
<section class="box">
<h1>{{ name }} 님의 상황</h1>
<slot />
<slot />
</section>
</template>
- slot의 attributes로 name을 사용하여 이름을 붙이면 별도의 다른 slot을 각각 렌더링할 수 있다
- 부모 컴포넌트에서는 < template > 에 v-slot 으로 slot name을 전달하면 된다
- v-slot 은 # 로 사용될 수 있다!
예) 기본값 사용: v-slot="default" (#default 와 동일) -> < slot > 에 전달됨
예) 명명된 slot 사용: v-slot="detail" (#detail) -> < slot name="detail" > 에 전달됨
자식 컴포넌트 OneSection.vue
<template>
<section class="box">
<h1>{{ name }} 님의 상황</h1>
<slot>
<p>문제 없습니다</p>
</slot>
<h4>상세 내용</h4>
<slot name="detail">
<p>특별히 없습니다</p>
</slot>
</section>
</template>
<script setup lang="ts">
interface Props {
name: string;
}
defineProps<Props>();
</script>
<style>
.box {
border: green 1px solid;
margin: 10px;
}
</style>
부모 컴포넌트 app.vue
<template>
<section>
<h2>Slot 이용</h2>
<OneSection :name="yeonghee">
<template #default>
<p>문제 발생</p>
</template>
<template #detail>
<ul>
<li v-for="r in 5" :key="r">{{ r }}번째 문제</li>
</ul>
</template>
</OneSection>
<OneSection :name="cheolsoo" />
</section>
</template>
<script setup lang="ts">
const yeonghee = ref("영희");
const cheolsoo = ref("철수");
</script>
- v-slot 에 동적인 값을 사용하려면 v-slot:[slotName] 형태로 사용한다
slot의 props 사용하기
- slot 에서 선언한 props를 사용할 수 있다
자식 컴포넌트 OneSection.vue
<template>
<section>
<slot :member-info="memberInfo">
<h1>{{ memberInfo.name }} 님의 상황</h1>
<p>{{ memberInfo.state }}</p>
</slot>
</section>
</template>
<script setup lang="ts">
const memberInfo = reactive({
name: "영희",
state: "문제없습니다",
});
</script>
부모 컴포넌트 app.vue
- v-slot="{ memberInfo }" 대신 #default="{ memberInfo }" 처럼 사용해도 된다 (slot name 없으므로)
- { slotPropsName } 형태로 전달하는 것은 slotProps = { slotPropsName } 처럼 한 번 꺼낸 것으로 이해하면 된다
<template>
<section>
<OneSection>
<template v-slot="{ memberInfo }">
<dl>
<dt>이름</dt>
<dd>{{ memberInfo.name }}</dd>
<dt>상황</dt>
<dd>{{ memberInfo.state }}</dd>
</dl>
</template>
</OneSection>
</section>
</template>
동적 컴포넌트 활용: KeepAlive
- < component > 엘리먼트를 사용하고 v-bind:is 어트리뷰트를 사용하여 동적으로 컴포넌트를 렌더링시킬 수 있다
=> 이 때, < component > 를 < KeepAlive > 로 래핑하면, 한 번 렌더링됐던 컴포넌트의 상태가 보존된다
=> Vue 컴포넌트 라이프사이클에 따르면 activated -> deactivated 상태로 되돌아가는 것을 의미한다. deactivated 상태는 unmounted 상태와 달리 컴포넌트의 상태를 보존하며 임시로 렌더링하지 않을 뿐이다
예) KeepAlive 로 래핑하여 자식 컴포넌트가 동적으로 변화할 때 기존 상태를 유지
<template>
<p>컴포넌트명: {{ currentCompName }}</p>
<KeepAlive>
<component :is="currentComp" />
</KeepAlive>
<button @click="switchComp">바꾸기</button>
</template>
<script setup lang="ts">
import AppInput from "~/components/AppInput.vue";
import AppRadio from "~/components/AppRadio.vue";
import AppSelect from "~/components/AppSelect.vue";
const currentComp = ref(AppInput);
const currentCompName = ref("Input");
const compList = [AppInput, AppRadio, AppSelect];
const compNameList = ["AppInput", "AppRadio", "AppSelect"];
const currentCompIndex = ref(0);
const switchComp = () => {
currentCompIndex.value += 1;
if (currentCompIndex.value >= 3) {
currentCompIndex.value = 0;
}
currentComp.value = compList[currentCompIndex.value];
currentCompName.value = compNameList[currentCompIndex.value];
};
</script>