들어는 보았나 Transparent Wrapper Component

장재영 (ChangJaeYoung)·2020년 6월 17일
8
post-thumbnail

Vuejs가 제공하는 유용한 props들이 있다. 가령 v-model, @focus 등의 이벤트 인터페이스 들은 input, textarea 등의 native component에 prop, attr으로 건내주는 것 만으로 기대되어지는(?) 작동을 잘 한다

<input v-model="myInputValue" />
<textarea 
  v-model="myTextAreaValue"
  v-on:hover="handleHover"
  rows="7"
  cols="20"
/>

이렇게 native component들에 v-model, v-on:hover 등 props를 넣으면 잘 작동한다.

그런데 Vue로 컴포넌트들 개발하다 보면 native component처럼 동작하면 좋을 것 같은 컴포넌트를 만들고 싶을 때가 있다.

<my-customized-input v-model="myInputValue" />
<my-customized-textarea 
  v-model="myTextAreaValue" 
  v-on:hover="handleHover" 
  rows="7" 
  cols="20" 
/>

이런식으로 말이다.

이렇게 내가 만든 component가 transparent 하게 prop들을 native component로 전달해줘서 native component 처럼 쓸 수 있게 만든 component 를 보고 Transparent Wrapper Component 라고 한다.

그럼 이 Transparent Wrapper Component를 어떻게 만들 것인가?
이 글의 저자는 3단계에 걸쳐서 Transparent Wrapper Component를 만들었다.

Step1) v-model 사용할 수 있게 만들기

// I want to use v-model like this
<my-customized-textarea v-model="myTextAreaValue" />

Vue가 제공하는 특별한 prop중 하나인 v-model을 value 라는 이름으로 data를 prop으로 넘겨주고, 그 data를 변화 시키는 함수를 input event listner에 넘겨준 것과 동일 하다는 것은 익히 아는 사실이다.

<input v-model="someValue">

equals

<input
  v-bind:value="someValue"
  v-on:input="someValue = $event.target.value"
>

(v-model의 원리에 대해 궁금한 분은 여기를 참고)

그렇다면 내가 만들 component가 value 라는 이름의 props를 받고, input 이라는 이름의 event를 emit시켜주도록 만들면 v-model을 사용 할 수 있게 된다

// In javascript
export default {
  name: 'MyTransparentWrapperComponent'
  props: ['value'],
  methods: {
    input(event) {
      this.$emit('input', event.target.value);
    },
  },
  template: `
    <textarea
      :value="value"
      @input="input"
    >
    </textarea>
  `
};

Step2) event interface 사용할 수 있게 만들기

// I want to use v-model, events like this
<my-customized-textarea 
  v-model='myTextAreaValue' 
  @blur="handleBlur"
  @click="handleClick"
  @focus="handleFocus"
  @mouseenter="handleMouseenter"
  @mouseleave="handleMouseleave"
  .
  .
  .
/>

v-model을 사용할 수 있지만 @focus, @mouseenter 등등의 event handler들을 사용하지 못하는 상황이다. 이 또한 native component들 처럼 바로 props로 건내 주어서 사용할 수 있게 만들고 싶은 것이다.
그렇다면 생각해 낼 수 있는 모든 event handler들을 대응해 주어야 할까?

// In javascript, OMG... there is too many events!!
export default {
  name: 'MyTransparentWrapperComponent'
  props: ['value'],
  methods: {
    input(event) {
      this.$emit('input', event.target.value);
    },
    blur() {
      this.$emit('blur');
    },
    click(event) {
      this.$emit('click', event.target.value);
    },
    focus() {
      this.$emit('focus');
    },
    mouseenter() {
      this.$emit('mouseenter');
    },
    mouseleave() {
      this.$emit('mouseleave');
    },
    .
    .
    .
  },
  template: `
    <textarea 
      :value="value"
      @input="input"
      @blur="blur"
      @click="click"
      @focus="focus"
      @mouseenter="mouseenter"
      @mouseleave="mouseleave"
      .
      .
      .
    >
    </textarea>`
  },
};

위의 코드 처럼 사용하는 모든 케이스의 event handler를 다 작성할 것인가? No way! 너무 손이 아픈 것이다!
우리에겐 Vue instance가 제공하는 $listeners 속성이 있다! $listeners는 이 컴포넌트가 props으로 받는 모든 event가 객체형태로 저장되어있다. 그렇다면 이 $listeners에 들어있는 (input event를 제외한) 모든 event들을 v-on을 이용하여 그대로 전달해 주면 간단하게 event를 passing 할 수 있다
(이에 관해 더 궁금하시다면 여기를 참고하는 것이 좋다.)

// In javascript, wow! very simplified!
export default {
  name: 'MyTransparentWrapperComponent'
  props: ['value'],
  methods: {
    input(event) {
      this.$emit('input', event.target.value);
    },
  },
  computed: {
    lisnters() {
      const { input, ...listeners } = this.$listeners;
      return listeners
    },
  },
  template: `
    <textarea
      :value="value"
      @input="input"
      v-on="lisnters"
    >
    </textarea>
  `
};

Step 3) attribute 사용할 수 있게 만들기

// I want to use v-model, events, attributes like this
<my-customized-textarea 
  v-model="myTextAreaValue" 
  @blur="handleBlur" 
  @click="handleClick"
  @focus="handleFocus"
  @mouseenter="handleMouseenter"
  @mouseleave="handleMouseleave"
  rows="7"
  cols="20"
/>

이벤트들도 다 전달 시켰으니 마지막으로 attribute들도 전달 시켜 주고 싶다. textarea tag를 예로 들면, rows라는 attribute는 이 element이 행 수를 결정 짓는다.

Vue instance의 속성 중 전달받은 attribute를 모아놓은 속성은 없을까? 물론 존재한다 ㅎㅎ.
Vue instance의 $attrs 속성에 전달받은 attr이 다 들어있다!
$attrsv-bind를 통해 전달 시킬 수 있다.

// In javascript
export default {
  name: 'MyTransparentWrapperComponent'
  props: ['value'],
  methods: {
    input(event) {
      this.$emit('input', event.target.value);
    },
  },
  computed: {
    lisnters() {
      const { input, ...listeners } = this.$listeners;
      return listeners
    },
    attrs() {
      return this.$attrs
    }
  },
  template: `
    <textarea
      :value="value"
      @input="input"
      v-on="lisnters"
      v-bind="attrs"
    >
    </textarea>
  `
};

짜잔! native component처럴 v-model을 사용할 수 있고 event interface와 attr을 그대로 내려 받는 Transparent Wrapper Component가 완성되었다!! 🎉🎉🎉

Bonus) inheritAttrs을 이용해서 attr 자동 흘러내림 막기

내가 만든 컴포넌트의 depth가 1단계가 아닐 수 있다. 위 예시를 depth가 2단계인 컴포넌트로 만들어 보겠다.

// In javascript
export default {
  name: 'MyTransparentWrapperComponent'
  props: ['value'],
  methods: {
    input(event) {
      this.$emit('input', event.target.value);
    },
  },
  computed: {
    lisnters() {
      const { input, ...listeners } = this.$listeners;
      return listeners
    },
    attrs() {
      return this.$attrs
    }
  },
  template: `
    <div class="root-element">
      <h1>My Transparent Wrapper Component ^^</h1>
      <textarea
        :value="value"
        @input="input"
        v-on="lisnters"
        v-bind="attrs"
      >
      </textarea>
    </div>
  `
};

// Use it like this
<my-transparent-wrapper-component
  v-model="myTextAreaValue" 
  v-on:hover="handleHover" 
  rows="10" 
  cols="29" 
>
</my-transparent-wrapper-component>

위의 코드 처럼 component에 attr을 넣어주면 아래 그림 처럼 자동적으로 컴포넌트의 root element에 attribute로 들어간다.


rows와 cols는 textarea에만 넣었는데 root element 에도 attribute로 들어간 이유는 Vue component옵션 중 inheritAttrs 옵션이 기본적으로 true이기 때문이다.
따라서 이 옵션을 false 로 바꿔주면 root element 에 자동적으로 들어간 attribute 들이 없어진다.

// In javascript
export default {
  name: 'MyTransparentWrapperComponent'
  inheritAttrs: false // here is the option ^^@
  props: ['value'],
  methods: {
    input(event) {
      this.$emit('input', event.target.value);
    },
  },
  computed: {
    lisnters() {
      const { input, ...listeners } = this.$listeners;
      return listeners
    },
    attrs() {
      return this.$attrs
    }
  },
  template: `
    <div class="root-element">
      <h1>My Transparent Wrapper Component ^^</h1>
      <textarea
        :value="value"
        @input="input"
        v-on="lisnters"
        v-bind="attrs"
      >
      </textarea>
    </div>
  `
};

// Use it like this
<my-transparent-wrapper-component
  v-model="myTextAreaValue" 
  v-on:hover="handleHover" 
  rows="10" 
  cols="29" 
>
</my-transparent-wrapper-component>

짜잔! root element에 자동적으로 들어가있던 attr인 rows와 cols들이 사라졌다 ^_^

모든 피드백 거대하게 감사합니다. :)

참고
https://zendev.com/2018/05/31/transparent-wrapper-components-in-vue.html
https://www.youtube.com/watch?v=7lpemgMhi0k
https://medium.com/@Dongmin_Jang/vuejs-%EC%88%A8%EA%B2%A8%EC%A7%84-vue-%ED%8C%A8%ED%84%B4%EB%93%A4-1ea3adc585ac
https://blog.woolta.com/categories/10/posts/139

profile
FrontEnd Developer

4개의 댓글

comment-user-thumbnail
2020년 6월 17일

이런 꿀팁이! 감사합니다.

1개의 답글
comment-user-thumbnail
2020년 6월 22일

꿀팁 감사합니다!

1개의 답글