본문 바로가기
front/vue

[vue] Render Functions & JSX

by juniKang 2022. 8. 10.

뷰는 대부분의 케이스에서 애플리케이션을 빌드하기위해 템플릿을 사용하는 것을 추천한다. 하지만, 자바스크립트의 프로그램적인 최대한의 파워가 필요한 상황이 있을 수 있다. 그 상황이 우리가 렌더 함수를 사용해야할 때이다.

가상돔과 렌더 함수의 개념을 처음 들어본다면, 렌더링 메커니즘 챕터를 먼저 읽어라.

Basic Usage

Vnodes 만들기

뷰는 vnode를 만드는 것에 h() 함수를 제공한다:

import { h } from 'vue'

const vnode = h(
  'div', // type
  { id: 'foo', class: 'bar' }, // props
  [
    /* children */
  ]
)

h()는 hyperscript의 줄임말이다. hyperscript는 "HTML(hypertext markup language)를 생산하는 자바스크립트"를 의미한다. 이 이름은 많은 가상 돔 구현체들에 의해 공유되는 관습적으로 내려오는 이름이다. 좀더 기술적인 이름은 createVnode()가 될 수 있다. 하지만 짧은 이름이 렌더 함수에서 여러번 이 함수를 호출해야 할 때 도움을 줄 수 있다.

 

h() 함수는 매우 유연하게 디자인되었다:

// 타입을 제외한 모든 아규먼트가 선택사항이다.
h('div')
h('div', { id: 'foo' })

// 애트리뷰트와 프로퍼티들 모두 props에 사용될 수 있다.
// 뷰는 자동으로 적절히 선택하여 할당한다.
h('div', { class: 'bar', innerHTML: 'hello' })

// .prop이나 .attr같은 접근제어자 props들은 
// '.' 과 '^' 접두어로 각각 더해 질 수 있다.
h('div', { '.name': 'some-name', '^width': '100' })

// 템플릿에서 class나 style을 쓸때와 동일하게,
// 객체나 배열을 사용할 수 있다.
h('div', { class: [foo, { bar }], style: { color: 'red' } })

// 이벤트 리스너는 onXxx 로 넘겨져야만 한다.
h('div', { onClick: () => {} })

// 자식은 문자열도 될 수 있다.
h('div', { id: 'foo' }, 'hello')

// 프로퍼티가 없을 때는 생략할 수 있다.
h('div', 'hello')
h('div', [h('span', 'hello')])

// 자식 배열은 vnodes와 문자열을 합친것을 담고있을 수 있다.
h('div', ['hello', h('span', 'hello')])

그결과 vnode는 다음과 같은 모양을 갖는다:

const vnode = h('div', { id: 'foo' }, [])

vnode.type // 'div'
vnode.props // { id: 'foo' }
vnode.children // []
vnode.key // null

 

렌더 함수 선언

컴포지션 API로 템플릿을 사용할 때, setup()훅의 리턴 값은 템플릿으로 노출되는 데이터가 사용된다. 하지만 렌더함수를 사용할 때는 대신 렌더함수를 바로 리턴할 수 있다.

import { ref, h } from 'vue'

export default {
  props: {
    /* ... */
  },
  setup(props) {
    const count = ref(1)
    
    // return the render function
    return () => h('div', props.msg + count.value)
  }
}

렌더함수는 setup()안에 선언되었다. 그래서 자연적으로 같은 범위에 있는 모든 선언된 reactive 상태와 props들에 접근할 수 있다. 

 

하나의 vnode를 리턴하는 것에 추가로, 문자열이나 다른 배열들도 리턴할 수 있다:

export default {
  setup() {
    return () => 'hello world!'
  }
}
import { h } from 'vue'

export default {
  setup() {
    // 여러개의 루트 노드들을 리턴하기위해 배열을 사용
    return () => [
      h('div'),
      h('div'),
      h('div')
    ]
  }
}
TIP
바로 값을 리턴하는 것 대신에 함수를 리턴해라! setup() 함수는 컴포넌트당 하나만 호출할 수 있지만, 리턴된 렌더함수는 여러번 호출될 수 있다.

렌더 함수형 컴포넌트가 어떤 인스턴스 상태도 필요하지 않다면, 간편함을 위해 함수로 즉시 선언할 수 있다.

function Hello() {
  return 'hello world!'
}

이건 유효한 뷰 컴포넌트이다! 이 문법의 보다 상세한 내용은 함수형 컴포넌트 파트를 보라.

 

Vnode 들은 반드시 유일해야 한다.

컴포넌트에 있는 모든 vnode들은 반드시 유일해야 한다. 그래서 아래 렌더 함수는 유효하지 않다.

function render() {
  const p = h('p', 'hi')
  return h('div', [
    // 이크! - 중복된 vnodes!
    p,
    p
  ])
}

정말로 여러번 같은 엘리먼트/컴포넌트를 중복하길 원한다면, 팩토리 함수로 할 수 있다. 예를들어, 아래 렌더 함수는 20개의 동일한 절을 렌더링하는 완벽하게 유효한 방법이다:

function render() {
  return h(
    'div',
    Array.from({ length: 20 }).map(() => {
      return h('p', 'hi')
    })
  )
}

JSX / TSX

JSX는 자바스크립트에서 XML처럼 코딩할 수 있게 해주는 확장팩이다. 아래처럼 쓸 수 있다:

const vnode = <div>hello</div>

JSX 표현식에서, 중괄호안에 동적인 값을 사용할 수 있다:

const vnode = <div id={dynamicId}>hello, {userName}</div>

create-vue와 Vue CLI 둘 다 JSX 지원을 설정해서 프로젝트에 포함시키는 옵션이 있다. JSX에 대해 메뉴얼이 궁금하다면, @vue/babel-plugin-jsx 의 공식문서에서 상세내용을 참조하라.

 

JSX는 리엑트에서 처음 소개 되었지만, JSX는 정의된 런타임 시멘틱이 없어서, 다양한 다른 아웃풋에서 컴파일될 수 있다. 만약 전에 JSX로 일해보았다면, Vue의 JSX 형태는 리엑트의 그것과는 다르다는 걸 주의하라. 그래서 리엑트의 JSX형태는 뷰 애플리케이션에서 사용할 수 없다. 대표적인 차이점은 :

  • class나 for 같은 HTML 애트리뷰트를 사용할 수 있다 - className이나 htmlFor 을 사용할 필요가 없다.
  • 칠드런을 컴포넌트들로 넘기는것이 다르게 동작한다(즉 slots).

뷰의 타입 정의는 또한 TSX 사용을 위한 타입 인터페이스를 제공한다. TSX를 사용할 때, tsconfig.json에 "jsx":"prserve"로 특정해야, JSX 대신 TSX를 감지한다.

 


렌더 함수 레시피

아래는 렌더함수와 JSX로 동일하게 템플릿 기능들을 구현하기 위한 자주 사용되는 레시피다.  

v-if

템플릿:

<div>
  <div v-if="ok">yes</div>
  <span v-else>no</span>
</div>

렌더함수:

h('div', [ok.value ? h('div', 'yes') : h('span', 'no')])

JSX:

<div>{ok.value ? <div>yes</div> : <span>no</span>}</div>

 

v-for

템플릿:

<ul>
  <li v-for="{ id, text } in items" :key="id">
    {{ text }}
  </li>
</ul>

렌더 함수:

h(
  'ul',
  // assuming `items` is a ref with array value
  items.value.map(({ id, text }) => {
    return h('li', { key: id }, text)
  })
)

JSX:

<ul>
  {items.value.map(({ id, text }) => {
    return <li key={id}>{text}</li>
  })}
</ul>

 

v-on

대문자 한 글자와 함께 on으로 시작하는 이름의 프로퍼티는 이벤트리스너로 여겨진다. 예를 들어, onClick은 템플릿에서 @click과 동일하다.

h(
  'button',
  {
    onClick(event) {
      /* ... */
    }
  },
  'click me'
)
<button
  onClick={(event) => {
    /* ... */
  }}
>
  click me
</button>

 

Event Modifiers

.passive, .captue, .once 이벤트 접근제어자는 낙타표기법으로 이벤트 이름 뒤에 연달아 작성할 수 있다.

 

예를 들어 :

h('input', {
  onClickCapture() {
    /* listener in capture mode */
  },
  onKeyupOnce() {
    /* triggers only once */
  },
  onMouseoverOnceCapture() {
    /* once + capture */
  }
})
<input
  onClickCapture={() => {}}
  onKeyupOnce={() => {}}
  onMouseoverOnceCapture={() => {}}
/>

다른 이벤트나 키 접근제어자는, withModifiers 헬퍼를 통해 사용될 수 있다:

import { withModifiers } from 'vue'

h('div', {
  onClick: withModifiers(() => {}, ['self'])
})
<div onClick={withModifiers(() => {}, ['self'])} />

 

컴포넌트

컴포넌트로 vnode를 만들 때는, h()의 첫번쨰 아규먼트가 컴포넌틔 정의여야만 한다. 렌더함수를 사용할 때, 컴포넌트를 등록할 필요가 없다는 것을 의미한다.- import한 컴포넌트를 바로 쓰기만 하면 된다.

import Foo from './Foo.vue'
import Bar from './Bar.jsx'

function render() {
  return h('div', [h(Foo), h(Bar)])
}
function render() {
  return (
    <div>
      <Foo />
      <Bar />
    </div>
  )
}

보는 것처럼, h 는 유효한 뷰 컴포넌트이기만 하면 어떤 파일 포멧에서 import한 컴포넌트이든 사용할 수 있다.

 

동적인 컴포넌트들도 렌더함수로 간단히 사용할 수 있다:

import Foo from './Foo.vue'
import Bar from './Bar.jsx'

function render() {
  return ok.value ? h(Foo) : h(Bar)
}
function render() {
  return ok.value ? <Foo /> : <Bar />
}

만약 컴포넌트가 이름으로 등록되었고, import할 수 없다면 (라이브러리로 전역적으로 등록되었다든지 해서), resolveComponent() 헬퍼를 사용해서 프로그램적으로 풀 수 있다.

 

Rendering Slots

렌더 함수에서, slots들은 setup() 컨텍스트에서 접근될 수 있다. slots 객체에있는 각각의 slots들은 vnodes의 배열을 리턴하는 함수이다:

export default {
  props: ['message'],
  setup(props, { slots }) {
    return () => [
      // default slot:
      // <div><slot /></div>
      h('div', slots.default()),

      // named slot:
      // <div><slot name="footer" :text="message" /></div>
      h(
        'div',
        slots.footer({
          text: props.message
        })
      )
    ]
  }
}

JSX:

// default
<div>{slots.default()}</div>

// named
<div>{slots.footer({ text: props.message })}</div>

 

Passing Slots

컴포넌트로 칠드런들을 넘기는 것은 칠드런들을 엘리먼트로 넘기는 것과는 약간 다르게 동작한다. 배열 대신에, 슬롯 함수들의 객체나 슬롯한수를 넘기는 것이 필요하다. 슬롯 함수들은 일반적인 렌더 함수가 리턴할수 있는 모든 것을 리턴할 수 있다. 자식 컴포넌트에 접근할 때 vnode들의 배열은 언제나 표준화 된다.

// 하나의 기본 슬롯
h(MyComponent, () => 'hello')

// 이름지어진 슬롯
// 슬롯 객체들이 props로 여겨지는 걸 피하기 위해
// null을 사용했다.
h(MyComponent, null. {
  default: () => 'default slot',
  foo: () => h('div', 'foo'),
  bar: () => [h('span', 'one'), h('span', 'two')]
})

JSX:

// default
<MyComponent>{() => 'hello'}</MyComponent>

// named
<MyComponent>{{
  default: () => 'default slot',
  foo: () => <div>foo</div>,
  bar: () => [<span>one</span>, <span>two</span>]
}}</MyComponent>

 

슬롯을 함수로 넘기는 것은 자식 컴포넌트에의해 lazy로 호출되도록 해준다. 이것은 부모 대신에 자식에 의해 추적되는 슬롯의 의존성을 만들어준다. 그래서 좀더 정확하고 효율적으로 업데이트할 수 있다.

 

내장 컴포넌트

<KeepAlive>, <Transition>, <TransitionGroup>, <Teleport> <Suspense> 같은 내장 컴포넌트들은 렌터함수에서 사용하기 위해 반드시 import 해야 한다:

import { h, KeepAlive, Teleport, Transition, TransitionGroup } from 'vue'

export default {
  setup () {
    return () => h(Transition, { mode: 'out-in' }, /* ... */)
  }
}

 

v-model

v-model 은 템플릿 컴파일 도중 modelValue와 onUpdate:modelValue 프로퍼티로 확장된다. 이 프로퍼티들을 우리가 제공해줘야만 한다:

export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    return () => 
      h(SomeComponent, {
        modelValue: props.modelValue,
        'onUpdate:modelValue': (value) => emit('update:modelValue', value)
      })
  }
}

 

Custom Directives

커스텀 지시어들은 withDirectives를 사용해서 vnode에 적용할 수 있다:

import { h, withDirectives } from 'vue'

// a custom directive
const pin = {
  mounted() { /* ... */ },
  updated() { /* ... */ }
}

// <div v-pin:top.animate="200"></div>
const vnode = withDirectives(h('div'), [
  [pin, 200, 'top', { animate: true }]
])

만약 지시어가 이름으로 등록되어서 import 할 수 없다면, resolveDirective 헬퍼를 통해 리졸브 할 수 있다.

 


Functional Components

함수형 컴포넌트는 스스로 어떤 상태도 가지지 않는 컴포넌트를 나타내는 형태이다. 순수한 함수처럼 동작한다: props를 받고, vnodes를 내보낸다. 함수형 컴포넌트는 컴포넌트 인스턴스를 만들지 않고 렌더링 된다(즉 this가 없다). 그리고 컴포넌트 라이프 사이클 훅이 없다.

 

함수형 컴포넌트를 만들기 위해 뷰 옵션 객체 대신에 순수한 함수를 사용한다. 함수는 사실상 컴포넌트를 위한 reder 함수이다.

 

함수형 컴포넌트의 기본 구조는 setup() 훅과 같다:

function MyComponent(props, { slots, emits, attrs }) {
  // ...
}

컴포넌트를 위한 대부분의 설정 옵션들은 함수형 컴포넌트에는 적용할 수 없다. 하지만, 프로퍼티로 추가해서 props와 emits를 정의하는 것은 가능하다:

MyComponent.props = ['value']
MyComponent.emits = ['click']

만약 props 옵션이 특정되지 않으면, 함수로 넘겨지는 props 객체는 attrs와 동일하게 모든 애트리뷰트를 포함할 것이다. props 이름들은 props 옵션이 특정되지 않으면, 낙타표기법으로 일반화되지 않을 것이다.

 

함수형 컴포넌트들은 일반적인 컴포넌트처럼 등록되고 사용될 수 있다. h()의 첫번째 아규먼트로 함수를 넘긴다면, 함수형 컴포넌트로 여겨질 것이다.

'front > vue' 카테고리의 다른 글

[vue] Rendering Mechanism  (0) 2022.08.11
[vue] Vue and Web Components  (1) 2022.08.09
[vue] TransitionGroup  (0) 2022.08.08
[vue] Transition  (0) 2022.08.08
[vue] watch()  (0) 2022.08.08

댓글