티스토리 뷰
1. 기본
대부분의 경우 Vue는 템플릿을 사용하여 HTML을 작성하는 것을 권장합니다. 하지만 때로는 JavaScript를 사용하여 HTML을 작성을 해야 할 때가 있습니다. 이럴 때 render
함수를 사용하면 됩니다.
<h1>
<a name="hello-world" href="#hello-world">
Hello world!
</a>
</h1>
위의 코드와 같이 작성된 HTML이 있을 때,
<anchored-heading :level="1">Hello world!</anchored-heading>
위의 코드와 같이 컴포넌트를 작성하여 사용할 수 있습니다. 이 때 level
속성으로 h
태그의 종류를 바꿀 수 있는 컴포넌트를 작성한다면,
<script type="text/x-template" id="anchored-heading-template">
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-else-if="level === 2">
<slot></slot>
</h2>
<h3 v-else-if="level === 3">
<slot></slot>
</h3>
<h4 v-else-if="level === 4">
<slot></slot>
</h4>
<h5 v-else-if="level === 5">
<slot></slot>
</h5>
<h6 v-else-if="level === 6">
<slot></slot>
</h6>
</script>
Vue.component('anchored-heading', {
template: '#anchored-heading-template',
props: {
level: {
type: Number,
required: true
}
}
})
위의 코드와 같은 컴포넌트가 작성될 수 있습니다. 하지만 위의 예제는 코드 중복이 많아 장황하게 보입니다. render
함수를 사용하여 다시 작성해 보면,
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // 태그 이름
this.$slots.default // 자식의 배열
)
},
props: {
level: {
type: Number,
required: true
}
}
})
위의 코드와 같이 될 수 있습니다. 코드 중복이 없기 때문에 템플릿으로 작성된 코드보다 간단하게 보일 수 있습니다. 이 때, <anchored-heading>
컴포넌트 안에 있는 Hello wold!
는 slot
속성이 정의 되어 있지 않기 때문에, $slots.default
에 배열로 저장됩니다.
2. Node, Tree, Virtual DOM
render
함수에 대해 이야기하기 전에 브라우저 작동 방식을 먼저 이야기 해 보도록 하겠습니다.
<div>
<h1>My title</h1>
Some text content
<!-- TODO: Add tagline -->
</div>
브라우저가 위의 코드를 읽게 되면, 아래 그림과 같이 DOM 노드 트리를 만듭니다.
모든 엘리먼트와 텍스트, 심지어 주석도 노드입니다. 노드는 페이지의 조각입니다. 위의 트리에서 볼 수 있듯이 각 녿는 자식을 가질 수 있습니다. 노드를 효율적으로 업데이트 하는 것은 어렵습니다. 하지만 다행이도 수동으로 업데이트 할 필요는 없습니다.
<h1>{{ blogTitle }}</h1>
템플릿에서 위의 코드와 같이 작성하거나,
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
위의 코드와 같이 render
함수를 사용하면, Vue는 자동으로 페이지를 업데이트 합니다.
1) Virtual DOM
Vue는 실제 DOM에 필요한 변경사항을 추적하기 위해 virtual DOM을 만듭니다.
return createElement('h1', this.blogTitle)
createElement
는 Virtual Node(VNode
)를 리턴합니다. Virtual Node는 실제 DOM 엘리먼트와 정확하게 일치하지는 않습니다. Virtual DOM은 컴포넌트 트리로 만들어진 VNode
트리입니다.
3. createElement
전달인자
// @returns {VNode}
createElement(
// {String | Object | Function}
// HTML 태그 이름, 컴포넌트 옵션 또는 함수 중
// 하나를 반환하는 함수입니다. 필수 사항.
'div',
// {Object}
// 템플릿에서 사용할 속성에 해당하는 데이터 객체입니다
// 데이터 객체입니다. 선택 사항.
{
// (아래 다음 섹션에 자세히 설명되어 있습니다.)
},
// {String | Array}
// VNode 자식들. `createElement()`를 사용해 만들거나,
// 간단히 문자열을 사용해 'text VNodes'를 얻을 수 있습니다. 선택사항
[
'Some text comes first.',
createElement('h1', 'A headline'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
1) 데이터 객체 깊이 알아보기
createElement
의 두번째 전달인자인 데이터 객체를 좀 더 자세히 이야기 해보도록 하겠습니다.
v-bind:class
와 v-bind:style
이 템플릿에서 특별하게 처리되는 것과 비슷하게 VNode
데이터 객체의 최상위 필드에 class
와 style
이 있습니다. 또한 일반적인 HTML 속성 뿐만 아니라 innerHTML
과 같은 DOM 속성도 가지고 있습니다.
{
// `v-bind:class` 와 같음
'class': {
foo: true,
bar: false
},
// `v-bind:style` 와 같음
style: {
color: 'red',
fontSize: '14px'
},
// 일반 HTML 속성
attrs: {
id: 'foo'
},
// 컴포넌트 props
props: {
myProp: 'bar'
},
// DOM 속성
domProps: {
innerHTML: 'baz'
},
// `v-on:keyup.enter`와 같은 수식어가 지원되지 않으나
// 이벤트 핸들러는 `on` 아래에 중첩됩니다.
// 수동으로 핸들러에서 keyCode를 확인해야 합니다.
on: {
click: this.clickHandler
},
// 컴포넌트 전용.
// `vm.$emit`를 사용하여 컴포넌트에서 발생하는 이벤트가 아닌
// 기본 이벤트를 받을 수 있게 합니다.
nativeOn: {
click: this.nativeClickHandler
},
// 사용자 지정 디렉티브.
// Vue는 이를 관리하기 때문에 바인딩의 oldValue는 설정할 수 없습니다.
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// 범위 지정 슬롯. 형식은
// { name: props => VNode | Array<VNode> } 입니다.
scopedSlots: {
default: props => createElement('span', props.text)
},
// 이 컴포넌트가 다른 컴포넌트의 자식인 경우, 슬롯의 이름입니다.
slot: 'name-of-slot',
// 기타 최고 레벨 속성
key: 'myKey',
ref: 'myRef'
}
2) 전체 예제
var getChildrenTextContent = function (children) {
return children.map(function (node) {
return node.children
? getChildrenTextContent(node.children)
: node.text
}).join('')
}
Vue.component('anchored-heading', {
render: function (createElement) {
// kebabCase id를 만듭니다.
var headingId = getChildrenTextContent(this.$slots.default)
.toLowerCase()
.replace(/\W+/g, '-')
.replace(/(^\-|\-$)/g, '')
return createElement(
'h' + this.level,
[
createElement('a', {
attrs: {
name: headingId,
href: '#' + headingId
}
}, this.$slots.default)
]
)
},
props: {
level: {
type: Number,
required: true
}
}
})
3) 제약사항
컴포넌트 토리의 모든 VNode
는 고유해야 합니다.
render: function (createElement) {
var myParagraphVNode = createElement('p', 'hi')
return createElement('div', [
// 이런 - Vnode가 중복입니다!
myParagraphVNode, myParagraphVNode
])
}
위의 코드는 VNode
가 중복되었기 때문에 잘못 작성된 코드입니다.
render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}
같은 엘리먼트 혹은 컴포넌트를 여러번 사용해야 할 경우 위의 코드와 같이 복제하여 사용해야 합니다.
4. 템플릿 기능을 일반 JavaScript로 변경하기
1) v-if
와 v-for
<ul v-if="items.length">
<li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
위의 코드와 같이 v-if
와 v-for
을 사용한 코드는
render: function (createElement) {
if (this.items.length) {
return createElement('ul', this.items.map(function (item) {
return createElement('li', item.name)
}))
} else {
return createElement('p', 'No items found.')
}
}
위의 코드와 같이 render
함수로 작성될 수 있습니다.
2) v-model
render
함수에는 v-model
과 대응되는 기능이 없어, 직접 구현해야 합니다.
render: function (createElement) {
var self = this
return createElement('input', {
domProps: {
value: self.value
},
on: {
input: function (event) {
self.value = event.target.value
self.$emit('input', event.target.value)
}
}
})
}
위의 코드와 같이 더 깊은 수준으로 코드를 작성해야 하지만 v-model
에 비해 세부 사항까지 더 많은 제어가 가능합니다.
3) 이벤트 및 키 수식어
수식어 | 접두어 |
| & |
| ! |
| ~ |
| ~! |
.pssive
, .capture
, .once
이벤트 수식어는 위의 테이블과 같이 접두어를 제공합니다.
on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
`~!mouseover`: this.doThisOnceInCapturingMode
}
위의 코드와 같이 .pssive
, .capture
, .once
이벤트 수식어를 사용할 수 있습니다. 다른 이벤트 수식어 및 키 수식어의 경우, 이벤트의 메소드를 사용할 수 있어 고유한 접두사가 필요하지 않습니다.
수식어 | 동등한 핸들러 |
| |
| |
| |
키 : | |
Modifiers Keys : | |
on: {
keyup: function (event) {
// 이벤트를 내보내는 요소가 이벤트가 바인딩 된 요소가 아닌 경우
// 중단합니다.
if (event.target !== event.currentTarget) return
// 키보드에서 뗀 키가 Enter키 (13)이 아니며
// Shift키가 동시에 눌러지지 않은 경우
// 중단합니다.
if (!event.shiftKey || event.keyCode !== 13) return
// 전파를 멈춥니다.
event.stopPropagation()
// 엘리먼트 기본 동작을 방지합니다.
event.preventDefault()
// ...
}
}
4) Slots
render: function (createElement) {
return createElement('div', this.$slots.default)
}
위의 코드와 같이 this.$slots
를 사용하여 정적 슬롯 내용을 구현 할 수 있습니다.
render: function (createElement) {
return createElement('div', [
this.$scopedSlots.default({
text: this.msg
})
])
}
특정 범위를 가지는 슬롯을 또한 위의 코드와 같이 this.$scopedSlots
에서 VNode
를 리턴하는 함수를 사용하여 구현 할 수 있습니다.
render (createElement) {
return createElement('div', [
createElement('child', {
// 데이터 객체의 `scopedSlots`를 다음 형식으로 전달합니다
// { name: props => VNode | Array }
scopedSlots: {
default: function (props) {
return createElement('span', props.text)
}
}
})
])
}
위의 코드와 같이 scopedSlots
를 사용하여 자식 컴포넌트로 특정 범위를 가지는 슬롯을 넘겨 줄 수 있습니다.
5. JSX
createElement(
'anchored-heading', {
props: {
level: 1
}
}, [
createElement('span', 'Hello'),
' world!'
]
)
render
함수를 많이 사용하면 위의 코드와 같이 고통스러워 질 수 있습니다... Vue와 JSX를 함께 사용할 수 있는 Babel plugin를 이용할 수 있습니다.
import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
el: '#demo',
render (h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
render
함수의 전달인자를 보면, createElement
를 h
라는 별칭으로 사용하는 것을 볼 수 있습니다. Vue 생태계에서 주로 사용되는 별칭입니다. render
함수에서 JSX를 사용할 때, h
가 항상 전달인자로 선언되어 있어야 합니다.
6. 함수형 컴포넌트
앞에서 이야기 한 <anchored-heading>
컴포넌트는 단순한 컴포넌트 입니다. 어떤 상태도 없고 상태를 감시할 필요도, 라이프 사이클 관련 메소드도 없습니다. 단지 props
를 가지는 기능만 있습니다. 이런 컴포넌트로 함수형 컴포넌트로 만들 수 있습니다. 즉 컴포넌트의 상태(data
)가 없고, 인스턴스화 (this
컨텍스트가 없음)할 필요가 없을 때 함수형 컴포넌트를 사용할 수 있습니다.
Vue.component('my-component', {
functional: true,
// 인스턴스의 부족함을 보완하기 위해
// 이제 2번째에 컨텍스트 인수가 제공됩니다.
render: function (createElement, context) {
// ...
},
// Props는 선택사항입니다.
props: {
// ...
}
})
2.3.0 이전의 버전에서는 함수형 컴포넌트에서 props
를 사용하려면 props
옵션을 정의해야 했지만, 2.3.0 이상에서는 props
옵션을 생략할 수 있습니다. 컴포넌트 노드에서 발견된 모든 속성은 암시적으로 props
로 추출됩니다.
<template functional>
</template>
2.5.0+ 이후에서는 싱글 파일 컴포넌트(.vue
파일)를 사용할 경우, 위의 코드와 같이 템플릿 기반의 함수형 컴포넌트를 정의할 수 있습니다.
props
: 전달받은props
객체children
:VNode
자식의 배열slots
: 슬롯 객체를 반환하는 함수data
: 컴포넌트에 전달된 전체 데이터 객체parent
: 상위 컴포넌트에 대한 참조listeners
: (2.3.0+) 부모에게 등록된 이벤트 리스너 객체data.on
의 별칭입니다.injections
: (2.3.0+) inject 옵션을 사용하면 resolved injection을 가집니다.
함수형 컴포넌트의 render
함수의 전달인자인 context
는 객체로 위의 값들을 가집니다.
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // 태그 이름
this.$slots.default // 자식의 배열
)
},
props: {
level: {
type: Number,
required: true
}
}
})
앞 부분에서 <achored-heading>
컴포넌트를 위의 코드와 같이 render
함수로 구현하였습니다. 위의 컴포넌트를 함수형 컴포넌트로 변경하는 것은 단순합니다.
Vue.component('anchored-heading', {
functional: true,
render: function (createElement, context) {
return createElement(
'h' + context.props.level, // 태그 이름
context.children // 자식의 배열
)
},
props: {
level: {
type: Number,
required: true
}
}
})
functional: true
를 추가합니다.render
함수의 전달인자에context
를 추가합니다.this.$slots.default
를context.children
으로 변경합니다.this.level
을context.props.level
로 변경합니다.
위의 코드와 같이 함수형 컴포넌트로 변경 할 수 있습니다.
함수형 컴포넌트는 단순한 함수이기 때문에 랜더링에 들어가는 비용이 작습니다. 그러나 Vue 크롬 개발자 도구의 컴포넌트 트리에서 함수형 컴포넌트를 볼 수는 없습니다.
var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }
Vue.component('smart-list', {
functional: true,
render: function (createElement, context) {
function appropriateListComponent () {
var items = context.props.items
if (items.length === 0) return EmptyList
if (typeof items[0] === 'object') return TableList
if (context.props.isOrdered) return OrderedList
return UnorderedList
}
return createElement(
appropriateListComponent(),
context.data,
context.children
)
},
props: {
items: {
type: Array,
required: true
},
isOrdered: Boolean
}
})
또한 함수형 컴포넌트는 래퍼 컴포넌트로도 유용하게 사용할 수 있습니다.
1) slots()
VS children
slots().default
와 children
은 유사하지만 다릅니다.
<my-functional-component>
<p slot="foo">
first
</p>
<p>second</p>
</my-functional-component>
위의 코드와 같이 함수형 컴포넌트에 두개의 자식 엘리먼트들이 있을 때, children
은 두개의 단란을 반환하고, slots().default
는 두번째 단락을 반환합니다.
참고
'Vue.JS' 카테고리의 다른 글
[Vue.JS] 필터 (2) | 2019.02.19 |
---|---|
[Vue.JS] 플러그인 (0) | 2019.02.18 |
[Vue.JS] 사용자 정의 디렉티브 (1) | 2019.02.07 |
[Vue.JS] Mixins (0) | 2019.02.05 |
[Vue.JS] 컴포넌트 (고급:Handling Edge Cases) (0) | 2019.02.02 |