기존 Javascript에서 html element에 접근하기 위해서 doucument.querySelector()
등의 메서드를 사용해왔다. 하지만 document.querySelector()
는 html 문서를 처음부터 탐색하여 컴포넌트 단위로 탐색할 수 없다.
이를 위해 ref
속성과 $refs
를 사용할 수 있다. html element의ref
속성에 이를 식별할 수 있는 키워드를 등록하면 전역 객체인 $refs
에 등록되어 this.$refs.name
과 같은 형식으로 등록해 놓은 요소에 접근이 가능하다.
컴포넌트의 속성을 v-bind
를 이용해서 동적으로 이용할 수 있듯이 컴포넌트 또한 동적으로 변경이 가능하다. 동적 컴포넌트를 사용하기 위해서 component
태그가 사용되며, 이 태그에 is
속성으로 컴포넌트의 이름을 지정한다.
이 방식은 v-if
와 유사하게 is
속성 값이 변경될 때마다 (즉, 동적 컴포넌트가 바뀔 때 마다) 기존의 컴포넌트는 unmount
되고 새로운 컴포넌트가 create
되는 동작이 반복되어 변경이 일어날 때의 비용이 크다.
keep-alive
태그keep-alive
태그 안에 있는 동적 컴포넌트는 컴포넌트가 변경되어 unmount
되어도 이를 캐싱하고 다시 사용할 때 create
과정을 거치지 않는다. 하지만 캐싱에도 메모리 등의 자원이 소모되므로 사용에 주의해야한다.
컴포넌트 태그 사이에 text content 또는 html element가 존재하는 경우 이 요소들은 모두 컴포넌트의 template
에 존제하는 slot
태그 위치에 위치하게 된다.
v-slot
만약 태그 사이에 여러개의 요소들이 존재하고 각 요소들의 위치를 다른 곳에 위치하고 싶을 때 v-slot
디렉티브를 사용할 수 있다. v-slot="after"
속성을 가진 element는name="after"
속성을 가진 slot에 위치하게 된다. 이를 위해서 slot
태그는 name
속성을 지정해 줘야한다.
v-slot
디렉티브는 #
으로 대체가 가능하다. 즉, v-slot="after"
는 #after
로 표기할 수 있다.
v-slot
다른 디렉티브처럼 v-slot
도 대괄호로 감싸 동적으로 사용이 가능하다.
요소의 속성이 #[slotName]
이라면 slotName
에 저장된 값에 따라서 위치가 동적으로 바뀐다.
만약 하나의 함수가 여러 컴포넌트에서 공통적으로 사용된다면 이를 plugin
으로 만들어 사용할 수 있다.
plugin
은 install
함수를 가진 객체 리터럴로 생성된다. install
함수는 app
과 option
을 매개변수로 입력받으며, 이 매개변수 중 app
의 app.config.globalProperties
에 플러그인의 이름과 동작할 함수를 key-value 쌍으로 선언한다.
// '$fetch' plugin을 생성하는 경우
export default {
install(app, options) {
app.config.globalProperties['$fetch'] = () => {
// $fetch plugin 동작 코드
}
}
}
전역 컴포넌트의 등록을 위해서 어플리케이션 인스턴스에 component()
메서드를 사용하듯이 plugin
은 use()
메소드를 이용해서 등록한다.
const app = Vue.createApp(App)
app.use(fetchPlugin, {})
app.mount('#app')
위의 과정을 거쳐 등록된 plugin
은 component안에서 this
로 바로 사용이 가능하다.
this.$fetch()
scss의 @mixin
에서 유추할 수 있듯 mixin은 컴포넌트 내부의 값들이 반복될 때 코드의 중복을 줄이기 위해서 사용할 수 있다.
methods
, props
, computed
등 컴포넌트의 옵션이 중복되는 부분을 별도의 객체 리터럴로 선언한다.
이후 mixin
이 필요한 컴포넌트의 mixins
옵션에 배열 리터럴로 mixin
을 추가한다.
component()
, plugin()
과 동일하게 mixin()
메서드로 전역 mixin을 등록할 수 있다.
// mixin 생성
const myMixin = {
props: {
name: {
type: String,
default: ''
}
},
methods: {
duplicatedMethod() {
// do something
}
}
}
// mixin 사용
export default {
mixins: ['myMixin', 'anotherMixin']
}
mixin 안에 있는 옵션과 컴포넌트 안에 있는 옵션 중 이름이 같은 것이 존재한다면 컴포넌트 안에 있는 옵션이 우선권을 가진다.
(mixin의 옵션이 컴포넌트의 옵션으로 덮어 쓰여진다.)
created
, mounted
등과 같은 life cycle hook
의 경우 배열에 병합되어 모두 실행되며 mixin hook -> component hook 순으로 실행된다.
조상-부모-자식 3 개의 컴포넌트가 존재하고 조상 컴포넌트의 데이터를 자식 컴포넌트가 써야하는 상황을 가정하자. 만약 props
를 이용해서 데이터를 전달한다면 조상 컴포넌트의 데이터를 부모 컴포넌트를 거쳐 자식 컴포넌트에 전달해야하는 아주 불필요한 과정이 필요하다. 데이터를 공유해야하는 두 컴포넌트 사이에 다른 컴포넌트들이 많을 수록 문제는 더욱 심각해진다.
이 문제점을 해결하기 위해서 provide
와 inject
를 사용할 수 있다.
공유할 데이터를 가진 컴포넌트에서 provide
옵션을 지정한다. 이 때 provide
는 데이터를 전달하므로 반드시 함수로 선언한다.
export default {
provide() {
return {
msg: this.msg
}
}
}
데이터가 필요한 컴포넌트에서 inject
옵션을 배열 리터럴 안에 지정한다.
export default {
inject: ['msg']
}
위처럼 값을 provide
에 바로 입력할 경우 이 데이터는 반응형 데이터가 아니며 따라서 값이 변경되도 렌더링이 적용되지 않는다. 반응형 데이터로 만들기 위해서 vue
의 computed
함수를 사용해야한다.
(이 때도 computed()
의 매게 변수에 들어가는 데이터는 함수로 입력한다.)
export default {
provide() {
return {
msg: computed(() => this.msg)
}
}
}
props
의 문제점을 provide & inject
를 사용해서 해결했지만 이 또한 한계가 존재한다. provide & inject
는 어디까지나 데이터의 전달만이 이루어지기 때문에 데이터의 수정은 데이터를 가지고 있는 컴포넌트만이 가능하다.
전역 스토어를 사용하면 이 문제점을 해결할 수 있다.
전역 스토어는 4개의 구성요소를 가지고 있다.
각 구성요소들은 정해진 규칙 없이 자유롭게 작성이 가능하지만 상태를 보관하는 state의 경우 반응형 데이터로 사용하기 위해서 vue
의 reactive()
를 사용한다. reactive()
는 객체 리터럴을 입력받아 반응형 데이터인 proxy를 반환하는 함수이다.
vuex는 위의 전역 스토어를 좀 더 체계적으로 구현한 라이브러리다.
전역 스토어와 구조 자체는 같을 수 있지만 동작 방식에 차이가 있다. vuex에서는 Mutaions의 함수가 동작하기 위해서 commit
함수를 사용하며, Action의 함수가 동작하기 위해서 dispatch
함수를 사용한다.
vuex
의 createStore
함수를 사용하며 컴포넌트의 옵션처럼 객체 리터럴에 필요한 값을 지정하여 매개변수로 입력한다.
component의 data와 같은 이유로 state는 함수로 작성한다.
getters
와 mutations
에서 함수의 첫 번째 매게변수를 통해서 state의 값에 접근이 가능하며, actions
에 있는 함수의 첫 번째 매게변수인 context
는 state
, getters
, commit
, dispatch
가 저장된 객체이다.
// vuex store 생성 예시
const myStore = createStore({
state() {
return {
data: 'something'
}
},
getters: {
important(state) {
return data + 'important'
}
},
mutations: {
updateData(state, nextData) {
state.data = nextData
}
},
actions: {
fetchData(context) {
//fetch data
}
}
})
가장 먼저 전역으로 사용하기 위해서 어플리케이션 인스턴스에 use()
메서드로 등록이 필요하다.
const app = Vue.createApp(App)
app.use(myStore)
app.mount('#app')
컴포넌트 내부에서 store를 사용하는 경우
state
this.$store.state
로 접근
getters
this.$store.getters
로 접근
action
this.$store.dispatch()
로 함수 호출
mutations
this.$store.commit()
로 함수 호출
// state
this.$store.state.data
// getters
this.$store.getters.important
// action
this.$store.dispatch('fetchData')
// state
this.$store.state.commit('updateData')
하나의 store가 모든 컴포넌트의 데이터를 관리할 경우 코드가 길어져 유지보수가 어려울 수 있으며 이 외에도 여러 단점이 존재한다. 모듈화는 과도하게 커질 수 있는 store를 여러 개로 나누어 관리할 수 있게 해준다.
모듈은 전역 스토어 생성시 createStore
에 입력하는 객체와 거의 동일하다. 추가적으로 namespaced: true
값을 객체 리터럴에 추가해주면 끝이다.
const myModule = {
namespaced: true,
state() {},
getters: {},
mutations: {},
actions: {}
}
생성된 모듈은 전역 스토어의 옵션 중 modules: {...}
에 입력하여 등록할 수 있다.
const myStore = {
modules: {
moduleName: myModule
}
}
기존의 전역스토어에 namespace
명을 추가하는 방식으로 사용한다.
// namespace 가 'module'인 경우
// state
this.$store.state.module.data
// getters
this.$store.getters.['module/important']
// action
this.$store.dispatch('module/fetchData')
// state
this.$store.state.commit('module/updateData')
vuex
의 함수 mapState
, mapGetters
, mapActions
, mapMutations
를 사용해 Store의 값을 컴포넌트의 옵션에 빠르게 mapping이 가능하다. 모든 함수의 입력은 아래와 같다.
(namespace 이름, [사용할 값 또는 함수의 이름, ...])
전역 스토어의 경우 namespace 이름은 생략한다. 참고 링크