31일차 - Vue: Component, Composition API

변승훈·2022년 5월 11일
0

1. Component

컴포넌트란 Vue의 가장 강력한 기능중 하나이며 기본 HTML 엘리먼트를 확장하여 재사용 가능한 코드를 캡슐화 하는데 도움이 된다.

1-1. Props

컴포넌트가 실행될 때 속성처럼 받아내는 내용들을 받아줄 수 있는 옵션이다.
해당 컴포넌트에 html속성처럼 붙어있을때, 내용에 값이 연결되어 있을 때 컴포넌트 내부에서 어떻게 처리 할지를 정의하는 개념으로 props옵션을 사용한다.

부모-자식간의 데이터 통신이라고도 한다. 부모 컴포넌트에서 자식 컴포넌트로 특정 데이터를 전달하는 용도로 사용하기 때문이다.

App.vue(부모 컴포넌트)

<template>
  <MyBtn>Banana</MyBtn>
  <MyBtn 
    :color="color">
    <span style="color:red;">Banana</span>  
  </MyBtn>
  <MyBtn 
    large
    color="royalblue">
    Apple  
  </MyBtn>
  <MyBtn>Cherry</MyBtn>
</template>
<script>
import MyBtn from '~/components/MyBtn'

export default {
  components: {
    MyBtn
  },
  data() {
    return {
      color: '#000'
    }
  }
}
</script>

MyBtn.Vue(연결된 자식 컴포넌트)

<template>
  <div 
    :class="{ large }"
    :style="{ backgroundColor: color }"
    class="btn">
    <slot></slot>
  </div>
</template>
<script>
export default {
  // props: 컴포넌트가 실행될 때 속성처럼 받아내는 내용들을 받아줄 수 있는 옵션이다.
  // 해당 컴포넌트에 html속성처럼 붙어있을때, 내용에 값이 연결되어 있을 때 컴포넌트 내부에서 어떻게 처리 할지를 정의하는 개념으로 props옵션을 사용한다.
  props: {
    color: {
      type: String,
      default: 'gray'
    },
    large: {
      type: Boolean,
      default: false
    },
    text: {
      type: String,
      default: ''
    }
  }
}
</script>
<style scoped lang="scss">
.btn {
  display: inline-block;
  margin: 4px;
  padding: 6px 12px;
  border-radius: 4px;
  background-color: gray;
  color: white;
  cursor: pointer;
    &.large {
    font-size: 20px;
    padding: 10px 20px;
  }
}
</style>

1-2. 속성, 상속

  • 속성: 컴포넌트에서 부여한 속성의 값이 연결된 컴포넌트에 적용이 된다. 단, 연결된 컴포넌트에 속성이 있어야 한다.

  • 상속: 부모 컴포넌트에서 사용하는 것을 자식 컴포넌트에서도 사용하는 것이다. 최상위 요소가 하나만 있을 때에도 상속하지 않겠다고 자식 컴포넌트에서 inheritAttrs: false로 선언할 수 있다.
    이후 원하는 요소를 연결을 해주고 싶다면 v-bind="$attrs"$attrs를 사용한다.

  • template의 바로 아래 자식요소를 최상위(루트)요소라고 부른다.
    최상위 요소가 2개 이상인 경우 부모 컴포넌트에서 제대로 적용이 되지 않는다.

App.vue(부모 컴포넌트)

<template>
  <MyBtn 
    class="Hun"
    style="color: red;"
    title="Hello world!">
    Banana
  </MyBtn>
</template>
<script>
import MyBtn from '~/components/MyBtn'

export default {
  components: {
    MyBtn
  }
}
</script>

MyBtn.vue(연결된 자식 컴포넌트)

<template>
  <div class="btn">
    <slot></slot>
  </div>
  <h1 v-bind="$attrs"></h1>
  <!--
  <h1 
    :class="$attrs.class"
    :style="$attrs.style"></h1>
	-->
</template>
<script>
export default {
  // 최상위 요소가 하나만 있을 때에도 상속하지 않겠다고 선언
  // 이후 원하는 요소를 연결을 해주고 싶다면 위에 $attrs을 사용한다.
  inheritAttrs: false,
  created() {
    console.log(this.$attrs)
  }
}
</script>
<style scoped>
.btn {
  display: inline-block;
  margin: 4px;
  padding: 6px 12px;
  border-radius: 4px;
  background-color: gray;
  color: white;
  cursor: pointer;
}
</style>

1-3. emit

이벤트를 원하는 이름을 사용해도 상관 없고 정확하게 emits옵션에서 받아서(특정 이벤트를 상속 받아서) emits에 연결하여 어디에서 어떻게 사용할 것인지를 $emit()을 통하여 정의해준다.

쉽게 표현하자면 부모 컴포넌트의 특정한 이벤트를 상속 받아서 emits옵션에 연결을 하여 $emit()메소드를 통해 작성하여 실행할 수 있다.
이렇게 부모요소에 연결되어 있는 이벤트를 실행할 수 있다.

App.vue(부모 컴포넌트)

<template>
  <MyBtn @click="log" @change-msg="logMsg">
    Banana
  </MyBtn>
</template>
<script>
import MyBtn from '~/components/MyBtn'

export default {
  components: {
    MyBtn
  },
  methods: {
    log(event) {
      console.log('Click!')
      console.log(event)
    },
    logMsg(msg) {
      console.log(msg)
    } 
  }
}
</script>

MyBtn.vue(연결된 자식 컴포넌트)

<template>
  <div class="btn">
    <slot></slot>
  </div>
  <h1 @click="$emit('click', $event)">ABC</h1>
  <input 
    type="text"
    v-model="msg" />
</template>
<script>
export default {
  // 이벤트를 원하는 이름을 사용해도 상관 없고 정확하게 emits옵션에서 받아서
  // (특정 이벤트를 상속 받아서) emits에 연결하여 어디에서 어떻게 사용할 것인지를 $emit()을 정의해준다.
  // 부모요소에 연결되어 있는 이벤트를 실행할 수 있다.
  emits: [
    'click',
    'changeMsg'
  ],
  data() {
    // 양 방향 데이터 바인딩으로 갱신이 된다.
    return {
      msg: ''
    }
  },
  watch: {
    msg() {
      this.$emit('changeMsg', this.msg)
    }
  }
}
</script>
<style scoped>
.btn {
  display: inline-block;
  margin: 4px;
  padding: 6px 12px;
  border-radius: 4px;
  background-color: gray;
  color: white;
  cursor: pointer;
}
</style>

1-4. v-slot: (#)

  • 하위 컴포넌트를 그대로 유지 하면서 다른 부모 컴포넌트들 마다 다른 내용을 적용하고 싶을 때 사용한다.
  • slot부분에 name속성으로 정확한 위치를 조정할 수 있다. 이를 이름을 갖는 슬롯이라 한다.
  • Fallback Contents: slot태그 사이에 내용이 없을때 출력되는 기본 내용

App.vue(부모 컴포넌트)

<template>
  <MyBtn>
    <template #text>
      <span>Banana</span>
    </template>
    <template #icon>
      <span>(B)</span>
    </template>
  </MyBtn>
</template>
<script>
import MyBtn from '~/components/MyBtn'

export default {
  components: {
    MyBtn
  }
}
</script>

MyBtn.vue(연결된 자식 컴포넌트)

<template>
  <div class="btn"> -->
    <!-- Fallback Contents -->
    <!-- <slot>Apple</slot> -->
    <slot name="icon"></slot>
    <slot name="text"></slot>
  </div>
</template>
<script>
export default {
}
</script>
<style scoped>
.btn {
  display: inline-block;
  margin: 4px;
  padding: 6px 12px;
  border-radius: 4px;
  background-color: gray;
  color: white;
  cursor: pointer;
}
</style>

1-5. Provide / Inject

Child 기준 조상 컴포넌트, 자식 컴포넌트로 데이터를 내려줄때 props를 사용한다.
props는 바로 아래의 자식 컴포넌트에게만 적용이 되기 때문에 이 과정을 생략하기 위해 provide와 inject를 사용한다. 단 반응성을 가지지 않는다는 단점을 가지게 된다.
그래서 computed()를 사용하여 반응성을 가지게 만든다.

App.vue(부모 1 컴포넌트)

 <template>
  <button @click="message = 'Good?'">
    Click!
  </button>
  <h1>App: {{ message }}</h1> -->
  <!-- <Parent :msg="message" /> -->
  <Parent />
</template>
<script>
import Parent from '~/components/Parent'
import { computed } from 'vue'

export default {
  components: {
    Parent
  },
  data() {
    return {
      message: 'Hello world!'
    }
  },
  provide() {
    // 반응성을 유지해주는 데이터를 만들기 위해 computed() 사용
    return {
      msg: computed(() => this.message)
    }
  }
}
</script>

Parent.vue(부모 2 컴포넌트)

<template>
  <!-- <Child :msg="msg" /> -->
  <Child />
</template>
<script>
import Child from '~/components/Child'

export default {
  components: {
    Child
  }  
  // provide 사용 시 필요 없어짐
  // ,
  // props: {
  //   msg: {
  //     type: String,
  //     default: ''
  //   }
  // }
}
</script>

Child.vue(자식 컴포넌트)

<!-- 컴포넌트 Provide / Inject -->
<template>
  <div>
    Child: {{ msg.value }}
  </div>
</template>
<script>
export default {
  // provide사용 시 inject를 사용
  // props: {
  //   msg: {
  //     type: String,
  //     default: ''
  //   }
  // }
  inject :['msg']
}
</script>

1-6. refs

  • 컴포넌트 Refs(reference: 참조): 해당하는 요소를 참조한다.
    해당 요소들은 html과 연결된 직후에만 사용할 수 있다. 즉, created()에는 사용할 수 없고 mounted()에는 사용할 수 있다.

  • 컴포넌트에서 ref를 통해 참조할 경우 참조된 이름의 뒤쪽에 $el을 사용해야 참조할 수 있다.

  • 최상위 요소가 여러개이면 객체 데이터로 추출된다.
    최상위 요소가 여러개인 곳에서 원하는 부분을 참조하려면 원하는 최상위 요소에 ref를 작성하여 참조를 해주면 된다.

App.vue(부모 컴포넌트)

<template>
  <!-- 1. -->
  <!-- <h1 id="hello"> -->
  <!-- 2. ref사용  -->
  <!-- <h1 ref="hello">
    Hello world!
  </h1> -->
  <!-- 3. import해서 사용 -->
  <Hello ref="hello" />
</template>
<script>
import Hello from '~/components/Refs'

export default {
  components: {
    Hello
  },
  mounted() {
    // 1.
    // const h1El = document.querySelector('#hello')
    // console.log(h1El.textContent)
    
    // 2. ref사용
    // this.$refs.hello
    // console.log(this.$refs.hello.textContent)
    
    // 3. import 해서 사용
    console.log(this.$refs.$el) // <h1>Hello~</h1>

    // 4. 최상위 요소가 여러개인 경우
    console.log(this.$refs.hello.$refs.good)
  }
}
</script>

Refs.vue(자식 컴포넌트)

<template>
  <h1>
    Hello~
  </h1>
  <h1 ref="good">
    Good?
  </h1>
</template>

2. Composition API

Vue 컴포넌트를 만들면 재사용이 가능하지만 수백개의 컴포넌트를 생각하게 된다면 관리가 힘들다. 코드 공유와 재사용을 위해서 위의 컴포넌트를 Composition API로 깔끔하게 정리하는 방법이라고 생각하면 된다.

2-1. 반응형 데이터(반응성)

기본 적으로 반응성을 가지게 하려면 Vue.js에서 제공하는 ref라는 기능을 가지고 와서 함수처럼 실행을 해야한다.

아래의 예시를 살펴 보자. 먼저 컴포넌트만 사용할 경우의 코드이다.

<template>
  <div @click="increase">
    {{ count }}
  </div>
</template>
<script>
  export default {
    // Composition Api 사용 전
    data() {
     return {
        count: 0
      }
    },
    methods: {
      increase() {
        this.count += 1
       }
     }
  }
</script>

이를 Composition API로 바꾼 코드이다.

<template>
  <div @click="increase">
    {{ count }}
  </div>
</template>
<script>
export default {
    // Composition Api 사용 후
    setup() {
      // 반응성 x
      let count = 0;
       function increase() {
         count += 1
       }

      return {
        count,
        increase
      }
    }
  }
</script>

그런데 setup() 메소드 내부에서 count라는 데이터를 정의했는데 이렇게 만들어진 데이터는 반응성이 없다. 즉, 화면의 갱신을 보장해주지 않는다.

그래서 Vue.js에서 지원하는 ref기능을 활용하여 script부분을 다음과 같이 수정해 줘야 반응성을 가지게 된다.

대신 ref를 사용하게 되면 count에 객체 데이터가 반환이 된다. 그러므로 count를 데이터로 사용할 수 없으므로 객체 데이터 내부에 value속성을 통해 접근을 해서 사용해야 한다.

<script>
// 반응성을 위해 ref를 가져와서 사용
import { ref } from 'vue'

  export default {

    setup() {
      // ref추가 후 반응성 O
      let count = ref(0);
      function increase() {
        // 데이터 처럼 사용해야 한다.
        count.value += 1
      }

      return {
        count,
        increase
      }
    }
  }
</script>

2-2. 기본 옵션과 라이프 사이클

라이프 사이클 훅은 import를 사용하여 직접적으로 onX함수로 등록할 수 있다.

먼저 기존 컴포넌트를 사용하는 코드를 보자.

<template>
  <h1 @click="increase">
  {{ count }} / {{ doubleCount }}
  </h1>
  <h1>
  {{ message }} / {{ reversedMessage }}
  </h1>
</template>
<script>
export default {
  data() {
    return {
      message: 'Hello world!!',
      count: 0
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    },
    reversedMessage() {
      return this.message.split('').reverse().join('')
    }
  },
  watch: {
    message(newValue) {
      console.log(newValue)
    }
  },
  created() {
    console.log(this.message)
  },
  mounted() {
    console.log(this.message)
  },
  methods: {
    increase() {
      this.count += 1
    }
  }
}
</script>

이렇게 컴포넌트로 구성된 코드를 Composition API를 사용하게 되면 아래와 같이 바꿀 수 있다.

<template>
  <h1 @click="increase">
  {{ count }} / {{ doubleCount }}
  </h1>
  <h1>
  {{ message }} / {{ reversedMessage }}
  </h1>
</template>
<script>
import { ref, computed, watch, onMounted } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const doubleCount = computed(() => count.value * 2)
    function increase() {
      count.value += 1
    }

    const message = ref('Hello World!')
    const reversedMessage = computed(() => {
      return message.value.split('').reverse().join('')
    })

    watch(message, newValue => {
      console.log(newValue)
    })
    function changeMessage() {
      message.value = 'Good?!'
    }

    // created 라이프 사이클
    console.log(message.value)

    onMounted(() => {
      console.log(count.value)
    })

    return {
      count,
      doubleCount,
      increase,
      message,
      changeMessage,
      reversedMessage
    }

  }
}
</script>

기본 옵션을 보면 import안에 computed, watch를 선언한 것을 볼 수 있다.
Composition API에서 setup()안에 위의 기본 옵션들을 사용하려면 기본적으로 import로 선언을 한 다음에 사용해야 하는 것을 알 수 있다.

라이프 사이클을 코드에서 살펴보면 import에 onMounted로 라이프 사이클을 확인한 것을 알 수 있다.
여기서 중요한 것이 있는데 바로 created관련 API는 없다는 것이다.
created 라이프 사이클을 확인하려면 setup() 안의 mounted관련 라이프 사이클 앞에서 console.log()로 확인해 보면 된다.

2-3. Props/ Context

컴포넌트가 생성되기 전에 props가 반환이 된면 실행되는 컴포넌트 옵션이다.
Composition API의 진입점 역할을 한다.

Composition API를 사용하지 않는 경우 메소드나 라이프사이클 등에서 this를 참조하여 접근을 한다.
하지만 API를 사용해서 setup()을 사용하는 경우 this를 참조할 수 없기 때문에 props를 통해서 데이터를 확인한다.
mounted에 있는 v-bind를 통한 모든 속성들이 상속이 된 객체를 context와 상속이 된 객체 앞의 $을 제거하여 사용할 수 있다.

아래의 예시를 보고 사용법을 알아보자!

먼저 App.vue 파일이다.

<template>
  <MyBtn
    class="Hun"
    style="color: red;"
    color="#ff0000"
    @hello="log">
    Apple  
  </MyBtn>
</template>
<script>
import MyBtn from '~/components/MyBtn'

export default {
  components: {
    MyBtn
  },
  methods: {
    log() {
      console.log('Hello world!')
    }
  }
}
</script>

다음은 연결된 MyBtn.vue 파일이다.

<template>
  <div  
    v-bind="$attrs"
    class="btn"
    @click="hello">
    <slot></slot>  
  </div>
</template>
<script>
import { onMounted } from 'vue'

export default {
  inheritAttrs: false,
  props: {
    color: {
      type: String,
      default: 'gray'
    }
  },
  emits: ['hello'],
  // mounted() {
  //   console.log(this.color)
  //   console.log(this.$attrs)
  // },
  // methods: {
  //   hello() {
  //     this.$emit('hello')
  //   }
  // },
  // 기존에 this를 통해 바로 활용할 수 있지만,
  // setup에서는 mounted를 props와 context를 이용해야한다.

  setup(props, context) {
    function hello() {
      context.emit('hello')
    }
    onMounted(() => {
      console.log(props.color)
      console.log(context.attrs)
    }) 

    return {
      hello
    }
  }
}

</script>
profile
잘 할 수 있는 개발자가 되기 위하여

0개의 댓글