Vue 3 핵심 개념: Composition API와 Reactivity

2024-01-01 13:12:06
#vue#vue3#composition-api#frontend#javascript

Vue 3의 두 가지 API 스타일

Vue 3는 컴포넌트를 작성하는 두 가지 방식을 제공합니다.

Options API

Vue 2에서 사용하던 방식입니다. data, methods, computed 등 옵션별로 코드를 분리합니다.

export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  computed: {
    doubled() {
      return this.count * 2
    }
  }
}

Composition API

Vue 3에서 도입된 방식입니다. 관련 로직을 함께 모아서 작성할 수 있습니다.

import { ref, computed } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const doubled = computed(() => count.value * 2)

    function increment() {
      count.value++
    }

    return { count, doubled, increment }
  }
}

Composition API의 장점은 관련 기능을 한 곳에 모을 수 있다는 것입니다. Options API에서는 하나의 기능이 data, methods, computed에 흩어져 있지만, Composition API에서는 하나의 함수로 묶을 수 있습니다.

참고로 두 API는 한 프로젝트에서 혼용할 수 있어서, 기존 Options API 프로젝트에 Composition API를 점진적으로 도입하는 것도 가능합니다.

ref vs reactive

Vue 3를 처음 접할 때 가장 헷갈리는 게 ref와 reactive입니다.

ref()

기본값(primitive)이나 단일 값을 반응형으로 만들 때 사용합니다.

import { ref } from 'vue'

const count = ref(0)
const name = ref('Vue')
const isVisible = ref(true)

// 값 접근/변경 시 .value 필요
console.log(count.value)  // 0
count.value++

템플릿에서는 .value 없이 사용합니다.

<template>
  <p>{{ count }}</p>  <!-- .value 불필요 -->
</template>

reactive()

객체를 반응형으로 만들 때 사용합니다. .value 없이 직접 접근합니다.

import { reactive } from 'vue'

const state = reactive({
  count: 0,
  user: {
    name: 'Vue',
    age: 3
  }
})

// .value 없이 직접 접근
state.count++
state.user.name = 'Vue 3'

언제 뭘 써야 하나?

상황 추천
단일 값 (숫자, 문자열, 불리언) ref()
객체나 배열 ref() 또는 reactive()
기본값으로 ref() 사용

실무에서도 ref()를 기본으로 쓰는 편이고, Vue 공식 문서도 ref()를 기본으로 사용하는 것을 권장합니다. 이유는:

  1. ref()는 모든 타입에서 동작합니다
  2. reactive()는 구조분해 시 반응성이 깨집니다
  3. 일관성 있게 .value를 사용하면 혼란이 줄어듭니다
// reactive()의 함정: 구조분해 시 반응성 손실
const state = reactive({ count: 0 })
const { count } = state  // 반응성 손실!
count++  // UI 업데이트 안됨

// 해결: toRefs() 사용
import { toRefs } from 'vue'
const { count } = toRefs(state)  // 반응성 유지

Vue의 Reactivity 시스템

Proxy 기반 반응성

Vue 3는 JavaScript Proxy를 사용해 반응성을 구현합니다. data()가 반환하는 객체는 Proxy로 래핑됩니다.

Vue.createApp({
  data() {
    return {
      items: []  // ES6 Proxy 객체로 래핑됨
    }
  }
})

배열의 데이터를 변경하는 메서드들(push(), splice(), sort())도 함께 래핑되어, 변경 시 자동으로 UI가 업데이트됩니다.

데이터를 변경할 때는 항상 Proxy 객체를 통해서 변경해야 합니다. Vue 인스턴스 외부에서 원본 배열을 직접 변경하면 반응성이 동작하지 않습니다.

렌더링 디렉티브

텍스트 렌더링

v-text: HTML 태그를 이스케이프 처리하여 문자열로 출력합니다.

<h2 v-text="message"></h2>
<!-- 내부적으로 innerText에 연결 -->

v-html: HTML 태그를 파싱하여 렌더링합니다. XSS 공격에 주의해야 합니다.

<div v-html="htmlContent"></div>
<!-- 내부적으로 innerHTML에 연결 -->

데이터 바인딩

v-bind: 단방향 바인딩입니다. 모델 → 뷰 방향으로만 동기화됩니다.

<!-- 전체 문법 -->
<input type="text" v-bind:value="message" />
<img v-bind:src="imgPath" />

<!-- 축약 문법 -->
<input type="text" :value="message" />
<img :src="imgPath" />

v-model: 양방향 바인딩입니다. 폼 요소에서 사용합니다.

<input type="text" v-model="message" />

사용자가 입력하면 message가 자동으로 업데이트됩니다.

<!-- 체크박스, 셀렉트, 라디오에도 사용 가능 -->
<div id="app">
  <h2>개발자</h2>
  <input type="checkbox" id="devA" value="A" v-model="developer" />
  <label for="devA">찬수</label>
  <input type="checkbox" id="devB" value="B" v-model="developer" />
  <label for="devB">준영</label>

  <h3>도메인</h3>
  <select v-model="domain">
    <option value="C1">의료</option>
    <option value="C2">금융</option>
    <option value="C3">커머스</option>
  </select>
</div>

<script>
const vm = Vue.createApp({
  data() {
    return {
      developer: [],  // 체크박스는 배열로
      domain: ""      // 셀렉트는 문자열로
    }
  }
}).mount("#app")
</script>

v-model 수식어 (Modifiers)

<!-- .number: 숫자로 자동 변환 -->
<input type="text" v-model.number="age" />

<!-- .trim: 앞뒤 공백 제거 -->
<input type="text" v-model.trim="name" />

<!-- .lazy: change 이벤트 시에만 동기화 (input 아님) -->
<input type="text" v-model.lazy="message" />

<!-- 여러 개 조합 가능 -->
<input type="text" v-model.number.trim="value" />

조건부 렌더링

v-if vs v-show

<!-- v-if: 조건이 false면 DOM에서 제거 -->
<p v-if="isVisible">보임</p>
<p v-else-if="isPending">대기중</p>
<p v-else>숨김</p>

<!-- v-show: 조건이 false면 display:none -->
<p v-show="isVisible">보임</p>
특성 v-if v-show
렌더링 조건이 true일 때만 항상 렌더링
DOM 조건 변경 시 생성/제거 CSS로 숨김 처리
초기 비용 낮음 높음
토글 비용 높음 낮음
추천 상황 거의 바뀌지 않는 조건 자주 토글되는 경우

반복 렌더링 (v-for)

기본 사용법

<!-- 배열 -->
<div v-for="(item, index) in items" :key="item.id">
  {{ index }}: {{ item.name }}
</div>

<!-- 객체 -->
<div v-for="(value, key, index) in object" :key="key">
  {{ key }}: {{ value }}
</div>

key 속성의 중요성

<!-- 나쁜 예: index를 key로 사용 -->
<div v-for="(item, index) in items" :key="index">
  {{ item.name }}
</div>

<!-- 좋은 예: 고유한 ID를 key로 사용 -->
<div v-for="item in items" :key="item.id">
  {{ item.name }}
</div>

key에는 고유한 값(DB의 PK 등)을 사용해야 합니다. index를 사용하면:

  • 항목 순서가 바뀔 때 불필요한 리렌더링 발생
  • 입력 필드의 상태가 꼬일 수 있음

template으로 그룹화

<template v-for="(item, index) in items" :key="item.id">
  <tr>
    <td>{{ item.name }}</td>
    <td>{{ item.age }}</td>
  </tr>
  <hr v-show="index % 2 === 0" />
</template>

기타 유용한 디렉티브

<!-- v-pre: 컴파일하지 않고 그대로 출력 -->
<span v-pre>{{ message }}</span>
<!-- 출력: {{ message }} -->

<!-- v-once: 최초 한 번만 렌더링 -->
<span v-once>{{ timestamp }}</span>

<!-- v-cloak: 컴파일 전까지 숨김 (깜빡임 방지) -->
<style>
  [v-cloak] { display: none; }
</style>
<div id="app" v-cloak>
  {{ message }}
</div>

동적 인자 (Dynamic Arguments)

디렉티브의 인자를 동적으로 지정할 수 있습니다.

<!-- 동적 속성 바인딩 -->
<img v-bind:[attrName]="attrValue" />
<img :[attrName]="attrValue" />

<!-- 동적 이벤트 바인딩 -->
<button v-on:[eventName]="handler">클릭</button>
<button @[eventName]="handler">클릭</button>
data() {
  return {
    attrName: 'src',
    attrValue: 'https://example.com/image.png',
    eventName: 'click'
  }
}

여러 속성을 한 번에 바인딩할 수도 있습니다.

<img v-bind="imageAttrs" />
data() {
  return {
    imageAttrs: {
      src: 'https://example.com/image.png',
      alt: '이미지 설명',
      width: 200
    }
  }
}

Lifecycle Hooks

컴포넌트의 생명주기에 맞춰 로직을 실행할 수 있습니다. API 호출이나 이벤트 리스너 등록처럼 특정 시점에 실행해야 하는 코드가 있을 때 유용합니다.

Options API

export default {
  created() {
    console.log('컴포넌트 생성됨')
  },
  mounted() {
    console.log('DOM에 마운트됨')
  },
  updated() {
    console.log('데이터 변경으로 리렌더링됨')
  },
  unmounted() {
    console.log('컴포넌트 제거됨')
  }
}

Composition API

import { onMounted, onUpdated, onUnmounted } from 'vue'

export default {
  setup() {
    onMounted(() => {
      console.log('DOM에 마운트됨')
    })

    onUpdated(() => {
      console.log('리렌더링됨')
    })

    onUnmounted(() => {
      console.log('컴포넌트 제거됨')
    })
  }
}
Options API Composition API 시점
beforeCreate setup() 시작* 인스턴스 초기화 직후
created setup() 끝* 반응성 설정 완료
beforeMount onBeforeMount DOM 마운트 직전
mounted onMounted DOM 마운트 완료
beforeUpdate onBeforeUpdate 리렌더링 직전
updated onUpdated 리렌더링 완료
beforeUnmount onBeforeUnmount 언마운트 직전
unmounted onUnmounted 언마운트 완료

*Composition API의 setup()은 beforeCreate와 created 사이에 실행됩니다. 별도의 훅 함수 없이 setup() 안에서 직접 코드를 작성하면 됩니다.

Reference

프로필 이미지
@chani
바둑, 스타크래프트 등 고전 게임을 좋아하는 내향인 개발자입니다

댓글