ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [가이드] 컴포넌트 기초(2) - 필수요소 | Vue
    Front-end/Vue.js 2020. 8. 19. 16:27
    반응형

    슬롯을 사용한 콘텐츠 배포

    컴포넌트를 사용할 때 다음과 같이 컴포넌트를 구성하는 것이 좋습니다.

    <app>
      <app-header></app-header>
      <app-footer></app-footer>
    </app>

    주목해야 할 두 가지 사항이 있습니다.

     

    1. <app> 컴포넌트는 어떤 콘텐츠를 받을지 모릅니다. 그것은 <app>이 사용하는 컴포넌트에 의해 결정됩니다.

    2. <app> 컴포넌트에는 자체 템플릿이 있을 가능성이 큽니다.

    위 구성으로 작동하도록 하려면 부모 “content”와 컴포넌트의 자체 템플릿을 섞는 방법이 필요합니다. 이것은 콘텐츠 배포 프로세스입니다. Vue.js는 현재 웹 컴포넌트 사양 초안을 모델로 한 콘텐츠 배포 API를 구현하며 원본 콘텐츠의 배포판 역할을 하기 위해 특수한 <slot> 엘리먼트를 사용합니다.

     

    범위 컴파일

    API를 파헤치기 전에 먼저 내용이 컴파일되는 범위를 명확히 해야 합니다. 다음과 같은 템플릿이 있다고 상상해보겠습니다.

    <child-component>
      {{ message }}
    </child-component>

    message가 부모 데이터 또는 자식 데이터중 어디에 바인딩되어야 할까요? 대답은 부모입니다. 컴포넌트 범위에 대한 간단한 법칙은 다음과 같습니다.

     

    상위 템플릿의 모든 내용은 상위 범위로 컴파일됩니다. 하위 템플릿의 모든 내용은 하위 범위에서 컴파일됩니다.

     

    일반적인 실수는 부모 템플릿의 하위 속성/메소드에 디렉티브를 바인딩하려고 하는 것입니다.

    <!-- 작동하지 않습니다 -->
    <child-component v-show="someChildProperty"></child-component>

    someChildProperty가 자식 컴포넌트의 속성이라고 가정하면, 위의 예제는 작동하지 않을 것입니다. 상위 템플릿은 하위 컴포넌트의 상태를 인식하지 못합니다.

     

    컴포넌트 루트 노드에서 하위 범위 디렉티브를 바인딩 해야하는 경우 하위 컴포넌트의 자체 템플릿에서 하위 범위 디렉티브를 바인딩해야 합니다.

    Vue.component('child-component', {
      // 이제 작동합니다. 올바른 위치에 놓여 있습니다.
      template: '<div v-show="someChildProperty">Child</div>',
      data: function () {
        return {
          someChildProperty: true
        }
      }
    })

    마찬가지로 분산된 콘텐츠는 상위 범위에서 컴파일됩니다.

     

    단일 슬롯

    하위 컴포넌트 템플릿에 최소한 하나의 <slot> 콘텐츠가 포함되어 있지 않으면 부모 콘텐츠가 삭제 됩니다. 속성이 없는 슬롯이 하나뿐인 경우 전체 내용 조각이 DOM의 해당 위치에 삽입되어 슬롯 자체를 대체합니다.

     

    원래 <slot> 태그 안에 있는 내용은 대체 콘텐츠로 간주됩니다. 대체 콘텐츠는 하위 범위에서 컴파일되며 호스팅 엘리먼트가 비어 있고 삽입할 콘텐츠가 없는 경우에만 표시됩니다.

     

    다음 템플릿으로 my-component라는 컴포넌트가 있다고 가정하십시오.

    <div>
      <h2>나는 자식 컴포넌트의 제목입니다</h2>
      <slot>
        제공된 컨텐츠가 없는 경우에만 보실 수 있습니다.
      </slot>
    </div>

    그리고 그 컴포넌트를 사용하는 부모는

    <div>
      <h1>나는 부모 컴포넌트의 제목입니다</h1>
      <my-component>
        <p>이것은 원본 컨텐츠 입니다.</p>
        <p>이것은 원본 중 추가 컨텐츠 입니다</p>
      </my-component>
    </div>

    아래처럼 렌더링 됩니다.

    <div>
      <h1>나는 부모 컴포넌트의 제목입니다</h1>
      <div>
        <h2>나는 자식 컴포넌트의 제목 입니다</h2>
        <p>이것은 원본 컨텐츠 입니다.</p>
        <p>이것은 원본 중 추가 컨텐츠 입니다</p>
      </div>
    </div>

     

    이름을 가지는 슬롯

    <slot> 엘리먼트는 특별한 속성 인 name을 가지고 있습니다. 이 속성은 어떻게 내용을 배포해야 하는 지를 커스터마이징 하는 데 사용할 수 있습니다. 이름이 다른 슬롯이 여러 개 있을 수 있습니다. 이름을 가진 슬롯은 내용 조각에 해당 slot 속성이 있는 모든 엘리먼트와 일치합니다.

     

    명명되지 않은 슬롯이 하나 있을 수 있습니다. 기본 슬롯은 일치하지 않는 콘텐츠의 포괄적인 콘텐츠 역할을 합니다. 기본 슬롯이 없으면 일치하지 않는 콘텐츠가 삭제됩니다.

     

    예를 들어, 다음과 같은 템플릿을 가진 app-layout 컴포넌트가 있다고 가정해보십시오 :

    <div class="container">
      <header>
        <slot name="header"></slot>
      </header>
      <main>
        <slot></slot>
      </main>
      <footer>
        <slot name="footer"></slot>
      </footer>
    </div>

    부모 마크업

    <app-layout>
      <h1 slot="header">여기에 페이지 제목이 위치합니다</h1>
    
      <p>메인 컨텐츠의 단락입니다.</p>
      <p>하나 더 있습니다.</p>
    
      <p slot="footer">여기에 연락처 정보입니다.</p>
    </app-layout>

    아래와 같이 렌더링 됩니다.

    <div class="container">
      <header>
        <h1>여기에 페이지 제목이 위치합니다</h1>
      </header>
      <main>
        <p>메인 컨텐츠의 단락입니다.</p>
        <p>하나 더 있습니다.</p>
      </main>
      <footer>
        <p>여기에 연락처 정보입니다.</p>
      </footer>
    </div>

    콘텐츠 배포 API는 함께 구성할 컴포넌트를 디자인할 때 매우 유용한 메커니즘입니다.

     

    범위를 가지는 슬롯

    2.1.0에 새롭게 추가됨.

     

    범위가 지정된 슬롯은 이미 렌더링 된 엘리먼트 대신 재사용 가능한 템플릿(데이터를 전달할 수 있음)으로 작동하는 특별한 유형의 슬롯입니다.

     

    prop을 컴포넌트에게 전달하는 것처럼, 하위 컴포넌트에서 단순히 데이터를 슬롯에 전달하면 됩니다.

    <div class="child">
      <slot text="hello from child"></slot>
    </div>

    부모에서, 특별한 속성 slot-scope를 가진 <template> 엘리먼트가 있어야 합니다. 이것은 범위를 가지는 슬롯을 위한 템플릿임을 나타냅니다. slot-scope의 값은 자식으로부터 전달된 props 객체를 담고 있는 임시 변수의 이름입니다:

    <div class="parent">
      <child>
        <template slot-scope="props">
          <span>hello from parent</span>
          <span>{{ props.text }}</span>
        </template>
      </child>
    </div>

    위를 렌더링 하면 출력은 다음과 같습니다.

    <div class="parent">
      <div class="child">
        <span>hello from parent</span>
        <span>hello from child</span>
      </div>
    </div>

     

    2.5.0 버전 이상에서, slot-scope는 더 이상 <template> 뿐 아니라 컴포넌트나 엘리먼트에서도 사용할 수 있습니다.

     

    범위가 지정된 슬롯의 일반적인 사용 사례는 컴포넌트 사용자가 리스트의 각 항목을 렌더링 하는 방법을 사용자 정의할 수 있는 리스트 컴포넌트입니다.

    <my-awesome-list :items="items">
      <!-- scoped slot 역시 이름을 가질 수 있습니다 -->
      <li
        slot="item"
        slot-scope="props"
        class="my-fancy-item">
        {{ props.text }}
      </li>
    </my-awesome-list>

    그리고 리스트 컴포넌트의 템플릿 :

    <ul>
      <slot name="item"
        v-for="item in items"
        :text="item.text">
        <!-- 대체 컨텐츠는 여기입니다. -->
      </slot>
    </ul>

     

    디스트럭처링

    slot-scope 값은 실제로 함수 서명의 인수 위치에 나타날 수 있는 유효한 JavaScript 표현식입니다. 이는 지원되는 환경 (싱글 파일 컴포넌트 또는 최신 브라우저)에서 ES2015 디스트럭처를 사용할 수 있다는 것을 의미합니다.

    <child>
      <span slot-scope="{ text }">{{ text }}</span>
    </child>

     

     

    동적 컴포넌트

    같은 마운트 포인트를 사용하고 예약된 <component> 엘리먼트를 사용하여 여러 컴포넌트 간에 동적으로 트랜지션 하고 is 속성에 동적으로 바인드 할 수 있습니다.

    var vm = new Vue({
      el: '#example',
      data: {
        currentView: 'home'
      },
      components: {
        home: { /* ... */ },
        posts: { /* ... */ },
        archive: { /* ... */ }
      }
    })
    <component v-bind:is="currentView">
      <!-- vm.currentView가 변경되면 컴포넌트가 변경됩니다! -->
    </component>

    원하는 경우 컴포넌트 객체에 직접 바인딩할 수도 있습니다.

    var Home = {
      template: '<p>Welcome home!</p>'
    }
    
    var vm = new Vue({
      el: '#example',
      data: {
        currentView: Home
      }
    })

     

    Keep-alive

    트랜지션 된 컴포넌트를 메모리에 유지하여 상태를 보존하거나 다시 렌더링 하지 않도록 하려면 동적 컴포넌트를 <keep-alive> 엘리먼트에 래핑 할 수 있습니다.

    <keep-alive>
      <component :is="currentView">
        <!-- 비활성화 된 컴포넌트는 캐시 됩니다! -->
      </component>
    </keep-alive>

    동적 컴포넌트를 감싸는 경우 <keep-alive>는 비활성 컴포넌트 인스턴스를 파괴하지 않고 캐시 합니다.

    <transition>과 비슷하게 <keep-alive>는 추상 엘리먼트입니다. DOM 엘리먼트 자체는 렌더링 하지 않고 컴포넌트 부모 체인에는 나타나지 않습니다.

     

    API 참조에서 <keep-alive><keep-alive>에 대한 자세한 내용을 확인하십시오.

     

     

    기타

    재사용 가능한 컴포넌트 제작하기

    컴포넌트를 작성할 때 나중에 다른 곳에서 다시 사용할 것인지에 대한 여부를 명심하는 것이 좋습니다. 일회용 컴포넌트가 단단히 결합되어도 상관없지만 재사용 가능한 컴포넌트는 깨끗한 공용 인터페이스를 정의해야 하며 사용된 콘텍스트에 대한 가정을 하지 않아야 합니다.

     

    Vue 컴포넌트의 API는 prop, 이벤트 및 슬롯의 세 부분으로 나뉩니다.

    • Props는 외부 환경이 데이터를 컴포넌트로 전달하도록 허용합니다.

    • 이벤트를 통해 컴포넌트가 외부 환경에서 사이드 이펙트를 발생할 수 있도록 합니다.

    • 슬롯을 사용하면 외부 환경에서 추가 콘텐츠가 포함된 컴포넌트를 작성할 수 있습니다.

     

    v-bind와 v-on을 위한 전용 약어문을 사용하여 의도를 명확하고 간결하게 템플릿에 전달할 수 있습니다.

    <my-component
      :foo="baz"
      :bar="qux"
      @event-a="doThis"
      @event-b="doThat"
    >
      <img slot="icon" src="...">
      <p slot="main-text">Hello!</p>
    </my-component>

     

    자식 컴포넌트 참조

    props나 이벤트가 있었음에도 불구하고 때때로 JavaScript로 하위 컴포넌트에 직접 액세스 해야 할 수도 있습니다. 이를 위해 ref를 이용하여 참조 컴포넌트 ID를 자식 컴포넌트에 할당해야 합니다. 예:

    <div id="parent">
      <user-profile ref="profile"></user-profile>
    </div>
    
    <script>
      var parent = new Vue({ el: '#parent' })
      // 자식 컴포넌트 인스턴스에 접근합니다.
      var child = parent.$refs.profile
    </script>

    ref가 v-for와 함께 사용될 때, 얻을 수 있는 ref는 데이터 소스를 미러링 하는 자식 컴포넌트를 포함하는 배열이 될 것입니다.

     

    `$refs` 는 컴포넌트가 렌더링 된 후에만 채워지며 반응적이지 않습니다. 그것은 직접 자식 조작을 위한 escape 해치를 의미합니다 - 템플릿이나 computed 속성에서 `$refs`를 사용하지 말아야 합니다.

     

    비동기 컴포넌트

    대규모 응용 프로그램에서는 응용 프로그램을 더 작은 덩어리로 나누고 실제로 필요할 때만 서버에서 컴포넌트를 로드해야 할 수도 있습니다. Vue를 사용하면 컴포넌트 정의를 비동기식으로 해결하는 팩토리 함수로 컴포넌트를 정의할 수 있습니다. Vue는 컴포넌트가 실제로 렌더링 되어야 할 때만 팩토리 기능을 트리거하고 이후의 리렌더링을 위해 결과를 캐시 합니다. 예 :

    Vue.component('async-example', function (resolve, reject) {
      setTimeout(function () {
        // 컴포넌트 정의를 resolve 콜백에 전달합니다.
        resolve({
          template: '<div>I am async!</div>'
        })
      }, 1000)
    })

    팩토리 함수는 resolve 콜백을 받습니다. 이 콜백은 서버에서 컴포넌트 정의를 가져왔을 때 호출되어야 합니다. 또한 reject (reason)을 호출하여 로드가 실패했음을 알릴 수 있습니다. 여기서 setTimeout 은 데모 용입니다. 컴포넌트를 검색하는 방법은 전적으로 귀하에게 달려 있습니다. 권장되는 접근법 중 하나는 Webpack의 코드 분할 기능과 함께 비동기 컴포넌트를 사용하는 것입니다.

    Vue.component('async-webpack-example', function (resolve) {
      // 이 특별한 require 구문은 Webpack이 Ajax 요청을 통해
      // 로드되는 번들로 작성된 코드를 자동으로 분리하도록 지시합니다.
      require(['./my-async-component'], resolve)
    })

    factory 함수에서 Promise를 반환할 수도 있습니다. 그래서 Webpack 2 + ES2015 구문을 사용하면 다음을 할 수 있습니다 :

    Vue.component(
      'async-webpack-example',
      // `import` 함수는 `Promise`를 반환합니다.
      () => import('./my-async-component')
    )

    지역 등록을 사용하는 경우, Promise를 반환하는 함수를 제공할 수 있습니다.

    new Vue({
      // ...
      components: {
        'my-component': () => import('./my-async-component')
      }
    })

     

    비동기 컴포넌트를 사용하려는 Browserify 사용자인 경우, 작성자는 불행히도 비동기 로드가 Browserify에서 지원하지 않는 것이라고 [분명하게 주장합니다.] 적어도 공식적으로. Browserify 커뮤니티는 기존 및 복잡한 응용 프로그램에 도움이 될 수 있는 [몇 가지 해결 방법]을 발견했습니다. 다른 모든 시나리오의 경우 기본 제공되는 비동기식 지원을 위해 Webpack을 사용하는 것이 좋습니다.

     

    고급 비동기 컴포넌트

    2.3.0 버전에서 추가

     

    2.3 버전부터 비동기 컴포넌트 팩토리는 다음 형태의 객체를 반환할 수 있습니다.

    const AsyncComp = () => ({
      // 로드하는 컴포넌트입니다. 반드시 Promise이어야합니다.
      component: import('./MyComp.vue'),
      // 비동기 컴포넌트가 로드되는 동안 사용할 컴포넌트
      loading: LoadingComp,
      // 실패했을 경우 사용하는 컴포넌트
      error: ErrorComp,
      // 로딩 컴포넌트를 보여주기전 지연하는 정도. 기본값: 200ms.
      delay: 200,
      // 시간이 초과되면 에러용 컴포넌트가 표시됩니다
      // 기본값: Infinity.
      timeout: 3000
    })

    vue-router에서 라우트 컴포넌트로 사용하는 경우 라우트 내비게이션이 발생하기 전에 비동기 컴포넌트가 먼저 작동하기 때문에 이러한 특성은 무시됩니다. 라우트 컴포넌트에서 위의 문법을 사용하려면 vue-router 2.4.0 이상을 사용해야 합니다.

     

    컴포넌트 이름 규약

    컴포넌트 (또는 prop)를 등록할 때 kebab-case, camelCase 또는 PascalCase를 사용할 수 있습니다.

    // 컴포넌트 정의에서
    components: {
      // kebab-case를 사용한 등록
      'kebab-cased-component': { /* ... */ },
      // camelCase를 사용한 등록
      'camelCasedComponent': { /* ... */ },
      // PascalCase를 사용한 등록
      'PascalCasedComponent': { /* ... */ }
    }

    HTML 템플릿 내에서 kebab-case와 동등한 것을 사용해야 합니다.

    <!-- HTML 템플릿에서 항상 kebab-case를 사용하세요 -->
    <kebab-cased-component></kebab-cased-component>
    <camel-cased-component></camel-cased-component>
    <pascal-cased-component></pascal-cased-component>
    

    그러나 문자열 템플릿을 사용할 때 HTML의 대소문자를 구분하지 않습니다. 즉, 템플릿에서도 CamelCase, PascalCase 또는 kebab-case를 사용하여 컴포넌트와 prop을 참조할 수 있습니다.

    • kebab-case

    • camelCase를 사용하여 컴포넌트가 정의된 경우 camelCase 또는 kebab-case

    • PascalCase를 사용하여 컴포넌트가 정의된 경우 kebab-case, camelCase or PascalCase

     

    <kebab-cased-component></kebab-cased-component>
    
    <camel-cased-component></camel-cased-component>
    <camelCasedComponent></camelCasedComponent>
    
    <pascal-cased-component></pascal-cased-component>
    <pascalCasedComponent></pascalCasedComponent>
    <PascalCasedComponent></PascalCasedComponent>
    
    <script>
      new Vue({
        // ...
        components: {
          'kebab-cased-component': { /* ... */ },
          camelCasedComponent: { /* ... */ },
          PascalCasedComponent: { /* ... */ }
        }
      })
    </script>

    이것은 PascalCase가 가장 보편적인 선언적 컨벤션임을 의미하며 케밥 케이스는 가장 보편적으로 사용하는 컨벤션입니다.

     

    컴포넌트가 slot 엘리먼트를 통해 내용을 전달받지 못하면 이름 뒤에 /를 사용하여 자체적으로 닫을 수도 있습니다.

    <my-component/>

    다시 말하지만, 이것은 자기 닫는 사용자 정의 엘리먼트가 유효한 HTML이 아니므로 문자열 템플릿 내에서만 작동하며 브라우저의 기본 파서는 이를 이해하지 못합니다.

     

    재귀 컴포넌트

    컴포넌트는 자신의 템플릿에서 재귀적으로 호출할 수 있습니다. 그러나, 그들은 name 옵션으로만 가능합니다.

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

    Vue.component를 사용하여 컴포넌트를 전역적으로 등록하면, 글로벌 ID가 컴포넌트의 name 옵션으로 자동 설정됩니다.

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

    주의하지 않으면 재귀적 컴포넌트로 인해 무한 루프가 발생할 수도 있습니다.

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

    위와 같은 컴포넌트는 “최대 스택 크기 초과” 오류가 발생하므로 재귀 호출이 조건부 (즉, 마지막에 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를 통해 오류가 발생합니다.

     

    컴포넌트를 마운트 하지 못했습니다 : 템플릿 또는 렌더링 함수가 정의되지 않았습니다.

     

    설명을 위해 두 개의 컴포넌트 A와 B가 있다고 합시다. 모듈 시스템은 우선 A가 필요하다고 합니다. 하지만 첫 번째 A는 B를 필요로 하고, B는 A를 필요로 합니다. 첫번째 A의 의존성을 해결하지 않고서는 무한 루프에 빠져버립니다. 이를 해결하려면 모듈 시스템에 “A는 B를 필요로 하나 B를 먼저 해결할 필요가 없습니다.”라고 말할 수 있는 지점을 제공해야 합니다.

     

    여기에서는 tree-folder 컴포넌트를 그 지점으로 삼겠습니다. 패러독스를 만드는 자식은 tree-folder-contents 컴포넌트이므로, beforeCreate 라이프 사이클 훅 시점까지 기다렸다가 해당 컴포넌트를 등록합니다.

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

    또 다른 방법으론, 컴포넌트를 지역 등록하는 경우에 Webpack의 비동기 import를 이용할 수 있습니다.

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

    문제가 해결되었습니다!

     

    인라인 템플릿

    하위 컴포넌트에 inline-template이라는 특수한 속성이 존재할 때, 컴포넌트는 그 내용을 분산된 내용으로 취급하지 않고 템플릿으로 사용합니다. 따라서 보다 유연한 템플릿 작성이 가능합니다.

    <my-component inline-template>
      <div>
        <p>이것은 컴포넌트의 자체 템플릿으로 컴파일됩니다.</p>
        <p>부모가 만들어낸 내용이 아닙니다.</p>
      </div>
    </my-component>

    그러나, inline-template 은 템플릿의 범위를 추론하기 더 어렵게 만듭니다. 가장 좋은 방법은 template 옵션을 사용하거나 .vue 파일의 template 엘리먼트를 사용하여 컴포넌트 내부에 템플릿을 정의하는 것입니다.

     

    X-Templates

    템플리트를 정의하는 또 다른 방법은 text/x-template 유형의 스크립트 엘리먼트 내부에 ID로 템플릿을 참조하는 것입니다. 예:

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

    이 기능은 큰 템플릿이나 매우 작은 응용 프로그램의 데모에는 유용할 수 있지만 템플릿을 나머지 컴포넌트 정의와 분리하기 때문에 피해야 합니다.

     

    v-once를 이용한 비용이 적게 드는 정적 컴포넌트

    일반 HTML 엘리먼트를 렌더링 하는 것은 Vue에서 매우 빠르지만 가끔 정적 콘텐츠가 많이 포함된 컴포넌트가 있을 수 있습니다. 이런 경우, v-once 디렉티브를 루트 엘리먼트에 추가함으로써 캐시가 한 번만 실행되도록 할 수 있습니다.

    Vue.component('terms-of-service', {
      template: '\
        <div v-once>\
          <h1>Terms of Service</h1>\
          ... a lot of static content ...\
        </div>\
      '
    })
    반응형

    댓글

Luster Sun