vue-component
이번 포스트에서는 Vue.js의 기본 개념을 익혔다는 가정하에 Vue.js의 강력한 기능 중인 하나인 컴포넌트(Component)에 대해서 알아보자.

Vue.js의 컴포넌트 개념은 Vue.js Framework에서도 중요한 부분을 차지하고 어떻게 Vue.js 컴포넌트를 생성하느냐에 따라 Vue.js의 개발속도와 코드의 가독성, 그리고 효율성이 현저히 차이가 난다. 우리는 이번 포스트에서 Vue.js의 컴포넌트 생성 방법에 대해 알아보고 실제로 Vue.js의 컴포넌트를 직접 만들어보기로 하자. 이번 포스트는 공식 문서에 나와 있는 컴포넌트 생성 방법의 여러 가지 방법 중에 어떤 것이 실무에서 효율적인지를 파악하는 Vue.js Tutorial이다.

먼저 포스팅을 하기 전에 앞 써서 기본 Vue.js에 대한 개념과 추세를 모르거나 알고 싶다면 빠르게 배우는 Vue.js 포스트를 먼저 확인하자.

컴포넌트에 대해서 포스팅할 내용은 많지는 않다. 개념부터 사용법까지 본다면 분량은 얼마 되지는 않지만 가장 중요하기 때문에 깊게 봐야 할 필요성이 있다. 실무에서 특정 프로젝트에 Vue.js를 도입을 한다면 더욱이 깊게 봐야 한다. Vue.js 컴포넌트는 API 문서대로 전역 등록을 통한 컴포넌트 생성과 지역 등록을 통한 컴포넌트 등록이 있고 이 두 형태를 가지고 문법대로 생성하면 끝이다. 하지만 중요한 것은 컴포넌트의 생성 범위이다. 어떤 컴포넌트를 전역/지역으로 등록을 할지, 그리고 어떤 특정 부분을 컴포넌트로 만들어야 할지가 중요하다. 즉, 쉽게 말한다면 설계 가 가장 중요하다고 볼 수 있다.

Vue.js 컴포넌트를 배우고 만들기에 앞서 가장 중요한 컴포넌트의 설계를 먼저 보자.

Vue.js Component 설계의 중요성

위에서 언급했듯이 컴포넌트를 생성할 때에는 설계가 가장 중요하다. 컴포넌트를 만드는 방법이야 Vue.js 공식 API를 보고 문법을 익혀 만들면 그만이다. 하지만 컴포넌트를 어느 곳에 사용하고 어느 부분이 컴포넌트 화 해야할지에 대해 정확한 설계가 없다면 이 후 전반적으로 문제를 야기한다. 물론 Application을 개발 할 때 모든 항목들을 컴포넌트 단위로 잘게 쪼개도 Application 성능에 크게 영향을 주지는 않지만 규모가 크거나 또는 점점 규모가 커지는(고도화) 프로젝트에서는 해당 프로젝트의 유지관리와 개발 진행 단계에 영향을 준다.

명확한 설계를 무시하고 Vue.js 컴포넌트를 생성했을 때 아래와 같은 문제점들이 있다.

  1. 전반적인 코드의 가독성과 유지관리 효율성 저하
  2. 컴포넌트 구조의 복잡성 증가
  3. 독립적인 컴포넌트로의 변이

1. 전반적인 코드의 가독성과 유지관리 효율성 저하

무분별한 컴포넌트의 생성은 코드의 가독성과 앞으로 있을 유지관리에 대한 효율성을 현저히 저하시킨다. 대부분 특정 부분을 컴포넌트화를 한다고 했을 때 굳이 컴포넌트를 안 해도 되는 부분까지 나눠서 컴포넌트화 하는 경우가 많다.

아주 간단하게 로그인 페이지를 만든다고 했을 때 ID, Password 입력 폼을 각각의 컴포넌트로 만들었다고 가정하자. 이렇게 되면 메인이 되는 App은 매우 심플해지겠지만 유지관리 시에 우리는 ID, Password 폼을 각각 찾아다니며 분석하고 수정해야 한다.

로그인 페이지 하나 만드는데 무슨 컴포넌트를 쓰고 가독성과 복잡성을 논하는가 라고 생각하는가? 그렇다면 이미 설계의 중요성을 알고 있는 것이다. 하지만 당신은 로그인 페이지가 아니라 수십 개의 페이지가 존재하는 Application을 구현할 때 이미 알고 있고 느끼는 부분을 망각하고 개발하게 될 수 있다. Application의 전반적인 내용을 알고 있다 하더라도 설계를 무시하고 한다면 당연시하게 되는 일이다. 그만큼 특정 부분을 컴포넌트로 나누고 생성한다는 것은 어려운 일이다.

그렇다면 어떻게 해야 가독성이 높은 컴포넌트와 효율성을 올릴 수 있을까?
먼저 컴포넌트화 해야 하는 부분을 명확하게 나눠야 한다. A라는 부분이 N개의 페이지에서 사용하는 공통적인 항목이라면 컴포넌트로 분리해야하는 것이 바람직하다. 그리고 컴포넌트화 해야 하는 덩어리(chunk)를 굳이 세분화해서 나눌 필요는 없다. 우리는 컴포넌트를 작성할 때 과감하게 큰 덩어리(chunk) 단위로 나눌 필요도 있다.

2. 컴포넌트 구조의 복잡성 증가

뒤에서도 배우겠지만 대부분 컴포넌트의 작성은 TemplateScript 그리고 Style Sheet를 하나의 파일에 작성하는 단일 파일 컴포넌트(Single File Component)로 작성하게 된다. 이런 컴포넌트에서 props, watch, methods 등의 속성이 무수히 많고 정확한 설계 없이 동작만을 목적으로 하고 작성한다면 이미 이 컴포넌트 복잡한 구조를 가진 컴포넌트이다. 뿐만 아니라 부모-자식의 참조 역시 어려워지게 된다.

가장 중요한 propswatch에 대한 이유를 확인해보자.

props

props가 많다는 것은 이미 부모 컴포넌트(Parent Component)에서 많은 속성을 전달하고 있다는 것이다. 이렇게 된다면 이미 이 컴포넌트는 특정한 부모 컴포넌트에 종속된 것이나 다름없는 것이다. 물론 다른 페이지 또는 컴포넌트에서 해당 컴포넌트를 가져가 사용할 수 있겠지만, 알겠는가? 여러 페이지에 바인딩 된 컴포넌트에서 실제로 전달된 props가 무엇이고 유지 보수 시에 무엇이 필요한지 아닌지를 말이다.
그뿐만 아니라 props는 해당 컴포넌트에서 직접적으로 변경이 불가능하기 때문에 이미 넘어온 props를 변경하기 위해서는 바인딩 되어 있는 propsdata에 재 바인딩해야 하는 경우가 많다. 이렇게 되면 자연적으로 watch와 같은 감시자와 상위 컴포넌트로 이벤트를 전달하는 $emit이 많아지게 된다.

watch

watch가 많다는 것은 이미 해당 컴포넌트에서 반강제적으로 반응 적인 모델이 필요하다는 경우이다. 이런 watch가 많아지게 되면 해당 컴포넌트를 다른 곳에 반인딩하였을 때 의도치 않은 동작을 야기할 수 있다. 특히 이런 경우엔 유지보수가 매우 어렵다. 이미 이 watch에 종속된 기능이 거미줄처럼 엉켜있기 때문이다. 특히나 Vue.js의 반응 적 모델은 Application의 성능에 직접적인 연관을 주기도 하기 때문에 watch를 최소화하는 것이 좋다.
Vue.js의 성능 처리에 관련해서 Vue.js 대용량 데이터의 처리 방법과 성능 최적화 방법 (Vue.js Performance) 를 참고하자.

위와 같은 컴포넌트의 복잡성 증가를 해결하기 위해서는

  • props는 해당 컴포넌트에서 절대적으로 필요한 요소로 생성하고
  • watch의 사용을 최소화하고
  • 공통적인 methods와 같은 Script들은 javaScript 파일로 별로 분리하는 것이 좋으며
  • 컴포넌트 간의 깊은 바인딩(deep)은 자제해야 한다.

3. 독립적인 컴포넌트로의 변이

이렇게 무분별하게 컴포넌트를 생성하고 하나의 컴포넌트가 복잡한 구조를 가진 컴포넌트 생성하다 보면 결국 해당 컴포넌트는 어느 순간부터 특정 페이지 또는 컴포넌트에 종속되어 버리고 단 하나의 독립적인 컴포넌트가 생성된다. 이렇게 독립적인 컴포넌트가 작성되면 Vue.js 컴포넌트의 목적에 어긋나고 특성을 활용하지 못한 방치되는 컴포넌트가 되고 만다.


컴포넌트를 개발하기 전 컴포넌트에서의 설계가 왜 중요한지 알아보았다.
개발에 있어서 설계가 중요하다는 것은 누구나 아는 것이다. 하지만 설계가 Application에 대한 전반적인 설계라고 한다면 나는 컴포넌트 간의 설계만 별도로 작성하는 것을 추천한다. 시간이 난다면 컴포넌트 간의 다이어그램도 한번 그려보기 바란다. 많은 도움이 될 것이다. 이 밖에도 설계가 부족할 경우 야기되는 문제들이 많지만 모두 나열할 수는 없어도 분명한 것은 단 하나의 판단 미스로 나비효과를 일으킬 수 있다는 것을 명심하자.

이제 본격적으로 Vue.js Component 대해 알아보자.

Vue.js Component

Vue.js Component은 HTML Element를 확장하고 재사용 가능한 형태로 구현하는 것을 말한다. Vue.js에서 사용된 모든 컴포넌트는 하나하나가 Vue.js의 인스턴스이기도 하다. 컴포넌트의 생성 과정은 단순히 등록 -> 사용 패턴으로만 이루어진다.

테스트 프로젝트 생성

먼저 테스트할 프로젝트를 생성하자.

Vue 프로젝트 생성은 Vue-CLI 3를 이용하여 생성할 것이다. Vue-CLI 3에 대해서 Vue-CLI 3 시작하기에서 배워볼 수 있다. Vue-CLI 3를 배웠다는 가정하에 진행하겠다.

1
2
$ vue create vue-component
$ cd vue-component

프로젝트가 생성되었으면 기본으로 생성되는 코드들을 정리하자.
HelloWorld.vue 파일은 삭제하고 App.vue는 아래와 같이 초기 상태로 돌려놓자.

App.vue
1
2
3
4
5
6
7
8
9
10
<template>
<div id="app"></div>
</template>

<script>
export default {
name: 'app',
components: {}
}
</script>

컴포넌트의 등록과 사용

컴포넌트의 등록에는 전역등록(Global Registration)지역등록(Local Registration)으로 나눌 수 있다.

전역등록 (Global Registration)

컴포넌트 전역등록은 프로그래밍에서 전역 변수와 같은 의미이다. 인스턴스 생성 후 어느 페이지 또는 컴포넌트에서 사용할 수 있게 Global 하게 등록할 수 있다.

테스트로 생성한 프로젝트에서 component 경로에 global-component.vue 파일을 만들고 생성하자.

global-component.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div>
<button @click="showTitle">Click</button>
<span>title</span>
</div>
</template>

<script>
export default {
name: 'global-component',
data () {
return {
title: void 0
}
},
methods: {
showTitle () {
this.title = 'Global Component!!'
}
}
}
</script>

<style scoped>
div {
padding: 20px
}

div span {
margin: 20px
}
</style>

이렇게 전역 등록할 컴포넌트를 만들었다. 이제 생성한 컴포넌트를 전역적으로 등록하자. main.js를 열어 아래와 같이 작성하자.

main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

import GlobalComponent from './components/global-component'

Vue.component(GlobalComponent.name, GlobalComponent)

new Vue({
render: h => h(App),
}).$mount('#app')

Vue.component()를 통해서 우리는 앞에서 생성한 컴포넌트를 Vue 인스턴스에 바인딩시켰다. 이로써 이제 우리는 템플릿에서 Custom Element를 사용할 수 있게 되었다.

App.vue에 직접 삽입하고 http://localhost:8080/를 확인해보자.

App.vue
1
2
3
4
5
6
7
8
9
10
11
<template>
<div id="app">
<global-component></global-component>
</div>
</template>

<script>
export default {
name: 'app'
}
</script>

지역등록 (Local Registration)

사실상 컴포넌트 등록에 있어서 전역등록 보다는 지역등록을 가장 많이 쓰고 보편적으로 사용한다. 웹팩같은 빌드 시스템을 사용하면 전역등록 된 사용되지 않는 모든 컴포넌트가 빌드에 포함되기 때문이다.

component 경로에 local-component.vue 파일을 생성하자.

local-component.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div>
<button @click="showTitle">Click</button>
<span>`{`{` title `}`}`</span>
</div>
</template>

<script>
export default {
name: 'local-component',
data () {
return {
title: void 0
}
},
methods: {
showTitle () {
this.title = 'Local Component!!'
}
}
}
</script>

<style scoped>
div {
padding: 20px
}

div span {
margin: 20px
}
</style>

전역등록과는 다르게 생성된 컴포넌트는 사용될 곳에 바로 삽입하여 사용하면 된다. App.vue를 아래와 같이 수정 후 삽입하고 http://localhost:8080/를 확인해보자.

App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div id="app">
<global-component></global-component>
<local-component></local-component>
</div>
</template>

<script>
import LocalComponent from './components/local-component'

export default {
name: 'app',
components: { LocalComponent }
}
</script>

여기까지 해서 컴포넌트의 등록 방법과 등록 유형별로 등록된 컴포넌트를 사용하는 방법을 배워봤다. 간단한 예제였지만 사실 여기에는 컴포넌트의 모듈 시스템까지 배운 것이다. 모듈 단위의 단일 파일 컴포넌트 (Single File Component)를 작성하고 모듈 형태로 삽입까지 했기 때문이다.

Component의 기본 작성

Vue.js 컴포넌트를 생성할 때에 정해진 틀은 없다. 하지만 통상 컴포넌트를 생성하고 사용할 때는 부모-자식 관계의 구조로 이루어진다. 컴포넌트의 집합인 컴포넌트를 만들어도 부모-자식 관계가 된다. 이러한 관계일 경우 부모와 자식 컴포넌트 간 데이터를 전달해야 하는 경우가 있는데 부모에서 자식으로 는 props를 사용하고, 반대로 자식에서 부모로 전달할 때는 events($emit)를 사용한다.

아래 예제를 보자.

parent-component.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div>
<child-component :message="msg"
@print="printMessage(val)"></child-component>
</div>
</template>

<script>
import ChildComponent from './components/child-component'

export default {
name: 'parent-component',
components: { ChildComponent },
data () {
return {
msg: '안녕하세요.'
}
},
methods: {
printMessage (val) {
console.log(val)
}
}
}
</script>
child-component.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<button @click="sendEvent()">Print</button>
</div>
</template>

<script>
export default {
name: 'child-component',
props: {
parentMessage: {
type: String,
default: void 0
}
}
methods: {
sendEvent () {
this.$emit('print', this.parentMessage)
}
}
}
</script>

parent-component에서 child-component를 바인딩하고 message라는 속성을 v-bind (:)를 통해 전달한다. 이 관계가 부모에서 자식으로 데이터를 보낼 경우이다. 그리고 v-on (@)을 통해 child-parent에서 전달되는 print라는 이벤트를 수신하고 있다.

이제 child-component를 보자. 여기서는 propsparentMessage 통해 이미 부모에서 전달된 값을 받고 있으며, $emit을 통해 부모에게 다시 이벤트를 전달하고 있다. Vue.js 컴포넌트에서는 이러한 관계가 구성이 되어야 하고 만약 parent-componentprops라는 속성이 있고 받는 값이 있다면 parent-component 역시 자식 컴포넌트가 되게 된다.

이러한 구조에서 조심해야 할 사항은 바로 props인데 Vue.js 전체적으로 본다면 양방향 바인딩이지만 이 props는 단방향 바인딩을 형성한다. 상위 속성 즉 parent-component에서 보내는 message가 업데이트되면 하위로 흐르는 props 역시 업데이트가 되지만 반대로 propsparentMessage를 변경한다고 상위 속성이 업데이트되지는 않는다. 오히려 props는 변경하지 않고 원시 데이터로 사용하는 것을 추천하며 만약 변경 시 Vue.js의 경고를 보게 될 것이다.


여기까지 해서 Vue.js에서 가장 핵심적인 개념인 컴포넌트를 알아보고 생성해 보았다. 위에서 언급했듯이 컴포넌트는 부모-자식 관계를 가지고 부모가 자식이 될 수도 있다. 이러한 구조로 인해 최초에 말했던 설계가 가장 중요하다는 것이다. 아무리 컴포넌트의 개념을 이해하고 적합한 컴포넌트를 생성한다 하더라도 복잡성이 높아지면 결과적으로 효율적이지 않은 컴포넌트가 생성된다.


더 알아보기

빠르게 배우는 Vue.js
Vue.js의 Vuex Store를 바인딩하는 4가지 방법!!
Vue.js 대용량 데이터의 처리 방법과 성능 최적화 방법 (Vue.js Performance)
Vuex Store의 state를 효율적으로 초기화하기