데브코스

[Day 36-2] 컴포넌트 등록, props, 커스텀 이벤트

라다디 2022. 12. 9. 13:08

컴포넌트 등록

컴포넌트는 전역 등록, 지역 등록이 가능하다.

컴포넌트로 등록을 하기 위해서는 컴포넌트의 이름을 지정해야 하는데 파스칼 케이스 혹은 케밥 케이스(= 대쉬 케이스)로 작성이 가능하다.

 

전역 등록

전역으로 컴포넌트를 등록하면 컴포넌트 어디에서든지 import를 하지 않고 등록한 컴포넌트를 사용 가능하다.

main.js에서 생성한 app에 component 메소드를 통해 등록한다. 

// main.js
import {createApp} from 'vue'
import App from '~/App'
import Btn from '~/components/Btn'

const app = createApp(App)
app.component('Btn', Btn) // 전역 등록
app.mount('#app')
<!-- App.vue -->
<template>
  <h1>Hello Vue!</h1>
  <!-- import 없이 바로 사용 가능 --> 
  <Btn /> 
</template>

 

지역 등록

지역적으로 컴포넌트를 등록하기 위해서는 components 옵션 안에 정의해야 한다. 

<!-- App.vue -->
<template>
  <h1>Hello Vue!</h1>
  <Hello />
</template>

<script>
import Hello from '~/components/Hello'
export default {
  components: {
    Hello
  }
}
</script>

 

컴포넌트 Props

단방향 데이터 흐름

상위 컴포넌트에서 전달하는 props의 데이터는 하위 컴포넌트에서는 수정할 수 없다.

<!-- App.vue -->
<template>
  <button @click="reverseMsg">click!</button>
  <Hello :message="msg" />
</template>

<script>
import Hello from '~/components/Hello'

export default {
  components: {
    Hello,
  },
  data() {
    return {
      msg: 'Hello Vue!',
    }
  },
  methods: {
    reverseMsg() {
      this.msg = this.msg.split('').reverse().join('')
    }
  }
}
</script>
<!-- Hello.vue -->
<template>
  <h1>{{ message }}</h1>
</template>

<script>
export default {
  props: ['message']
}
</script>

 

따라서 props를 받은 데이터를 컴포넌트 내부에서 데이터를 따로 담아서 해당 데이터를 변경하는 방식으로 해결할 수 있다.

<!-- Hello.vue -->
<template>
  <h1 @click="updateMessage">
    {{ message }}
  </h1>
</template>

<script>
export default {
  props: ['message'],
  data() {
    return {
      newMessage: this.message // props 데이터를 새로운 변수에 담음
    }
  },
  methods: {
    updateMessage() {
      this.newMessage = 'Good!'
    }
  }
}
</script>

 

Prop 타입

props는 데이터 타입을 명시해줄 수 있으며, 기본값을 지정할 수도 있다.

props: {
  title: String, // { type: String, default: 기본값 }
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise 
}

 

객체나 배열과 같은 참조형 데이터의 default값은 항상 함수로부터 반환이 되어야 한다. (= 팩토리 함수)

props:{
  id: Number,
  title: [String, Number],
  email:{
    type: String,
    default: '1111@abc.com'
  }
  data:{
    type: Object,
    // 객체나 배열의 기본 값은 항상 팩토리 함수로부터 반환
    default: function() {
      return { message: 'hello' }
    }
  },
},

 

아래와 같이 커스텀 유효성 검사 함수를 작성할 수도 있다.

props: {
  prop: {
    validator: function(value) {
      // 값이 꼭 아래 세 문자열 중 하나와 일치해야 함
      return ['success', 'warning', 'danger'].indexOf(value) !== -1
    }
  }  
}

 

HTML 속성명은 대소문자를 구분하지 않기 때문에 props를 통해 전달할 데이터 이름은 html상에서 케밥 케이스로 작성해야 한다.

props: ['postTitle']
<Post post-title="hello~" />

 

객체의 속성 전달

객체의 모든 속성을 props로 전달하려면 v-bind를 사용할 수 있다. (:prop-name 대신 v-bind 사용)

<!-- App.vue -->
<template>
  <Hello v-bind="post" />
  <!-- 위 코드는 아래 코드와 동일 -->
  <!-- <Hello :id="post.id" :title="post.title" :email="post.email"/> -->
</template>

<script>
import Hello from '~/components/Hello'

export default {
  components: {
    Hello,
  },
  data() {
    return {
      post: {
        id: 1,
        title: 'Hello!',
        email: 'hello@gmail.com'
      },
    }
  },
}
</script>
<!-- Hello.vue -->
<template>
  <h1>{{ id }} / {{ title }} / {{ email }}</h1>
</template>

<script>
export default {
  props: {
    id: Number, 
    title: String,
    email: {
      type: String,
      default: 'leon@abc.com'
    },
  },
}
</script>

 

컴포넌트 Non-Prop 속성

Non-prop 속성은 컴포넌트에 전달되지만 props나 emits에 정의된 특성을 지니고 있지 않은 속성(class, style, id) 또는 이벤트 리스너를 의미한다. 

이러한 속성들은 $attrs 프로퍼티를 통해 접근할 수 있다.

 

속성 상속

상위 컴포넌트에서 하위 컴포넌트에 속성을 전달할 때 하위 컴포넌트 내부에 최상위 요소가 하나일 경우 속성이나 이벤트가 어디에 적용될 것인가가 명확하기 때문에 잘 적용이 되는 반면, 최상위 요소가 여러 개일 경우 어디에 적용해야 하는지 알 수 없기 때문에 적용이 되지 않는다.

<!-- App.vue -->
<template>
  <h1>{{ msg }}</h1>
  <Hello
    class="hello"
    style="font-size: 100px;"
    @click="msg += '!'" />
</template>

<script>
import Hello from '~/components/Hello'

export default {
  components: {
    Hello,
  },
  data() {
    return {
      msg: 'Hello'
    }
  },
}
</script>
<!-- Hello.vue -->
<template>
  <h1>Hello</h1>
  <h2>Haha</h2>
</template>

 

이렇게 최상위 요소가 여러 개일 경우 $attrs를 사용하여 전달된 속성을 나누어 줄 수 있다.

$attrsattributes 객체로 컴포넌트가 가지고 있는 여러가지 속성들을 담고 있는 객체이다.

 

주의할 점은 props로 명시 된 속성들은 $attrs에 들어가지 않는다는 사실이다.

$attrsprops를 통해 지정되지 않은 나머지 속성들을 다룬다.

<!-- Hello.vue -->
<template>
  <h1
    :class="$attrs.class"
    :style="$attrs.style">
    Hello
  </h1>
  <h2 @click="$attrs.onClick">
    Haha
  </h2>
</template>

 

v-bind를 통해 $attrs에 담긴 속성들을 요소에 전부 적용할 수도 있다.

<!-- Hello.vue -->
<template>
  <h1 v-bind="$attrs">
    Hello
  </h1>
</template>

 

만일 최상위 요소가 하나일 경우 속성들이 적용되는 것을 원하지 않는다면 inheritAttrs: false를 통해 자동 상속을 방지한다.

<!-- Hello.vue -->
<template>
  <h1>Hello</h1>
</template>

<script>
export default {
  inheritAttrs: false,
  mounted() {
    console.log(this.$attrs)
  }
}
</script>

 

컴포넌트 커스텀 이벤트

v-on(@) 디렉티브 emit을 사용해서 커스텀 이벤트를 정의하고 사용할 수 있다.

<!-- App.vue -->
<template>
  <h1>{{ msg }}</h1>
  <Hello
    :message="msg"
    @please="reverseMsg" />
</template>

<script>
import Hello from '~/components/Hello'

export default {
  components: {
    Hello,
  },
  data() {
    return {
      msg: 'Hello Vue!'
    }
  },
  methods: {
    reverseMsg() {
      this.msg = this.msg.split('').reverse().join('')
    }
  }
}
</script>
<!-- Hello.vue -->
<template>
  <h1 @click="$emit('please')">
    {{ message }}
  </h1>
</template>

<script>
export default {
  props: {
    message: {
      type: String,
      default: ''
    }
  },
  emits: ['please']
}
</script>

emits 옵션을 통해 커스텀 이벤트를 정의해줘야 한다.

정의하지 않아도 동작은 하지만 해당 컴포넌트에서 어떤 커스텀 이벤트를 쓸 수 있는지 판단하기 위해서 모든 이벤트를 정의하는 것이 권장된다.

 

다만 네이티브 이벤트(e.g., click)를 주의해야 한다.

<!-- App.vue -->
<template>
  <h1>{{ msg }}</h1>
  <Hello
    @click="msg += '?!'" />
</template>
<!-- Hello.vue -->
<template>
  <div>
    <h1>A</h1>
    <h1>B</h1>
  </div>
</template>

<script>
export default {
  emits: ['click']
}
</script>

만약 emits 옵션을 이용해 네이티브 이벤트가 재정의 된 경우, 컴포넌트에 정의된 커스텀 이벤트가 네이티브 이벤트를 덮어쓴다.

 

커스텀 이벤트 검사

<!-- App.vue -->
<template>
  <h1>{{ msg }}</h1>
  <Hello
    @click="msg += '?!'"
    @please="msg += '~~'" />
</template>
<!-- Hello.vue -->
<template>
  <div>
    <h1 @click="$emit('please', 6)">please</h1>
    <h1 @click="$emit('click')">click</h1>
  </div>
</template>

<script>
export default {
  emits: {
    click: null, // 값이 null이면 검사 실행 x
    please: (number) => {
      if (number > 10) {
        return true
      } else {
        console.error('숫자가 10보다 크지 않음')
        return false
      }
    }
  }
}
</script>

 

양방향 데이터 바인딩

아래 예제의 방식을 통해 양방향 데이터 바인딩을 구현할 수 있지만 복잡해보인다.

<!-- App.vue -->
<template>
  <h1>{{ msg }}</h1>
  <Hello
    :message="msg"
    @update="msg = $event" />
</template>
<!-- Hello.vue -->
<template>
  <label>
    <input
      :value="message"
      @input="$emit('update', $event.target.value)" />
  </label>
</template>

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

 

이럴 때 사용할 수 있는 것이 modelValue props이다.

modelValue props를 사용하면 자동으로 v-model 디렉티브와 연결된다.

<!-- App.vue -->
<template>
  <h1>{{ msg }}</h1>
  <Hello v-model="msg" />
</template>
<!-- Hello.vue -->
<template>
  <label>
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)" />
  </label>
</template>

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

 

만일 modelValue라는 props 명을 변경하고 싶다면 v-model에 전달인자를 넘겨줌으로써 이름을 변경할 수 있다

<!-- App.vue -->
<template>
  <h1>{{ msg }}</h1>
  <Hello v-model:message="msg" />
</template>
<!-- Hello.vue -->
<template>
  <label>
    <input
      :value="message"
      @input="$emit('update:message', $event.target.value)" />
  </label>
</template>

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

 

아래는 여러개의 양방향 데이터 바인딩 예시이다.

<!-- App.vue -->
<template>
  <h1>{{ msg }}</h1>
  <h1>{{ name }}</h1>
  <Hello
    v-model:message="msg"
    v-model:name="name" />
</template>
<!-- Hello.vue -->
<template>
  <label>
    <input
      :value="message"
      @input="$emit('update:message', $event.target.value)" />
  </label>
  <label>
    <input
      :value="name"
      @input="$emit('update:name', $event.target.value)" />
  </label>
</template>

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

 


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

[Day 36] Vue (4)