ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Vue.js] 컴포넌트 톺아보기
    Front-End(Web)/Vue 2021. 6. 14. 14:44
    반응형

    Vuex 공부를 마치고, 캡틴판교님의 인프런 강의도 레벨3부터는 실제구현에 집중되다보니, 공식문서 공부를 좀 더 하려고 했다.

    그래서, 공식문서 필수요소 바로 다음에 이어지는 "컴포넌트 톺아보기" 섹션에 대해 정리해보려고 한다.

     

    아무래도, React 프로젝트를 진행하면서도 컴포넌트를 어떻게 쪼개고 재활용할지, 심지어 이름을 어떻게 지을지도 고민이 많았다.

    이러한 컴포넌트의 선언과 활용에 대한 많은 팁이 담겨있는 해당 섹션을 윤독하면서 중요한 내용을 정리해보려고 한다.

     

    * 이 글은 캡틴판교님의 Vue.js 강의 및 블로그를 기반으로 학습한 내용을 정리하고 있습니다.


    💚 컴포넌트 톺아보기

     

    1. 컴포넌트 등록

    먼저 컴포넌트를 명명하는 2가지 방법(kebab-case, CamelCase), 각각의 모듈화된 지역 컴포넌트들을 활용하는 방법이 소개된다.

    import ComponentA from './ComponentA'
    import ComponentC from './ComponentC'
    
    export default {
      name: "ComponentB",
      components: {		// 컴포넌트 등록방법!
        ComponentA,
        ComponentC
      },
      // ...
    }

     

     

    2. Props

    props 역시 script에선 CamelCase, template에선 kebab-case로 주로 작성된다. (하지만 나는 편의상 카멜로 통일)

     

    - Props 타입

    다음으로, props의 타입 선언에 대한 소개가 이어진다. 기존에 props를 배열로 받아오는게 아니라, 객체형태로 타입을 지정할 수 있다.

    // 전 - 배열형태
    props: ['title', 'likes', 'isPublished', 'commentIds', 'author'];
    
    // 후 - 객체형태
    props: {
      title: String,
      likes: Number,
      isPublished: Boolean,
      commentIds: Array,
      author: Object,
      callback: Function,
      contactsPromise: Promise // or any other constructor
    }

     

    * Type Checks : String, Number, Boolean, Array, Object, Date, Function, Symbol, 객체 인스턴스

    // Type Checks : 객체 인스턴스
    function Person (firstName, lastName) {
      this.firstName = firstName
      this.lastName = lastName
    }
    
    Vue.component('blog-post', {
      props: {
        author: Person
      }
    })

     

    즉, Props는 언뜻 보면 v-bind를 통해 정적인 문자열 형태로 내려가는 것 같지만, 모든 타입의 값이 적절하게 내려간다.

    <!-- String -->
    <blog-post title="My journey with Vue"></blog-post>
    
    <!-- Number -->
    <blog-post v-bind:likes="42"></blog-post>
    
    <!-- Boolean -->
    <blog-post is-published></blog-post>	<!-- =true -->
    <blog-post v-bind:is-published="false"></blog-post>
    
    <!-- Array -->
    <blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>
    
    <!-- Object -->
    <blog-post
      v-bind:author="{
        name: 'Veronica',
        company: 'Veridian Dynamics'
      }"
    ></blog-post>

    또한, 객체(Object) 자체를 v-bind 하는 경우 아래와 같이 하위의 필드들이 각각의 바인딩으로 전달된다.

    post: {
      id: 1,
      title: 'My Journey with Vue'
    }
    <blog-post v-bind="post"></blog-post>
    
    <!-- 아래와 같이 동작! -->
    <blog-post
      v-bind:id="post.id"
      v-bind:title="post.title"
    ></blog-post>

     

    - Props 유효성 검사

    위 타입에서 언급했듯, props를 이름뿐만 아니라 타입도 지정할 수 있었다.

    이외에도, required(필수여부), default(기본값), validator(유효성 검사) 등도 설정할 수 있다.

    Vue.component('my-component', {
      props: {
        // 기본 타입 체크 (`Null`이나 `undefinded`는 모든 타입을 허용합니다.)
        propA: Number,
        // 여러 타입 허용
        propB: [String, Number],
        // 필수 문자열
        propC: {
          type: String,
          required: true
        },
        // 기본값이 있는 숫자
        propD: {
          type: Number,
          default: 100
        },
        // 기본값이 있는 오브젝트
        propE: {
          type: Object,
          // 오브젝트나 배열은 꼭 기본값을 반환하는
          // 팩토리 함수의 형태로 사용되어야 합니다. 
          default: function () {
            return { message: 'hello' }
          }
        },
        // 커스텀 유효성 검사 함수
        propF: {
          validator: function (value) {
            // 값이 항상 아래 세 개의 문자열 중 하나여야 합니다. 
            return ['success', 'warning', 'danger'].indexOf(value) !== -1
          }
        }
      }
    })

     

     

    3. 커스텀 이벤트

    - 이벤트명

    이벤트명은 CamelCase <-> kebab-case 간 자동변환이 적용되지 않는다. 또한, 항상 소문자로 변환되기에 kebab-case가 권장된다.

    this.$emit('myEvent')
    
    <my-component v-on:my-event="doSomething"></my-component>
    // 감지하지 않음!

     

    - v-model 커스터마이징

     v-model의 props가 value가 아닌 다른 값으로 활용되어야 할 때 사용되는 기법이다. (체크박스의 경우가 대표적)

    Vue.component('base-checkbox', {
      model: {
        prop: 'checked',
        event: 'change'
      },
      props: {
        checked: Boolean
      },
      template: `
        <input
          type="checkbox"
          v-bind:checked="checked"
          v-on:change="$emit('change', $event.target.checked)"
        >
      `
    })

    이처럼, model 필드를 제작한 뒤, 여기에 연동될 prop 명을 작성해준다. 이 prop은 반드시 props 필드 내에서 선언되어야 한다.

     

    - 네이티브 이벤트를 컴포넌트에 바인딩

    컴포넌트에서 상위(혹은 루트) 컴포넌트의 Native Event를 직접 감지해야하는 경우가 있을 수 있다. 이 땐, .native 키워드가 활용된다.

    <base-input v-on:focus.native="onFocus"></base-input>

     

    혹은, 아래처럼 $listeners 메서드를 통해 v-model 을 갱신할 수 있다.

    Vue.component('base-input', {
      props: ['label', 'value'],
      computed: {
        inputListeners: function () {
          var vm = this
          return Object.assign({},
            this.$listeners,
            {
              input: function (event) {
                vm.$emit('input', event.target.value)
              }
            }
          )
        }
      },
      template: `
        <label>
          {{ label }}
          <input
            v-bind="$attrs"
            v-bind:value="value"
            v-on="inputListeners"
          >
        </label>
      `
    })
    1. v-model을 갱신하는 computed 메서드inputListeners를 선언한다.
    2. 먼저, vm에 this를 저장한다. 그리고, 병합된 객체를 반환하기 위해 Object.assign() 문법을 활용한다.
    3. 우선, this.$listeners부모 엘리먼트의 모든 이벤트 리스너를 추가한다.
    4. 그리고, 기존처럼 this.$emit 하는 리스너를 추가한다. (vm의 this는 활용되는 <input>, $listeners는 <base-input> 자체)

     

    - .sync 수식어

    <input> 을 감싸는 부모의 경우, 인풋과 본인 모두 갱신하는 "양방향 바인딩" 이 필요한 경우가 있다.

    이 때, v-model과 유사하게 쓰이는 문법으로 .sync 키워드가 있다. (2.3버전 이상)

    차이점은 자식 컴포넌트에 value prop을 별도로 지정하지 않으며, 대신 props 명칭과 하달하는 데이터 명칭이 같으면 된다.(size)

    <template> 
      <doggie :size.sync="size" /> 
    </template> 
    
    <script> 
    export default { 
      data: { size: 'little' } 
    } 
    </script>
    

     

     

    4. Slots(슬롯)

    Vue.js에서 컴포넌트 마크업을 확장하는 방법인 slot 문법이 존재한다. 이를 통해, 컴포넌트의 마크업 측면에서 재사용성이 높아진다!

    <!-- ButtonTab.vue -->
    <template>
      <div class="tab panel">
        <!-- 탭 헤더 -->
        <slot></slot>
        <!-- 탭 본문 -->
        <div class="content">
          Tab Contents
        </div>
      </div>
    </template>

    가장 기본적인 1개의 슬롯을 사용하는 경우이다. <button-tab> 컴포넌트 내 마크업은 <slot>으로 들어가고, 아래 content가 나온다.

    <!-- TabContainer.vue -->
    <template>
      <button-tab>
        <!-- slot 영역 -->
        <h1>First Header</h1>
      </button-tab>
      <button-tab>
        <!-- slot 영역 -->
        <h1>Second Header</h1>
      </button-tab>
    </template>
    
    <script>
    export default {
      components: {
        ButtonTab
      }
    }
    </script>

     

    - Named Slot

    여러 개의 슬롯을 사용하는 경우도 많을 것이며, 이 때 각 슬롯에 이름(name)을 부여하는 방법이다.

    <!-- ButtonTab.vue -->
    <template>
      <div class="tab panel">
        <!-- 탭 헤더 -->
        <slot name="header"></slot>
        <!-- 탭 본문 -->
        <slot name="content"></slot>
      </div>
    </template>
    <!-- TabContainer.vue -->
    <template>
      <button-tab>
        <h1 slot="header">First Header</h1>
        <div slot="content" class="content">Tab Contents #1</div>
      </button-tab>
      <button-tab>
        <h1 slot="header">Second Header</h1>
        <div slot="content" class="content">Tab Contents #2</div>
      </button-tab>
      <button-tab>
        <h1 slot="header">Third Header</h1>
        <div slot="content" class="content">Tab Contents #3</div>
      </button-tab>
    </template>

    이처럼, 재활용되는 컴포넌트의 <slot> 각각에 이름(name)을 부여한다.

    그리고, 이 컴포넌트를 사용할 때 slot에 들어갈 엘리먼트에 slot 속성을 부여한다. 여기의 할당값을 <slot>의 name으로 설정한다.

     

     

    5. 동적 & 비동기 컴포넌트

    - 동적 컴포넌트(keep-alive)

    기존에 컴포넌트 전환을 위해 <component> 태그와 v-bind:is 속성을 사용했다.

    <component v-bind:is="currentTabComponent"></component>

    여기에 최근 컴포넌트를 캐싱하기 위해 새로운 문법인 <keep-alive> 로 이를 감싸준다. 그러면, 리로드 등에도 최신 상태를 유지한다.

    <!-- Inactive components will be cached! -->
    <keep-alive>
      <component v-bind:is="currentTabComponent"></component>
    </keep-alive>

     

    - 비동기 컴포넌트

    비동기 요청의 결과여부에 따라 컴포넌트가 로드되는 경우도 존재할 것이다. 아래처럼, 비동기 로직의 resolve(reject) 로 로드시킨다.

    Vue.component("async-example", function(resolve, reject) {
      setTimeout(function() {
        resolve({
          template: "<div>I am async!</div>"
        });
      }, 1000);
    });

     

    공식문서에 개선된 문법들이 소개됬으며, 여기선 비동기 컴포넌트 팩토리의 Handling Loading State만 소개한다. (2.3버전)

    const AsyncComponent = () => ({
      // 로드 할 컴포넌트(Promise여야 합니다.)
      component: import("./MyComponent.vue"),
      // 비동기 컴포넌트가 로딩중일 때 사용할 컴포넌트
      loading: LoadingComponent,
      // 비동기 컴포넌트 로딩이 실패했을 때 사용할 컴포넌트
      error: ErrorComponent,
      // 로딩 컴포넌트를 보여주기 전의 지연시간. (기본값: 200ms)
      delay: 200,
      // 초과되었을 때 에러 컴포넌트를 표시할 타임아웃. (기본값: 무한대)
      timeout: 3000
    });

     

    6. 예외적인 상황들

    • $root, $parent 등 메서드로 상위 컴포넌트 인스턴스에 접근할 수 있다. 다만, 이는 정교함이 부족하여 디버깅과 유지보수에 불리
    • 마찬가지로 $refs 메서드를 통해 부모 컴포넌트가 자식에 직접 접근할 수 있다. 부모 컴포넌트는 ref 필드로 바인딩한다.
    • 상위 컴포넌트는 provide 필드로 모든 하위에 data, methods를 제공할 수 있다. 하위에서는 inject로 가져온다. (랩핑 최소화)
    • 프로그래밍적 이벤트 리스너 기법이 있다. $on(name, handler) 외에, $once(name, handler), $off(name, handler)
    • 이외에도, 템플릿을 확장하는 인라인 템플릿, 업데이트를 강제하는 $forceUpdate, 리렌더를 지양하는 v-once 등이 나온다.

    Vue 컴포넌트에 관련된 다양한 기법들을 볼 수 있는 섹션이었다. 특히, Props 검사, Slots(슬롯), 동적 컴포넌트는 실제 유용할 것 같다. 

    다음은, Vue 공식문서와 캡틴판교 인프런 강의(Lv.3) 학습을 병행해보면서 부가적으로 중요한 주제들을 다루려고 한다.

    (Vue-Router, 플러그인, Mixin 등 짚고 넘어가야 할 부분들이 많아보인다.!)

     

     

    [출처]

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

    - 캡틴판교 님의 블로그(Slots 관련) : https://joshua1988.github.io/web-development/vuejs/slots/

    - devtimothy 님의 블로그(sync 관련) : https://devtimothy.tistory.com/135

    반응형
Designed by Tistory.