Vue.js에서 슬롯 사용하기

게시 됨: 2022-03-10
빠른 요약 ↬ 슬롯은 Vue.js에서 재사용 가능한 구성 요소를 생성하기 위한 강력한 도구이지만 이해하기 가장 간단한 기능은 아닙니다. 슬롯을 사용하는 방법과 Vue 애플리케이션에서 슬롯을 사용하는 방법에 대한 몇 가지 예를 살펴보겠습니다.

Vue 2.6의 최근 릴리스에서는 슬롯 사용 구문이 더 간결해졌습니다. 슬롯에 대한 이러한 변경으로 인해 Vue 기반 프로젝트에 재사용성, 새로운 기능 및 더 명확한 가독성을 제공하는 슬롯의 잠재적인 힘을 발견하는 데 다시 관심을 갖게 되었습니다. 슬롯은 진정으로 무엇을 할 수 있습니까?

Vue가 처음이거나 버전 2.6의 변경 사항을 보지 못한 경우 계속 읽으십시오. 슬롯에 대해 배우기 위한 가장 좋은 리소스는 Vue 자체 문서일 수 있지만 여기에서 요약을 제공하려고 합니다.

슬롯이란?

슬롯은 엄격한 부모-자식 관계가 아닌 다른 방식으로 구성 요소를 구성할 수 있게 해주는 Vue 구성 요소의 메커니즘입니다. 슬롯은 콘텐츠를 새로운 위치에 배치하거나 구성 요소를 보다 일반적으로 만들 수 있는 콘센트를 제공합니다. 그들을 이해하는 가장 좋은 방법은 행동을 보는 것입니다. 간단한 예부터 시작하겠습니다.

 // frame.vue <template> <div class="frame"> <slot></slot> </div> </template>

이 구성 요소에는 래퍼 div 가 있습니다. div 가 콘텐츠 주위에 스타일 프레임을 만들기 위해 있다고 가정해 보겠습니다. 이 구성 요소는 일반적으로 원하는 콘텐츠 주위에 프레임을 래핑하는 데 사용할 수 있습니다. 어떻게 사용하는지 봅시다. 여기서 frame 구성 요소는 위에서 방금 만든 구성 요소를 나타냅니다.

 // app.vue <template> <frame><img src="an-image.jpg"></frame> </template>

여는 프레임 태그와 닫는 frame 태그 사이에 있는 내용은 slot 이 있는 frame 구성 요소에 삽입되어 slot 태그를 대체합니다. 가장 기본적인 방법입니다. 다음을 채우는 것만으로 슬롯에 들어갈 기본 콘텐츠를 지정할 수도 있습니다.

 // frame.vue <template> <div class="frame"> <slot>This is the default content if nothing gets specified to go here</slot> </div> </template>

이제 대신 다음과 같이 사용합니다.

 // app.vue <template> <frame /> </template>

"여기로 이동하도록 지정되지 않은 경우 기본 콘텐츠입니다."라는 기본 텍스트가 표시되지만 이전과 같이 사용하면 기본 텍스트가 img 태그에 의해 무시됩니다.

점프 후 더! 아래에서 계속 읽기 ↓

다중/명명된 슬롯

구성 요소에 여러 슬롯을 추가할 수 있지만 그렇게 할 경우 그 중 하나를 제외한 모든 슬롯에 이름이 있어야 합니다. 이름이 없는 슬롯이 있으면 기본 슬롯입니다. 여러 슬롯을 만드는 방법은 다음과 같습니다.

 // titled-frame.vue <template> <div class="frame"> <header><h2><slot name="header">Title</slot></h2></header> <slot>This is the default content if nothing gets specified to go here</slot> </div> </template>

동일한 기본 슬롯을 유지했지만 이번에는 제목을 입력할 수 있는 header 라는 슬롯을 추가했습니다. 다음과 같이 사용합니다.

 // app.vue <template> <titled-frame> <template v-slot:header> <!-- The code below goes into the header slot --> My Image's Title </template> <!-- The code below goes into the default slot --> <img src="an-image.jpg"> </titled-frame> </template>

이전과 마찬가지로 기본 슬롯에 콘텐츠를 추가하려면 titled-frame 구성 요소 내부에 직접 삽입하면 됩니다. 이름이 지정된 슬롯에 콘텐츠를 추가하려면 v-slot 지시문을 사용하여 template 태그의 코드를 래핑해야 했습니다. v-slot 뒤에 콜론( : )을 추가한 다음 콘텐츠를 전달할 슬롯의 이름을 작성합니다. v-slot 은 Vue 2.6의 새로운 기능이므로 이전 버전을 사용하는 경우 사용되지 않는 슬롯 구문에 대한 문서를 읽어야 합니다.

범위 지정 슬롯

한 가지 더 알아야 할 것은 슬롯이 데이터/기능을 자식에게 전달할 수 있다는 것입니다. 이것을 보여주기 위해 우리는 슬롯이 있는 완전히 다른 예제 구성 요소가 필요합니다. 하나는 이전 구성보다 훨씬 더 고안되었습니다. 현재 사용자에 대한 데이터를 해당 슬롯에 제공하는 구성 요소를 만들어 문서에서 예제를 복사하도록 정렬해 보겠습니다.

 // current-user.vue <template> <span> <slot v-bind:user="user"> {{ user.lastName }} </slot> </span> </template> <script> export default { data () { return { user: ... } } } </script>

이 구성 요소에는 사용자에 대한 세부 정보가 포함된 user 라는 속성이 있습니다. 기본적으로 구성 요소는 사용자의 성을 표시하지만 사용자 데이터를 슬롯에 바인딩하기 위해 v-bind 를 사용하고 있습니다. 이를 통해 이 구성 요소를 사용하여 하위 항목에 사용자 데이터를 제공할 수 있습니다.

 // app.vue <template> <current-user> <template v-slot:default="slotProps">{{ slotProps.user.firstName }}</template> </current-user> </template>

슬롯에 전달된 데이터에 액세스하려면 v-slot 지시문 값으로 범위 변수의 이름을 지정합니다.

여기에 몇 가지 참고 사항이 있습니다.

  • default 의 이름을 지정했지만 기본 슬롯에는 필요하지 않습니다. 대신 v-slot="slotProps" 를 사용할 수 있습니다.
  • 이름으로 slotProps 을 사용할 필요가 없습니다. 원하는 대로 부를 수 있습니다.
  • 기본 슬롯만 사용하는 경우 내부 template 태그를 건너뛰고 v-slot 지시문을 current-user 태그에 직접 넣을 수 있습니다.
  • 단일 변수 이름을 사용하는 대신 객체 구조화를 사용하여 범위가 지정된 슬롯 데이터에 대한 직접 참조를 생성할 수 있습니다. 즉, v-slot="slotProps" 대신 v-slot="{user}" 를 사용하고 slotProps.user 대신 user 를 직접 사용할 수 있습니다.

이러한 참고 사항을 고려하여 위의 예는 다음과 같이 다시 작성할 수 있습니다.

 // app.vue <template> <current-user v-slot="{user}"> {{ user.firstName }} </current-user> </template>

몇 가지 더 염두에 두어야 할 사항:

  • v-bind 지시문을 사용하여 둘 이상의 값을 바인딩할 수 있습니다. 따라서 예제에서는 user 이상의 작업을 수행할 수 있었습니다.
  • 범위가 지정된 슬롯에도 함수를 전달할 수 있습니다. 많은 라이브러리가 이것을 사용하여 나중에 보게 될 재사용 가능한 기능 구성 요소를 제공합니다.
  • v-slot 의 별칭은 # 입니다. 따라서 v-slot:header="data" 를 작성하는 대신 #header="data" 를 작성할 수 있습니다. 범위 지정 슬롯을 사용하지 않을 때 v-slot:header 대신 #header 를 지정할 수도 있습니다. 기본 슬롯의 경우 별칭을 사용할 때 default 이름을 지정해야 합니다. 즉, #="data" #default="data" 를 작성해야 합니다.

문서에서 배울 수 있는 몇 가지 사소한 사항이 더 있지만 이 문서의 나머지 부분에서 우리가 말하는 내용을 이해하는 데 충분합니다.

슬롯으로 무엇을 할 수 있습니까?

슬롯은 단일 목적을 위해 구축되지 않았거나, 적어도 그렇다면, 원래 의도를 넘어 다양한 작업을 수행할 수 있는 강력한 도구로 발전했습니다.

재사용 가능한 패턴

구성 요소는 항상 props 할 수 있도록 설계되었지만 일부 패턴은 단일 "일반" 구성 요소로 적용하는 것이 실용적이지 않습니다. props 를 통해 콘텐츠의 많은 부분과 잠재적으로 다른 구성 요소를 전달합니다. 슬롯은 패턴의 "외부" 부분을 포함하고 다른 HTML 및/또는 구성 요소가 그 안에 배치되어 "내부" 부분을 사용자 정의할 수 있도록 하여 슬롯이 있는 구성 요소가 패턴을 정의하고 구성 요소에 주입되도록 할 수 있습니다. 슬롯은 고유합니다.

첫 번째 예에서는 버튼과 같은 간단한 것부터 시작하겠습니다. 귀하와 귀하의 팀이 Bootstrap*을 사용하고 있다고 상상해 보십시오. 부트스트랩을 사용하면 버튼이 기본 `btn` 클래스와 `btn-primary`와 같이 색상을 지정하는 클래스로 묶이는 경우가 많습니다. `btn-lg`와 같은 크기 클래스를 추가할 수도 있습니다.

* 나는 당신이 이것을 하는 것을 권장하지도 낙담하지도 않습니다. 나는 단지 나의 예를 위해 뭔가가 필요했고 그것은 꽤 잘 알려져 있습니다.

이제 단순함을 위해 앱/사이트가 항상 btn-primarybtn-lg 를 사용한다고 가정해 보겠습니다. 버튼에 항상 세 가지 클래스를 모두 작성해야 하는 것은 원하지 않거나, 세 가지를 모두 수행하는 것을 기억하는 신인을 신뢰하지 않을 수도 있습니다. 이 경우 세 가지 클래스를 모두 자동으로 포함하는 구성 요소를 만들 수 있지만 콘텐츠를 사용자 정의하려면 어떻게 해야 합니까? button 태그에는 모든 종류의 HTML이 포함될 수 있으므로 prop 은 실용적이지 않으므로 슬롯을 사용해야 합니다.

 <!-- my-button.vue --> <template> <button class="btn btn-primary btn-lg"> <slot>Click Me!</slot> </button> </template>

이제 원하는 콘텐츠와 함께 모든 곳에서 사용할 수 있습니다.

 <!-- somewhere else, using my-button.vue --> <template> <my-button> <img src="/img/awesome-icon.jpg"> SMASH THIS BUTTON TO BECOME AWESOME FOR ONLY $500!!! </my-button> </template>

물론 버튼보다 훨씬 더 큰 것을 사용할 수도 있습니다. 부트스트랩을 고수하면서 모달 또는 최소한 HTML 부분을 살펴보겠습니다. 아직 기능에 대해서는 다루지 않겠습니다.

 <!-- my-modal.vue --> <template> <div class="modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <slot name="header"></slot> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <slot name="body"></slot> </div> <div class="modal-footer"> <slot name="footer"></slot> </div> </div> </div> </div> </template>

이제 이것을 사용합시다:

 <!-- somewhere else, using my-modal.vue --> <template> <my-modal> <template #header><!-- using the shorthand for `v-slot` --> <h5>Awesome Interruption!</h5> </template> <template #body> <p>We interrupt your use of our application to let you know that this application is awesome and you should continue using it every day for the rest of your life!</p> </template> <template #footer> <em>Now back to your regularly scheduled app usage</em> </template> </my-modal> </template>

슬롯에 대한 위 유형의 사용 사례는 분명히 매우 유용하지만 더 많은 작업을 수행할 수 있습니다.

기능 재사용

Vue 구성 요소는 HTML 및 CSS의 전부가 아닙니다. JavaScript로 제작되었으므로 기능에 관한 것이기도 합니다. 슬롯은 기능 을 한 번 만들고 여러 위치에서 사용하는 데 유용할 수 있습니다. 모달 예제로 돌아가서 모달을 닫는 함수를 추가해 보겠습니다.

 <!-- my-modal.vue --> <template> <div class="modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <slot name="header"></slot> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <slot name="body"></slot> </div> <div class="modal-footer"> <!-- using `v-bind` shorthand to pass the `closeModal` method to the component that will be in this slot --> <slot name="footer" :closeModal="closeModal"></slot> </div> </div> </div> </div> </template> <script> export default { //... methods: { closeModal () { // Do what needs to be done to close the modal... and maybe remove it from the DOM } } } </script>

이제 이 구성 요소를 사용할 때 모달을 닫을 수 있는 버튼을 바닥글에 추가할 수 있습니다. 일반적으로 부트스트랩 모달의 경우 버튼에 data-dismiss="modal" 을 추가하면 됩니다. 그러나 우리는 부트스트랩 특정 항목을 이 모달 구성요소에 넣을 구성요소로부터 숨기고 싶습니다. 그래서 우리는 그들에게 그들이 호출할 수 있는 함수를 전달하고 그들은 부트스트랩의 참여에 대해 더 현명하지 않습니다:

 <!-- somewhere else, using my-modal.vue --> <template> <my-modal> <template #header><!-- using the shorthand for `v-slot` --> <h5>Awesome Interruption!</h5> </template> <template #body> <p>We interrupt your use of our application to let you know that this application is awesome and you should continue using it every day for the rest of your life!</p> </template> <!-- pull in `closeModal` and use it in a button's click handler --> <template #footer="{closeModal}"> <button @click="closeModal"> Take me back to the app so I can be awesome </button> </template> </my-modal> </template>

렌더리스 컴포넌트

마지막으로 슬롯을 사용하여 재사용 가능한 기능을 전달하고 거의 모든 HTML을 제거하고 슬롯만 사용할 수 있습니다. 이것이 본질적으로 렌더리스 구성 요소입니다. HTML 없이 기능만 제공하는 구성 요소입니다.

구성 요소를 진정으로 렌더리스로 만드는 것은 루트 요소의 필요성을 제거하기 위해 템플릿을 사용하는 대신 render 기능을 작성해야 하기 때문에 약간 까다로울 수 있지만 항상 필요한 것은 아닙니다. 먼저 템플릿을 사용할 수 있는 간단한 예를 살펴보겠습니다.

 <template> <transition name="fade" v-bind="$attrs" v-on="$listeners"> <slot></slot> </transition> </template> <style> .fade-enter-active, .fade-leave-active { transition: opacity 0.3s; } .fade-enter, .fade-leave-to { opacity: 0; } </style>

이것은 JavaScript가 포함되어 있지 않기 때문에 렌더리스 구성 요소의 이상한 예입니다. 이는 주로 내장된 렌더리스 기능인 transition 의 사전 구성된 재사용 가능한 버전을 생성하기 때문입니다.

예, Vue에는 렌더리스 구성 요소가 내장되어 있습니다. 이 특정 예는 Cristi Jora의 재사용 가능한 전환에 대한 기사에서 가져왔으며 애플리케이션 전체에서 사용되는 전환을 표준화할 수 있는 렌더리스 구성 요소를 만드는 간단한 방법을 보여줍니다. Cristi의 기사는 훨씬 더 깊이 있고 재사용 가능한 전환의 고급 변형을 보여 주므로 확인하는 것이 좋습니다.

다른 예에서 우리는 Promise의 다른 상태(보류 중, 성공적으로 해결됨, 실패함) 동안 표시되는 전환을 처리하는 구성 요소를 만들 것입니다. 이것은 일반적인 패턴이며 많은 코드가 필요하지 않지만 재사용성을 위해 논리를 빼지 않으면 많은 구성 요소를 혼란스럽게 할 수 있습니다.

 <!-- promised.vue --> <template> <span> <slot name="rejected" v-if="error" :error="error"></slot> <slot name="resolved" v-else-if="resolved" :data="data"></slot> <slot name="pending" v-else></slot> </span> </template> <script> export default { props: { promise: Promise }, data: () => ({ resolved: false, data: null, error: null }), watch: { promise: { handler (promise) { this.resolved = false this.error = null if (!promise) { this.data = null return } promise.then(data => { this.data = data this.resolved = true }) .catch(err => { this.error = err this.resolved = true }) }, immediate: true } } } </script>

여기에서 무슨 일이 일어나고 있습니까? 먼저, promisePromise 라는 prop을 받고 있음을 주목하세요. watch 섹션에서 우리는 Promise의 변경 사항을 관찰하고 변경될 때(또는 immediate 속성 덕분에 구성 요소 생성 즉시) 상태를 지우고 then 을 호출하고 Promise를 catch 하여 성공적으로 완료되거나 상태가 업데이트될 때 상태를 업데이트합니다. 실패.

그런 다음 템플릿에서 상태에 따라 다른 슬롯을 표시합니다. 템플릿을 사용하기 위해 루트 요소가 필요했기 때문에 진정으로 렌더링 없는 상태로 유지하는 데 실패했습니다. 관련 슬롯 범위에도 dataerror 를 전달하고 있습니다.

다음은 사용된 예입니다.

 <template> <div> <promised :promise="somePromise"> <template #resolved="{ data }"> Resolved: {{ data }} </template> <template #rejected="{ error }"> Rejected: {{ error }} </template> <template #pending> Working on it... </template> </promised> </div> </template> ...

렌더리스 구성 요소에 somePromise 를 전달합니다. 완료되기를 기다리는 동안 pending 슬롯 덕분에 "작업 중..."이 표시됩니다. 성공하면 "Resolved:"와 해상도 값이 표시됩니다. 실패하면 "거부됨:"과 거부를 일으킨 오류가 표시됩니다. 이제 해당 부분이 자체 재사용 가능한 구성 요소로 추출되기 때문에 이 구성 요소 내의 약속 상태를 더 이상 추적할 필요가 없습니다.

그러면 promised.vue 의 슬롯을 둘러싸는 span 에 대해 무엇을 할 수 있습니까? 이를 제거하려면 template 부분을 제거하고 구성 요소에 render 기능을 추가해야 합니다.

 render () { if (this.error) { return this.$scopedSlots['rejected']({error: this.error}) } if (this.resolved) { return this.$scopedSlots['resolved']({data: this.data}) } return this.$scopedSlots['pending']() }

여기에 너무 까다로운 일이 없습니다. 우리는 단지 몇 가지 if 블록을 사용하여 상태를 찾은 다음 올바른 범위의 슬롯을 반환하고( this.$scopedSlots['SLOTNAME'](...) 을 통해) 관련 데이터를 슬롯 범위에 전달합니다. 템플릿을 사용하지 않을 때 script 태그에서 JavaScript를 가져와서 .js 파일에 삽입하여 .vue 파일 확장명 사용을 건너뛸 수 있습니다. 이렇게 하면 해당 Vue 파일을 컴파일할 때 약간의 성능 저하가 발생합니다.

이 예제는 vue-promised의 일부를 제거하고 약간 수정한 버전으로, 위의 예제가 몇 가지 잠재적인 함정을 다루기 때문에 위의 예제를 사용하는 것보다 권장합니다. 렌더리스 구성 요소의 다른 훌륭한 예도 많이 있습니다. Baleada는 이와 같은 유용한 기능을 제공하는 렌더리스 구성 요소로 가득 찬 전체 라이브러리입니다. 또한 화면에 보이는 것을 기반으로 목록 항목의 렌더링을 제어하기 위한 vue-virtual-scroller 또는 DOM의 완전히 다른 부분으로 콘텐츠를 "텔레포트"하기 위한 PortalVue가 있습니다.

나는 아웃

Vue의 슬롯은 구성 요소 기반 개발을 완전히 새로운 수준으로 끌어 올렸고, 슬롯을 사용할 수 있는 많은 훌륭한 방법을 시연했지만 그 밖에도 셀 수 없이 많은 방법이 있습니다. 어떤 훌륭한 아이디어를 생각할 수 있습니까? 슬롯을 업그레이드할 수 있는 방법은 무엇이라고 생각합니까? 아이디어가 있다면 Vue 팀에 아이디어를 가져오십시오. 신의 축복과 행복한 코딩.