뷰 컴포넌트 디자인 패턴 (Vue.js Component Design Patterns)

Minho Yoo·2022년 12월 28일
3

Vue.js

목록 보기
9/12
post-thumbnail

일반적인 컴포넌트 구조화 방식

첫 번째 컴포넌트 디자인 패턴은 일반적인 컴포넌트 구조화 방식이다.
뷰 컴포넌트, 컴포넌트 통신 방식을 배우고 나면 자연스럽게 아래와 같이 컴포넌트를 구현하게 된다.

<template>
  <app-header @refresh="refreshPage"></app-header>
  <app-content :list="items" @fetch="fetchData"></app-content>
  <app-footer :right="message"></app-footer>
</template>

<script>
  import AppHeader from './AppHeader.vue';
  import AppContent from './AppContent.vue';
  import AppFooter from './AppFooter.vue';
  
  export default {
    components: {
  		AppHeader,
  		AppContent,
  		AppFooter
    }
  }
</script>

위와 같은 방식은 등록된 컴포넌트가 여러 곳에 쓰이지 않을 때 사용하기 좋은 방식이다.
실질적인 코드의 재사용성보다는 템플릿 코드의 가독성과 명시적인 코드의 모양새를 더 중점으로 두고 있다.

결합력 높은 컴포넌트

두 번째 컴포넌트 디자인 패턴의 핵심은 v-model 디렉티브이다.
v-model의 내부가 어떻게 동작하는지 이해하고 이를 응용하여 좀 더 결합력이 높은 컴포넌트를 만들어보자.

<template>
  <input type="text" v-model="inputText">
</template>

<script>
  export default {
    data() {
  	  return {
  		inputText: 'hi'
  	  }
    }
  }
</script>

위 코드를 실행하면 인풋 박스의 초기 값으로 'hi'가 지정되어 있다.
그리고 인풋 박스의 내용을 바꾸면 inputText의 값도 같이 바뀐다.
해당 내용은 뷰 개발자 도구에서 확인할 수 있다.

v-model 디렉티브의 동작 방식

v-model 디렉티브가 어떻게 뷰 인스턴스의 데이터와 정보를 주고 받았는지는 아래의 코드를 보자.

<template>
  <ipnut type="text" :value="inputText" @input="inputText = $event.target.value">
</template>

<script>
  export default {
    data() {
  	  return {
  		inputText: 'hi'
  	  }
    }
  }
</script>

inputText의 값을 :value에 연결하고 인풋 박스의 입력을 모두 $event.target.valueinputText에 넣는다.
위의 코드는 v-model 디렉티브와 동일하게 동작한다.

TIP

위와 같은 코드는 커스텀 디렉티브를 작성할 때 많이 사용된다.

v-model로 체크 박스 컴포넌트 만들기

앞에서 살펴본 v-model로 HTML 인풋 체크 박스를 컴포넌트화 할 수 있다.
먼저 상위 컴포넌트인 App.vue이다.

<!-- App.vue -->
<template>
  <check-box v-model="checked"></check-box>
</template>

<script>
import CheckBox from './Checkbox.vue';
  
export default {
  component: {
    CheckBox
  },
  data() {
    return {
  	  checked: false,
    }
  }
}
</script>
<!-- CheckBox.vue -->
<template>
  <ipnut type="checkbox" :value="value" @click="toggle">
</template>

<script>
exports default {
  props: ['value'],
  methods: {
  	toggle() {
  		this.$emit('input', !this.value);
    }
  }
}
</script>

이 코드를 실행하여 체크 박스를 클릭하면 checked 값이 정상적으로 true에서 false로 전환되는 것을 확인 할 수 있다.

슬롯을 이용한 컴포넌트 템플릿 확장

세 번째로 살펴볼 컴포넌트 디자인 패턴은 슬롯을 이용한 컴포넌트 설계 방법이다.
슬롯은 하위 컴포넌트의 템플릿을 상위 컴포넌트에서 유연하게 확장할 수 잇는 기능이다.

슬롯은 탭, 모달(팝업), 버튼 등 흔히 사용되는 UI 컴포넌트를 모두 재사용 할 수 있게 도와준다.

<!-- BaseButton.vue -->
<template>
  <button type="button" class="btn primary">
    <slot></slot>
  </button>
</template>
<!-- App.vue -->
<template>
  <div>
    <!-- 텍스트로 버튼 이름만 정의 -->
    <base-button>Show Alert</base-button>
    <!-- 아이콘과 텍스트로 버튼을 UI 확장 -->
    <base-button>
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/></svg>
      Download
    </base-button>
  </div>
</template>

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

위 코드는 버튼의 최소 마크업을 갖는 BaseButton 컴포넌트를 생성한 후 슬롯을 이용하여 버튼의 내용을 확장될 수 있게 구조화한 코드이다.
BaseButton.vue에서 <slot> 태그를 넣어놨기 때문에 BaseButton 컴포넌트를 등록하여 사용할 때 상위 컴포넌트에서 텍스트를 넣어 버튼의 이름만 바꾸거나, 텍스트와 아잌노을 함께 넣어 버튼의 UI를 꾸밀 수 있다.

컴포넌트의 코드마저 재사용하는 하이 오더 컴포넌트

정의

뷰의 하이 오더 컴포넌트는 리액트의 하이 오더 컴포넌트에서 기원된 것이다.
리액트의 하이 오더 컴포넌트 소개 페이지를 보면 아래와 같이 정확한 정의가 나와 있다.
A higher-order component (HOC) is an advanced technique in React for reusing component logic. 이 말을 정리해보면 다음과 같다.
하이 오더 컴포넌트는 컴포넌트의 로직(코드)을 재사용하기 위한 고급 기술이다.

반복되는 컴포넌트 로직

여기서 컴포넌트의 로직을 재사용한다는 말이 무슨 의미냐면 인스턴스 옵션을 재사용한다는 뜻이다.

<!-- ProductList.vue -->
<template>
  <section>
    <ul>
      <li v-for="product in products">
        ...
      </li>
    </ul>
  </section>
</template>

<script>
import bus from './bus.js';

export default {
  name: 'ProductList',
  mounted() {
    bus.$emit('off:loading');
  },
  // ...
}
</script>
<!-- UserList.vue -->
<template>
  <div>
    <div v-for="product in products">
      ...
    </div>
  </div>
</template>

<script>
import bus from './bus.js';

export default {
  name: 'UserList',
  mounted() {
    bus.$emit('off:loading');
  },
  // ...
}
</script>

위 코드는 ProductList 라는 컴포넌트와 UserList 컴포넌트의 로직을 정의한 코드이다.
두 컴포넌트가 각각 상품과 사용자 정보를 서버에서 받아와 표시해주는 컴포넌트라고 가정했을 때, 공통적으로 들어간느 코드는 다음과 같다.

name: '컴포넌트 이름',
mounted () {
  bus.$emit('off:loading');
},

name은 컴포넌트의 이름을 정의해주는 속성이고, mounted() 에서 사용한 이벤트 버스는 서버에서 데이터를 다 받아왔을 때 스피너나 프로그레스 바와 같은 로딩 상태를 완료해주는 코드이다.
이 두 컴포넌트 이외에도 서버에서 데이터 목록을 받아와 표시해주는 컴포넌트가 있다면 또 비슷한 로직이 반복될 것이다.

이때 이 반복되는 코드를 줄여줄 수 있는 패턴이 바로 하이 오더 컴포넌트이다.

하이 오더 컴포넌트로 반복 코드 줄이기

이 반복되는 코드를 줄이기 위해 하이 오더 컴포넌트를 구현해보겠다.

// CreateListComponent.js
import bus from './bus.js';
import ListComponent from './ListComponent.vue';

export default function createListComponent(componentName) {
  return {
    name: comonentName,
    mounted() {
      bus.$emit('off:loading');
    },
    render(h) {
      return h(ListComponent);
    }
  }
}

위 코드는 CreateListComponent라는 하이 오더 컴포넌트를 구현한 코드이다.
하이 오더 컴포넌트를 적용할 컴포넌트들의 공통 코드들(mounted, name 등)을 미리 정의했다.
그럼 이제 이 하이 오더 컴포넌트를 어떻게 사용할까?

// router.js
import createListComponent from './createListComponent.js';

new VueRouter({
  routes: [
    {
      path: 'products',
      component: createListComponent('ProductList'),
    },
    {
      path: 'users',
      component: createListComponent('UserList'),
    }
  ]
})

위와 같은 방식으로 하이 오더 컴포넌트를 임포트 하고, 각 컴포넌트의 이름만 정의를 해주면 컴포넌트의 기본 공용 로직인 mounted(), name를 가지고 컴포넌트가 생성된다.
따라서, 컴포넌트마다 불 필요하게 반복되는 코드를 정의하지 않아도 된다.

profile
Always happy coding 😊

0개의 댓글