react의 children 포지션.
개발자는 기본적으로 중복을 굉장히 싫어한다.
그러면서 앞서 포스팅했던 내용을 보면 styling을 scoped로 지역으로 관리하면서 문제가 같은 코드를 반복하여 사용해야한다는 단점이 생겼다.
하지만 이 slot을 이용하면 해당 컴포넌트와 함께 styling도 지정이 가능하다.
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {}
</script>
<style scoped>
div {
margin: 2rem auto;
max-width: 30rem;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
padding: 1rem;
}
</style>
프로젝트를 작성하다보면 어떠한 UI컴포넌트안에 또 다른 컴포넌트들이 들어있을 경우가 있다.
이는 simple하게 <slot>을 2번 쓰면 되는데 문제는 vue가 어떤 children이 어떤 <slot>으로 들어가야하는지를 모른다는 것이다.
그래서 slot에 이름을 지정해주어야한다.
<template>
<div>
<header>
<slot name="header"></slot>
</header>
<slot></slot>
</div>
</template>
<script>
export default {}
</script>
<style scoped>
div {
margin: 2rem auto;
max-width: 30rem;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
padding: 1rem;
}
</style>
name
속성으로 <slot>을 지정해주면 된다. 그리고 굳이 모든 <slot>에 이름을 지정해줄 필요는 없다. 이름이 지정이 안 된 <slot>이 있다면 그것은 자동으로 default <slot>이 된다.<template>
<BaseCard>
<template v-slot:header>
<h3>{{ fullName }}</h3>
<base-badge :type="role" :caption="role.toUpperCase()"></base-badge>
</template>
<template v-slot:default>
<p>{{ infoText }}</p>
</template>
<slot></slot>
</BaseCard>
...
</template>
사용하는 html 코드에서는 해당 컴포넌트에 v-slot
속성을 추가하여 <slot>에 설정한 이름값을 부여하면 연결된다.
물론 v-slot:default를 지정주지 않아도 되지만 가독성과 코드 이해력을 높이기 위해 작성하였다. 이때 잘 못 이해하면 안되는 부분이 있는데
default를 <slot>으로 이해해선 안된다는 것이다.
무슨말이냐면
default는 이름(name)이 없는 슬롯을 의미하는 것이 아니고 수신하는 콘텐츠가 없는 슬롯에 default 콘텐츠를 렌더링할 수 있다는 의미이다.
이때 <template>는 </>처럼 아무것도 렌더링하지 않기 때문에 wapper로 쓰기 좋다.
v-slot:
의 축약어이다.<template>
<BaseCard>
<template #header>
<h3>{{ fullName }}</h3>
<base-badge :type="role" :caption="role.toUpperCase()"></base-badge>
</template>
<template #default>
<p>{{ infoText }}</p>
</template>
<slot></slot>
</BaseCard>
...
</template>
당연하게도 슬롯 밖에서 스타일링을 해도 슬롯까지 영향을 미치진 않는다. 왜그럴까?
Vue.js는 다른 컴포넌트에 콘텐츠를 보내기 전에 템플릿을 분석하고 컴파일하고 평가한다.
따라서 UserInfo.vue 내부에서 정의된 모든 항목에 액세스할 수 있고 여기에 정의된 스타일링도 해당 컴포넌트의 <template>에 영향을 준다. 하지만 콘텐츠를 보내는 컴포넌트의 마크업에는 적용되지 않는다.
<template>
<div>
<header>
<slot name="header">
<div>default header</div>
</slot>
</header>
<slot></slot>
</div>
</template>
템플릿에 위와같이 slot name 태그 안에 어떠한 값을 설정해두면, 위 BaseCard.vue
를 사용하는 곳에 같은 위치에 children이 있다면 그 요소를 보여주고 children이 없다면 설정해둔 기본값(default)이 보인다는 것이다.
그럼 굳이 default 값을 설정해야하는 이유는 무엇일까?
<script>
mounted : {
console.log(this.$slots.네임속성값)
}
</script>
즉, 해당 값에 접근할 수 있다면 해당 값으로 v-if로 동적 렌더링을 할 수 있다는 것이다.
그렇게 된다면 빈 html 파일이 나올 염려도 줄게 된다.
<template>
<div>
<header v-if="this.$slots.header">
<slot name="header">
<div>default header</div>
</slot>
</header>
<slot></slot>
</div>
</template>
즉,
$slots
를 사용해서 특정 슬롯에 대한 데이터를 수신하는지 확인하고 아니라면 해당 정보를 사용해서 특정 요소를 렌더링하지 않을 수 있다.
<template>
<ul>
<li v-for="goal in goals" :key="goal">
{{goal}}
</li>
</ul>
</template>
{{goal}}
부분에 goal 데이터에 뭔가 다양한 마크업을 하고싶을 때<template>
<ul>
<li v-for="goal in goals" :key="goal">
<slot :item="goal" another-props="..."></slot>
</li>
</ul>
</template>
...
<CourseGoals>
<template #default="slotProps">
<h2>{{ slotProps.item }}</h2>
<p>{{ slotProps['another-props'] }}</p>
</template>
</CourseGoals>
...
데이터에 접근하기 위해 <slot>에 대한 마크업을 template로 감싼다. 슬롯에 보내고 싶은 마크업을 감싸는 것이다.
단, <slot> 내부에 추가 element가 없다면 <template>를 쓰지 않고 부모태그에 써도 된다. 여기서는 <CourGaols>에 #default
속성을 써도 된다는 뜻이다.
위 코드중 "slotProps"의 값은 언제나 객체이다.
커스텀 태그 <slot>에서 정의한 모든 속성값이 병합된 객체이다.
JavaScript 코드로 유효하지 않은 점 표기법을 사용할 수 없기 때문에 대괄호로 표기하여 대시를 넣은 이름도 동작하게 하였다.
PS. 나는 추가 속성값을
kebab-case
가 아닌carmelCase
로 넣었더니 동작하였다.