[Vue.js] 기초-슬롯과 믹스인

Taeho Min·2021년 10월 11일
0

Vue.js

목록 보기
7/7

1. 슬롯

Vue.js 컴포넌트는 기본적으로 시작 태그와 종료 태그 사이에 오는 콘텐츠를 무시하고 렌더링 된다. 그러나 컴포넌트가 사용되는 상황에 따라 외부로부터 콘텐츠를 전달받는 편이 컴포넌트를 재사용하기에 유리한 경우가 있다. 이렇게 외부로 부터 콘텐츠를 전달받는 수단을 슬롯이라고 한다.
슬롯은 크게 단일 슬롯과 이름을 가지는 슬롯의 두 종류로 나뉜다.

1.1 단일 슬롯

[단일슬롯 예시]

<div id="app">
    <!-- 콘텐츠를 포함하는 컴포넌트 마운트 -->
    <my-button>전송</my-button>
    
    <!-- 콘텐츠 없이 컴포넌트 마운트 -->
    <my-button></mybutton>
</div>
<script src="./app.js"></script>
...
var MyButton = {
    template: `
        <button>
            <!-- 부모 컴포넌트에서 받아온 컨텐츠로 갈아 끼움 -->
            <slot>OK</slot>
        </button>
    `
}

new Vue({
    el: '#app',
    components: {
        MyButton: MyButton
    }
});

컴포넌트 템플릿에 포함된 slot 요소가 바로 콘텐츠가 삽입되는 자리다. HTML에 첫 번째 <my-button> 요소를 배치하면서 '전송' 이라는 문자열을 콘텐츠로 전달했다. 그러면 자식 컴포넌트의 <slot>OK</slot> 부분이 '전송' 이라는 문자열로 치환된다. 결국 최종 렌더링 결과는 전송이 된다.
두 번째 <my-button>요소는 콘텐츠를 지정하지 않았다. 이 경우는 자식 컴포넌트의 slot 요소에 포함된 콘텐츠의 기본값이 사용된다.

1.2 이름을 갖는 슬롯

slot 요소에 name 속성을 이용해 슬롯에 이름을 붙일 수 있다. 이 이름으로 특정한 슬롯을 지정해 콘텐츠를 삽입할 수 있다.
컴포넌트 중에도 여러 가지 콘텐츠로 구성되는 것들이 있다. 페이지 레이아웃이나 모달 윈도우 등은 헤더와 바디, 푸터 등의 부분으로 이루어 진다. 이런 경우 각각의 부분을 개별 슬롯으로 다룰 수 있도록 이름을 붙인다.

다음 페이지 레이아웃을 예제를 살펴보면, 이 컴포넌트는 slot 요소를 3개 포함한다. 이 중 헤더와 푸터 슬롯에는 이름이 붙어 있고, 바디는 이름 없는 슬롯, 그러니까 앞서 설명한 단일 슬롯이다. 이런 식으로 단일 슬롯과 이름을 갖는 슬롯을 함께 사용할 수 있다.

[이름을 갖는 슬롯 예시 - 페이지 레이아웃]

var MyPage = {
    template: `
        <div>
            <header>
                <!-- 헤더 슬롯(이름을 갖는 슬롯) -->
                <slot name="header"></slot>
            </header>
            <main>
                <!-- 바디 슬롯 -->
                <slot></slot>
            </main>
            <footer>
                <!-- 푸터 슬롯(이름을 갖는 슬롯) -->
                <slot name="footer"></slot>
            </footer>
        </div>
    `
}

new Vue({
    el: '#app',
    components: {
        MyPage: MyPage
    }
})
<!DOCTYPE html>
<title>Vue app</title>
<script scr="https://unpkg.com/vue@2.5.17"></script>

<div id="app">
    <my-page>
        <!-- name 속성값이 header의 <slot>과 치환됨 -->
        <h1 slot="header">This is my page</h1>
        
        <!-- 단일 슬롯과 치환되는 콘텐츠 -->
        <p>
            Lorem ipsum dolor sit amet, duo ex illum debet inermis....
        </p>
        
        <!-- name 속성값이 footer의 <slot>과 치환 됨 -->
        <p slot="footer">This is footer</p>
    </my-page>
</div>
<script src="./app.js"></script>

1.3 슬롯의 범위

아래 예제에서 textLabel은 어느 쪽 데이터와 바인딩 되는가? 정답은 부모 컴포넌트다. 다시 말해 {{ textLabel }}에 들어갈 내용은 parent가 된다.
슬롯에 삽입되는 콘텐츠는 부모 컴포넌트의 유효 범위에 속한다. Vue.js는 부모 컴포넌트의 템플릿에서 일어난 데이터 바인딩은 슬롯으로 삽입되는 콘텐츠라도 부모 컴포넌트의 유효 범위를 적용한다.

<!DOCTYPE html>
<title>Vue app</title>
<script scr="https://unpkg.com/vue@2.5.17"></script>

<div id="app">
    <!-- "parent"와 "child" 중 어느 것이 참조 되는가 -->
    <my-button>{{ textLabel }}</my-button>
</div>
<script src="./app.js"></script>
var MyButton = {
    data: function() {
        return {
            textLabel: 'child'
        }
    },
    template: `
        <button>
            <slot>OK</slot>
        </button>
    `
}

new Vue({
    el: '#app',
    data: function() {
        return {
            textLabel: 'parent'
        }
    },
    components: {
        MyButton: MyButton
    }
})

1) 범위를 가지는 슬롯

슬롯에 삽입되는 컨텐츠에 대한 데이터 바인딩은 부모 컴포넌트의 유효 범위가 적용되는데, 그러나 컴포넌트를 사용하는 쪽에서 컴포넌트의 동작을 제어하려는 상황에서는 자식 컴포넌트의 데이터에 접근할 필요가 있다.

TODO리스트를 예로 살펴보면, 리스트뷰의 기본 로직은 그대로 두고, 리스트 아이템의 뷰 구조를 리스트 아이템을 배치할 위치에 맞춰 수정하거나 표시할 데이터를 요약해야 하는 경우가 있다.

이런 경우 보모 컴포넌트에서 각 리스트 아이템의 데이터(자식 컴포넌트의 데이터)에 접근해야 한다.

다음은 TodoList 컴포넌트의 정의다. 자식 컴포넌트에서 부모 컴포넌트로 데이터를 전달하기 위해 자식 컴포넌트의 slot요소에 v-bind를 사용했다.

var TodoList = {
    props: {
        todos: {
            type: Array,
            required: true
        }
    },
    template: `
        <ul>
            <template v-for="todo in todos">
                <!-- v-bind 디렉티브를 사용해 todo를 부모 컴포넌트에 전달 -->
                <slot :todo="todo">
                    <li :key="todo.id">
                        {{ todo.text }}
                    </li>
                </slot>
            </template>
        </ul>
    `
}

new Vue({
    el: '#app',
    data: function() {
        return {
            todos: [
                { id: 1, text: 'C++', isCompleted: true },
                { id: 2, text: 'JavaScript', isCompleted: false },
                { id: 3, text: 'Java', isCompleted: true },
                { id: 4, text: 'Perl', isCompleted: false },
            ]
        }
    },
    components: {
        TodoList: TodoList,
    }
})

데이터를 전달받을 때는 slot-scope 프로퍼티를 사용해 데이터를 범위(객체)로 만들어 전달 받는다. 속성값인 slotProps가 이 범위의 이름이 된다. slotProps를 통해 todo 데이터를 참조할 수 있다. 이 예제는 isComplated 프로퍼티가 true인 데이터만을 노출하도록 한 것이다.

<!DOCTYPE html>
<title>Vue app</title>
<script scr="https://unpkg.com/vue@2.5.17"></script>

<div id="app">
    <todo-list :todos="todos">
        <li slot-scope="slotProps" v-if="slotProps.todo.isCompleted">
            {{ slotProps.todo.text }}
        </li>
    </todo-list>
</div>
<script src="./app.js"></script>

예제를 조금 수정해서 개선해 보자. 넘겨받은 데이터를 사용하기 위해 매번 slotProps에 접근하는 것은 번거롭다. ES2015의 분할 대입 문법을 활용해서 매번 slotProps에 접근할 필요 없이 데이터를 사용해 보자.
속성값을 분할 대입하는 표현식으로 수정하고 변수 todo를 선언하면 템플릿에서 직접 todo를 참조할 수 있다. 분할 대입은 배열이나 객체에 값을 대입하는 문법을 간략화한 것이다.

<!DOCTYPE html>
<title>Vue app</title>
<script scr="https://unpkg.com/vue@2.5.17"></script>

<div id="app">
    <todo-list :todos="todos">
        <li slot-scope="{ todo }" v-if="todo.isCompleted">
            {{ todo.text }}
        </li>
    </todo-list>
</div>
<script src="./app.js"></script>

컴포넌트와 믹스인의 차이점

사용자 정의 디렉티브는 컴포넌트나 미스인과는 별개의 것이다.
믹스인과 사용자 정의 디렉티브, 컴포넌트는 모두 코드 재사용을 돕는 기능이라는 공통점은 있으나, 재사용을 어떤 방식으로 하느냐에 차이가 있다.

컴포넌트는 커다란 Vue 인스턴스(컴포넌트)를 여러 개의 부품으로 분할해 요소로 재사용하는 데 적합하다. 컴포넌트는 여러 개의 HTML 요소로 구성되며 템플릿을 포함한다.

이와 달리 믹스인은 템플릿을 다루지 않는다. 여러 개의 컴포넌트나 인스턴스에서 공유할 수 있도록 로직을 재사용 가능한 덩어리로 분할하는 데 적합하다.

사용자 정의 디렉티브는 앞에서 설명했듯이 저수준 DOM 접근 기을을 요소에 추가하기 위한 기능이다. 이 중 한가지를 선택하기 전에 해결하려는 문제에 적합한 기능이 무엇인지 충분히 검토해야 한다.

기능 재사용 방법
사용자 정의 디렉티브 DOM 요소에 접근하는 처리를 공통화한 것. 속성으로 지정함
컴포넌트 재사용, 관리가 편하도록 Vue 인스턴스를 분할, 요소로 지정함
믹스인 Vue 인스턴스와 컴포넌트 간에 공유할 수 있는 기능을 분리하려는 용도, 컴포넌트와 달리 템플릿을 포함하지 않음

2. 믹스인

믹스인은 기능을 재사용하기 위한 메커니즘이다. 객체로 표현한 기능을 각 컴포넌트에 전달 할 수 있다.

Vue.js 애플리케이션을 구현하다 보면 컴포넌트를 여러 개 정의하게 된다. 이 중 서로 다른 컴포넌트인데도 같은 기능을 공유하는 경우가 상당히 자주 있다.

UI 조작에 따라 구글 애널리틱스에 이벤트를 전송하는 기능을 예로 들 수 있다. 믹스인은 이렇게 범용 기능만을 따로 추출해서 여러 컴포넌트가 공유할 수 있게 한 것이다.

같은 코드는 한곳에 한벌만 존재해야 나중에 수정하기도 쉽고 유지 보수성도 향상된다. 여러 컴포넌트에 같은 코드를 반복해서 작성하고 있다면 리팩토링을 통해 이 반복되는 코드를 믹스인으로 추출하는 것이 좋다.

Vue.js의 믹스인은 단일 기능을 여거 컴포넌트에서 공유하는 경우뿐만 아니라 여러 책임을 갖는 단일 컴포넌트의 코드를 분할하는 데도 유용한 기능이다.

2.1 믹스인으로 기능 재사용하기

[SNS 공유버튼 예시]

<!DOCTYPE html>
<title>Vue app</title>
<link href="https://use.fontawesome.com/releases/v.5.0.6/css/all.css" rel="stylesheet">
<script scr="https://unpkg.com/vue@2.5.17"></script>

<div id="app">
    <icon-share-button></icon-share-button>
    <text-share-button></text-share-button>
</div>
<script src="./app.js"></script>

var IconShareButton = {
    template: `
    	<button @click="share"><i class="fas fa-share-aquare"></i></button>
    `,
    data: function() {
        return {
            _isProcessing: false
        }
    },
    mothods: {
        share: function() {
            if (this.is_Processing) {
                return
            }
            if ( !window.confirm('공유하시겠습니까?') ) {
                return
            }
            this._isProcessing = true
            // 실제 구현이라면 SNS서비스의 API를 호출할 부분
            setTimeout(() => {
                window.alert('공유되었습니다.')
                this._isProcessing = false
            }, 300)
        }
    }
}
var TextShareButton = {
    template: `
    	<button @click="share">{{ buttonLabel }}</button>
    `,
    data: function() {
        return {
            _isProcessing: false
        }
    },
    mothods: {
        share: function() {
            if (this.is_Processing) {
                return
            }
            if ( !window.confirm('공유하시겠습니까?') ) {
                return
            }
            this._isProcessing = true
            // 실제 구현이라면 SNS서비스의 API를 호출할 부분
            setTimeout(() => {
                window.alert('공유되었습니다.')
                this._isProcessing = false
            }, 300)
        }
    }
}

new Vue({
    el: '#app',
    components: {
        IconShareButton,
        TextShareButton
    }
})

위 HTML을 실행하면 공유 아이콘 버튼과 '공유하기' 레이블이 붙은 버튼이 나란히 보인다. 두 버튼 모두 share라는 메서드를 갖고 있다. 코드를 보면 알 수 있듯이 IconShareButton과 TextShareButton 컴포넌트는 UI의 외관은 다르지만, 동작하는 로직은 완전히 같다. 이 공통되는 로직을 믹스인으로 추출한다.

[수정된 SNS 공유버튼 예시]

var Sharable = {
    data: function() {
        return {
            _isProcessing: false
        }
    },
    mothods: {
        share: function() {
            if (this.is_Processing) {
                return
            }
            if ( !window.confirm('공유하시겠습니까?') ) {
                return
            }
            this._isProcessing = true
            // 실제 구현이라면 SNS서비스의 API를 호출할 부분
            setTimeout(() => {
                window.alert('공유되었습니다.')
                this._isProcessing = false
            }, 300)
        }
    }
}

var LogoShareButton = {
    mixins: [Sharable],
    template: `
        <button @click="share"><i class="fas fa-share-aquare"></i></button>
    `
}

var TextShareButton = {
    mixins: [Sharable],
    template: `
        <button @click="share">{{ buttonText }}</button>
    `,
    data: function() {
        return {
            buttonLabel: '공유하기'
        }
    }
}

new Vue({
    el: '#app',
    components: {
        LogoShareButton,
        TextShareButton
    }
})

Sharable 믹스인을 정의했다. 코드를 보면 알 수 있듯이 이 믹스인은 컴포넌트의 옵션과 같은 프로퍼티를 갖는 평범한 객체다.
믹스인이 갖는 기능을 컴포넌트에 추가하려면 컴포넌트 옵션의 mixins 프로퍼티의 배열에 믹스인 객체를 요소로 추가한다.
mixins 프로퍼티의 값이 배열인 이유는 컴포넌트에 여러 믹스인의 기능을 추가할 수 있도록 하기 위한 것이다.
믹스인 객체는 컴포넌트 옵션과 같은 프로퍼티를 갖는다. 믹스인 옵션에 담긴 정보는 컴포넌트 옵션과 통합된다. mounted와 create 같은 훅 함수도 믹스인과 컴포넌트에서 정의된 것을 모두 컴포넌트에서 호출할 수 있다.

var Sharable = {
    data: function() {
        return {
            _isProcessing: false
        }
    },
    created: function() {
        console.log('Sharable 믹스인의 훅이 호출됨')
    },
    mothods: {
        share: function() {
            if (this.is_Processing) {
                return
            }
            if ( !window.confirm('공유하시겠습니까?') ) {
                return
            }
            this._isProcessing = true
            // 실제 구현이라면 SNS서비스의 API를 호출할 부분
            setTimeout(() => {
                window.alert('공유되었습니다.')
                this._isProcessing = false
            }, 300)
        }
    }
}

var IconShareButton = {
    mixins: [Sharable],
    created: function () {
        console.log('IconShareButton의 훅이 호출되었음.')
    },
    template: `
        <button @click="share"><i class="fas fa-share-aquare"></i></button>
    `
}

var TextShareButton = {
    mixins: [Sharable],
    created: function () {
        console.log('TextShareButton의 훅이 호출되었음.')
    },
    template: `
        <button @click="share">공유하기</button>
    `
}

new Vue({
    el: '#app',
    components: {
        IconShareButton,
        TextShareButton
    }
})

콘솔에서 출력된 내용을 통해 믹스인부터 컴포넌트 순서로 훅 함수가 호출된다는 것을 알 수 있다. 믹스인을 하나 이상 추가했다면 mixins 옵션값 배열에 들어 있는 순서대로 훅이 실행되며 그 다음에 컴포넌틑 훅 함수가 호출된다.

Sharable 믹스인의 훅이 호출됨.
IconShareButton의 훅이 호출되었음.
Sharable 믹스인의 훅이 호출됨.
TextShareButton의 훅이 호출되었음.

method, components, directives 등의 옵션도 믹스인과 컴포넌트의 옵션이 합쳐져 하나의 옵션으로 취급된다.
여기서 주의할 점은 믹스인과 컴포넌트가 같은 프로퍼티를 갖고 있는 경우 컴포넌트 옵션이 우선한다는 점이다.

2.2 전역 미스인

애플리케이션 전체에 적용되는 믹스인을 전역 믹스인 이라고 한다. 이 믹스인은 애플리케이션에서 생성한 모든 Vue 인스턴스에 영향을 미친다.
각 컴포넌트의 mixins 프로퍼티에 믹스인 객체를 담을 필요 없이 모든 컴포넌트에 적용된다. 영향 범위가 넓으므로 신중하게 사용해야 한다.

그렇다면 전역 미스인은 어떤 경우에 사용하는 기능일까? 대표적인 케이스로 모든 Vue.js 컴포넌트와 인스턴스의 옵션 객체에 사용자 정의 옵션을 추가하는 경우를 들 수 있다.

예를 들어 로그인 기능이 있는 애플리케이션에는 비 로그인 상태에서는 보여주고 싶지 않은 페이지가 있을 것이다. 이런 로직을 각 컴포넌트에서 일일이 다시 구현하기는 번거롭다. 그러나 전역 미슷인을 사용하면 로직을 믹스인으로 정의한 다음, 옵션에 auth: true 라고 지정하기만 하면 한곳에 있는 로직을 선언적으로 사용할 수 있다.

전역 믹스인을 사용하는 또 한 가지 경우는 애플리케이션 전체에서 참조하는 상태나 프로퍼티가 필요한 경우다. 앞서 언급한 로그인 기능으로 치면 로그인한 사용자를 나타내는 객체가 이에 해당한다.

[전역 미스인 예시]

Vue.mixin({
    data: function() {
        return {
            loggedInUser: null
        }
    },
    created: function() {
        var auth = this.$options.auth
        this.loggedInUser = JSON.parse(sessionStorage.getItem('loggedInUser'))
        if ( auth && !this.loggedInUser ) {
            window.alert('이 페이지는 로그인이 필요합니다.)
        }
    }
})

var LoginRequiredPage = {
    auth: true,
    template: `
        <div>
            <p v-if="!loggedInUser">
                이 페이지는 로그인이 필요합니다
            </p>
            <p v-else>
                {{ loggedInUser.name }}님으로 로그인했습니다
            </p>
        </div>
    `
}

new Vue({
    el: '#app',
    components: {
        LoginRequiredPage
    }
})

<!DOCTYPE html>
<title>Vue app</title>
<script scr="https://unpkg.com/vue@2.5.17"></script>

<div id="app">
    <login-required-page></login-required-page>
</div>
<script src="app.js"></script>

전역 믹스인을 등록하려면 Vue.mixin에 믹스인 객체를 전달하면 된다. created 훅으로 스토리지에서 로그인된 사용자의 정보를 받은 다음, 스토리지에 데이터가 없으면 컴포넌트 옵션의 auth 프로퍼티가 참이 경우에만 "이 페이지는 로그인이 필요합니다."라는 알림창을 표시한다.

믹스인의 명명규칙
믹스인은 로직을 공통화시킬 수 있는 편리한 기능이다. 그러나 믹스인을 사용할 때는 주의가 필요하다. 믹스인 하나에 너무 많은 기능을 욱여 넣으면 컴포넌트에 불필요한 기능이 들어가 오히려 각각의 기능을 재사용하기 어렵게 된다.
가능한 한 단일하고 작은 기능을 믹스인으로 정의하고, 믹스인의 이름에 어떤 기능을 담고 있는지 한눈에 알 수 있도록 하는 것이 좋다.
믹스인의 이름을 정하는 한 가지 원칙으로 '동사 + able', 즉 '할 수 있는 것'이라는 의미로 이름을 붙이는 방법이 있다. 예를 들어 모달창을 여는 기능의 openMocal 메서드의 기능을 믹스인으로 제공한다면 'ModalOpenable' 이라고 이름을 붙이면 된다.

# 참고자료

  • Vue.js 철저 입문 [위키북스]
profile
개발자

0개의 댓글