원글: https://medium.com/scrumpy/dynamic-component-templates-with-vue-js-d9236ab183bb

Vue.js에서의 다이나믹 컴포넌트 템플릿

컴포넌트들이 항상 같은 구조로 되어 있진 않다. 그리고 때론 관리해야할 상태들이 많을때도 있다. 이런 문제는 비동기적인 방법으로 해결할 수도 있다.

유즈 케이스

스크럼피는 알림, 댓글, 첨부 파일 등에서 컴포넌트 템플릿을 사용한다. 우선 댓글 부분을 살펴보면서 정확히 어떤 것을 말하는지 알아보자.

최근의 댓글은 더 이상 단순한 텍스트 필드가 아니다. 링크를 등록하고, 이미지와 비디오도 올리기도 하고 그밖에 여러 가지를 할 수 있다. 이렇게 전혀 다른 엘리먼트들이 댓글 안에서 그려지는 것이다. 만약 이것들을 단 하나의 컴포넌트에서 그리려 한다면 , 금세 지저분한 모습이될 것이다.

img

오픈 그래프 데이터, 이미지, 비디오등의 링크에 대한 일반적인 미리보기

이런 문제는 어떻게 해결할 수 있을까? 아마 대부분의 사람은 모든 케이스들을 확인하고 필요한 컴포넌트들을 명시적으로 사용할 것이다. 아래와 같이 말이다.

<template>
    <div class="comment">
        // 댓글 텍스트
        <p>...</p>

        // 오픈 그래프 이미지
        <link-open-graph v-if="link.type === 'open-graph'" />
        // 일반 이미지
        <link-image v-else-if="link.type === 'image'" />
        // 비디오
        <link-video v-else-if="link.type === 'video'" />
        ...
    </div>
</template>

하지만 이런 방식은 제공되는 컴포넌트가 많아질수록 지저분해지고 반복적이게 될 것이다. 스크럼피의 댓글만 해도 유튜브 혹은 트위터, 깃헙, 사운드클라우드, 비메오, 피그마등 지원할 수 있는 목록은 끝이 없다.

다이나믹 컴포넌트 템플릿

다른 방법으로, 일종의 로더를 두고 그 로더에서 정확히 필요한 템플릿을 로드하게 할 수 있다. 이런 방법은 컴포넌트의 코드를 깔끔하게 유지할 수 있게 해준다. 아래의 코드를 보자

<template>
    <div class="comment">
        // 댓글 텍스트
        <p>...</p>

        // type은 오픈 그래프 혹은 이미지 비디오등이 될 수 있다.
        <dynamic-link :data="someData" :type="type" />
    </div>
</template>

일단 아까보다 보기 좋아졌다. 이 컴포넌트가 어떻게 동작하는지 살펴보자. 첫 번째로 템플릿을 위해 폴더 구조를 변경해야 했다.

img

다이나믹 컴포넌트 템플릿을 위한 폴더 구조

개인적으로 각각의 컴포넌트당 한 개의 폴더를 만드는 것을 선호하는데 스타일이나 테스트를 위해 파일들이 추가될 수 있기 때문이다. 물론 구조를 어떻게 만들지는 각자의 방법이 있을것이다.

이제 <dynamic-link /> 컴포넌트의 코드를 살펴보자.

<template>
    <component :is="component" :data="data" v-if="component" />
</template>
<script>
export default {
    name: 'dynamic-link',
    props: ['data', 'type'],
    data() {
        return {
            component: null,
        }
    },
    computed: {
        loader() {
            if (!this.type) {
                return null
            }
            return () => import(`templates/${this.type}`)
        },
    },
    mounted() {
        this.loader()
            .then(() => {
                this.component = () => this.loader()
            })
            .catch(() => {
                this.component = () => import('templates/default')
            })
    },
}
</script>

자 어떻게 동작할까? Vue.js는 기본적으로 다이나믹 컴포넌트를 Vue.js에서 기본으로 제공하지만, 사용하고자 하는 모든 컴포넌트를 import/register 해야 한다는 단점이 있다. 👎

<template>
    <component :is="someComponent"></component>
</template>
<script>
import someComponent from './someComponent'
export default {
    components: {
        someComponent,
    },
}
</script>

컴포넌트를 다이나믹하게 사용하고 싶은 상황에서는 위와 같은 코드는 아무것도 도움이 되지 않는다. 이때 여기서 필요한 것이 바로 웹팩의 다이나믹 imports다. 이걸 computed 프로퍼티와 같이 사용하는 순간 마법이 시작된다. computed 프로퍼티는 함수를 리턴할 수 있다는 점을 이용하면 말이다. 매우 유용하다!

computed: {
    loader() {
        if (!this.type) {
           return null
        }
        return () => import(`templates/${this.type}`)
    },
},

컴포넌트가 마운트 되면 사용할 템플릿을 로드한다. 로드하다가 무언가 잘못되면 대안의 템플릿을 사용한다. 아마 이 템플릿을 통해 에러메세지를 유저에게 노출한다면 유용할것이다.

mounted() {
    this.loader()
        .then(() => {
           this.component = () => this.loader()
        })
        .catch(() => {
           this.component = () => import('templates/default')
        })
},

결론

  • 한 컴포넌트가 여러 개의 뷰를 포함하는 경우 유용하다.
  • 확장하기 쉽다.
  • 비동기로 진행되며, 템플릿은 필요한 시점에 로드된다.
  • 코드가 항상 DRY하게 유지된다.

이게 전부이다. 이런 종류의 기술을 이미 사용하고 있다면 들어보고 싶다.

할 말이나 질문이 있다면 트위터를 통해 부담 없이 남겨주기 바란다.