프론트엔드 데브코스 5기 TIL 40 - Vue심화

김영현·2023년 11월 24일
1

TIL

목록 보기
48/129

컴포넌트 커스텀 이벤트

저번 시간에 배웠던 $emit을 말하는거다.

<ChildComponent @click="$emit('custom')" />

위와같은 코드는 클릭이벤트가 발생하면 custom이벤트를 발생시킨다.

참고로 자식 컴포넌트의 옵션 emits에 명시적으로 등록하는걸 권장한다.(등록하지 않아도 동작함)

<script>
export default{
.... ,
emits:['custom']
}
</script>

주의할점은 기본적인 이벤트click,keyup...등을 emits에 적는다면 덮어쓰기함. 이때 명시적으로 요소에 커스텀 이벤트click을 할당하지 않는다면 동작x

양방향 데이터 바인딩

<!-- 부모 컴포넌트 -->
<Hello :message="msg" @update="msg = $event"/>
...
<!-- 자식 컴포넌트 -->
<template>
  <input :value="message" @input="$emit('update', $event.target.value)">
</template>

<script>
  export default{
    props:{
      message:{
       type:String,
       default:'' 
      }
    }
  }
</script>

props는 수정 불가능 => 커스텀 이벤트로 업데이트 디스패칭
이를 한번에 해결 해주는것이 v-model

<!-- 부모 컴포넌트 -->
<Hello v-model="msg"/>
...
<!-- 자식 컴포넌트 -->
<template>
  <input :value="modelValue" 
  @input="$emit('update:modelValue', $event.target.value)">
</template>

<script>
  export default{
    props:{
      modelValue:{
        type:String,
        default:''
      }
    }
  }
</script>

여기서 프롭스의 modelValuev-model로들어오는 데이터. 예약어다. 그리고 update:modelValue도 예약어임.

<!-- 부모 -->
<Hello v-model:propname="msg"/>
<!-- 자식 -->
<input :value="modelValue" 
  @input="$emit('update:propname' $event.target.value)">
</template>
...props:{
      propname:{
        type:String,
        default:''
      }

이런식으로 작성도 가능함


slot

template에 slot에...왜 보면볼수록 웹컴포넌트같을까?


진짜였네

<!-- 부모 -->
<Hello>나는 슬롯</Hello>
<!-- 자식 -->
<template>
<h1>Hello~</h1>
<slot>default</slot>
</template>


컴포넌트 열고닫는태그 사이에 값을 넣어주면 slot내부값이 변경되고 아니면 default로 렌더링됨. 웹컴포넌트와 똑같은 기능.

slot에 이름을 부여해줄수 있는 것도 똑같다

<!-- 부모 -->
  <Btn>
    <template #top>여긴 탑</template>
    <template #mid></template>
    <template #bot>여긴 봇</template>
  </Btn>
  <!-- 자식 -->
    <button>
    <slot name="top">top</slot>
    <slot name="mid">mid</slot>
    <slot name="bot">bot</slot>
  </button>

참고로 #v-slot의 약어이다.

slot에서 데이터를 밖으로 전달해줄수도 있다.

<!-- 부모 -->
  <Btn>
    <template #top="slotProps">
      <h1>{{ slotProps.hello }}</h1>
    </template>
  </Btn>
  <!-- 자식 -->
  <button>
	<slot name="top" :hello="123">top</slot>
  </button>

slotProps또한 예약어다.

직접 slot을 사용할 일은 많지 않고 각종 플러그인을 사용할때 쓰인다. slot이름을 동적제어도 가능함.

<template>
  <h1 @click="slotName = 'top'">{{ msg }}</h1>
  <Hello>나는 슬롯</Hello>
  <Btn>
    <template #[slotName]="slotProps">
      <h1>{{ slotProps.hello }}</h1>
    </template>
  </Btn>
</template>

<script>
import Hello from '~/components/Hello';
export default{
  components:{
    Hello
  },
  data(){
    return{
      msg:"hello vue!",
      slotName:'동적슬롯이름'
    }
  },
}
</script>

<!-- 자식 -->
<template>
  <button>
    <slot name="top" :hello="123">top</slot>
  </button>
</template>

<style scoped lang="scss">
button{
  padding: 10px 20px;
  &:hover{
    background-color: orange;
    transform: scale(1.2);
  }
}
</style>

h1을 누르면 슬롯이름이 top이 되어서, 원하는 슬롯 내부 값을 넣어줌.

h1 클릭전

h1 클릭후


동적 컴포넌트

<template>
  <button @click="currentComponent = 'Hello'">Hello!</button>
  <button @click="currentComponent = 'World'">World!</button>
  <component :is="currentComponent" />
</template>

<script>
import Hello from '~/components/Hello';
export default{
  components:{
    Hello
  },
  data(){
    return{
      msg:"hello vue!",
      currentComponent:'Hello'
    }
  },

}
</script>

컴포넌트의 이름을 동적으로 지정해줄 수 있음. 버튼을 클릭하면 동적으로 컴포넌트가 바뀐다. 이때 이전 인스턴스는 날아감.
=> 전환을 자주해야한다면 전환비용이 아쉽다. 이때 이전 인스턴스를 캐싱해둘 수 있는 예약어가 있음

...
<keep-alive>
  <component :is="currentComponent" />
</keep-alive>

이렇게하면 이전 인스턴스가 캐싱되어서, 상태도 저장된다.


Refs

요소 참조. document.querySelector를 사용하지 않고도 바로 가져올 수 있음

<template>
<h1 ref="hello">Hello~</h1>
</template>

<script>
  export default{
    mounted(){
      const h1El = this.$refs;
      console.log(h1El);
    }
  }
</script>

컴포넌트 자체를 참조할 수도있다.

<template>
  <h1>{{ msg }}</h1>
  <Hello ref="hello"/>
</template>

<script>
import Hello from '~/components/Hello';
export default{
  components:{
    Hello
  },
  data(){
    return{
      msg:"hello vue!",
    }
  },
  mounted(){
    console.log(this.$refs.hello.$el)
  }

}
</script>

만약 참조하는 컴포넌트의 최상위 요소가 여러개일땐 $el요소를 제대로 사용할 수 없다. 그때 하위컴포넌트중 필요한 특정 요소 ref를 사용하면 참조할 수 있다.

데이터 변경시 참조

반응형은 데이터가 바뀌자마자 참조가 불가능함.
=> 리덕스 만들면서 알게된 사실!!

이를 보장해주는 것이 setTimeOut..이라는데...응....??
나도 이 방법을 생각해보지 않은건 아닌데, 좋은방법이 아닌것 같았다.
실제로도 사용하는구나?
=> 시간을 0초로 주어서 태스크 큐로 넘겨버림. 데이터 변경이 다 일어난 후 태스크 큐에서 넘어오니 순서 보장됨.
헉..!!! 큐를 직접 조작하는 메서드는 없나?

있다. vue에는!

 this.$nextTick(() => this.$refs.editor.focus())

와우!


플러그인

Vue.js는 범용성이 뛰어남! 여러 플러그인이을 추가하거나 레거시 환경 일부분에도 적용시키기가 쉽다.

플러그인이란 전역적으로 기능을 추가하는 것임ㅎㅎ. 커스텀훅같은 기능인가보다.

//fetch.js
//객체 리터럴을 반환해야하고 install메서드가 들어있어야 한다.
export default {
  install(app, options) {
    app.config.globalProperties.$fetch = (url, opts) => {
      return fetch(url, opts).then((res) => res.json());
    };
  },
};

//main.js
...
import fetchPlugin from "~/plugins/fetch";

const { createApp } = Vue;
const app = createApp(App);
//전역으로 플러그인 등록
app.use(fetchPlugin);
...

//사용하는 컴포넌트
  created(){
    this.$fetch('https://jsonplaceholder.typicode.com/todos/1')
    .then(res=>console.log(res))
  }


잘 들어온다!

아하 유틸함수를 따로 빼두는 느낌이구먼?


믹스인

컴포넌트에 재사용 가능한 기능을 배포해준다. 모든 컴포넌트의 옵션을 포함할 수 있음! ex)created, methods...
상속같은 느낌인가?
참고로 컴포넌트 옵션과 이름이 충돌하면 컴포넌트의 옵션이 우선권을 가진다. 하지만 같은 이름의 메서드는 믹스인의 메서드가 호출되고 컴포넌트의 메서드가 호출됨. 오잉?

그리고 methods, components등의 객체값을 요구하는 옵션은 같은 객체로 병합된다. 중복되는 키는 컴포넌트의 키가 우선됨.

//이런식으로 만들고...
export default {
  data() {
    return {
      count: 1,
      msg: "hi~",
    };
  },
};
//이런식으로 불러온다
export default{
  components:{
    Hello
  },
  mixins:[sampleMixin],
  data(){
    return{
      //믹스인으로 가져온 msg데이터를 덮어씌운다.
      msg:"hello vue!",
    }
  },
}

동적으로 컴포넌트 + 믹스인

<template>
  <h1>설문조사</h1>
  <component
    :is="field.component"
    v-for="field in fields"
    :key="field.title"
    v-model="field.value"
    :title="field.title"
    :items="field.items"
  />
  <h1>결과</h1>
  <div v-for="field in fields" :key="field.title">{{ field.value }}</div>
</template>

<script>
import TextField from "~/fields/TextField";
import SimpleRadio from "~/fields/SimpleRadio";

export default {
  components: {
    TextField,
    SimpleRadio,
  },
  data() {
    return {
      fields: [
        {
          component: "TextField",
          title: "이름",
          value: "",
        },
        {
          component: "SimpleRadio",
          title: "나이대",
          value: "",
          items: ["20대", "30대", "40대", "50대"],
        },
      ],
    };
  },
};
</script>

이렇게 동적으로 컴포넌트를 생성함.
그런데, 겹치는 프로퍼티가 있고 수정사항 발생시 하나씩 바꿔야해서 곤란하다. 이때 믹스인을 사용해볼 수 있음.

//mixin.js
export default {
  props: {
    title: {
      type: String,
      default: "",
    },
    modelValue: {
      type: String,
      default: "",
    },
    items: {
      type: Array,
      default: () => [],
    },
  },
  emits: ["update:modelValue"],
};
//각 컴포넌트
import fieldMixin from "./mixin";

export default {
  mixins: [fieldMixin],
};

파일 경로를 이런식으로 관리해줄 수도 있따.

//같은 경로의 컴포넌트들을 사용하는 부모
import * as FieldComponents from "~/components/fields/index.js";

export default {
  components: {
    ...FieldComponents,
  },
//component/fields/index.js
export { default as TextField } from "./TextField";
export { default as SimpleRadio } from "./SimpleRadio";

객체로 담아와서 스프레드 연산자로 뿌려줌.
재사용할때 굉장히 유용하겠구나


Teleport

리액트의 portal같은 기능이다. 예를들어 모달컴포넌트가 있다고 하면.

<!--HTML구조-->
<div class="outer">
  <h3>Vue Teleport Example</h3>
  <div>
    <MyModal />
  </div>
</div>

<!--모달컴포넌트 내부-->
<script setup>
import { ref } from 'vue'

const open = ref(false)
</script>

<template>
  <button @click="open = true">모달 열기</button>

  <div v-if="open" class="modal">
    <p>짜자잔~ 모달입니다!</p>
    <button @click="open = false">닫기</button>
  </div>
</template>

<style scoped>
.modal {
  position: fixed;
  z-index: 999;
  top: 20%;
  left: 50%;
  width: 300px;
  margin-left: -150px;
}
</style>

문제점이 좀 있음
1. fixed는 부모요소에 transform, perspective등이 적용되면 동작x
2. <div class="outer">와 겹치고 z-index가 더 높은 또다른 엘리먼트가 존재한다면 모달창이 가려짐.

이때 Teleport를 사용해볼수 있음.

<button @click="open = true">모달 열기</button>

<Teleport to="body">
  <div v-if="open" class="modal">
    <p>짜자잔~ 모달입니다!</p>
    <button @click="open = false">닫기</button>
  </div>
</Teleport>

to대상은 CSS셀렉터나 실제 DOM노드여야함.
또한 disable프롭을 지원한다.

<Teleport :disabled="isMobile">
  ...
</Teleport>

Teleport를 여러개 적용할 수도 있다.

<Teleport to="#modals">
  <div>A</div>
</Teleport>
<Teleport to="#modals">
  <div>B</div>
</Teleport>

<!-- 결과 -->
<div id="modals">
  <div>A</div>
  <div>B</div>
</div>

Provide, Inject(제공, 주입)

Props drilling을 해결하기위한 기능이다.


글로벌Store처럼 provide하고 각 컴포넌트에서 inject해서 사용하는 방식이구먼?

provide함수를 사용하면 컴포넌트 하위 항목에 데이터를 제공해줄 수 있다.
이때 <script setup>을 사용하지 않는 경우 내부에서 provide가 동기적으로 호출되는지 확인해야함.

<!-- 컴포넌트 단위 -->
<script setup>
import { provide } from 'vue'
provide(/* 키 */ 'message', /* 값 */ '안녕!')
</script>

<!-- 글로벌 스토어 -->
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 키 */ 'message', /* 값 */ '안녕!')

<!-- 사용할땐 이렇게 키를 기반으로 -->
<script setup>
import { inject } from 'vue'

const message = inject('message', '값이 없을땐 이렇게 기본값 지정')
</script>

참고로 제공된 값이 ref인 경우 래핑이 해제되지 않는다.
=> 제공한 컴포넌트에 대한 반응성 연결을 유지할 수 있음.

제공자측에서 전달한 값이 변경되어야하는데, 어떻게 하면좋을까?
=> 제공자측에서 상태 변경을 담당하는 함수까지 제공해준다.

<!-- 제공자 컴포넌트 내부 -->
<script setup>
import { provide, ref } from 'vue'

const location = ref('북극')

function updateLocation() {
  location.value = '남극'
}

provide('location', {
  location,
  updateLocation
})
</script>

<!-- 주입되는 컴포는트 내부 -->
<script setup>
import { inject } from 'vue'

const { location, updateLocation } = inject('location')
</script>

<template>
  <button @click="updateLocation">{{ location }}</button>
</template>

참고로 제공되는 값이readonly가 아님! 읽기전용으로 하려면 readonly키워드로 감싸주자.

profile
모르는 것을 모른다고 하기

0개의 댓글