끊임없이 변화하는 데이터 스트림을 다루는 비동기적 프로그래밍으로 이벤트를 발견하는 관찰자와 발생한 이벤트에 반응하는 핸들러의 구성을 다룬다. 실시간으로 사용자가 발생한 이벤트로 생긴 데이터의 변화를 새롭게 DOM으로 렌더링해야 되는 프론트엔드의 프로그래밍 또한 반응형 프로그래밍의 특징을 가지고 있다.
아래 코드는 A2가 A0과 A1의 값에 의존하고 있지만 A0의 변화를 A2는 반영하지 못해 반응형 프로그래밍의 특징을 구현할 수 없는 문제점을 보인다.
let A0 = 1
let A1 = 2
let A2 = A0 + A1
console.log(A2) // 3
A0 = 2
console.log(A2) // Still 3
이를 개선하기 위해 아래 코드와 같이 update 함수를 구현하고 A2가 의존하는 A0과 A1의 데이터가 변화할 때 update 함수를 호출하는 방식을 통해 A2도 실시간으로 의존하는 데이터가 변화할 때마다 이를 반영할 수 있게 된다.
let A2
function update() {
A2 = A0 + A1
}
vue는 자바스크립트 객체를 Proxy 객체로 감싼 후 getter와 setter를 인터셉트해서 의존하는 데이터를 탐지하고 데이터의 변화에 따라 업데이트해줄 수 있는 반응형 객체를 생성해준다.
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
}
})
}
읽기 작업을 하기 전 해당 데이터를 의존하는 구독자 정보 목록을 업데이트 한다.
let activeEffect
function track(target, key) {
if (activeEffect) {
const effects = getSubscribersForProperty(target, key)
effects.add(activeEffect)
}
}
쓰기 작업을 마친 후 해당 데이터를 의존하는 구독자들에 대한 업데이트를 수행한다.
function trigger(target, key) {
const effects = getSubscribersForProperty(target, key)
effects.forEach((effect) => effect())
}
compostion api는 선언한 반응형 상태가 담긴 자바스크립트 객체를 리턴하는 setup() 함수를 정의하는 것을 통해 템플릿에서 사용할 수 있게 된다.
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return {
count
}
}
}
</script>
script setup을 통해 더욱 간단한 코드 선언이 가능하다.
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
vue가 제공하는 api는 반응형 객체를 구현하기 위해 프록시 객체를 생성하여 반환하기 때문에 원래 전달한 객체와 동일하지 않다는 점을 주의해서 사용해야 한다. reactive는 깊은 반응형 상태로 객체를 변환하기 때문에 내부에 있는 객체 역시 반응형 객체로 변환된다.
하지만 reactive는 다음과 같은 2가지 단점을 가지고 있다.
vue는 이러한 2가지의 단점을 보완하는 ref를 지원하고 있다. ref의 경우 ref변수명.value의 형태로 사용이 가능하고 가장 상위 객체일 경우 text interpolation에서 .value 생략이 가능하다.
import { reactive, ref } from 'vue'
const state = reactive({ count: 0 })
const count = ref(0)
const raw = {}
const proxy = reactive(raw)
// proxy is NOT equal to the original.
console.log(proxy === raw) // false
함수 내부에서 의존하는 반응형 데이터의 변화가 감지된 경우에 대해서만 내부의 로직을 실행하고 캐시를 업데이트하고 변화가 없는 경우 캐시에 저장된 계산값을 재사용한다.
<script setup>
import { reactive, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// a computed ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
반응형 데이터의 변화와 함께 동작되어야 하는 로직을 구현하기 위해 vue는 watch 함수를 지원한다. watch함수의 첫번째 인자에 변화를 감지할 source를 지정해야 하는데 ref, reactive 객체, getter 함수, ref나 reactive 객체로 구성된 배열을 지정할 수 있다. watchEffect 함수는 콜백 함수 내부에서 의존하는 반응형 데이터를 자동으로 감지하여 해당 데이터가 변화할 때 콜백 함수가 동작되도록 지원한다.
const x = ref(0)
const y = ref(0)
// single ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// array of multiple sources
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
vue의 template은 v-html로 시작하는 지시어를 통해 compostion api로 뷰 컴포넌트의 속성과 함수를 바인딩한 화면을 구성할 수 있도록 지원한다.
자주 사용되는 기본적인 데이터 바인딩으로 이중 중괄호를 사용하여 컴포넌트의 속성이 저장한 값으로 대치되어 화면에 보여지게 된다.
<span>Message: {{ msg }}</span>
컴포넌트의 속성을 html 속성과 바인딩하는 지시어로 축약된 표현으로 사용이 가능하다.
<div v-bind:id="dynamicId"></div>
<!-- shorthand -->
<div :id="dynamicId"></div>
해당 지시어의 true, false를 통해 하위 요소의 화면 표시 여부를 제어할 수 있다.
<p v-if="seen">Now you see me</p>
배열, 오브젝트, 숫자, 문자 형태로 선언된 데이터에 대한 반복적인 렌더링 작업을 지원한다. 렌더링된 개별 DOM 노드들을 추적하고 기존 엘리먼트를 재사용, 재정렬하기 위해 v-for 각 항목들에 고유한 key 속성를 동적 값에 바인딩해야 한다.
<div v-for="item in items" :key="item.id">
{{ item.text }}
</div>
반복적인 렌더링 작업을 트리거하는 배열의 변이 메소드는 다음과 같다.
push()
pop()
shift()
unshift()
splice()
reverse()
호출된 원본 배열을 변형하지 않고 항상 새 배열을 반환하는 메소드는 다음과 같다.
filter()
concat()
slice()
javaScript의 제한으로 vue는 다음과 같은 배열의 변이 작업은 감지할 수 없기 때문에 배열의 변이 메소드 splice를 사용해야 한다.
<script setup>
const items = ref(['a','b','c'])
items.value[1] = 'x' // not reactive
items.value.length = 2 // not reactive
items.value.splice(1, 1, 'x') // reactive
items.value.splice(2) // reactive
</script>
DOM의 이벤트 리스너를 바인딩하는 지시어로 컴포넌트에 등록된 함수명을 넣어서 사용한다.
<a v-on:click="doSomething"> ... </a>
<!-- shorthand -->
<a @click="doSomething"> ... </a>
input의 값을 컴포넌트 속성과 바인딩하게 되면 사용자가 input에 전달한 값을 실시간으로 컴포넌트 속성도 반영할 수 있는 양방향 바인딩을 지원한다.
//v-model 사용 X
<input
:value="searchText"
@input="searchText = $event.target.value"
/>
//v-model 사용으로 간소화된 작성
<input v-model="text">
또한 양방향 데이터 바인딩을 해주기 전에 입력한 값에 대한 전처리를 하는 v-model modifiers 기능을 제공한다.
vue에서 제공하는 기본 modifier뿐만 아니라 컴포넌트의 props.modelModifiers에 선언한 후 update:modelValue를 호출하기 전에 modifier 로직을 구현해서 사용하면 된다.
<!-- 부모 컴포넌트 -->
<MyComponent v-model.capitalize="myText" />
<!-- 자식 컴포넌트 -->
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
const emit = defineEmits(['update:modelValue'])
function emitValue(e) {
let value = e.target.value
//
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
<template>
<input type="text" :value="modelValue" @input="emitValue" />
</template>
vue 어플리케이션은 createApp 함수에 루트 컴포넌트를 등록하고 해당 컴포넌트를 실제 DOM 요소로 마운트를 한다.
import { createApp } from 'vue'
const app = createApp({
/* root component options */
})
app.mount('#app')
컴포넌트는 전역으로 등록하여 모든 컴포넌트에서 사용이 가능하도록 하거나 특정 컴포넌트에서만 사용이 가능한 2가지 방식을 지원한다.
import MyComponent from './App.vue'
//global registration
app.component('MyComponent', MyComponent)
<script setup>
//local registration
import ComponentA from './ComponentA.vue'
</script>
<template>
<ComponentA />
</template>
전역으로 컴포넌트를 등록하게 되면 한 번의 import문을 통해 모든 컴포넌트에서 사용이 가능한 편리함을 제공하지만 몇 가지 단점을 가지고 있다.
key는 Vue의 가상 DOM 알고리즘이 새로운 노드 목록을 이전 목록과 비교할 때 VNode를 식별하기 위한 힌트로 사용해서 key가 없으면 요소의 이동을 최소화하고 같은 유형의 요소를 제자리에서 패치/재사용하려는 알고리즘을 사용하고 key를 사용하면 key의 순서 변경에 따라 엘리먼트를 재정렬하고 key가 존재하지 않는 요소는 항상 제거, 삭제된다. 또한 엘리먼트나 컴포넌트를 재사용하지 않고 강제로 대체할 때도 사용할 수 있어 다음과 같은 경우 활용할 수 있다.
<Component :key="someVariableUnderYourControl"></Component>
// key가 변경되면 컴포넌트가 다시 빌드
<transition>
<span :key="text">{{ text }}</span>
</transition>
상위 컴포넌트에서 하위 컴포넌트로 속성을 전달하기 위해 제공된 방법으로 composition api의 다음과 같은 방법들을 통해 상위로부터 전달받을 속성의 명칭을 등록하여 사용한다.
<!-- script setup -->
<script setup>
const props = defineProps(['greetingMessage'])
console.log(props.greetingMessage)
</script>
<!-- export default -->
export default {
props: ['greetingMessage'],
setup(props) {
console.log(props.greeting-message)
}
}
camelCase로 작성된 컴포넌트의 props 명칭을 HTML 태그에서 사용할 때는 kebab-case나 camelCase로 사용할 수 있다.
<MyComponent greeting-message="hello" />
<MyComponent greetingMessage="hello" />
상위 컴포넌트에서 하위 컴포넌트로 함수를 전달하기 위해 제공된 방법으로 composition api의 다음과 같은 방법들을 통해 상위로부터 전달받을 함수의 명칭을 등록하여 사용한다.
<script setup>
defineEmits(['inFocus', 'submit'])
</script>
export default {
emits: ['inFocus', 'submit'],
setup(props, ctx) {
//전달받은 함수를 동작시키는 코드
ctx.emit('submit')
}
}
컴포넌트의 props는 부모 컴포넌트가 자식 컴포넌트에게 전달하는 값으로 자식 컴포넌트에서는 이 값을 변경하는 것이 불가능한 readOnly 속성을 가지고 있다.
따라서 커스텀 컴포넌트에서 사용자의 입력에 따라 값을 업데이트할 수 있는 양방향 데이터 바인딩을 제공하기 위해서는 다음 2가지를 구현해야 한다.
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<CustomInput v-model="searchText" />
<MyComponent v-model:title="bookTitle" />
<!-- CustomInput.vue -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
상위 컴포넌트의 내부 컨텐츠를 전달받은 하위 컴포넌트는 slot 태그를 통해 내부 컨텐츠의 배치를 제어할 수 있다.
<FancyButton>
Click me! <!-- slot content -->
</FancyButton>
<button class="fancy-btn">
<slot></slot> <!-- slot outlet -->
</button>
하나의 컴포넌트에서 여러 slot을 사용하게 되어 구분이 필요한 경우 상위 컴포넌트에서는 v-slot:구분자나 #구분자를 통해 지정하고 전달받은 하위 컴포넌트에서는 slot name="구분자" 형태로 사용하면 된다.
<BaseLayout>
<template v-slot:header>
<!-- content for the header slot -->
</template>
</BaseLayout>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
다른 컴포넌트 간의 데이터를 주고 받기 위해 vue에서 제공하는 개념으로 컴포넌트에서 데이터를 전달하기 위해 이벤트를 발행하면 이벤트를 구독한 다른 컴포넌트에서 이벤트가 발생하면 구독할 때 등록한 콜백 함수를 동작하게 된다.
// 이벤트버스 생성
var EventBus = new Vue()
// 이벤트 발행
EventBus.$emit('message', 'hello world');
// 이벤트 구독
EventBus.$on('message', function(text) {
console.log(text);
});
vue의 상태관리 패턴을 제공하는 라이브러리로 여러 컴포넌트가 공유한 상태를 전역 싱글톤으로 관리해 컴포넌트가 업데이트한 상태를 다른 컴포넌트에서 동기화 필요 없이 참조할 수 있도록 제공한다.
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
// 상태
state: {
count: 0
},
// 상태를 업데이트하는 동작
mutations: {
increment (state) {
state.count++
},
decrement (state) {
state.count--
}
}
// 상태를 변화시키는 동작
actions: {
incrementTwice (state) {
increment(state)
increment(state)
}
}
})
dom 요소에 대한 직접적인 접근을 하기 위해 제공하는 기능으로 요소의 속성으로 ref을 선언하고 값에는 dom 요소를 바인딩할 ref 변수명을 명시하면 된다.
<script setup>
import { ref, onMounted } from 'vue'
// declare a ref to hold the element reference
// the name must match template ref value
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>
<script setup>
를 사용한 컴포넌트의 접근 제어자는 기본으로 private이기 때문에 부모 컴포넌트가 해당 컴포넌트를 ref를 통해 접근하는 경우 접근할 수 있는 자식 컴포넌트의 속성이나 메소드는 defineExpose 함수에 전달해야 한다.
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
// Compiler macros, such as defineExpose, don't need to be imported
defineExpose({
a,
b
})
</script>
vue 애플리케이션에서의 composable은 뷰의 상태 관리 로직을 가진 재사용 가능한 함수의 형태이다. 유틸 함수와 다르게 composable은 vue의 composition api를 활용한 로직으로 구성된다는 stateful함을 가지고 있어 시간에 따라 변하는 상태 값을 관리하는 로직을 포함하고 있다.
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
// by convention, composable function names start with "use"
export function useMouse() {
// state encapsulated and managed by the composable
const x = ref(0)
const y = ref(0)
// a composable can update its managed state over time.
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// a composable can also hook into its owner component's
// lifecycle to setup and teardown side effects.
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// expose managed state as return value
return { x, y }
}