데브코스

[Day 37-1] Slots, Refs, Plugin, Mixin, Teleport

라다디 2022. 12. 10. 18:41

컴포넌트 Slots

컴포넌트 태그 사이에 콘텐츠을 입력하면 slot 태그 위치에 작성된다.

만일 작성된 콘텐츠가 없다면 slot 태그 사이에 작성한 내용이 출력되는데 이를 Fallback 콘텐츠라고 한다. 

<!-- App.vue -->
<template>
  <h1>{{ msg }}</h1>
  <Hello>Hello Vue?</Hello>
</template>
<!-- Hello.vue -->
<template>
  <h1>Hello</h1>
  <slot></slot>
</template>

 

이름을 가지는 슬롯

slot에는 이름을 부여할 수도 있다.

<slot name="abc"></slot>

상위 컴포넌트에서 v-slot 디렉티브로 접근이 가능하다. 

v-slot 디렉티브는 #의 약어를 사용할 수 있다. 

<template #abc>
  <h2>ABC</h2>
</template>

 

범위를 가지는 슬롯

slot은 범위를 가질 수 있다.

범위는 해당 슬롯이 사용되는 구간을 의미한다. 

<slot :hello="123"></slot>
<!-- 슬롯에 이름이 없으면 #default로 접근 가능하며, default 슬롯이라 함 -->
<template #default="slotProps">
  <h2>Hello {{ slotProps.hello }}</h2>
</template>

<!-- 구조 분해 할당 가능-->
<template v-slot:default="{ hello }">
  <h2>Hello {{ hello }}</h2>
</template>

슬롯의 이름은 동적으로 관리할 수도 있다.  

 

동적 컴포넌트

컴포넌트를 동적으로 전환하는 것은 is라는 특별한 속성을 가진 <component> 엘리먼트에 의해 만들 수 있다.

<!-- currentTabComponent가 변하면 컴포넌트가 바뀜 -->
<component :is="currentTabComponent"></component>

동적 컴포넌트는 전환 비용이 높기 때문에 자주 전환될 때는 사용하지 않는 것이 좋다.

 

이 문제를 해결하기 위해서 동적 컴포넌트를 <keep-alive> 엘리먼트로 래핑하는 방법이 있다.

<template>
  <h1 @click="currentComponent = 'World'">{{ msg }}</h1>
  <keep-alive>
    <component :is="currentComponent" />
  </keep-alive>
</template>

keep-alive로 래핑하면 한번 렌더링한 컴포넌트를 캐싱하여 다시 렌더링을 시키지 않는다.

다만, 캐싱을 하면 메모리를 소모하기 때문에 토글이 자주 일어나는 곳에서만 사용하는 것이 좋다.

 

Refs

보통 특정한 html 엘리먼트를 찾기 위해 querySeletor을 사용한다.

하지만 querySeletor은 해당 컴포넌트의 전체 document를 스캔하여 원하는 요소를 찾는다.

따라서 효율성을 위해 ref 속성을 사용할 수 있다.

<h1 ref="hello">Hello</h1>
const $h1El = this.$refs.hello; 
console.log($h1El);

ref 속성은 html 요소가 연결된 이후(mounted)부터 사용할 수 있다. 

 

특정 component 태그에 ref 속성을 줄 수도 있다.

component 내부 요소에 직접 접근하기 위해서는 $el을 사용한다.

const $componentEl = this.$refs.hello.$el;

만일 최상위 요소가 여러개일 때는 $el을 제대로 사용할 수 없다.

그럴 경우 $refs.hello.$refs.world처럼 접근한다.

 

$nextTick

데이터가 변경된 이후에 요소가 바로 렌더링되지는 않는다.

따라서 데이터가 변경된 이후에 특정 요소에 접근해서 렌더링 속성을 조작해도 해당 조작이 정상적으로 동작하지 않을 때가 많다.

 

이럴 때 setTimeout을 통해 시간을 지연시킨 후에 적용시키는 방법도 있지만, vue의 내장 메서드인 $nextTick을 사용하면 같은 방식으로 구현할 수 있다.

 

$nextTick은 다음 렌더링 사이클 이후 실행될 콜백 함수를 등록할 수 있는 기능을 제공하는 메서드이다.

데이터가 변경된 이후 화면이 바뀌고 나서 내부의 콜백을 실행하는 것이다.

onEdit() {
  this.data = true;
  this.$nextTick(() => {
    this.$refs.editor.focus()
  });
}

$nextTick$refs와 대부분 같이 사용된다.

 

플러그인

플러그인은 여러 컴포넌트에서 사용되는 (전역) 기능을 만들 때 사용한다.

// plugins/fetch.js
// $fetch와 같이 $가 붙는 경우는 this로 접근해서 사용할 수 있다는 Vue의 통상적인 의미
export default {
  install: (app, options) => {
    app.config.globalProperties[options.pluginName || "$fetch"] = (url, opts) => {
      return fetch(url, opts).then(res => res.json());
    };
  }
};

 

// main.js
import { createApp } from "vue";
import App from "./App.vue";
import fetchPlugin from "~/plugins/fetch";

const app = createApp(App);
// use로 등록, 특정 이름을 지정 가능
app.use(fetchPlugin, {
  pluginName: '$myname'
});
app.mount("#app");

이제 어플리케이션 어디에서든지 $fetch를 활용할 수 있다.

created() {
  this.init();
},
methods: {
  async init(){
    const res = await this.$myName('https://jsonplaceholder.typicode.com/todos/1');
    console.log(res, 'Done!');
}

라이프 사이클은 비동기를 보장하지 않기 때문에 가능하면 따로 method를 만들어서 작성하는 것이 권장된다.

 

믹스인

믹스인재사용 가능한 기능을 미리 정의하고 컴포넌트에서 불러와서 사용할 수 있는 기능이다.

 

병합시 믹스인의 옵션과 컴포넌트의 옵션이 중복될 경우, 컴포넌트의 옵션이 우선되어 적용된다.

훅 함수(라이프 사이클)같은 경우는 모두 호출된다. (믹스인 훅이 먼저 호출)

 

[참고] $options는 컴포넌트가 가진 여러 옵션에 대한 정보가 들어있는 내장 객체이다.

// mixins/sample.js
export default {
  data(){
    return {
      count:1,
      msg: 'Hi~'
    };
  }
};
<!-- App.vue -->
<template>
  <h1>
    {{ msg }} <!-- Hello 출력: 컴포넌트 옵션 우선 적용 -->
    {{ count }} 
  </h1>
</template>

<script>
import sampleMixin from './mixins/sample';

export default {
  mixins: [sampleMixin],
  data() {
    return {
      msg: 'Hello',
    };
  },
};
</script>

 

믹스인을 활용한 설문조사 예제

<!-- App.vue -->
<template>
  <!-- 동적 컴포넌트 -->
  <component
    :is="field.component"
    v-for="field in fields"
    :key="'component-' + field.title"
    v-model="field.value"
    :title="field.title"
    :items="field.items"
  />

  <h1>결과</h1>
  <div v-for="field in fields" :key="'result-' + field.title">
    {{ field.value }}
  </div>

  <button @click="submit">제출</button>
</template>

<script>
// 컴포넌트 한 번에 불러오기
import * as FieldComponents from "~/components/fields/index";

export default {
  components: {
    ...FieldComponents,
  },
  data() {
    return {
      fields: [
        {
          component: "TextField",
          title: "이름",
          value: "",
        },
        {
          component: "SimpleRadio",
          title: "나이대",
          value: "",
          items: ["20대", "30대", "40대", "50대"],
        },
      ],
    };
  },
  methods: {
    submit() {
      const results = this.fields.map(({ title, value }) => ({
        title,
        value,
      }));
      console.log(results);
    },
  },
};
</script>
// fields/index.js
export { default as TextField } from "./TextField";
export { default as SimpleRadio } from "./SimpleRadio";
// fields/mixin.js
export default {
  props: {
    items: {
      type: Array,
      default: () => [],
    },
    title: {
      type: String,
      default: "",
    },
    modelValue: {
      type: String,
      default: "",
    },
  },
  emits: ["update:modelValue"],
};
<!-- SimpleRadio.vue -->
<template>
  <h3>{{ title }}</h3>
  <ul>
    <li v-for="item in items" :key="item">
      <label>
        <input
          type="radio"
          :value="item"
          :name="title"
          @input="$emit('update:modelValue', $event.target.value)"
        />
        {{ item }}
      </label>
    </li>
  </ul>
</template>

<script>
import fieldMixin from "./mixin";

export default {
  mixins: [fieldMixin],
};
</script>
<!-- TextField.vue -->
<template>
  <div>
    <h3>{{ title }}</h3>
    <input
      :value="modelValue"
      type="text"
      @input="$emit('update:modelValue', $event.target.value)"
    />
  </div>
</template>

<script>
import fieldMixin from "./mixin";

export default {
  mixins: [fieldMixin],
};
</script>

 

Teleport

teleport 태그를 사용하여 컴포넌트 요소를 순간이동 시킬 수 있다.

<template>
  <div @click="onModal">
    <slot name="activator"></slot>
  </div>
  <!-- teleport 태그 내부의 요소들을 body 태그로 순간이동 시킴 -->
  <teleport to="body">
    <template v-if="isShow">
      <div class="modal" @click="offModal">
        <div
          :style="{ width: `${parseInt(width, 10)}px` }"
          class="modal__inner"
          @click.stop
        >
          <slot></slot>
        </div>
      </div>
    </template>
  </teleport>
</template>

position: fixed의 상위 요소에 filter나 tranform 등이 있는 경우 뷰포트 대신 그 조상을 컨테이닝 블록으로 삼는다.

하지만 이런 상황을 원하지 않는 경우 해당 요소를 원하는 별도 요소의 위치로 이동시키고 싶을 때 teleport를 사용할 수 있다. 

 

teleport를 활용한 모달 예제

<!-- App.vue -->
<template>
  <Modal
    v-model="isShow"
    width="300px">
    <template #activator>
      <button>Modal Open!</button>
    </template>
    <h3>내용</h3>
  </Modal>
</template>

<script>
export default {
  data() {
    return {
      isShow: false
    };
  },
};
</script>

 


출처: 프로그래머스 프론트엔드 데브코스 

[Day 37] Vue (5)