[Vue.js] Component

Vue.js 2018.12.18 01:39

Component 

대규모 어플리케이션인 경우 확장자가 .vue인 단일 파일 컴포넌트(SIngle File Component)형태로 많이 개발되지만 이번에는 컴포넌트 개념부터 정확히 잡아보려고 한다.


대규모 어플리케이션에서 사용되는 단일 파일 컴포넌트를 작성하기 위해서는 ECMAScript 6에 대한 지식이 있어야한다. 


컴포넌트의 장점은 아래와 같다.


1. 뛰어난 재사용성

한 애플리케이션에서 내부에 사용되는 UI와 기능은 반복되는 것들이 꽤 많다. 반복되는 부분을 컴포넌트로 작성해두면 여러 곳에서 재사용할 수 있어 생상선을 높일 수 있다.


2. 테스트가 용이하다.

컴포넌트 단위로 기능을 테스트할 수 있기 때문이다. Karma, Mocha와 같은 단위 테스트도구를 이용해 쉽게 테스트 할 수 있다.


3. 디버깅이 편하다.

Vue devtools와 같은 도구를 이용해 컴포넌트 단위로 전달된 속성(Props)을 손쉽게 살펴볼 수있고, 부모 컴포넌트로 전달된 이벤트 정보를 손쉽게 파악이 가능하다.




1. Component 조합

Vue.js는 컴포넌트들을 조합해 전체 애플리케이션을 작성한다. 컴포넌트들은 부모0자식 관계로 트리 구조를 형성하는데 부모 컴포넌트가 자식 컴포넌트를 포함하는 형태이다. 컴포넌트들은 속성(Props)을 통해서 자식 컴포넌트로 정보를 전달할 수 있다. 전달 방향은 주로 부모에게 자식(단방향), 양방향으로 데이터를 전달할 수 있지만 애플리케이션의 복잡도가 높아지고 유지 보수에 어려움이 있어 권장하지는 않는다.




부모 컴포넌트는 자식 컴포넌트로 속성을 전달할 수 있고, 자식 컴포넌트는 부모 컴포넌트로 이벤트를 발신할 수 있다. 속성 전달과 이벤트 발식이 부모-자식 컴포넌트 간의 상호 작용을 일으키는 방법이다.


data, methods, computed, watch 옵션과 같은 대부분의 Vue 인스턴스의 옵션을 컴포넌트 수준에서도 사용할 수 있다. 한가지 주의할 점은 data 옵션이다. 컴포넌트 기반으로 개발할 때 data 옵션은 각 컴포넌트의 로컬 상태를 관리하기 위한 용도로 사용한다. 또한 하나의 컴포넌트를 애플리케이션에서 여러 번 사용할 경우에 모두 다른 상태 정보를 가져야한다. 




2. Component 작성


컴포넌트를 작성하는 메서드는 다음과 같다.


Vue.component(tagname,option)

tagname : 컴포넌트를 사용할 태그명

option : 컴포넌트에서 렌더링할 templet등을 지정


<body>
<script tpye="text/javascript">
Vue.component('hello-component', {
template: '<div>hello world</div>'
})
</script>
</body>


작성된 컴포넌트를 사용할 때에는 등록한 태그명을 사용한다. Vue 컴포넌트의 template 옵션에 템플릿 문자열을 사용했다. 이와 같은 방식을 인라인 템플릿이라고 하는데 권장 하는 방법은 아니다. 


<body>
<script tpye="text/javascript">
Vue.component('hello-component', {
template: '<div>hello world</div>'
})
</script>
<div id="app">
<hello-component></hello-component>
<hello-component></hello-component>
<hello-component></hello-component>
</div>
<script tpye="text/javascript">
var v = new Vue({
el: '#app'
})
</script>
</body>


이제 VSCode의 Plugin중 하나인 live-server로 핫리로딩이라는 기능을 통해 코드가 변경되어 저장되면 브라우저 화면에 즉시 갱신되도록 할 예정이다.


npm을 이용해 아래의 명령어로 설치한다


npm intsall -g live-server (windows)

sudo npm intsall -g live-server (mac)


그후 VSCode의 ctrl + `키를 눌러 터미널 실행후 아래의 명령어로 실행 


live-server [--port=포트명]


PS C:\vue2> live-server --port=8090
Serving "C:\vue2" at http://127.0.0.1:8090
Ready for changes
GET /favicon.ico 404 33.162 ms - 150
Change detected C:\vue2\index.html
Change detected C:\vue2\index.html
Change detected C:\vue2\event.html
Change detected C:\vue2\index.html


(실행후 dev tools 모습)



템플릿을 지정할 때 인라인 방식으로도 가능하지만 템플릿 문자열을 포함하고있는 <template> 태그, <script type="text/x-template"> 태그의 di를 지정해도 가능하다.




3. DOM Template 구문 작성시 주의사항

컴포넌트를 이용해 개발하면서 템플릿 문자열을 사용할 때 주의할 점이 있다. HTML요소들은 자식 요소로 포함시킬 수 있는 요소들이 정해져 잇는 경우가 있고, 이 사항을 브라우저가 구문 분석을 숳행한다. 이러한 경우에 Vue 컴포넌트가 사용되면 떄떄로 오류가 발생한다.


<body>
<script tpye="text/javascript">
Vue.component('option-component', {
template: '<option>hello</option>'
})
</script>
<div id="app">
<select>
<option-component></option-component>
<option-component></option-component>
</select>
</div>
<script tpye="text/javascript">
Vue.config.devtools = true;
var v = new Vue({
el: '#app'
})
</script>
</body>


<select> 태그 안에서 <opton-component>라는 태그를 사용할수 있다라는 것이 브라우져에 등록되어 있지 않다. 그래서 브라우져는 이 태그들을 구문 분석하는 작업을 먼저 수행한 후 Vue 컴포넌트를 렌더링한다. 구문 분석단계에서 DOM 요소가 올바르지 않다고 판단하기에 제대로 렌더링하지 않는 문제가 발생한다.


<select>
<option is="option-component"></option-component>
<option is="option-component"></option-component>
</select>



요렇게 is특성을 사용하면 정상적으로 렌더링 되는걸 확인할 수있다. 하지만 .vue 확장자를 상용하는 단일 파일 컴포넌트를 작성하는 경우에는 굳이 is 특성을 사용하지 않아도 된다.


한가지 더 주의할 점은 템플릿 문자열에서의 루트 요소는 하나여야 한다는 것이다. 만일 템플리 내부에서 여러 요소를 작성해야 한다면 <div>로 감싸주어 하나의 루트 요소가 되게끔 해주어야한다.




4. Component Data 옵션

컴포넌트 내부의 로컬 상태 정보를 저장하기 위해 data옵션을 사용할 수 있다. 하지만 이제까지 작성했듯이 data 옵션에 객체를 직접 지정하면 컴포넌트가 정상적으로 렌더링되지 않고 오류가발생된다.


<template id='timeTemplate'>
<div>
<span>{{nowTS}}</span>
<button @click="timeClick">현재 시간</button>
</div>
</template>
<body>
<script tpye="text/javascript">
Vue.component('time-component', {
template: '#timeTemplate',
data: {
nowTS: 0
},
methods: {
timeClick: function(e) {
this.nowTS = (new Date()).getTime();
}
}
})
</script>
<div id="app">
<time-component></time-component>
<time-component></time-component>
</div>
<script tpye="text/javascript">
Vue.config.devtools = true;
var v = new Vue({
el: '#app'
})
</script>
</body>



정상적으로 렌더링 되려면 data 옵션에 함수가 주어져야 한다. 정확하게 표현하자면 '함수가 호출되어 리턴된 객체가 data 옵션에 주어진다'라고 표현 할 수있다. 아래와 같이 수정하면 정상적으로 출력된다.


Vue.component('time-component', {
template: '#timeTemplate',
data: function() {
return {
nowTS: 0
}
},
methods: {
timeClick: function(e) {
this.nowTS = (new Date()).getTime();
}
}
})

data 옵션에 함수를 지정하는 이유는 동일한 컴포넌트가 여러 번 사용되더라도 동일한 객체를 가리키는 것이 아니라 함수가 호출될 때마다 만들어진 객체가 리턴되기 때문이다. 매번 만들어진 객체가 리턴되기 때문에 서로 다른 객체를 참조한다.




5. props와 event

부모 컴포넌트와 자식 컴포넌트 사이에 속성(props)와 이 벤트를 이용해서 상호작용하여 통신할 수 있다. Vue 컴포넌트들이 부모-자식 관계로 형성되었을 때 각 컴포넌트 내부의 데이터는 캡슐화 되기 때문에 다른 컴포넌트에서 접근할 수 없다. 따라서 부모 컴포넌트에서 자식 컴포넌트로 필요한 정보를 전달하기 위해서는 속성(props)을 이요해야한다. 주의할 점은 부모에서 자식으로 단방향으로만 전달이 가능하다.


반대로 자식 컴포넌트에서 부모 컴포넌트로 전달하는 방법은 이벤트를 이요한다. v-on 디렉티브를 이용해서 이벤트를 처리하는 방법이다. 



5.1) props를 이용한 정보 전달

props를 이요한 정보전달 방법은 간단한다 . Vue 컴포넌트를 정의할 때 props라는 옵션을 작성하고 props명을 배열로 나열하면된다.

<template id='listTemplate'>
<li>{message}}</li>
</template>
<body>
<script tpye="text/javascript">
Vue.component('list-component', {
template: '#timeTemplate',
props: ['message']
})
</script>
<div id="app">
<list-component message="Hello"></list-component>
<list-component message="안녕하세요"></list-component>
</div>
<script tpye="text/javascript">
Vue.config.devtools = true;
var v = new Vue({
el: '#app'
})
</script>
</body>

list-componenet 컴포넌트를 작성하며 message라는 이름의 속성을 정의했다. 이 속성을 통해서 전달된 정보는 9행에서 템플릿 문자열을 통해서 출력된다. 속성을 통해서 정보를 전달하기 위해서는 컴포넌트를 사용할때 특성처럼 전달한다.


컴포넌트 작성시 속성명을 부여할때 카멜 표기법(camel casing)을 사용했다면 태그에서 속성명을 사용할 정보를 전달할 때는 반드시 케밥(kebob casing)을 사용해야 한다. 태그 작성시 특성은 대소문자를 구분하지 않기 때문이다.


ex) myMessage (x) => my-message(o)


속성을 정의할때 배열 형태로 나여할 수도 있지만 속성에 대한 엄격한 유효성 검증이 필요하다면 배열이 아닌 객체 형태를 사용할 수 있다. 


<script tpye="text/javascript">
Vue.component('list-component', {
template: '#listTemplate',
props: {
message: {
type: String,
default: '안녕하세여'
},
count: {
type: Number,
required: true
}
}
})
</script>
<div id="app">
<list-component message="Hello" count="100"></list-component>
<list-component message="안녕하세요" count="21"></list-component>
<list-component message="Ni hao"></list-component>
<list-component count="1000"></list-component>
</div>


배열 형식이 아닌 객체 형식으로 지정했는데 message의 속성은 문자형이며 기본값으로 '안녕하세여'라는 값을 갖고 있다. count 속성은 숫자형이고 필수 입력해야하는 속성이다.


실제로 실행을 해보면 정상출력되나 콘솔에 에러가 찍힌다. 이유인 즉슨 넘겨주는 속성 count가 Number아 아니라 String 형태로 들어가기 때문이다. "21"과 같은 리터럴은 자바스크립트 구문으로 인식되지 않고 문자열 값으로 그대로 전달된다.


이 문제를 해결하기 위해서는 v-bind 디렉티브를 이용한다.


<div id="app">
<list-component message="Hello" v-bind:count="100"></list-component>
<list-component message="안녕하세요" :count="21"></list-component>
<list-component message="Ni hao"></list-component>
<list-component :count="1000"></list-component>
</div>


한 가지 더 주의할 부분이 있는데 속성으로 전달할 값이 배열이나 객체인 경우이다. 이경우에 기본값(default value)를 부여하려면 반드시 함수를 사용해야한다.


countries 속성에 입력한 값이 정상적으로 props에 들어가있는걸 확인할 수있다.




5.2) event를 이용한 정보 전달

event를 이용해서 전달하는 방법은 사용자 정의 이벤트를 활용한다. 자식 컴포넌트에서 이벤트를 발신(emit)하고 부모 컴포넌트에서 v-on 디렉티브를 이용해 이벤트를 수신한다.


<!-- 자식놈 -->
<style>
.buttonstyle {
width: 120px;
height: 30px;
text-align: center;
}
</style>
<template id='childTemplate'>
<div>
<button class="buttonstyle" @click="clickEvent"
:data-lang="buttonInfo.value">{{buttonInfo.text}}</button>
</div>
</template>
<script tpye="text/javascript">
Vue.component('child-component', {
template: '#childTemplate',
props: ['buttonInfo'],
methods: {
clickEvent: function(e) {
this.$emit('timeclick', e.target.innerText,
e.target.dataset.lang);
}
}
})
</script>
<!-- 자식 끝 -->
<!-- 부모 -->
<template id='parentTemplate'>
<div>
<child-component v-for="s in buttons" :button-info="s"
@timeclick="timeclickEvent"></child-component>
<hr/>
<div>{{msg}}</div>
</div>
</template>
<script tpye="text/javascript">
Vue.component('parent-component', {
template: '#parentTemplate',
props: ['buttons'],
data: function() {
return {
msg: ""
}
},
methods: {
timeclickEvent: function(k, v) {
this.msg = k + ", " + v
}
}
})
</script>
<!-- 부모 끝 -->
<body>

<div id="app">
<parent-component :buttons="buttons"></parent-component>
</div>
<script tpye="text/javascript ">
Vue.config.devtools = true;
var v = new Vue({
el: '#app',
data: {
buttons: [{
text: "hello",
value: "영어"
}, {
text: "신짜오",
value: "베트남어"
}, {
text: "니하오",
value: "중국어"
}]
}
})
</script>
</body>


buttonInfo 속성을 정의하였고 이 속성은 부모 컴포넌트로부터 값을 전달받아 버튼 리스트를 생성한다.  자식 컴포넌트를 사용하는 부모 컴포넌트는 buttons 속성과 msg 데이터 옵션을 포함하고 있다. 데이터 옵션은 해당 컴포넌트 내에서만 사용하기 위해 정의한다. buttons 속성은 vm Vue 인스턴스의 buttons 데이터를 전달 받아 v-for 디렉티브를 사용해 반복적으로 생성되는 자식 컴포넌트 배열 값을 바인딩한다.


자식 컴포넌트 내부에서 버튼이 클릭되면 $emit() 메서드를 통해 timeclick 이벤트를 발신한다. 부모 컴포넌트에서는 v-on 디렉티브를 통해 timeclick이벤트를 처리하는 것이다. 


timeclickEvent(k, v) 함수는 두개의 인자값을 받는데 바로 buttonInfo.text와 buttonInfo.value 값이다. 




5.3) 이벤트 버스를 이용한 데이터 전달

부모 - 자식 간계 이외에도  손자 증손자 관계인 컴포넌트들 사이에도 정보를 전달하는 방법이 있다. 바로 이벤트 버스이다. 비어있는 Vue 인스턴스를 만들어 사용하면 된다.


예제에서는 두개의 컴포넌트로 나누었다. 값을 입력하는 input-component와 todolist를 나타내는 부분 list-component이다. 이 두개의 컴포넌트 사이에 데이터의 상호작용이 필요하다. 


<head>
<meta charset="utf-8">
<title>hello vue.js</title>
<script src="https://unpkg.com/vue@2.5.16/dist/vue.js"></script>
<style>
* {
box-sizing: border-box;
}
.header {
background-color: purple;
padding: 30px 30px;
color: yellow;
text-align: center;
}
.header:after {
content: "";
display: table;
clear: both;
}
</style>
</head>
<script tpye="text/javascript">
var eventBus = new Vue()
</script>
<style>
ul {
margin: 0;
padding: 0;
}
ul li {
cursor: pointer;
position: relative;
padding: 8px 8px 8px 40px;
background: #eee;
font-size: 14px;
transition: 0.2s;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
ul li:hover {
background: #ddd
}
ul li.checked {
background: #BBB;
color: #fff;
text-decoration: line-through
}
ul li.checked::before {
content: '';
position: absolute;
border-color: #fff;
border-style: solid;
border-width: 0px 1px 1px 0px;
top: 10px;
left: 16px;
transform: rotate(45deg);
height: 8px;
width: 8px;
}
.close {
position: absolute;
right: 0px;
top: 0px;
padding: 12px 16px 12px 16px
}
.close:hover {
background-color: #f44336;
color: white;
}
</style>
<template id='list-template'>
<ul id="todolist">
<li v-for="a in todolist" :class="checked(a.done)"
@click="doneToggle(a.id)">
<span>{{a.todo}}</span>
<span v-if="a.done">(완료)</span>
<span class="close" @click.stop="deleteTodo(a.id)">&#x00D7;</span>
</li>
</ul>
</template>
<script tpye="text/javascript">
Vue.component('list-component', {
template: '#list-template',
created: function() {
eventBus.$on('add-todo', this.addTodo);
},
data: function() {
return {
todolist: [{
id: 1,
todo: "영화보기",
done: false
}, {
id: 2,
todo: "주말 산책",
done: true
}, {
id: 3,
todo: "ES6 공부",
done: false
}, {
id: 4,
todo: "Vue.js 공부",
done: false
}, ]
}
},
methods: {
checked: function(done) {
if (done) return {
checked: true
};
else return {
checked: false
}
},
addTodo: function(todo) {
if (todo !== "") {
this.todolist.push({
id: new Date().getTime,
todo: todo,
done: false
})
}
},
doneToggle: function(id) {
var index = this.todolist.findIndex(function(item) {
return item.id === id
})
this.todolist[index].done = !this.todolist[index].done;
},
delete: function(id) {
var index = this.todolist.findIndex(function(item) {
return item.id === id
})
this.todolist.splice(index, 1)
}
}
})
</script>
<style>
.input {
border: none;
width: 75%;
height: 35px;
padding: 10px;
float: left;
font-size: 16px;
}
.addbutton {
padding: 10%px;
width: 25%;
height: 35px;
background: #d9d9d9;
color: #555;
float: left;
text-align: center;
font-size: 13px;
cursor: pointer;
transition: 0.3s;
}
.addbutton:hover {
background-color: #bbb;
}
</style>
<template id='inputTemplate'>
<div>
<input class="input" type="text" id="task" v-model.trim="todo"
placdholder="입력 후 엔터!" @keyup.enter="addTodo">
<span class="addbutton" @click="addTodo">추 가</span>
</div>
</template>
<script tpye="text/javascript">
Vue.component('input-component', {
template: '#inputTemplate',
data: function() {
return {
todo: ""
}
},
methods: {
addTodo: function() {
eventBus.$emit('add-todo', this.todo);
this.todo = "";
}
}
})
</script>

<body>
<div id="todolistapp">
<div id="header" class="header">
<h2>Todo List App</h2>
<input-component></input-component>
</div>
<list-component></list-component>
</div>
<script tpye="text/javascript">
var vm = new Vue({
el: "#todolistapp"
})
</script>
</body>






'Vue.js' 카테고리의 다른 글

[Vue.js] axios 서버통신  (0) 2018.12.22
[Vue.js] Vue CLI(GUI)  (0) 2018.12.18
[Vue.js] Component  (0) 2018.12.18
[Vue.js] Event 처리  (0) 2018.12.17
[Vue.js] Vue instance (뷰 인스턴스)  (0) 2018.12.16
[Vue.js] computed Property (계산형 속성)  (0) 2018.12.15
Posted by 루우지