티스토리 뷰

Vue.JS

[Vue.JS] Render Functions & JSX

버미노트 2019. 2. 14. 00:07

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 노드 트리를 만듭니다.

DOM 노트 트리
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:classv-bind:style이 템플릿에서 특별하게 처리되는 것과 비슷하게 VNode 데이터 객체의 최상위 필드에 classstyle이 있습니다. 또한 일반적인 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-ifv-for

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>

위의 코드와 같이 v-ifv-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) 이벤트 및 키 수식어

 수식어

 접두어

 .passive

 &

 .capture

 !

 .once

 ~

 .capture.once 또는

 .once.capture

 ~!

.pssive, .capture, .once 이벤트 수식어는 위의 테이블과 같이 접두어를 제공합니다.

on: {
  '!click': this.doThisInCapturingMode,
  '~keyup': this.doThisOnce,
  `~!mouseover`: this.doThisOnceInCapturingMode
}

위의 코드와 같이 .pssive, .capture, .once 이벤트 수식어를 사용할 수 있습니다. 다른 이벤트 수식어 및 키 수식어의 경우, 이벤트의 메소드를 사용할 수 있어 고유한 접두사가 필요하지 않습니다.

수식어

 동등한 핸들러

 .stop

 event.stopPropagation()

 .prevent

 event.preventDefault()

 .self

 if (event.target !== event.currentTarget) return

 키 : .enter, .13

 if (event.keyCode !== 13) return (13는 키 코드로 다른 키 코드 사용이 가능합니다.)

 Modifiers Keys : .ctrl, .alt, .shift, .meta

 if (!event.ctrlKey) return (ctrlKeyaltKey, shiftKey 또는 metaKey로 각각 변경하십시오.)

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 함수의 전달인자를 보면, createElementh라는 별칭으로 사용하는 것을 볼 수 있습니다. 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
    }
  }
})
  1. functional: true 를 추가합니다.
  2. render 함수의 전달인자에 context를 추가합니다.
  3. this.$slots.defaultcontext.children으로 변경합니다.
  4. this.levelcontext.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().defaultchildren은 유사하지만 다릅니다.

<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
댓글
공지사항
최근에 올라온 글