137 엣지 케이스 처리 - $root, $parent, $refs, provide, inject, 프로그래밍 방식 이벤트 리스너, Circular References

source: categories/study/vue-experiance/vue-experiance_9-99_38.md

137 엣지 케이스 처리

이 페이지의 모든 기능은 Vue의 규칙을 약간 구부려야하는 비정상적인 상황을 의미하는 예외적인 경우의 처리를 문서화한 것이다.
그러나 모두 위험할 수 있는 단점이나 상황이 있음을 유의하라.
이는 각각의 경우에 명시되어 있으므로 각 기능을 사용하기로 결정할 때 염두해.

Element & Component 접근

대부분의 경우, 다른 component 인스턴스에 접근하거나 DOM 요소를 수동으로 조작하지 않는 것이 가장 좋다.
그러나 적절할 수 있는 경우가 있다.

Root 인스턴스 접근

새 Vue 인스턴스의 모든 하위 컴포넌트에서 이 루트 인스턴스는 $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()


이것은 데모나 몇 가지 컴포넌트가 있는 매우 작은 앱에 편리할 수 있다.
하지만 중간 또는 대규모 애플리케이션에서는 이러한 패턴이 확장되지 않으므로
대부분의 경우 Vuex를 사용하여 상태를 관리하는 것이 좋다.

Parent 컴포넌트 인스턴스 접근

$root와 유사하게 $parent 속성을 사용하여 자식에서 부모 인스턴스에 접근할 수 있다.
이것은 prop으로 데이터를 전달하는 방법을 대신할 수 있다.

대부분의 경우 부모에 접근하면, 특히 부모의 데이터를 변경하는 경우 애플리케이션을 디버그하고 이해하기가 더 어려워진다.
나중에 해당 컴포넌트를 볼 때 해당 돌연변이가 어디에서 왔는지 파악하기가 매우 어렵기 때문이다.

그러나 공유된 컴포넌트 라이브러리에서 적절한 경우가 있다.
예를 들어, 다음과 같은 가상의 Google 지도 컴포넌트와 같이 HTML을 렌더링하는 대신
JavaScript API와 상호작용하는 추삭 컴포넌트에서:



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


<google-map> 컴포넌트는 모든 하위 컴포넌트가 접근해야 하는 지도 props을 정의할 수 있다.
이 경우 <google-map-markers>는 마커 세트를 추가하기 위해 this.$parent.getMap과 같은 것으로 해당 지도에 접근할 수 있다.
여기에서 이 패턴이 작동하는 것을 볼 수 있다.

그러나 이 패턴으로 구축된 컴포넌트 요소는 여전히 본질적으로 취약하다는 점을 명심해라.
예를 들어, 새로운<google-map-region> 컴포너트를 추가하고 그 안에 <google-map-markers>가 있고, 해당 영역에 속하는 마커만 렌더링해야한다고 가정해보겠다.



<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-markers> 내부에서 다음과 같은 해킹에 도달할 수 있다.



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


이것은 빨리 손에서 벗어났다.
그렇기 때문에 종속 컴포넌트에 컨텍스트 정보를 임의로 깊게 제공하기 위해 의존성 주입을 권장한다.

child component instance 및 child 요소 접근

propsevents가 있음에도 불구하고 때로는 JavaScript의 자식 컴포넌트에 직접 접근해야할 수도 있습니다.
이를 위해 ref 속성을 사용하여 하위 컴포넌트에 참조 id를 할당할 수 있습니다.
예를 들어:



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


이제 이 ref를 정의한 컴포넌트를 사용할 수 있습니다.



this.$refs.usernameInput


<base-input> 인스턴스에 접근합니다.
이것은 예를 들어 부모로부터 프로그램적으로 이 입력에 초점을 맞추고자할 때 유용하다.
이 경우, <base-input> 컴포넌트는 ref를 사용하여 다음과 같이 내부의 특정 요소에 대한 접근을 제공할 수 있다.



<input ref="input">


그리고 부모쪽에 사용할 메소드도 정의한다.



methods: {
  // Used to focus the input from the parent
  focus: function () {
    this.$refs.input.focus()
  }
}


따라서 상위 컴포넌트가 다음을 사용하여 <base-input> 내부의 입력에 초점을 맞출 수 있다.



this.$refs.usernameInput.focus()


refv-for와 함께 사용되면 얻는 ref는 데이터 소스를 미러링하는 자식 컴포넌트를 포함하는 배열이 된다.

$ref는 컴포넌트가 렌더링된 후에만 채워지며 반응하지 않는다.
이것은 직접적인 자식 조작을 위한 탈출용 해치일 뿐이다.
templates이나 computed 속성 내에서 $ref에 엑세스하는 것을 피해야한다.

Dependency Injection (의존성 주입)

이전에 Parent Component Instance에 접근하는 것에 대해 설명할 때, 다음과 같은 예제를 보여줬었다.



<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>의 모든 자손은 상호작용할 지도를 알기 위해 getMap 메소드에 대한 접근이 필요했다.
불행히도 $parent 속성을 사용하면 더 깊이 중첩된 컴포넌트로 확장되지 않았었다.
두 가지 새로운 인스턴스 옵션인 provide(제공)inject(주입)을 사용하여 종속성 주입이 유용할 수 있는 곳이다.

provide 옵션을 사용하면 하위 컴포넌트에 제공하려는 데이터/메서드를 지정할 수 있다.
이 경우 <google-map> 내부의 getMap 메서드이다.



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


그런 다음 모든 자손에서 inject(주입) 옵션을 사용하여 해당 인스턴스에 추가하려는 특정 속성을 받을 수 있다.



inject: ['getMap']


$parent를 사용하는 것보다 장점은 <google-map>의 전체 인스턴스를 노출하지 않고도 모든 하위 컴포넌트에서 getMap에 접근할 수 있다는 것이다.
이를 통해 하위 컴포넌트가 의존하는 것을 변경/제거할 수 있다는 두려움 없이 해당 컴포넌트를 더 안전하게 지속해서 개발할 수 있다.
이러한 컴포넌트간의 인터페이스는 props와 마찬가지로 명확하게 정의된 상태로 유지된다.

사실 의존성 주입은 다음을 제외하고는 일종의 "장거리 소품"으로 생각할 수 있다.

  • 조상 컴포넌트는 제공하는 속성을 사용하는 자손을 알 필요가 없다.
  • 자손 컴포넌트는 주입된 속성이 어디에서 오는지 알 필요가 없다.

그러나 의존성 주입에는 단점이 있다.
어플리케이션의 컴포넌트를 현재 구성된 방식과 연결하여 리팩토링을 더 어렵게 만든다.
제공된 속성도 반응하지 않는다.(no-reactive)
이것은 의도적으로 설계된 것이다.
중앙 데이터 저장소를 사용하는 것은 같은 목적으로 $root를 사용하는 것만큼 제대로 확장되지 않기 때문이다.
공유하려는 속성이 일반이 아니라 앱에 특정한 경우 또는 조상 내부에서 제공된 데이터를 업데이트하려는 경우 Vuex와 같은 실제 상태관리 솔루션이 대신 필요할 수 있다는 좋은 신호이다.

API 문서에서 종속성 주입에 대해 자세히 알아보세요.

프로그래밍 방식 이벤트 리스너

지금까지 v-on으로 $emit한 이벤트를 감지하게했다.
Vue 인스턴스는 이벤트 인터페이스에서 다른 메서드도 제공한다.

  • $on(eventName, eventHandler)로 이벤트 수신
  • $once(eventName, eventHandler)를 사용하여 이벤트를 한번만 수신 대기
  • $off(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.
// 컴포넌트가 파괴되기 직전,
// datepicker도 파괴하십시오.
beforeDestroy: function () {
  this.picker.destroy()
}


여기에는 두 가지 잠재적 문제가 있다.

  • 오직 lifecycle hooks만으로 접근가능할 때, 컴포넌트 인스턴스에 picker를 저장해야될 필요가 있다.
  • 설정 코드는 정리 코드와 별도로 유지되므로 설정한 항목을 프로그래밍 방식으로 정리하기가 더 어렵다.

프로그래밍 방식 리스너로 두 가지 문제를 모두 해결할 수 있다.



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

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


이 전략을 사용하여 Pickaday를 여러 입력 요소와 함께 사용할 수도 있습니다.
각 새 인스턴스는 자동으로 정리됩니다.



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()
    })
  }
}


전체 코드는 이 예제를 참조하세요.
그러나 단일 컴포넌트 내에서 많은 설정과 정리를 수행해야 하는 경우 가장 좋은 솔루션은 일반적으로 더 많은 모듈식 컴포넌트를 만드는 것이다.
이 경우 재사용 가능한 <input-datapicker> 컴포넌트를 만드는 것이 좋다.

프로그래밍 방식 리스너에 대해 자세히 알아보려면 이벤트 인스턴스 메서드용 API를 확인하세요.

Vue의 이벤트 시스템은 브라우저의 EventTarget API와 다르다.
유사하게 작동하지만 $emit, $on, 및 $offdispatchEvent, addEventListenerremoveEventListener의 별칭이 아니다.

Circular References (순환 참조)

재귀 구성 요소 (Recursive Components)

컴포넌트는 자체 템플릿에서 자신을 재귀적으로 호출할 수 있다.
그러나 이름 옵션으로만 그렇게 할 수 있다.



name: 'unique-name-of-my-component'


Vue.component를 사용하여 전역적으로 컴포넌트를 등록하면 전역 ID가 컴포넌트의 이름 옵션으로 자동 설정된다.



Vue.component('unique-name-of-my-component', {
  // ...
})


주의하지 않으면 재귀 컴포넌트가 무한 루프로 이어질 수도 있다.



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


위와 같은 컴포넌트는 "max stack size exceeded" 에러를 발생시키므로 재귀 호출이 조건부인지 확인하십시오. (즉, 결국 false가 될 v-if 사용)

컴포넌트간 순환 참조

Finder 또는 파일 탐색기에서와 같이 파일 디렉토리 트리를 구축한다고 가정해보겠다.
이 템플릿에는 tree-folder 컴포넌트가 있을 수 있다.



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


tree-folder-contents 컴포넌트:



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


자세히 살펴보면 이러한 컴포넌트가 실제로 렌더 트리에서 서로의 자손이자 조상임을 알 수 있다.
역설이다.
Vue.component를 사용하여 전역적으로 컴포넌트를 등록하면 이 역설이 자동으로 해결된다.
그것이 당신이라면 여기서 여기에서 읽기를 멈출 수 있다.

그러나 모듈 시스템을 사용하여 컴포넌트를 필요로 하거나 가져오는 경우, 예를 들어 webpack 또는 Browserify를 통해 에러가 발생한다.



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


무슨 일이 일어나고 있는지 설명하기 위해 구성 요소 A와 B를 호출합시다.
모듈 시스템은 A가 필요하지만 먼저 A가 B를 필요로 하지만 B는 A를 필요로 하지만 A는 B를 필요로 합니다.
먼저 다른 구성 요소를 해결하지 않고 두 구성 요소 중 하나를 완전히 해결합니다.
이 문제를 해결하려면 모듈 시스템에 "A는 결국 B가 필요하지만 B를 먼저 해결할 필요는 없습니다."라고 말할 수 있는 지점을 제공해야 합니다.

우리의 경우 그 지점을 tree folder 구성 요소로 만들어 보겠습니다.
우리는 역설을 생성하는 자식이 tree-folder-contents 구성 요소라는 것을 알고 있으므로 이를 등록하기 위해 beforeCreate 수명 주기 후크를 기다릴 것입니다.



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


또는 구성 요소를 로컬로 등록할 때 Webpack의 비동기 가져오기를 사용할 수 있습니다.



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


문제 해결됨!

// 정리중..
// https://v2.vuejs.org/v2/guide/components-edge-cases.html#Alternate-Template-Definitions