티스토리 뷰

1. Element & Component Access

대부분의 경우 다른 컴포넌트의 인스턴스에 접근하거나 DOM을 직접 조작하는 것은 피해야 합니다. 그러나 때로는 다른 컴포넌트의 인스턴스에 접근하거나 DOM을 직접 조작해야 할 때가 있습니다.

1) 루트 인스턴스 접근

$root를 사용하면 루트 인스턴스에 접근 가능합니다.

// The root Vue instance
new Vue({
  data: {
    foo: 1
  },
  computed: {
    bar: function () { /* ... */ }
  },
  methods: {
    baz: function () { /* ... */ }
  }
})

위의 코드와 같이 루트 인스턴스가 있을 때,

// Get root data
this.$root.foo

// Set root data
this.$root.foo = 2

// Access root computed properties
this.$root.bar

// Call root methods
this.$root.baz()

모든 자식 컴포넌트는 위의 코드와 같이 루트 엘리먼트에 접근하여 루트 인스턴스의 값들을 사용할 수 있습니다.

2) 부모 컴포넌트 인스턴스 접근

$root와 유사하게 $parent를 사용하면 부모 인스턴스에 접근 가능합니다. $parent를 사용하여 부모 컴포넌트의 데이터를 변경할 경우, 어플리케이션이 커진다면, 어느 자식 컴포넌트에서 데이터를 변경하였는지 알 수 없어 디버깅하기 더 어려워 질 수 있습니다. 하지만 몇몇의 라이브러리들을 사용할 때 부모 인스턴스를 접근하는 것이 유용할 때가 있습니다.

<google-map>
  <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map>

예를 들어, 위의 코드와 같이 Google Maps 라이브러리를 사용하여 컴포넌트를 만든다고 할 때, <google-map> 컴포넌트는 map 프로퍼티로 맵 정보를 가지고 있고, 자식 컴포넌트인 <google-map-markers> 컴포넌트가 부모 컴포넌트인 <google-map> 컴포넌트의 맵 정보를 가져와야 할 때, this.$parent.getMap 을 이용하여 맵 정보를 가져 올 수 있습니다.

<google-map>
  <google-map-region v-bind:shape="cityBoundaries">
    <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
  </google-map-region>
</google-map>

위의 예제와 같이 <google-map-region> 이라는 컴포넌트가 <google-map><google-map-markers> 사이에 사용될 수도 있다고 했을 때(선택적으로 사용되지 않을 수도 있음), <google-map-markers> 컴포넌트는 <google-map> 컴포넌트의 맵 정보를,

var map = this.$parent.map || this.$parent.$parent.map

위의 코드와 같이 사용할 수 있습니다.

3) 자식 컴포넌트 인스턴스, 자식 엘리먼트 접근

때로, 직접 자식 컴포넌트에 직접 접근해야 할 때가 있습니다. ref를 사용하면 자식 컴포넌트에 접근이 가능합니다.

<base-input ref="usernameInput"></base-input>

위의 코드와 같이 부모 컴포넌트에서 자식 컴포넌트를 선언하여 사용한 후 ref를 정의 했다면,

this.$refs.usernameInput

위의 코드와 같이 자식 컴포넌트에 직접 접근이 가능합니다.

<input ref="input">

또한 위의 코드와 같이, ref를 사용하여 기본 엘리먼트에 접근도 가능합니다.

Vue.component('base-input', {
  template: '<input ref="input">',
  methods: {
    // Used to focus the input from the parent
    focus: function () {
      this.$refs.input.focus()
    }
  }
})

자식 컴포넌트가 위의 코드과 같이 작성되었다면,

this.$refs.usernameInput.focus()

부모 컴포넌트에서는 위의 코드를 사용하여 자식 컴포넌트의 특정 엘리먼트에 접근도 가능해 집니다.

<ul>
  <li v-for="num of len" :key="num" ref="refArray">{{ num }}</li>
</ul>

refv-for와 같이 사용할 수 있습니다. 위의 코드와 같이 refv-for을 함께 사용한다면,

{refArray: Array(...)}

this.$refs의 값은 위의 코드와 같이 key는 refArray이고 value는 Array가 됩니다.

참고 - $refs 사용시 유의 사항

$refs는 랜더링 됭 후 값이 채워집니다. 또한 $refs는 반응적이지 않습니다.($refs가 변경 되어도 watch, computed 등으로 감지 하지 못함) 그렇기 때문에 template에서나 computed에서 $refs를 사용하는 것은 피해야 합니다.

4) 의존성 주입

<google-map>
  <google-map-region v-bind:shape="cityBoundaries">
    <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
  </google-map-region>
</google-map>

위의 코드는 부모 컴포넌트 인스턴스 접근에서 다루었던 코드입니다. <google-map> 컴포넌트의 하위 컴포넌트에서 <google-map> 컴포넌트의 getMap 메소드를 접근하려면 $parent를 사용했습니다. provideinject를 사용한 의존성 주입을 통해 위의 방법을 구현 할 수 있습니다.

provide: function () {
  return {
    getMap: this.getMap
  }
}

<google-map> 컴포넌트에서는 하위 컴포넌트에서 사용할 수 있게 의존성을 주입할 메소드를 위의 코드와 깉이 정의해야 합니다. 위의 예제에서는 getMap을 하위 컴포넌트에서 사용할 수 있도록 할 것입니다.

inject: ['getMap']

하위 컴포넌트에서는 위의 코드와 같이 의정성 주입을 한 메소드를 사용할 수 있습니다. 위의 예제는 Fiddle에서 확인 할 수 있습니다.

위의 코드와 같이 의존성 주입을 사용하면 $parent를 사용하지 않고 모든 하위 컴포넌트에서 의존성 주입한 메소드를 사용할 수 있습니다. (위의 코드에서는 getMap 메소드) 의존성 주입을 사용하면 몇가지 장점이 있습니다.

  • 부모 컴포넌트는 어떤 하위 컴포넌트에서 의존성 주입한 메소드를 사용하는지 알 필요가 없습니다.
  • 하위 컴포넌트는 어떤 부모 컴포넌트에서 메소드를 의전송 주입을 하였는지 알 필요가 없습니다.
  • $parent는 전체 인스턴스를 노출시키지만, 의존성 주입을 사용하면 전체 인스턴스를 노출하지 않을 수 있습니다.

2. Programmatic Event Listeners

지금까지는 $emit으로 이벤트를 발생시키고 v-on으로 발생시킨 이벤트를 감지하는 형태를 사용하였습니다. Vue는 또 다른 이벤트 인터페이스를 제공합니다. 이 방법을 programmatic event listener라 이야기 하도록 하겠습니다.

  • $on(eventName, eventHandler) : eventName이라는 이벤트를 감지하여 이벤트가 발생할 경우 eventHandler를 실행합니다.
  • $once(eventName, eventHandler) : eventName이라는 이벤트를 한번만 감지하여 eventHandler를 실행합니다.
  • $off(eventName, eventHandler) : eventName이라는 이벤트 감지를 해제하여 eventHandler를 실행합니다.

위의 이벤트 리스너들은 일반적인 경우 사용할 필요는 없지만 컴포넌트의 인스턴스에서 수동으로 이벤트를 감지해야 할 때 주로 사용됩니다. 다른 서드 파트 라이브러리를 사용할 때 유용하게 사용할 수 있습니다.

// Attach the datepicker to an input once
// it's mounted to the DOM.
mounted: function () {
  // Pikaday is a 3rd-party datepicker library
  this.picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })
},
// Right before the component is destroyed,
// also destroy the datepicker.
beforeDestroy: function () {
  this.picker.destroy()
}

위의 코드는 2가지 잠제적인 이슈가 있습니다.

  • 라이프 사이클 훅에서 picker를 사용해야 할 경우, picker를 컴포넌트 인스턴스에 저장해야 합니다.
  • 초기화 코드와, 해제 코드가 분리되어 있어, 코드 분석이 어려울 수 있습니다.

위의 2가지 이슈를 programmatic event listener를 사용하여 해결 할 수 있습니다.

mounted: function () {
  var picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })

  this.$once('hook:beforeDestroy', function () {
    picker.destroy()
  })
}

위와 같은 방법을 사용하면 pikaday를 간편하게 여러번 사용할 수 있게 됩니다.

mounted: function () {
  this.attachDatepicker('startDateInput')
  this.attachDatepicker('endDateInput')
},
methods: {
  attachDatepicker: function (refName) {
    var picker = new Pikaday({
      field: this.$refs[refName],
      format: 'YYYY-MM-DD'
    })

    this.$once('hook:beforeDestroy', function () {
      picker.destroy()
    })
  }
}

위의 전체 예제 코드는 Fiddle에서 확인 할 수 있습니다.

3. 순환 참조

1) 재귀 컴포넌트

컴포넌트는 자신의 template에서 자신을 반복적으로 호출 할 수 있습니다.

name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'

주의하지 않으면 재귀 컴포넌트는 무한 루프를 만들 수도 있습니다. 그렇기 때문에 재귀 컴포넌트를 사용할 때 v-if 등을 사용하는 조건부 재귀 컴포넌트 인지 살펴보아야 합니다.

2) 컴포넌트간의 순환 참조

파일 탐색기와 같이 파일 디렉토리 드리를 작성해 보도록 하겠습니다.

<p>
  <span>{{ folder.name }}</span>
  <tree-folder-contents :children="folder.children"/>
</p>

위의 코드와 같이 tree-folder라는 컴포넌트를 만듭니다.

<ul>
  <li v-for="child in children">
    <tree-folder v-if="child.children" :folder="child"/>
    <span v-else>{{ child.name }}</span>
  </li>
</ul>

tree-folder-contents 컴포넌트는 위의 코드와 같습니다.

위의 코드를 살펴보면, 두 컴포넌트는 서로가 서로의 자식 컴포넌트이지 부모 컴포넌트입니다. 여기서 역설이 발생합니다. tree-folder 컴포넌트가 랜더링 되기 위해서는 tree-folder-contents 컴포넌트가 필요하고, tree-folder-contents 컴포넌트가 랜더링 되기 위해서는 tree-folder 컴포넌트가 필요합니다.

- 전역으로 컴포넌트 선언하여 순환 참조하기

이 역설은 Vue.component를 사용하여 전역으로 컴포넌트를 등록하면 해결 됩니다.

그러나 Webpack이나 Browserify등의 모듈러를 통해 지역으로 컴포넌트를 등록할 경우,

Failed to mount component: template or render function not defined.

위의 에러가 발생합니다. 위 에러를 해결하기 위한 방법으로 2가지가 있습니다.

- 지역으로 컴포넌트 선언하여 순환 참조하기

첫번째 방법은 tree-folder 컴포넌트를 시작점으로 잡고,

beforeCreate: function () {
  this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}

위의 코드와 같이 beforeCreate 라이프 사이클 훅을 사용하여 동적으로 컴포넌트를 등록하는 방법이 있습니다.

두번째 방법은 비동기 컴포넌트를 사용하는 방법이 있습니다.

components: {
  TreeFolderContents: () => import('./tree-folder-contents.vue')
}

위의 코드와 같이 비동기 컴포넌트를 사용하여 위에서 말한 에러를 해결 할 수 있습니다.

4. Alternate Template Definitions

1) Inline Templates

하위 컴포넌트에 inline-template 속성을 사용 할 때, 컴포넌트의 내용은 그 하위 컴포넌트의 내용이 됩니다. 보다 유연한 템플릿 작성을 가능하게 합니다.

<my-component inline-template>
  <div>
    <p>These are compiled as the component's own template.</p>
    <p>Not parent's transclusion content.</p>
  </div>
</my-component>

inline-template는 템플릿의 범위를 알아보기 힘들게 만듭니다. 그렇기 때문에 보통은 template 옵션을 사용하거나 .vue 파일의 template 엘리먼트를 사용하는 것이 좋습니다.

2) X-Templates

<script type=text/x-template">를 사용하여 템플릿을 정의하는 방법도 있습니다.

<script type="text/x-template" id="hello-world-template">
  <p>Hello hello hello</p>
</script>
Vue.component('hello-world', {
  template: '#hello-world-template'
})

위의 코드와 같은 방법으로 사용할 수 있습니다.

5. Controlling Updates

Vue의 반응형 시스템의 도움으로 언제 업데이트 되는지 알 수 있습니다. 그러나 반응형 데이터가 변경되지 않았음에도 불구하고 업데이트를 해야 할 경우나, 불필요한 업데이트를 막을 수 있는 방법도 있습니다.

1) 강제 업데이트

대부분의 경우 강제 업데이트 할 필요가 없습니다. 하지만 Array 데이터 주의 사항이나 Object 데이터 주의 사항을 지키지 않았을 경우 강제 업데이트를 해야 할 때가 있습니다. 이 경우 수동으로 업데이트를 해야 하는데 $forceUpdate를 사용하여 강제 업데이트를 진행 할 수 있습니다.

2) v-once

이름에도 알 수 있듯이 한번만 랜더링 되는 컴포넌트를 만들 때 사용됩니다. v-once를 사용하면 업데이트 되지 않는 정적인 컴포넌트를 랜더링 할 수 있습니다. v-once를 사용하여 업데이트 하지 않아도 되는 엘리먼트들을 정적으로 랜더링 한다면 더 나은 성능을 보일 수 있습니다.

참고

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

댓글
공지사항
최근에 올라온 글