똑같은 삽질은 2번 하지 말자

컴포넌트의 플러그인화, 컴포넌트 디자인 패턴(Component Design Pattern) 본문

Vue

컴포넌트의 플러그인화, 컴포넌트 디자인 패턴(Component Design Pattern)

곽빵 2021. 5. 1. 17:21

컴포넌트의 플러그인화

왜쓰냐? 전역으로 공유하고 싶은 요소가 있을때

Vue.use(MyPlugin)

new Vue({
  //... options
})

이런식으로 router store등등 이미 많이 써본 친구들인데

 

내가 직접 플러그인을 생성 작성해야하는 경우에는 어떻게 하면 될까? 

window의 width(브라우저의 사이즈)가 변할때 width값을 계속 갱신/습득 가능하게 하는 기능을 가진 플러그인을 만든 예제이다.

 

/plugins

import Vue from 'vue'
Vue.use({
  install(Vue) { // 두번째 param으로 option이라는 친구도 제공한다.
    const $_window = Vue.observable({
      width: 0
    })

    if (process.browser) {
      const onResize = () => {
        $_window.width = window.innerWidth
      }
      global.addEventListener('resize', onResize)
      onResize()
    }

    Vue.prototype.$_window = $_window // 어떤 객체의 속성을 확장하는 prototype을 이용해
  }                                   // 모든 컴포넌트에서 $window를 사용할 수 있게 해준다.
})

참고로 이건 Nuxt의 경우이며, 그냥 Vue를 쓰는경우에는 Vue.use 부분을 빼서 main.js에 Vue.use를 하면 된다.

 

↓ 공식문서 

https://kr.vuejs.org/v2/guide/plugins.html

 

플러그인 — Vue.js

Vue.js - 프로그레시브 자바스크립트 프레임워크

kr.vuejs.org

 

 

컴포넌트의 디자인 패턴

1. Common

제일 일반적인 방식이다.

props로 자식 컴포넌트에서 데이터를 전달.

자식 컴포넌트는 부모 컴포넌트한테 이벤트를 날리는 형식

 

App.vue

<template>
  <div>
    <app-header :title="title"></app-header>
    <app-content :items="items" @emitTest="updateItem"></app-content>
  </div>
</template>

<script>
import AppHeader from './components/AppHeader.vue'
import AppContent from './components/AppContent.vue'

export default {
  data() {
    return {
      items:[10, 20 ,30],
      title: 'Gwak Hee Won',
    }
  },
  components: {
    AppHeader,
    AppContent,
  },
  methods: {
    updateItem() {
      this.items = this.items.map(v => v = v + 10);
    }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

 

AppHeader.vue

<template>
  <header>
    <h1>{{ title }}</h1>
  </header>
</template>

<script>
export default {
  props: {
    title: String,
  }
}
</script>

<style>

</style>

 

AppContent.vue

<template>
  <div>
    <ul>
      <li v-for="item in items" :key="item.id">
        {{ item }}
      </li>
    </ul>
    <button type="button" @click="$emit('emitTest')">Up</button>
  </div>
</template>

<script>
export default {
  props: {
    items: Array,
  },
}
</script>

<style>

</style>

 

2. Slot

슬롯 컴포넌트를 함으로써 확장을 용이하게 만들수 있다. 보통 태그전체를 통째로 props로 하기는 번거로우니 slot을 이용하는것 같다.

요구사항에서 리스트중 두번째 아이템에는 버튼을 넣고싶다 세번째 아이템에는 사진을 넣고싶다.

네번째 아이템에는 특별한 스타일을 넣고싶다등등, 슬롯 컴포넌트는 그대로 재활용하면서 확장가능하다.

 

App.vue

<template>
  <div>
    <Item>
      item1
    </Item>
    <Item>
      item2
      <button>Click</button>
    </Item>
    <Item>
      <div>
        아이템 3
      </div>
      <img src="../public/img.png"/>
    </Item>
    <Item>
      <div style="color: blue;">
        아이템 4
      </div>
    </Item>
  </div>
</template>

<script>
import Item from './components/Item.vue';

export default {
  components: {
    Item,
  },
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

 

Item.vue

<template>
  <div>
    <slot></slot>
  </div>
</template>

 

3. Controlled

컴포넌트를 아주 작은 단위로 쪼갤때, 그 컴포넌트를 상위 컴포넌트에서 관리하는 관점에서의 디자인 패턴

예를 들면 체크박스로만 되어있는 컴포넌트의 checked 값을 어떻게 컨트롤 할 때,

하위 컴포넌트(체크박스)에서 상위 컴포넌트에서 받은 props값을 변경하려고 들면 warning이 뜬다.

왜? 경고가 뜨는가 그건 아래의 공식문서를 참조하자

단방향 데이터 흐름

 

컴포넌트 — Vue.js

Vue.js - 프로그레시브 자바스크립트 프레임워크

kr.vuejs.org

경고가 안뜨게 하면서 하위 컴포넌트(체크박스)에서 체크를 했을때, 체크값을 바꾸는 방법으로는

상위 컴포넌트에서 v-model로 하위 컴포넌트와 연결, v-model에서는 input 이벤트와 value 값으로 내려온다는걸 이용

 

App.vue

<template>
  <div>
    <check-box v-model="checked"></check-box>
  </div>
</template>

<script>
import CheckBox from './components/CheckBox.vue';

export default {
  data() {
    return {
      checked: false,
    }
  },
  components: {
    CheckBox,
  },
}
</script>

 

CheckBox.vue

<template>
  <div>
    <input type="checkbox" value="value" @click="toggleCheck" />
  </div>
</template>

<script>
export default {
  props: {
    value: Boolean
  },
  methods: {
    toggleCheck() {
      this.$emit('input', !this.value);
    }
  }
}
</script>

이 디자인 패턴의 핵심은 역시 슬롯과 마찬가지로 컴포넌트간의 의존성을 분리하는것이다.

나중에 vue-datapicker 같은 API를 쓸때도 상당히 용이하게 쓸 수 있을꺼 같다.

 

4. Renderless

음..말 그대로 Renderless 컴포넌트는 template가 없다.

즉, 표현을 안하는 컴포넌트들을 일 컫는다. 이런 친구들을 render 함수, scopedSlots를 이용해서 다른 컴포넌트들에 연결한다.

 

데이터, 로딩 두가지 data를 반환하는 Renderless 컴포넌트

<script>
import axios from 'axios';
export default {
  props: ['url'],
  data() {
    return {
      response: null,
      loading: true,
    }
  },
  created() {
    axios.get(this.url)
      .then(response => {
        this.response = response.data;
        this.loading = false;
      })
      .catch(error => {
        alert('[ERROR] fetching the data', error);
        console.log(error);
      });
  },
  render() {
    return this.$scopedSlots.default({
      response: this.response,
      loading: this.loading,
    });
  },
}
</script>

 

데이터, 로딩을 가지고와서 쓰는 상위컴포넌트

<template>
  <div>
    <fetch-data url="https://jsonplaceholder.typicode.com/users/1">
      <div slot-scope="{ response, loading }">
      	<div v-if="loading">
          loading...
        </div>
        <div v-else>
          {{ response }}
        </div>
      </div>
    </fetch-data>
  </div>
</template>

<script>
import FetchData from './components/FetchData.vue'
export default {
  components: {
    FetchData
  },
}
</script>

여기서 포인트는 데이터를 가져오는 로직을 컴포넌트로 분할했다는 점,

그걸 상위 컴포넌트에서 slot-scope로 이용해 data같은것들을 선언하지 않고 구현했다는 점이다.

근데 이건? 굳이? 차라리 mixins를 쓰는게 더 좋은거같다.

 

 

render 함수란? 꽤 많이 봐왔다. 좀 좋은 예시의 코드로

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">

  </div>
  
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
    new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue',
      },
      // template: '<p> {{ meesage }} </p>'
      render: function(createElement) {
        //return createElement('태그 이름', '태그 속성','하위 태그 내용');
        return createElement('p', this.message);
      }
    })
  </script>
</body>
</html>

템플릿 대신에 렌더함수를 이용해서 Virtual Dom을 만들고 있다.

Comments