6 컴포넌트 심화 학습

source: categories/study/vue-project/vue-project_6.md

6 컴포넌트 심화 학습

  • Vue의 가장 큰 장점 중 하나는 컴포넌트를 재활용하는 데 있습니다.
  • 이번 챕터에서는 컴포넌트에서 다른 컴포넌트를 사용하는 방법에 대해서 알아봅니다.
  • 컴포넌트 간의 데이터 및 이벤트 전달 방법 및 컴포넌트의 재활용성을 높여주는 기능 중 하나인 slot을 이용해서 일관성 있는 UI를 개발하는 방법에 대해서 익히게 됩니다.

6.1 컴포넌트 안에 다른 컴포넌트 사용하기

6.1.1 부모 컴포넌트와 자식 컴포넌트

<!-- PageTitle.vue -->
<template>
    <h2>Page Title</h2>
</template>
<!-- NestedComponent.vue -->
<template>
    <div>
        <PageTitle></PageTitle>
    </div>
</template>

<script>
    import PageTitle from '../components/PageTitle'; // 컴포넌트 import
    export default {
        components: {
            PageTitle, // 현재 컴포넌트에서 사용할 컴포넌트 등록
        }
    }
</script>

6.1.2 부모 컴포넌트에서 자식 컴포넌트로 데이터 전달하기: Props

  • 현재는 PageTitle은 지정된 타이틀인 "Page Title"이 출력되고 있습니다.
  • 우리는 PageTitle 컴포넌트를 호출할 때 각 페이지의 실제 타이틀을 데이터로 전달하고, PageTitle 컴포넌트에서는 이를 받아서 출력하도록 변경해 보겠습니다.
  • PageTitle.vue 파일을 다음과 같이 수정합니다.

<!-- PageTitle.vue -->
<template>
    <h2>{{ title }}</h2>
</template>

<script>
    export default {
        props: {
            title: {
                type: String,
                default: '페이지 제목입니다.'
            }
        }
    }
</script>

<!-- NestedComponent.vue -->
<template>
    <div>
        <PageTitle title="부모 컴포넌트에서 자식 컴포넌트로 데이터 전달"></PageTitle>
    </div>
</template>

<script>
    import PageTitle from '../components/PageTitle'; // 컴포넌트 import
    export default {
        components: {
            PageTitle, // 현재 컴포넌트에서 사용할 컴포넌트 등록
        }
    }
</script>
6.1.2.1 정적/동적 Prop 전달
  • 자식 컴포넌트인 PageTitle.vue로 title="컴포넌트 사용 예제 페이지" 정적 값을 전달하는 것을 확인했습니다.
  • v-bind or 약어(:) 문자를 사용해서 prop에 동적인 값을 전달할 수 있습니다.
<template>
    <page-title :title="title"></page-title>
</template>

<script>
    import PageTitle from '../components/PageTitle'; // 컴포넌트 import
    export default {
        components: {
            PageTitle, // 현재 컴포넌트에서 사용할 컴포넌트 등록
        },
        data() {
            return {
                title: '동적 페이지 타이틀'
            }
        }
    }
</script>
6.1.2.2 숫자형(Number) 전달
  • 숫자 값을 prop로 전달하기 위해서는 v-bind를 통해서만 가능합니다.
<blog-post likes="42"></blog-post>
  • v-bind를 사용하지 않은 경우는 위와 같이 전달하면 전달한 42는 숫자 42가 아니라, 문자 "42"가 됩니다.
  • 숫자 값으로 전달하기 위해선 v-bind를 사용해서 정적으로 전달하거나 동적으로 전달해야 합니다.
<!-- 42는 정적이지만, v-bind를 사용함으로써 전달되는 데이터가 자바스크립트 표현식이 됩니다. -->
<blog-post :likes="42"></blog-post>

<!-- 변수 값에 동적으로 할당합니다. -->
<blog-post :likes="post.likes"></blog-post>
6.1.2.3 논리 자료형(Boolean) 전달
  • 논리 자료형 역시 v-bind을 사용하지 않으면 문자열로 전달되기 때문에, v-bind를 사용해야 합니다.
<!-- true는 정적이지만, v-bind를 사용함으로써 전달되는 데이터가 자바스크립트 표현식이 됩니다. -->
<blog-post :is-published="true"></blog-post>

<!-- 변수 값에 동적으로 할당합니다. -->
<blog-post :is-published="isShow"></blog-post>
6.1.2.4 배열(Array) 전달
  • 배열 역시 v-bind을 사용하지 않으면 문자열로 전달되기 때문에, v-bind를 사용해야 합니다.
<!-- 배열이 정적이지만, v-bind를 사용함으로써 전달되는 데이터가 자바스크립트 표현식이 됩니다. -->
<blog-post :comment-ids="[234, 266, 273]"></blog-post>

<!-- 변수 값에 동적으로 할당합니다. -->
<blog-post :comment-ids="post.commentIds"></blog-post>
6.1.2.5 객체(Object) 전달
  • 객체(Object) 역시 v-bind을 사용하지 않으면 문자열로 전달되기 때문에, v-bind를 사용해야 합니다.
<!-- 객체가 정적이지만, v-bind를 사용함으로써 전달되는 데이터가 자바스크립트 표현식이 됩니다. -->
<blog-post :author="{name: 'Veronica', 'Veridian Dynamics'}"></blog-post>

<!-- 변수 값에 동적으로 할당합니다. -->
<blog-post :author="post.author"></blog-post>
6.1.2.6 객체(Object)의 속성 전달
  • 객체(Object) 역시 v-bind를 사용하지 않으면 문자열로 전달되기 때문에, v-bind를 사용해야 합니다.
  • 다음 두 개의 코드는 동일합니다.
<blog-post v-bind="post"></blog-post>

<blog-post :id="post.id" :title="post.title"></blog-post>

<script>
    export default {
        data() {
            return {
                post: {id: 1, title: 'Vue 3 프로젝트 투입 일주일 전'}
            }
        }
    }
</script>
6.1.2.7 prop 유효성 검사
  • 데이터 타입
  • 기본 값(default)
  • 필수 여부(required)
  • 유효성 검사 함수(validator)
<script>
    export default {
        props: {
            // 기본 타입 체크 ('null'과 'undefined'는 모든 타입 유효성 검사를 통과합니다.)
            propA: Number, // Number 타입 체크
            propB: [String, Number], // 여러 타입 체크
            propC: { // 문자형이고 부모 컴포넌트로부터 반드시 데이터가 필수로 전달되어야 함
                type: String,
                required: true,
            },
            propD: { // 기본 값(100)을 갖는 숫자형
                type: Number,
                default: 100,
            },
            propE: { // 기본 값을 갖는 객체 타입
                type: Object,
                // 객체나 배열의 기본 값은 항상 팩토리 함수로부터 반환되어야 합니다.
                default: function () {
                    return { message: 'hello' }
                }
            },
            propF: { // 커스텀 유효성 검사 함수
                validator: function (value) {
                    // value 값이 꼭 아래 세 문자열 중 하나와 일치해야 합니다.
                    return ['success', 'warning', 'danger'].indexOf(value) !== -1;
                }
            },
            propG: { // 기본 값을 갖는 함수
                type: Function,
                // 객체나 배열과 달리 아래 표현은 팩토리 함수가 아닙니다.
                // 기본값으로 사용되는 함수입니다.
                default: function () {
                    return 'Default function'
                }
            }
        }
    }
</script>

6.1.3 부모 컴포넌트에서 자식 컴포넌트의 이벤트 직접 발생시키기

  • 부모 컴포넌트에서 자식 컴포넌트의 버튼을 클릭하는 이벤트를 직접 발생시켜 보겠습니다.
<!-- ChildComponent.vue -->
<template>
    <button type="button" @click="childFunc" ref="btn">click</button>
</template>

<script>
    export default {
        methods: {
            childFunc() {
                console.log('부모 컴포넌트에서 직접 발생시킨 이벤트');
            }
        }
    }
</script>
  • 자식 컴포넌트에 버튼 객체에 ref="btn"로 접근할 수 있도록 작성되었습니다.

  • HTML 태그에 ref="id"를 지정하면 Vue 컴퍼넌트의 함수에서 this.$refs를 통해 접근이 가능하게 됩니다.
  • ref 속성은 HTML 태그에서 사용되는 id와 비슷한 기능을 한다고 생각하시면 됩니다.
  • ref는 유일한 키 값을 사용해야 합니다.
<!-- ParentComponent.vue -->
<template>
    <child-component @send-message="sendMessage" ref="child_component"></child-component>
</template>

<script>
    import ChildComponent from './ChildComponent';
    export default {
        components: {
            ChildComponent,
        },
        mounted() {
            this.$refs.child_component.$refs.btn.click();
        }
    }
</script>
  • 부모 컴포넌트에서 자식 컴포넌트인 child-component에 ref="child_component"를 지정하여, $refs로 접근할 수 있도록 했습니다.

  • 이렇게 설정하면 부모 컴포넌트에서 자식 컴포넌트 안에 정의된 HTML 객체에 대한 접근이 가능해지고, 자식 컴포넌트의 버튼 객체에 정의한 ref="btn" 이름으로 버튼 객체에 접근해서 click() 이벤트를 발생시킬 수 있게 됩니다.

6.1.4 부모 컴포넌트에서 자식 컴포넌트의 함수 직접 호출하기

  • 부모 컴포넌트에서 자식 컴포넌트에 정의된 함수를 직접 호출해 보겠습니다.
<!-- ChildComponent2.vue -->
<!-- ... -->
<script>
    export default {
        methods: {
            callFromParent() {
                console.log('부모 컴포넌트에서 직접 호출한 함수');
            }
        }
    }
</script>
  • 자식 컴포넌트에 함수가 정의되어 있습니다.
<template>
    <child-component @send-message="sendMessage" ref="child_component"></child-component>
</template>

<script>
    import ChildComponent from './ChildComponent2';
    export default {
        components: {
            ChildComponent,
        },
        mounted() {
            this.$refs.child_component.callFromParent();
        }
    }
</script>
  • 부모 컴포넌트에서는 자식 컴포넌트를 $refs를 사용하여 접근하게 되면 자식 컴포넌트 내에 정의된 모든 함수를 호출할 수 있습니다.

6.1.5 부모 컴포넌트에서 자식 컴포넌트의 데이터 옵션 값 직접 변경하기

  • this.$refs.child_component.msg = "부모 컴포넌트가 변경한 데이터";
  • $refs를 통해서 자식 컴포넌트에 접근하고 나면 자식 컴포넌트에 정의된 데이터 옵션을 직접 변경할 수 있게 됩니다.

6.1.6 자식 컴포넌트에서 부모 컴포넌트로 이벤트/데이터 전달하기(커스텀 이벤트)

  • 자식 컴포넌트에서 부모 컴포넌트로 이벤트를 전달하기 위해서 $emit을 사용합니다.
  • mounted안에.. this.$emit('send-message', this.msg)
  • 자식 컴포넌트가 mounted가 되면 $emit을 통해 부모 컴포넌트의 send-message 이벤트를 호출합니다.
  • 이때 msg 데이터를 파라미터로 전송합니다.

6.1.7 부모 컴포넌트에서 자식 컴포넌트의 데이터 옵션 값 동기화하기

  • 부모 컴포넌트에서 computed를 이용하면 자식 컴포넌트에 정의된 데이터 옵션값의 변경사항을 항상 동기화시킬 수 있습니다.
<template>
    <button type="button" @click="childFunc" ref="btn">자식 컴포넌트 데이터 변경</button>
</template>

<script>
    export default {
        data() {
            return {
                msg: '메시지'
            }
        },
        methods: {
            childFunc() {
                this.msg = '변경된 메시지';
            }
        }
    }
</script>
<template>
    <button type="button" @click="checkChild">자식 컴포넌트 데이터 조회</button>
    <child-component ref="child_component"></child-component>
</template>

<script>
    import ChildComponent from './ChildComponent';
    export default {
        components: {
            ChildComponent,
        },
        computed: {
            msg() {
                return this.$refs.child_component.msg;
            }
        },
        methods: {
            checkChild() {
                alert(this.msg);
            }
        }
    }
</script>
  • 부모 컴포넌트에는 computed 옵션을 사용해서, 자식 컴포넌트의 msg 값을 감지하도록 했습니다.
  • computed는 참조되고 있는 데이터의 변경사항을 바로 감지하여 반영할 수 있다고 했습니다.

  • computed 옵션을 이용하면 자식 컴포넌트의 데이터가 변경될 때마다 $emit을 통해 변경된 데이터를 전송하지 않아도 변경된 데이터 값을 항상 확인할 수 있습니다.

6.2 Slot

  • 우리는 앞서 컴포넌트는 재활용 가능하며, 컴포넌트는 여러 개의 컴포넌트를 자식 컴포넌트로 import해서 사용할 수 있다는 것을 배웠습니다.
  • 프로젝트를 진행하다 보면 어떤 화면의 경우는 굉장히 비슷한 UI와 기능을 가지고 있는데, 아주 일부만 다른 경우가 있습니다.

  • slot은 컴포넌트 내에서 다른 컴포넌트를 사용할 때 쓰는 컴포넌트의 마크업을 재정의하거나 확장하는 기능입니다.
  • 컴포넌트의 재활용성을 높여주는 기능입니다.
  • 다음과 같은 팝업(Modal)은 애플리케이션을 개발할 때 굉장히 많은 화면에서 사용하게 됩니다.
  • 일반적으로 팝업창은 header, main, footer로 이루어집니다.
<div class="container">
    <header>
        <!-- header 컨텐츠 -->
    </header>
    <main>
        <!-- main 컨텐츠 -->
    </main>
    <footer>
        <!-- footer 컨텐츠 -->
    </footer>
</div>
  • 좋은 어플리케이션은 단순한 팝업창일지라도 애플리케이션 내에서 사용되는 모든 팝업창의 디자인을 유형에 따라 동일하게 유지시킵니다.
  • 그래야 사용자에게 동일한 사용자 경험(UX)을 줄 수 있기 때문입니다.

  • 여러 명의 개발자가 애플리케이션을 개발하면 동일한 유형의 팝업창일지라도 디자인이 다르게 적용되는 경우가 종종 발생합니다.
  • 예를 들면 팝업창 타이틀 영역의 폰트 크기나 배경색이 조금씩 다르거나, 버튼이 있는 Footer 영역의 배경색이 다를 수 있습니다.
  • 이렇게 개발자마다 팝업창의 디자인을 다르게 만들면 전체 애플리케이션을 이용하는 사용자 입장에서는 일관성 없는 디자인으로 인해 좋지 않은 경험을 가질 수 있습니다.

  • Vue에서는 slot을 이용해서 이런 부분을 해결할 수 있습니다.
  • 팝업의 기본 틀에 해당하는 컴포넌트를 slot을 이용해서 만들고, 개발자에게 제공합니다.
  • 개발자는 팝업 디자인의 통일성을 유지하면서 컨텐츠에 해당하는 부분만 작성하면 됩니다.

  • 다음은 팝업의 기본 틀에 해당하는 modal-layout 컴포넌트입니다.
<!-- SlotModalLayout.vue -->
<div class="modal-container">
    <header>
        <slot name="header"></slot>
    </header>
    <main>
        <slot></slot>
    </main>
    <footer>
        <slot name="footer"></slot>
    </footer>
</div>
  • 이렇게 slot에 name을 지정해서 사용하는 slot을 ‘Named Slots'이라고 합니다.
  • 이 컴포넌트는 정해진 html 구조를 갖게 됩니다.
  • 이렇게 작성된 컴포넌트를 제공하고, 개발자는 각 slot에 해당하는 코드만 작성하면 되기 때문에, 어떤 개발자가 구현하더라도 동일한 디자인의 팝업을 만들 수 있게 됩니다.

  • slot을 사용하는 컴포넌트에서는 삽입한 컴포넌트(책에서는 modal-layout) 안에서 다음과 같이 template 태그를 사용하여 html 태그를 작성할 수 있습니다.
  • 이때 v-slot:(slot 이름) 디렉티브를 사용해서 동일한 이름의 slot 위치로 html 코드가 삽입됩니다.
  • Name이 없는 slot은 v-slot:default로 지정하면 됩니다.
<modal-layout>
    <template #header>
        <h1>팝업 타이틀</h1>
    </template>
    <template #default>
        <p>팝업 컨텐츠 1</p>
        <p>팝업 컨텐츠 2</p>
    </template>
    <template #footer>
        <button type="button">닫기</button>
    </template>
</modal-layout>
  • 이렇게 적용된 결과는 다음과 같습니다.
<div class="modal-container">
    <header>
        <h1>팝업 타이틀</h1>
    </header>
    <main>
        <p>팝업 컨텐츠 1</p>
        <p>팝업 컨텐츠 2</p>
    </main>
    <footer>
        <button type="button">닫기</button>
    </footer>
</div>
  • 컴포넌트 내에 여러 영역에 slot을 적용할 때는 name을 이용해서 적용하고, 하나의 영역에만 적용할 때는 굳이 slot에 name을 사용하지 않아도 됩니다.

  • 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 예제 코드로 Pagetitle.vue를 만들었습니다.

<template>
    <h2>{{ title }}</h2>
</template>

<script>
    export default {
        props: {
            title: {
                type: String,
                default: '페이지 제목입니다.',
            }
        }
    }
</script>

  • 이 코드를 slot을 이용하면 다음과 같이 바뀌게 됩니다.
<template>
    <h2><slot></slot></h2>
</template>
<page-title>컴포넌트 사용 예제 페이지</page-title>
  • 단순히 페이지 타이틀을 만들기 위해서 props를 정의할 필요도 없고, 부모에서 자식 컴포넌트로 props 데이터를 전달할 필요도 없게 됩니다.
  • 코드가 훨씬 간결하고 직관적으로 바뀌었습니다.
Tip
  • 프로젝트 개발 초기에 개발팀은 애플리케이션 전체에서 사용될 slot 기반의 컴포넌트를 구현해서 개발자에게 제공해야 합니다.
  • 애플리케이션 개발 시, 팝업, 페이지 타이틀 등 애플리케이션 전반에 걸쳐 다수의 컴포넌트에서 공통으로 사용해야 하는 공통 UI 요소가 있을 수 있습니다.
  • 이런 UI 요소를 slot 기반의 컴포넌트로 만들어서 제공하면, 전체 애플리케이션 개발 생산성 및 통일된 디자인을 통한 사용자 경험을 향상시킬 수 있습니다.
  • 이러한 개발은 프로젝트 초기에 이루어져야 합니다.
  • 한번 개발된 slot 기반의 컴포넌트는 다른 애플리케이션을 개발할 때도 사용할 수 있기 때문에, 개발팀의 자산으로 지속적으로 관리되어야 합니다.

6.3 Provide/Inject

  • 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달해야 하는 경우 props를 사용하면 된다는 것을 배웠습니다.
  • 그런데 만약에 컴포넌트 계층 구조가 복잡하게 얽혀 있어서 부모 컴포넌트로부터 자식 컴포넌트, 그리고 그 자식 컴포넌트의 자식 컴포넌트로 데이터를 전달하는 경우가 발생하면, props를 통해 데이터를 전달하는 것은 굉장히 복잡한 코드를 양산하게 됩니다.

  • 이러한 경우 사용할 수 있는 것이 Provide/Inject 입니다.
  • 컴포넌트의 계층 구조가 아무리 복잡하더라도 부모 컴포넌트에서는 provide 옵션을,
  • 자식 컴포넌트에서는 inject 옵션을 통해 데이터를 쉽게 전달할 수 있습니다.

  • 컴포넌트 구조는 다음과 같습니다.

  • ParentComponent 컴포넌트에서 ChildChildComponent2 컴포넌트로 데이터를 전달하려고 합니다.
  • props를 사용해서 데이터를 전달하려면 ParentComponent -> ChildComponent2 -> ChildChildComponent2 이렇게 3단계를 거쳐서 전달해야 합니다.
  • 하지만 provide / inject를 사용하면 한 번에 바로 전달할 수 있습니다.
<template>
    <provide-inject-child></provide-inject-child>
</template>

<script>
    import ProvideInjectChild from './ProvideInjectChild';
    export default {
        components: {
            ProvideInjectChild,
        },
        data() {
            return {
                items: ['A', 'B']
            }
        },
        provide() {
            return {
                itemLength: this.items.length
            }
        }
    }
</script>
  • provide 함수를 통해 배열 itemslength를 반환합니다. (자식 컴포넌트로 전달하고자 하는 데이터를 provide에 정의합니다.)
<!-- ProvideInjectChild.vue -->
<!-- ... -->
<script>
    export default {
        inject: ['itemLength'],
        mounted() {
            console.log(this.itemLength);
        }
    }
</script>
  • 부모 컴포넌트로부터 전달받고자 하는 데이터와 동일한 속성 이름으로 inject에 문자열 배열로 정의합니다.
  • 이렇게 provide / inject를 이용하면 아무리 컴포넌트 계층 구조가 복잡하더라도 원하는 자식 컴포넌트로 데이터를 한번에 전달할 수 있습니다.

  • 하지만 inject를 통해서 데이터를 전달받는 자식 컴포넌트에서는 전달받는 데이터가 어떤 부모 컴포넌트에서 전달되는지 확인이 안된다는 단점이 있습니다.

6.4 Template refs

  • Vue 개발 시 특별한 경우가 아니면 HTML 객체에 바로 접근해서 코드를 구현해야 할 일은 없습니다.
  • 하지만 어쩔 수 없이 자바스크립트에서 HTML 객체에 바로 접근해야 한다면 HTML 태그에 id 대신 ref를 사용하면 됩니다.
<input type="text" ref="title">
  • this.$refs를 이용해서 ref 속성에 지정된 이름으로 HTML 객체에 접근이 가능해집니다.
this.$refs.title.focus();