ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Vue.js] 재사용 & 컴포지션 - (1) Mixin, Directive, Render
    Front-End(Web)/Vue 2021. 7. 20. 21:58
    반응형

    내가 Vue를 사용, 공부하면서 가장 정리하고 싶은 섹션이 시작되었다!

     

    프론트엔드 작업을 하다보면 반복되는 컴포넌트와 기능들이 등장하기 마련이다.

    특히, 이런 기능들이 필요할 때마다 매번 컴포넌트에 작성하게 된다면, 이후 수정 등 유지보수가 매우 안 좋을 것이다.

     

    이러한 기능들을 공통화, 전역화하는 다양한 기법들이 Vue에 존재한다고 들었다. (믹스인, 플러그인 등)

    이들의 차이점은 무엇인지, 그리고 어떻게 사용하며 어떤 상황에서 특히 적합할지를 공부해보도록 하겠다.


    💚 Mixins(믹스인)

    Mixins는 Vue 컴포넌트에서 재사용 가능한 기능을 배포하는 유연한 방법이라고 소개한다.

    * 믹스인(Mixin) : 다중 상속을 지원하는, 구현된 인터페이스를 의미함

     

    의미 그대로, 컴포넌트에 원하는 기능을 실행하는 객체(컴포넌트와 유사)를 커스텀한 뒤 이를 혼합하는 기법이다.

    Mixin을 제대로 활용하려면, 컴포넌트 Lifecycle에 대한 이해가 어느정도 동반되어야 한다.

     

    // mixin.js
    var myMixin = {
      created: function () {
        this.hello()
      },
      methods: {
        hello: function () {
          console.log('hello from mixin!')
        }
      }
    }
    
    // component.vue
    var Component = Vue.extend({
      mixins: [myMixin]
    })
    
    var component = new Component() // => "hello from mixin!"

     

    1. 옵션 병합

    - Data 값 병합

    기본적인 data, computed 등의 속성들을 Mixin 으로 병합할 수 있다.

    단, 프로퍼티가 중첩되면 컴포넌트에서 선언한 값으로 재귀하여 병합된다.

    var mixin = {
      data: function () {
        return {
          message: 'hello',
          foo: 'abc'
        }
      }
    }
    
    new Vue({
      mixins: [mixin],
      data: function () {
        return {
          message: 'goodbye',
          bar: 'def'
        }
      },
      created: function () {
        console.log(this.$data)
        // => { message: "goodbye", foo: "abc", bar: "def" }
      }
    })

     

    - Lifecycle 함수(메서드) 병합

    같은 Lifecycle 에서 Hooks(함수)를 호출할 경우, Mixin의 Hooks가 컴포넌트의 Hooks 보다 먼저 호출된다.

    var mixin = {
      created: function () {
        console.log('mixin hook called')
      }
    }
    
    new Vue({
      mixins: [mixin],
      created: function () {
        console.log('component hook called')
      }
    })
    
    // => "mixin hook called"
    // => "component hook called"

     

    - 객체 값 병합 : methods, components, directives

    위 프로퍼티들과 같이 객체 값을 요구하는 옵션은 같은 프로퍼티에 병합된다.

    여기서 충돌하는 필드가 존재하면, 컴포넌트의 프로퍼티가 우선순위를 가진다.

    var mixin = {
      methods: {
        foo: function () {
          console.log('foo')
        },
        conflicting: function () {
          console.log('from mixin')
        }
      }
    }
    
    var vm = new Vue({
      mixins: [mixin],
      methods: {
        bar: function () {
          console.log('bar')
        },
        conflicting: function () {
          console.log('from self')
        }
      }
    })
    
    vm.foo() // => "foo"
    vm.bar() // => "bar"
    vm.conflicting() // => "from self

     

    2. Global Mixin (전역 믹스인)

    Mixin을 전역에서도 활용할 수 있다. 이를 적절히 활용하면 사용자 정의 옵션에 대한 처리로직을 주입할 수 있다.

    Vue.mixin({
      mounted() {
        console.log('hello from mixin!')
      }
    })
    
    new Vue({
      ...
    })

    * 단, Mixin을 전역으로 걸면 이후에 생성된 모든 전역 인스턴스에 영향을 미치므로 지양되는 패턴이긴 하다.

     

    3. 사용자 정의 옵션 병합 전략

    Vue.config.mixin에 함수를 만들고, 이를 통해 추가되는 프로퍼티(methods 등)에 값을 추가하는 병합 전략을 소개한다.


    💚 Custom Directives(사용자 지정 디렉티브)

    Vue 프로젝트에서 자주 사용하는 v-model, v-show 등 기본적으로 제공하는 Vue 디렉티브들이 존재한다.

    하지만, 이 뿐만 아니라 특정기능으로 사용자가 커스텀하여 디렉티브로서 사용할 수 있다. DOM 요소 조작동작에 통상 많이 적용된다.

     

    * Directives vs Components

    Vue 공식문서에서는 특정 기능구현을 위한 2가지 방법인 Directive와 Component(HOC) 의 구분을 권장한다.

    Directive는 DOM 조작만을 캡슐하하기 위해, Components는 뷰나 데이터 로직이 포함되는 단위 구성을 위해 사용한다.

     

    * 참고(Vue 기본 디렉티브)

     

    - 사용자 지정 디렉티브 구현 

    다음은, autofocous 기능을 하는 사용자 지정 디렉티브를 제작한 예시이다.

    <input v-focus>
    // 1) 전역 디렉티브
    Vue.directive('focus', {
      inserted: function (el) {
        el.focus()
      }
    })
    
    // 2) 지역 디렉티브 (컴포넌트 속성)
    directives: {
      focus: {
        inserted: function (el) {
          el.focus()
        }
      }
    }

    이처럼, 디렉티브를 등록하는 방법은 전역, 지역 2가지 방법이 있다. 지역선언을 위해 컴포넌트는 directives 속성을 지원한다.

     

    1. Hooks(훅 함수)

    디렉티브 정의 객체에서는 디렉티브의 적용 및 엘리먼트 상태에 따른 훅 함수를 제공한다. 괄호 안은 메서드의 매개변수이다. (필수로 사용해야하는 것은 아님)

    • bind : 디렉티브가 맨 처음 엘리먼트에 바인딩될 때 한 번만 호출 (el, binding, vnode)
    • inserted : 바인딩된 엘리먼트가 부모노드에 삽입되었을 때 호출 (el, binding, vnode)
    • update : 바인딩된 컴포넌트가 업데이트(자식 업데이트 전) 된 후 호출 (el, binding, vnode, oldVnode)
    • componentUpdated : 바인딩된 컴포넌트 및 자식 컴포넌트들이 업데이트 후 호출 (el, binding, vnode, oldVnode)
    • unbind : 디렉티브가 엘리먼트로부터 언바인딩될 경우 한 번만 호출 (el, binding, vnode)

     

    2. 디렉티브 훅 전달인자

    디렉티브의 훅은 다음 전달인자들을 사용한다. 또한, 이는 읽기 전용으로 절대 변경하면 안된다.

    • el : 디렉티브가 바인딩된 엘리먼트. 이를 사용하여 DOM 조작이 가능한 것이다.
    • binding : 아래의 프로퍼티들을 가진 객체
      • name : 디렉티브의 이름 (v- 다음)
      • value : 디렉티브에서 전달받은 값.
      • oldValue : 이전 값. update와 componentUpdated 에서만 사용할 수 있다. (값 변경확인)
      • expression : 표현식 문자열 (v-express="1 + 1" 에서 값은 2가 아닌 "1 + 1")
      • arg : 디렉티브의 전달인자. 있는 경우에만 존재한다. (v-argu:foo 에서 "foo")
      • modifiers : 포함된 수식어 객체. 있는 경우에만 존재한다. (v-modi.foo.bar 에서 { foo: true, bar: true })
    • vnode : Vue 컴파일러가 만드는 버추얼 노드
    • oldVnode : 이전의 버추얼 노드. 마찬가지로, update와 componentUpdated 에서만 사용 가능.

     

    * Hooks를 활용한 Custom Directives 예제

    Vue.directive('highlight', {
      bind(el, binding, vnode) {
        if (binding.arg == 'background') {
          el.style.backgroundColor = binding.value
        } else {
          el.style.color = binding.value
        }
      },
    })
    <p v-highlight="red">Hello World</p>			// 글씨색 변경
    <p v-highlight:background="blue">Hello World</p>	// 배경색 변경

    이처럼, binding의 arg 속성을 통해, background 가 바인딩되면 배경색을, 아니면 기본적으로 폰트색을 변경하는 디렉티브를 만들었다.

     

    * Dynamic Directives 인자

    Directive에 전달되는 argument를 동적으로 할당하는 문법이다. [arg명칭]을 디렉티브에 바인딩하면 된다.

    <div id="dynamicexample">
      <h3>Scroll down inside this section ↓</h3>
      <p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
    </div>
    Vue.directive('pin', {
      bind: function (el, binding, vnode) {
        el.style.position = 'fixed'
        var s = (binding.arg == 'left' ? 'left' : 'top')
        el.style[s] = binding.value + 'px'
      }
    })
    
    new Vue({
      el: '#dynamicexample',
      data: function () {
        return {
          direction: 'left'
        }
      }
    })

    direction은 기본값은 left이며, 이 디렉티브에 바인딩되는 arg들(left & top) 에 따라 동적으로 할당할 수 있게되는 것이다.

     

    4. 객체 리터럴

    디렉티브에 여러가지 값이 필요한 경우, 객체 형태로 할당할 수 있다. 각 key에 해당하는 value들이 하달된다.

    <div v-demo="{ color: 'white', text: 'hello!' }"></div>
    Vue.directive('demo', function (el, binding) {
      console.log(binding.value.color) // => "white"
      console.log(binding.value.text)  // => "hello!"
    })

    💚 Render Functions & JSX

    Vue는 <template> 내 HTML로 작성되는 것을 기본적으로 권장한다. 하지만, Javascript가 완전히 필요한 상황이 존재한다.

    아래 예시와 같이, 다양한 조건에 따라 처리가 필요하다면 직관적이긴 하나 유지보수에 불리하며 <h>태그, <slot> 등이 반복되는 비효율성이 잔존한다.

    <anchored-heading :level="1">Hello world!</anchored-heading>
    <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
        }
      }
    })

     

    1. 노드, 트리, 그리고 Virtual DOM

    HTML의 각 요소(노드), 이들로 이루어진 노드 트리, 그리고 렌더링 전에 비교를 위한 Virtual DOM 개념을 훑어준다.

    그래서, render() 메서드는 createElement 함수를 인자로 받고, VNode 라고 하는 노드객체를 반환하는 것이다.

    (이는 Virtual DOM에 속하게 된다.)

     

    2. createElement 전달인자

    createElement 함수는 다시 3가지 인자를 받는다. renderElement는 필수, definition 과 children 은 선택사항이다.

    • renderElement : HTML 태그명 혹은 컴포넌트 옵션 및 함수이다. (Type : String, Object, Function)
    • definition : 템플릿에서 사용할 속성에 해당하는 데이터 객체이다. (Type : Object)
    • children : VNode의 자식들. 역시 createElement로 추가하거나, text로만 작성해도 된다. (Type : String, Array)

     

    특히, definition(데이터 객체)는 class 같은 기본적인 HTML 속성부터, v-bind, v-html 등 디렉티브 바인딩도 가능하다.

    아래 예시와 같이, HTML, class, style 부터 이벤트, 디렉티브, 슬롯까지 설정할 수 있는걸 볼 수 있다.

    {
      // `v-bind:class` 와 같음
      // accepting either a string, object, or array of strings and objects.
      class: {
        foo: true,
        bar: false
      },
      // `v-bind:style` 와 같음
      // accepting either a string, object, or array of objects.
      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',
      // If you are applying the same ref name to multiple
      // elements in the render function. This will make `$refs.myRef` become an
      // array
      refInFor: true
    }

     

    * 단, VNodes는 고유해야 한다. 반복되야 할 경우에는, 이를 여러번 쓰는게 아니라 Array.map() 메서드를 활용한다.

    render: function (createElement) {
      return createElement('div',
        Array.apply(null, { length: 20 }).map(function () {
          return createElement('p', 'hi')
        })
      )
    }

     

    3. 템플릿 기능을 일반 Javascript로 변경하기

    v-if, v-for, v-model 과 같은 디렉티브를 render() 함수로 변환하는 방법, render()의 데이터 객체의 on에서 사용할 수 있는 이벤트 관련 수식어(capture, once, stopPropagation, preventDefault 등) 을 소개하고 있다.

     

    4. JSX

    createElement가 반환하는 VNode를 작성하는 건 JS문법에 가깝다.

    특히, 태그명을 첫 번째 인자로 설정하고, 두 번째 인자에 필요한 데이터 객체를 작성하면 직관성이 많이 떨어진다.

    createElement(
      'anchored-heading', {
        props: {
          level: 1
        }
      }, [
        createElement('span', 'Hello'),
        ' world!'
      ]
    )

     

    하지만, render() 함수는 JSX를 return 하므로 문법이 직관적이며, 이를 Babel이 JS로 트랜스파일링 해준다.

    import AnchoredHeading from './AnchoredHeading.vue'
    
    new Vue({
      el: '#demo',
      render: function (h) {
        return (
          <AnchoredHeading level={1}>
            <span>Hello</span> world!
          </AnchoredHeading>
        )
      }
    })

     

    5. 함수형 컴포넌트

    Vue 역시, React처럼 컴포넌트 중에 data(State)나 Lifecycle을 딱히 사용하지 않는 케이스가 존재한다.

    또한, this 관련된 속성을 별도로 사용하지 않아, 인스턴스화 되지 않은 간단한 컴포넌트의 케이스가 존재할수도 있다.

     

    이런 경우 보통, Props를 통해 뷰로 표현하는 정도의 역할을 수행하며, 이 컴포넌트는 함수형 또는 기능적으로 표시한다.

    Vue.component('my-component', {
      functional: true,
      // Props는 선택사항입니다.
      props: {
        // ...
      }
      // 인스턴스의 부족함을 보완하기 위해
      // 이제 2번째에 컨텍스트 인수가 제공됩니다.
      render: function (createElement, context) {
        // ...
      },
    })

    * 2.3.0 이전 버전에서는, 함수형 컴포넌트에도 props 속성이 필요했으나, 이후에는 생략할 수 있다.

    (모든 속성을 암시적으로 props로 간주한다.)

    * 2.5.0 버전 이후로, 싱글 파일 컴포넌트를 사용하는 경우, 컴포넌트 <template>에 "functional" 을 설정하면 된다.

    (<template functional>)

     

    - context

    인스턴스화 하지 않은 부족함을 보완하기 위해, context 인수를 통해 컴포넌트에 필요한 인수들을 활용할 수 있다.

    • props: 전달받은 props에 대한 객체
    • children: VNode 자식의 배열
    • slots: 슬롯 객체를 반환하는 함수
    • scopedSlots: (2.6.0+) An object that exposes passed-in scoped slots. Also exposes normal slots as functions.
    • data: 컴포넌트에 전달된 전체 데이터 객체
    • parent: 상위 컴포넌트에 대한 참조
    • listeners: (2.3.0+) 부모에게 등록된 이벤트 리스너를 가진 객체입니다. data.on의 알리아스입니다.
    • injections: (2.3.0+) inject 옵션을 사용하면 리졸브드 인젝션을 가집니다

     

    6. 템플릿 컴파일

    Vue.compile 을 통해 템플릿의 컴파일 과정을 소개하는 부분이다. (중요도는 낮음)


    Vue에서는 기능을 전역화하는 방법이 정말 많다고 막연하게 알고 있었다. (믹스인, 디렉티브, 플러그인 등.)

    이번 기회를 통해, 이들의 각각의 특징 및 차이를 알면서 어느 상황에 어떠한 방법을 써야 되는지 많이 느꼈다.

     

    믹스인같은 경우, HOC(Higher Order Component) 같아, 특정 라이프사이클 기반 기능 공통화에 좋을 것 같다.

    디렉티브의 경우는, 좀 더 HTML 요소 자체의 조작(추가 및 제거, 변화 감지) 에 좀 더 어울리는 것 같다.

     

    다음으로는, 공식문서에서 소개하는 나머지 재사용 & 컴포지션 기법인 플러그인, 필터에 대해 알아보겠다!

     

    [출처]

    - Vue.js 공식문서 : https://kr.vuejs.org/v2/guide/mixins.html  

    - IT 마이닝 님의 블로그(믹스인) : https://itmining.tistory.com/123  

    - B베이스캠프(디렉티브) : https://chansnotes.github.io/2019/06/15/tutorial12-custom-directives/

    반응형
Designed by Tistory.