Vue3 가이드 정리

dd_dev·2022년 9월 28일
1

# Vue는 무엇일까요?

핵심 가이드

프로그레시브 프레임워크

JAM stack 은 Javascript, Api, Markup Stack 의 약자이다.
SPA 과는 비슷하지만 다르다.

싱글 파일 컴포넌트

Vue SFC는 이름에서 알 수 있듯이 컴포넌트의 논리(JavaScript), 템플릿(HTML) 및 스타일(CSS)을 하나의 파일에 캡슐화

API 스타일

  • 옵션 API

    옵션의 data, methods 및 mounted 같은 객체를 사용하여 컴포넌트의 로직를 정의
    컴포넌트 인스턴스"(예제에서 볼 수 있는 this)의 개념을 중심으로 합니다

  • 컴포지션 API

    import해서 가져온 API 함수들을 사용하여 컴포넌트의 로직를 정의
    컴포지션 API는 일반적으로 <script setup> 과 함께 사용
    함수 범위에서 직접 반응형 변수를 선언하고 복잡성을 처리하기 위해 여러 함수의 상태를 함께 구성하는데 중점을 둡니다

  • 무엇을 선택해야 할까요?

Vue 앱 만들기

앱 인스턴스

모든 Vue 앱은 createApp 함수를 사용하여 새로운 앱 인스턴스를 생성하는 것으로 시작

앱 환경설정

app.config.errorHandler = (err) => {
/* 에러 처리 */
}

템플릿 문법

https://v3-docs.vuejs-korea.org/guide/essentials/template-syntax.html
렌더링된 DOM에 바인딩할 수 있는 HTML 기반 템플릿 문법을 사용
가상 DOM 개념에 익숙하고 JavaScript의 원시적인 작동법을 선호하는 경우, JSX 지원 옵션을 사용하여 템플릿 대신 렌더링 함수를 직접 작성할 수도 있습니다

제한된 전역 접근

const GLOBALS_WHITE_LISTED =
'Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,' +
'decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,' +
'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt'

app.config.globalProperties에 추가하여, Vue 내부의 모든 표현식에서 전역 속성에 접근 할 수 있도록 명시적으로 정의할 수 있습니다

동적인 인자

디렉티브의 인자를 대괄호로 감싸서 JavaScript 표현식으로 사용할 수도 있습니다:

<a :[attributeName]="url"> ... </a>
<a @[eventName]="doSomething">
<!-- 이렇게 하면 컴파일러 경고가 트리거됩니다. -->
<a :['foo' + bar]="value"> ... </a>
<!-- computed 속성을 사용하는 것이 더 나을 것입니다 -->

수식어

.prevent 수식어는 트리거된 이벤트에서 event.preventDefault()를 호출하도록 v-on 디렉티브에 지시

반응형 기초

data에 포함하지 않고 this에 직접 새 속성을 추가할 수는 있습니다. 그러나 이러한 방식으로 추가된 속성은 이후 반응형 업데이트 동작이 이루어지지 않습니다.

기본 제공되는 API를 노출할 때 $ 접두사를 사용
내부 속성에 대해서는 _ 접두사를 사용

반응형 재정의 vs 원본

Vue 3에서는 JavaScript Proxy를 활용하여 데이터를 반응형으로 만듭니다
원본을 반응형으로 재정의한 프락시 객체
Vue 2와 달리 원본 newObject 객체는 그대로 유지, 반응형으로 변하지 않습니다

DOM 업데이트 시기

DOM 업데이트는 동기적으로 적용되지 않는다
DOM 업데이트가 완료될 때까지 기다리려면 nextTick() 전역 API를 사용할 수 있습니다:

깊은 반응형

중첩된 객체나 배열을 변경할 때에도 변경 사항이 감지됩니다:

계산된 속성

계산된 캐싱 vs 메서드

표현식에서 메서드를 호출하여 동일한 결과를 얻을 수도 있습니다:
차이점은 계산된 속성은 의존된 반응형을 기반으로 캐시된다는 점
메서드 호출은 리렌더링이 발생할 때마다 항상 함수를 실행합니다.

수정 가능한 계산된 속성

드물게 "수정 가능한" 계산된 속성이 필요한 경우, getter와 setter를 모두 제공하여 속성을 만들 수 있습니다.

모범 사례

  • getter에서 사이드 이펙트는 금물

    getter에서 비동기 요청을 하거나 DOM을 변경하면 안됩니다!

  • 계산된 값을 변경하지 마십시오

    임시 스냅샷으로 생각하십시오.

클래스와 스타일 바인딩

Vue는 v-bind가 class 및 style과 함께 사용될 때 특별한 향상을 제공

HTML 클래스 바인딩

  • 객체로 바인딩 하기

    <div :class="{ active: isActive }"></div>

    객체를 반환하는 계산된 속성으로 바인딩할 수도 있습니다.

  • 배열로 바인딩 하기

    <div :class="[isActive ? activeClass : '', errorClass]"></div>
    <div :class="[{ active: isActive }, errorClass]"></div>
  • 컴포넌트에서 사용하기

    최상위(root) 엘리먼트가 하나로 구성된 컴포넌트에서 class 속성을 사용하면, 해당 클래스가 컴포넌트의 루트 엘리먼트에 이미 정의된 기존 클래스와 병합되어 추가됩니다.

    여러 개의 최상위 엘리먼트로 컴포넌트가 구성되어 있는 경우, 클래스를 적용할 엘리먼트를 정의해야 합니다. $attrs 컴포넌트 속성을 사용하여 이 작업을 수행할 수 있습니다.

    <!-- MyComponent 템플릿에서 $attrs 속성을 사용 -->
    <p :class="$attrs.class">안녕!</p>
    <span>반가워!</span>

인라인 스타일 바인딩

:style에 사용될 CSS 속성에 해당하는 키 문자열은 camelCase가 권장되지만, kebab-cased(실제 CSS에서 사용되는 방식)도 지원

  • 배열로 바인딩 하기

    <div :style="[baseStyles, overridingStyles]"></div>
  • 접두사 자동완성

    CSS 속성이 :style에 사용되면, 자동으로 해당 속성과 벤더 접두사가 조합된 여러 개의 특수한 속성을 테스트하고 지원되는 속성을 찾아서 추가합니다.

  • 다중 값

    스타일 속성에 다중 값을 배열로 제공할 수 있습니다. 예를 들면 다음과 같습니다:

    <div :style="{ display: ['flex', '-webkit-box', '-ms-flexbox'] }"></div>

    브라우저가 지원하는 배열 내 마지막 값을 렌더링합니다.

조건부 렌더링

v-if vs v-show

v-if는 "실제" 조건부 렌더링입니다. 왜냐하면 조건부 블록이 전환될 경우, 블록 내 이벤트 리스너와 자식 컴포넌트가 제대로 제거되거나 재생성되기 때문입니다.

게으르므로(lazy), 초기 조건이 false면 아무 작업도 수행하지 않습니다. 조건부 블록은 조건이 true가 될 때까지 렌더링되지 않습니다.

v-show는 훨씬 간단합니다. 엘리먼트는 CSS 기반으로 전환 되므로, 초기 조건과 관계없이 항상 렌더링 됩니다.

v-if는 전환 비용이 더 높고, v-show는 초기 렌더링 비용이 더 높습니다.

v-if with v-for

v-if와 v-for를 함께 사용하는 것은 권장되지 않습니다.
v-if와 v-for를 함께 사용하면 v-if가 먼저 평가

리스트 렌더링

in 대신 of를 구분 기호로 사용하여 JavaScript 반복문 문법처럼 사용할 수도 있습니다:

<div v-for="item of items"></div>

v-for의 아이템도 분해 할당해 사용할 수 있습니다:

<!-- index 에일리어스도 사용 -->
<li v-for="({ message }, index) in items">
  {{ message }} {{ index }}
</li>

객체에 v-for 사용하기

Object.keys()를 호출한 결과에 기반

<li v-for="(value, key, index) in myObject">
  {{ index }}. {{ key }}: {{ value }}
</li>

숫자 범위에 v-for 사용하기

이 경우 1...n 범위를 기준으로 템플릿을 여러 번 반복합니다.
여기서 n의 값은 0이 아니라 1부터 시작합니다.

v-if에 v-for 사용하기

이것들이 같은 노드에 존재할 때 v-if가 v-for보다 우선순위가 높기 때문에 v-if 조건문에서 v-for 변수에 접근할 수 없습니다:

key를 통한 상태유지

기본적으로 "in-place patch" 전략
리스트 렌더링 출력이 자식 컴포넌트 상태 또는 임시 DOM 상태(예: 양식 입력 값)에 의존하지 않는 경우에만 유효합니다.
<template v-for>를 사용할 때 key는 <template> 컨테이너에 있어야 합니다.
가능한 한 언제나 v-for는 key 속성과 함께 사용하는 것을 권장합니다.
key에는 문자열, 숫자, 심볼 형식의 값만 바인딩해야 합니다

배열 변경 감지

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

    계산된 속성에서 reverse()와 sort() 사용에 주의하십시오!

배열 교체

filter(), concat() 및 slice()
이전 배열을 다른 배열로 바꾸는 과정에서 서로 중복되는 객체를 가지는 부분을 매우 효율적으로 처리

이벤트 핸들링

인라인 핸들러에서 이벤트 객체 접근하기

<!-- 특수한 키워드인 $event 사용 -->
<button @click="warn('아직 양식을 제출할 수 없습니다.', $event)">
  제출하기
</button>

<!-- 인라인 화살표 함수 사용 -->
<button @click="(event) => warn('아직 양식을 제출할 수 없습니다.', event)">
  제출하기
</button>

이벤트 수식어

  • .stop
  • .prevent
  • .self
  • .capture
  • .once
  • .passive
<!-- 수식어를 연결할 수 있습니다. -->
<a @click.stop.prevent="doThat"></a>

<!-- 이벤트에 핸들러 없이 수식어만 사용할 수 있습니다. -->
<form @submit.prevent></form>

<!-- event.target이 엘리먼트 자신일 경우에만 핸들러가 실행됩니다. -->
<!-- 예를 들어 자식 엘리먼트에서 클릭 액션이 있으면 핸들러가 실행되지 않습니다. -->
<div @click.self="doThat">...</div>

수식어를 명시한 순서대로 관련 코드가 생성되기 때문에 수식어를 사용할 때 순서가 중요합니다.
따라서 @click.prevent.self를 사용하면 해당 엘리먼트 및 자식 엘리먼트가 클릭 되는 경우 실행되는 기본 동작을 방지합니다.
반면, @click.self.prevent는 해당 엘리먼트가 클릭 되는 경우에만 실행되는 기본 동작을 방지합니다.

.capture, .once 및 .passive 수식어는 네이티브 addEventListener 메서드의 옵션을 반영합니다:

<!-- 이벤트 리스너를 추가할 때 캡처 모드 사용 -->
<!-- 내부 엘리먼트에서 클릭 이벤트 핸들러가 실행되기 전에, 여기에서 먼저 핸들러가 실행됩니다. -->
<div @click.capture="doThis">...</div>

<!-- 클릭 이벤트는 단 한 번만 실행됩니다. -->
<a @click.once="doThis"></a>

<!-- 핸들러 내 `event.preventDefault()`가 포함되었더라도 -->
<!-- 스크롤 이벤트의 기본 동작(스크롤)이 발생합니다.        -->
<div @scroll.passive="onScroll">...</div>

.passive 수식어는 일반적으로 모바일 장치의 성능 향상을 위해 터치 이벤트 리스너와 함께 사용됩니다.

입력키 수식어

KeyboardEvent.key를 통해 유효한 입력키 이름을 kebab-case로 변환하여 수식어로 사용할 수 있습니다:

  • .enter
  • .tab
  • .delete ("Delete" 및 "Backspace" 키 모두 캡처)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

시스템 입력키 수식어

  • .ctrl
  • .alt
  • .shift
  • .meta

    eyup 이벤트와 함께 사용되는 경우, 키가 눌려저 있어야 이벤트가 발생합니다

.exact 수식어

정확한 조합을 제어할 수 있습니다.

마우스 버튼 수식어

  • .left
  • .right
  • .middle

Form 입력 바인딩

checkedNames 배열은 항상 현재 체크된 순서대로 값을 포함합니다.
select option 처리시, iOS에서는 이 경우 변경 이벤트를 발생시키지 않기 때문에 비활성화된 옵션에 빈 값을 제공하는 것이 좋습니다. value=""
다중 선택(배열로 바인딩 됨)
v-model은 문자열이 아닌 값의 바인딩도 지원합니다!

수식어

v-model은 각 input 이벤트 이후 데이터와 입력을 동기화합니다
대신 change 이벤트 이후에 동기화하기 위해 .lazy 수식어를 추가할 수 있습니다.
사용자 입력이 자동으로 숫자로 유형 변환되도록 하려면, v-model 수식어로 .number를 추가하면 됩니다:
값을 parseFloat()로 파싱할 수 없으면 원래 값이 대신 사용됩니다.
자동으로 트리밍되도록 하려면 v-model 수식어로 .trim을 추가

수명 주기 훅

감시자

옵션 API를 사용하는 경우, watch 옵션을 사용하여 반응형 속성이 변경될 때마다 함수를 실행
Composition API를 사용하는 경우, watch 함수를 사용하여 반응형 속성이 변경될 때마다 함수를 실행할 수 있습니다:

깊은 감시자

watch는 기본적으로 얕습니다.

  watch: {
    someObject: {
      handler(newValue, oldValue) {
        // 참고:
        // someObject가 다른 객체로 교체되지 않는 한,
        // newValue와 oldValue는 같습니다.
        // 둘 다 동일한 객체를 참고하고 있기 때문입니다!
      },
      deep: true
    }
  }

열성적인 감시자

handler 함수와 immediate: true 옵션으로 구성된 객체를 사용해 감시자를 선언함으로써 콜백이 즉시 실행되도록 할 수 있습니다:

콜백 실행 타이밍

개발자가 생성한 감시자 콜백은 Vue 컴포넌트가 업데이트되기 전에 실행
Vue에 의해 업데이트된 후 의 DOM을 감시자 콜백에서 접근하려면, flush: 'post' 옵션을 지정해야 합니다:
동일한 틱 내에 여러 번 상태 변경 시 마다 동기적으로 콜백을 호출해야 하는 경우, flush: 'sync' 옵션을 사용해야 합니다.

this.$watch()

  created() {
    this.$watch('question', (newQuestion) => {
      // ...
    })
  }

감시자 중지하기

컴포넌트가 마운트 해제될 때 자동으로 중지
마운트 해제되기 전에 감시자를 중지해야 하는 경우

const unwatch = this.$watch('foo', callback)

// ...나중에 감시자가 더 이상 필요하지 않을 때:
unwatch()

템플릿 참조

ref 값은 this.$refs에 노출됩니다:
ref가 v-for 내부에서 사용되면 ref 값은 해당 엘리먼트를 포함하는 배열이 됩니다:

함수로 참조하기

<input :ref="(el) => { /* el을 속성이나 ref에 할당 */ }">

엘리먼트가 마운트 해제되는 경우 인자는 null입니다.

컴포넌트에 ref 사용하기

ref는 자식 컴포넌트에 사용할 수도 있습니다. 이 경우 ref는 컴포넌트 인스턴스를 참조합니다:
자식 컴포넌트의 this와 동일
expose 옵션을 사용하여 하위 인스턴스에 대한 접근을 제한할 수 있습니다:

컴포넌트 기초

앱이 중첩된 컴포넌트의 트리로 구성되는 것은 일반적입니다:

  • Props 전달하기
  • emits

슬롯이 있는 컨텐츠 배포

<template>
  <div class="alert-box">
    <strong>이것은 데모용 에러입니다.</strong>
    <slot />
  </div>
</template>

동적 컴포넌트

<!-- currentTab이 변경되면 컴포넌트가 변경됩니다 -->
<component :is="currentTab"></component>
  • 등록된 컴포넌트의 이름 문자열
  • 실제 가져온 컴포넌트 객체

    내장된 <KeepAlive> 컴포넌트를 사용하여 비활성 컴포넌트를 "활성" 상태로 유지하도록 강제할 수 있습니다.

DOM 템플릿 파싱 주의 사항

다음 소스의 문자열 템플릿을 사용하는 경우에는 적용되지 않습니다:

  • 싱글 파일 컴포넌트(SFC)
  • 인라인 템플릿 문자열(예: template: '...')
  • <script type="text/x-template">

# 컴포넌트 심화

컴포넌트 등록

전역 등록

pp
  .component('ComponentA', ComponentA)
  .component('ComponentB', ComponentB)
  .component('ComponentC', ComponentC)
  • 컴포넌트를 제거하는 것(일명 "tree-shaking")을 방지합니다
  • 대규모 앱에서 의존 관계를 덜 명확하게 만듭니다

로컬 등록

export default {
  components: {
    ComponentA
  }
}

컴포넌트 이름 표기법

PascalCase 이름은 유효한 JavaScript 식별자입니다. 이렇게 하면 JavaScript에서 컴포넌트를 더 쉽게 가져오고 등록할 수 있습니다. IDE의 자동 완성 기능도 지원합니다.
<PascalCase />는 기본 HTML의 템플릿 엘리먼트가 아닌 Vue 컴포넌트임을 더 명확하게 합니다. 또한 Vue 컴포넌트를 사용자 정의 엘리먼트(웹 컴포넌트)와 구별합니다.
PascalCase를 사용하여 등록된 컴포넌트에 대한 kebab-case 태그 해석을 지원
MyComponent로 등록된 컴포넌트가 <MyComponent> 또는 <my-component>를 통해 템플릿에서 참조될 수 있음

Props

Props 이름 케이싱

obj.camelCase와 같이 camelCase를 사용
camelCase로 선언된 props 속성일지라도 관례적으로 HTML 속성 표기법과 동일하게 kebab-case로 표기해서 사용하도록 해야 합니다
<MyComponent greeting-message="안녕!" />
어떠한 타입의 값도 prop로 전달할 수 있습니다

객체로 여러 속성 바인딩하기

객체의 모든 속성을 props로 전달하려면 인자 없이 v-bind를 사용할 수 있습니다

<BlogPost v-bind="post" />
<BlogPost :id="post.id" :title="post.title" />

단방향 데이터 흐름

모든 props는 자식 속성과 부모 속성 사이에 하향식 단방향 바인딩을 형성
부모 컴포넌트가 업데이트될 때마다 자식 컴포넌트의 모든 props가 최신 값으로 업데이트

객체/배열 props 변경에 관하여

자식 컴포넌트는 바인딩된 prop을 변경할 수는 없지만, 객체 또는 배열의 중첩 속성을 변경할 수는 있습니다
가장 좋은 방법은 부모와 자식이 의도적으로 밀접하게 연결되어 있지 않는 한 이러한 변경을 피하는 것이며, 필요 시 자식은 부모가 변경을 수행할 수 있도록 emit 이벤트를 호출하는 방식으로 구현해야 합니다

Prop 유효성 검사

    propE: {
      type: Object,
      // 객체 또는 배열 기본값은 팩토리 함수에서 반환되어야 합니다.
      // 함수는 컴포넌트에서 받은 rawProps를 인자로 받습니다.
      // (rawProps: 부모 컴포넌트에게 받은 props 전체 객체)
      default(rawProps) {
        return { message: '안녕!' }
      }
    },
    // 사용자 정의 유효성 검사 함수
    propF: {
      validator(value) {
        // 값은 다음 문자열 중 하나와 일치해야 합니다.
        return ['성공', '경고', '위험'].includes(value)
      }
    },

prop의 타입이 Boolean이고 누락된 경우, false가 기본값이 됩니다. 의도에 따라서 default 값 정의가 필요할 수 있습니다.

실행 간 타입 체크

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol
export default {
  props: {
    author: Person
  }
}
class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}

author prop 값이 new Person으로 생성되었는지 확인합니다.

불리언 캐스팅

<!-- :disabled="true" 같음 -->
<MyComponent disabled />

<!-- :disabled="false" 같음 -->
<MyComponent />

예를 들어 prop이 여러 타입을 가질 수 있게 선언되었다면:
Boolean에 대한 캐스팅 규칙은 타입 선언 순서에 관계없이 적용됩니다.

컴포넌트 이벤트

.once 수식어는 컴포넌트 이벤트 리스너에서도 지원됩니다.

<MyComponent @some-event.once="callback" />

camelCase 형식으로 이벤트를 발신했지만, 부모에서 kebab-case 표기로 리스너를 사용하여 이를 수신할 수 있습니다

이벤트 인자

emit()에서이벤트이름뒤에전달된모든추가인자는리스너로전달됩니다.예를들어,emit()에서 이벤트 이름 뒤에 전달된 모든 추가 인자는 리스너로 전달됩니다. 예를 들어, `emit('foo', 1, 2, 3)을 사용하면 리스너 함수는 세 개의 인자`를 받습니다. \

발신되는 이벤트 선언하기

export default {
  emits: ['inFocus', 'submit']
}
export default {
  emits: ['inFocus', 'submit'],
  setup(props, { emit }) {
    emit('submit')
  }
}
export default {
  emits: {
    submit(payload) {
      // `true` 또는 `false` 값을 반환하여
      // 유효성 검사 통과/실패 여부를 알려줌

      // 페이로드는 전달되는 인자를 나타내는 것으로
      // `emit('submit', 'a', 'b', 'c')`와 같이 3개의 인자를 전달하는 경우,
      // `submit(pl1, pl2, pl3) { /* 유효성 검사 반환 로직 */ }`과 같이
      // 유효성 검사에 페이로드를 사용할 수 있습니다.
    }
  }
}

네이티브 이벤트(예: click)가 emits 옵션에 정의된 경우 리스너는 이제 컴포넌트에서 발생하는 click 이벤트만 수신 대기하고 네이티브 click 이벤트에 더 이상 응답하지 않습니다.

v-model과 함께 사용하기

컴포넌트에서 사용될 때 v-model은 대신 다음을 수행합니다:

<CustomInput
  :modelValue="searchText"
  @update:modelValue="newValue => searchText = newValue"
/>
<CustomInput v-model="searchText" />

<!-- child -->
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>

computed 속성을 사용

<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
}
</script>

<template>
  <input v-model="value" />
</template>

v-model 인자 변경

<MyComponent v-model:title="bookTitle" />

<script>
export default {
  props: ['title'],
  emits: ['update:title']
}
</script>

v-model 다중 바인딩

<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>

v-model 수식어 핸들링

<MyComponent v-model:title.capitalize="myText">

<script>
export default {
  props: {
    title: String,
    titleModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:title'],
  created() {
    console.log(this.titleModifiers) // { capitalize: true }
  },
  methods: {
    emitValue(e) {
      let value = e.target.value
      if (this.titleModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1)
      }
      this.$emit('update:title', value)
    }
  }
}
</script>

폴스루 속성

속성 상속

속성 또는 v-on 이벤트 리스너 이지만, 이것을 받는 컴포넌트의 props 또는 emits에서 명시적으로 선언되지 않은 속성

class, style, id

v-on 리스너 상속

<button>에 이미 v-on으로 바인딩된 click 리스너가 있는 경우 부모/자식 두 리스너가 모두 트리거됩니다

중첩된 컴포넌트 상속

전달되는 속성이 <MyButton>에서 props로 선언되었거나 emit으로 등록된 핸들러일 경우, <BaseButton>으로 전달되지 않습니다. <MyButton> 에서 명시적으로 선언되어 "사로잡혔기(consumed)" 때문입니다.
그러나 전달되는 속성이 <MyButton>의 템플릿에서 다시 선언된 경우, props나 핸들러로 허용될 수 있습니다.

속성 상속 비활성화

inheritAttrs: false

<span>폴스루 속성: {{ $attrs }}</span>

$attrs 객체에는 컴포넌트의 props 또는 emits 옵션으로 선언되지 않은 모든 속성(예: class, style, v-on 리스너 등)이 포함됩니다.

class 및 v-on 리스너와 같은 폴스루 속성이 외부 <div>가 아닌 내부 <button>에 적용되기를 원할 수 있습니다. inheritAttrs: false로 설정하고, v-bind="$attrs"로 이를 구현할 수 있습니다:

<div class="btn-wrapper">
  <button class="btn" v-bind="$attrs">클릭하기</button>
</div>

다중 루트 노드에서 속성 상속

$attrs가 명시적으로 바인딩된 경우, 경고가 표시되지 않습니다:

<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>

슬롯

슬롯 컨텐츠는 자식 컴포넌트의 데이터에 접근할 수 없습니다. 일반적으로 다음을 기억하십시오:

상위 템플릿의 모든 항목은 상위 범위에서 컴파일됩니다. 자식 템플릿의 모든 것은 자식 범위에서 컴파일됩니다.

대체 컨텐츠

<button type="submit">
  <slot>
    제출 <!-- 대체 컨텐츠 -->
  </slot>
</button>

이름이 있는 슬롯

<BaseLayout>
  <template #header>
    <h1>다음은 페이지 제목일 수 있습니다.</h1>
  </template>

  <!-- implicit default slot -->
  <p>주요 내용에 대한 단락입니다.</p>
  <p>그리고 또 하나.</p>

  <template #footer>
    <p>다음은 연락처 정보입니다.</p>
  </template>

  <!-- 동적 슬롯, 단축 문법 사용 -->
  <template #[dynamicSlotName]>
</BaseLayout>

범위가 지정된 슬롯

props를 컴포넌트에 전달하는 것처럼 속성을 슬롯 아울렛에 전달할 수 있습니다:

<!-- <MyComponent> 템플릿 -->
<div>
 <slot :text="greetingMessage" :count="1"></slot>
</div>

<MyComponent v-slot="{ text, count }">
 {{ text }} {{ count }}
</MyComponent>

슬롯의 name은 예약되어 있기 때문에 props에 포함되지 않습니다.

렌더리스 컴포넌트

로직을 포함하기만 하고 자체적으로 아무 것도 렌더링하지 않는 컴포넌트를 생각해낼 수 있습니다. 시각적 출력은 범위가 지정된 슬롯인 사용될 컴포넌트에 완전히 위임됩니다. 이러한 유형의 컴포넌트를 렌더리스 컴포넌트라고 합니다.

Provide(제공) / Inject(주입)

먼 조상 컴포넌트의 무언가가 필요한 경우
하위 트리의 모든 컴포넌트는 깊이에 관계없이 상위 체인의 컴포넌트에서 제공(provide)하는 의존성을 주입(inject)할 수 있습니다.

Provide

export default {
  data() {
    return {
      message: '안녕!'
    }
  },
  provide() {
    // 함수 구문을 사용하여 `this`에 접근할 수 있습니다.
    return {
      message: this.message
    }
  }
}

이것이 주입된 값을 반응형으로 만들지 않습니다.

앱 수준의 provide

import { createApp } from 'vue'

const app = createApp({})

app.provide(/* 키 */ 'message', /* 값 */ '안녕!')

Inject

export default {
  inject: ['message'],
  inject: {
    /* 로컬 키 */ localMessage: {
      from: /* 주입된 키 */ 'message',
      default: '이것은 기본 값 문자열 입니다.'
    },
    user: {
      // 생성하는 데 비용이 많이 드는 기본이 아닌 값 또는 컴포넌트 인스턴스마다
      // 고유해야 하는 값에 대해 팩토리 함수를 사용합니다.
      default: () => ({ name: '철수' })
    }
  }
  data() {
    return {
      // 주입된 값을 기반으로 하는 초기 데이터
      fullMessage: this.message
    }
  }
}

반응형으로 만들기

import { computed } from 'vue'

export default {
  data() {
    return {
      message: '안녕!'
    }
  },
  provide() {
    return {
      // 계산된 속성을 명시적으로 제공
      message: computed(() => this.message)
    }
  }
}

비동기 컴포넌트

필요할 때만 서버에서 컴포넌트를 로드

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...서버에서 컴포넌트를 로드하는 로직
    resolve(/* 로드 된 컴포넌트 */)
  })
})
// ... 일반 컴포넌트처럼 `AsyncComp`를 사용
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)
/* 컴포넌트 */
app.component('MyComponent', defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
))

로딩 및 에러 상태

const AsyncComp = defineAsyncComponent({
  // 로더 함수
  loader: () => import('./Foo.vue'),

  // 비동기 컴포넌트가 로드되는 동안 사용할 로딩 컴포넌트입니다.
  loadingComponent: LoadingComponent,
  // 로딩 컴포넌트를 표시하기 전에 지연할 시간. 기본값: 200ms
  delay: 200,

  // 로드 실패 시 사용할 에러 컴포넌트
  errorComponent: ErrorComponent,
  // 시간 초과 시, 에러 컴포넌트가 표시됩니다. 기본값: 무한대
  timeout: 3000
})

지연(suspense) 사용하기

비동기 컴포넌트는 내장 컴포넌트인 <Suspense>와 함께 사용할 수 있습니다.

재사용성

컴포저블

Vue 앱의 컨텍스트에서 컴포저블은 Vue 컴포지션 API를 활용하여 상태 저장 로직를 캡슐화하고 재사용하는 함수입니다.

마우스 위치 추적기 예제

// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// 관례상, 컴포저블 함수 이름은 "use"로 시작합니다.
export function useMouse() {
  // 컴포저블로 캡슐화된 내부에서 관리되는 상태
  const x = ref(0)
  const y = ref(0)

  // 컴포저블은 시간이 지남에 따라 관리되는 상태를 업데이트할 수 있습니다.
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  // 컴포저블은 또한 이것을 사용하는 컴포넌트의 수명주기에 연결되어
  // 사이드 이펙트를 설정 및 해제할 수 있습니다.
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // 관리 상태를 반환 값으로 노출
  return { x, y }
}
<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>마우스 위치: {{ x }}, {{ y }}</template>

컴포넌트 내부처럼 컴포저블에서는 컴포지션 API 함수의 전체를 사용할 수 있습니다
하지만 컴포저블의 멋진 부분은 중첩할 수도 있다는 것입니다. 하나의 컴포저블 함수는 하나 이상의 "다른 컴포저블 함수"를 호출할 수 있습니다.
각 컴포넌트 인스턴스는 서로 간섭하지 않도록 x와 y 상태의 자체 복사본을 생성합니다

// fetch.js
import { ref, isRef, unref, watchEffect } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  function doFetch() {
    // 가져오기 전에 상태 재설정..
    data.value = null
    error.value = null
    // unref()는 ref의 래핑을 해제합니다.
    fetch(unref(url))
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  }

  if (isRef(url)) {
    // 설정하기: 입력 URL이 ref인 경우 반응적 다시 가져오기
    watchEffect(doFetch)
  } else {
    // 그렇지 않으면 한 번만 가져 와서
    // 감시자의 오버 헤드를 피하합니다.
    doFetch()
  }

  return { data, error }
}

관례와 모범 사례

"use"로 시작하는 camelCase 이름으로 컴포저블 함수의 이름을 지정하는 것이 관례입니다.

입력 인자

import { unref } from 'vue'

function useFeature(maybeRef) {
  // 만약 mayRef가 실제로 ref라면,
  // mayRef.value가 반환될 것이고,
  // 그렇지 않을 경우, mayRef는 있는 그대로 반환될 것입니다.
  const value = unref(maybeRef)
}

입력이 ref일 때 컴포저블이 반응 효과를 생성하는 경우, watch()를 사용하여 ref를 명시적으로 감시하거나, watchEffect() 내부에서 unref()를 호출하여 올바르게 추적되도록 하십시오.

반환 값

컴포넌트에서 항상 ref 객체를 반환하여 반응성을 유지하면서 컴포넌트에서 구조화할 수 있도록 하는 것이 좋습니다
컴포저블 함수로 반환된 ref 상태를 감싸는 객체를 .value 접두사 없이 객체 속성처럼 사용하고 싶다면, 반환된 일반 객체를 reactive()로 래핑하여 ref가 래핑되지 않도록 할 수 있습니다:

const mouse = reactive(useMouse())
// mouse.x는 원본 ref에 연결되어 있습니다.
console.log(mouse.x)

사이드 이펙트

서버 사이드 렌더링(SSR)을 사용하는 앱에서 작업하는 경우, 마운트 후 수명주기 훅인 onMounted()에서 DOM 관련 사이드 이펙트를 수행해야 합니다. 이 훅은 브라우저에서만 호출되므로 내부 코드가 DOM에 접근할 수 있는지 알 수 있습니다.

onUnmounted()에서 사이드 이펙트를 마무리 지었는지 확인하십시오. 예를 들어, 컴포저블 코드에서 DOM 이벤트 리스너를 사용하는 경우, onUnmounted()에서 해당 리스너를 제거해야 합니다(useMouse() 예제에서 본 것처럼). useEventListener() 예제와 같이 자동으로 이를 수행하는 컴포저블 코드를 구성하는 것도 좋은 아이디어일 수 있습니다.

제한사항

컴포저블은 <script setup> 또는 setup() 훅에서만 동기적으로 호출되어야 합니다. 어떤 경우에는 onMounted()와 같은 수명주기 훅에서 호출할 수도 있습니다.
<script setup>은 await를 사용한 후 컴포저블 함수를 호출할 수 있는 유일한 곳입니다.

체계적인 코드를 위해 컴포저블로 추출하기

컴포지션 API는 논리적 문제를 기반으로 컴포넌트 코드를 더 작은 기능으로 구성할 수 있는 완전한 유연성을 제공

옵션 API에서 컴포저블 적용

컴포저블 함수는 setup() 내에서 호출되어야 하고 반환된 바인딩은 this와 템플릿에 노출되도록 setup()에서 반환되어야 합니다:

import { useMouse } from './mouse.js'
import { useFetch } from './fetch.js'

export default {
  setup() {
    const { x, y } = useMouse()
    const { data, error } = useFetch('...')
    return { x, y, data, error }
  },
  mounted() {
    // setup()에서 노출된 속성은 `this`에서 접근할 수 있습니다.
    console.log(this.x)
  }
  // ...다른 옵션 코드 로직
}

다른 기술과의 비교

vs. Mixins

  1. 어떤 인스턴스 속성이 어떤 mixins에 의해 주입되는지 명확하지 않아 구현을 추적하고 컴포넌트의 동작을 이해하기 어렵습니다
  2. 여러 mixins이 잠재적으로 동일한 속성 키를 등록하여 네임스페이스 충돌을 일으킬 수 있습니다.
  3. 여러 mixins은 공유 속성 키에 의존해야 하므로 암시적으로 결합됩니다.

    Vue 3에서는 더 이상 mixins를 사용하지 않는 것이 좋습니다

vs. 렌더리스 컴포넌트

렌더리스 컴포넌트에 비해 컴포저블의 주요 이점은 컴포저블이 추가적인 컴포넌트 인스턴스 오버헤드를 발생시키지 않는다는 것입니다.

권장 사항은 순수 로직을 재사용할 때 컴포저블을 사용하고 로직과 시각적 레이아웃을 모두 재사용할 때 컴포넌트를 사용하는 것입니다.

vs. React 훅

컴포지션 API는 부분적으로 React 훅에서 영감을 얻었고 Vue 컴포저블은 실제로 로직 구성 기능 측면에서 React 훅와 유사합니다.

추가적인 읽을거리

  • 반응형 심화: Vue의 반응형 시스템이 어떻게 작동하는지에 대한 심화 수준의 이해를 위해.
  • 상태 관리: 여러 컴포넌트가 공유하는 상태를 관리하는 패턴입니다.
  • 테스팅 컴포저블: 단위 테스트 컴포저블에 대한 팁.
  • VueUse: 계속 증가하는 Vue 컴포저블 컬렉션입니다. 또한 소스 코드는 훌륭한 학습 자료입니다.

커스텀 디렉티브

커스텀 디렉티브는 원하는 기능을 직접 DOM 조작을 통해서만 달성할 수 있는 경우에만 사용해야 합니다.

우리는 Vue에서 컴포넌트 기초와 컴포저블이라는 두 가지 형태의 코드 재사용을 도입했습니다. 컴포넌트는 주요 구성-요소(building-block)이고, 컴포저블은 상태 저장 로직을 재사용하는 데 중점을 둡니다
커스텀 디렉티브는 주로 일반 엘리먼트에 대한 저수준(low-level) DOM 접근과 관련된 로직을 재사용하기 위한 것입니다.

<script setup>에서 v 접두사로 시작하는 모든 camelCase 변수를 커스텀 디렉티브로 사용할 수 있습니다. 위의 예에서 vFocus는 템플릿에서 v-focus로 사용할 수 있습니다.

<script setup>을 사용하지 않는 경우, directives 옵션을 사용하여 커스텀 디렉티브를 등록할 수 있습니다:

앱 수준에서 커스텀 디렉티브를 전역적으로 등록하는 것도 일반적입니다:

const app = createApp({})

// 모든 컴포넌트에서 v-focus를 사용할 수 있도록 합니다.
app.directive('focus', {
  /* ... */
})

디렉티브 훅

const myDirective = {
  // 바인딩된 엘리먼트의 속성 또는
  // 이벤트 리스너가 적용되기 전에 호출됩니다.
  created(el, binding, vnode, prevVnode) {
    // 인자에 대한 자세한 내용은 아래를 참고.
  },
  // 엘리먼트가 DOM에 삽입되기 직전에 호출됩니다.
  beforeMount(el, binding, vnode, prevVnode) {},
  // 바인딩된 엘리먼트의 부모 컴포넌트 및
  // 모든 자식 컴포넌트의 mounted 이후에 호출됩니다.
  mounted(el, binding, vnode, prevVnode) {},
  // 부모 컴포넌트의 updated 전에 호출됩니다.
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 바인딩된 엘리먼트의 부모 컴포넌트 및
  // 모든 자식 컴포넌트의 updated 이후에 호출됩니다.
  updated(el, binding, vnode, prevVnode) {},
  // 부모 컴포넌트의 beforeUnmount 이후에 호출됩니다.
  beforeUnmount(el, binding, vnode, prevVnode) {},
  // 부모 컴포넌트의 unmounted 전에 호출됩니다.
  unmounted(el, binding, vnode, prevVnode) {}
}

훅 인자

  • el: 디렉티브가 바인딩된 엘리먼트입니다. DOM을 직접 조작하는 데 사용할 수 있습니다.
  • binding: 다음 속성을 포함하는 객체입니다.
    • value: 디렉티브에 전달된 값입니다. 예를 들어 v-my-directive="1 + 1"에서 value는 2입니다.
    • oldValue: 이것은 beforeUpdate 및 updated에서만 사용할 수 있습니다. 값이 변경되었는지 여부에 관계없이 사용 가능합니다.
    • arg: 디렉티브에 전달된 인자(있는 경우). 예를 들어 v-my-directive:foo에서 인자는 "foo"입니다.
    • modifiers: 수식어가 있는 경우 수식어를 포함하는 객체입니다. 예를 들어 v-my-directive.foo.bar에서 수식어 객체는 { foo: true, bar: true }입니다.
    • instance: 디렉티브가 사용되는 컴포넌트의 인스턴스입니다.
    • dir: 디렉티브를 정의하는 객체
  • vnode: 바인딩된 엘리먼트를 나타내는 기본 VNode.
  • prevNode: 이전 렌더링에서 바인딩된 엘리먼트를 나타내는 VNode입니다. beforeUpdate 및 updated 훅에서만 사용할 수 있습니다.

<div v-example:[arg]="value"></div>
내장 디렉티브와 유사하게 커스텀 디렉티브 인자는 동적일 수 있습니다

간단하게 함수로 사용하기

커스텀 디렉티브가 mounted 및 updated에 대해 동일한 동작을 갖는 것이 일반적이며, 다른 훅은 필요하지 않습니다. 이러한 경우 디렉티브를 객체가 아닌 함수로 정의할 수 있습니다:

app.directive('demo', (el, binding) => {
  console.log(binding.value.color) // => "white"
  console.log(binding.value.text) // => "안녕!"
})

컴포넌트에서 사용

커스텀 디렉티브는 폴스루 속성과 유사하게 항상 컴포넌트의 루트 노드에 적용됩니다.
속성과 달리 디렉티브는 v-bind="$attrs"를 사용하여 다른 엘리먼트에 전달할 수 없습니다.
일반적으로 컴포넌트에 커스텀 디렉티브를 사용하는 것은 권장되지 않습니다.

플러그인

Vue에 앱 레벨 기능을 추가하는 자체 코드입니다.

import { createApp } from 'vue'
const app = createApp({})
app.use(myPlugin, {
  /* 선택적인 옵션 */
})

/* myPlugin.js */
const myPlugin = {
  install(app, options) {
    // 앱 환경설정
  }
}

일반적인 시나리오
1. app.component()app.directive()를 사용하여 하나 이상의 전역 컴포넌트 또는 커스텀 디렉티브를 등록합니다.
1. app.provide()를 호출하여 앱 전체에 리소스를 주입 가능하게 만듭니다.
1. 일부 전역 인스턴스 속성 또는 메서드를 app.config.globalProperties에 첨부하여 추가합니다.
1. 위 목록의 몇 가지를 조합해 무언가를 수행해야 하는 라이브러리(예: vue-router).

플러그인 작성하기 (Provide / Inject 활용하기)

// plugins/i18n.js
export default {
  install: (app, options) => {
    // 전역적으로 사용 가능한 $translate() 메서드 주입
    app.config.globalProperties.$translate = (key) => {
      // `key`를 경로로 사용하여
      // `options`에서 중첩 속성을 검색합니다.
      return key.split('.').reduce((o, i) => {
        if (o) return o[i]
      }, options)
    }

    app.provide('i18n', options)
  }
}
<script setup>
import { inject } from 'vue'

const i18n = inject('i18n')

console.log(i18n.greetings.hello)
</script>

빌트인 컴포넌트

트랜지션

Vue는 상태 변화에 대응하기 위해 트랜지션 및 애니메이션 작업에 도움이 되는 두 가지 빌트인 컴포넌트를 제공합니다:

컴포넌트

해당 애니메이션은 다음 중 하나의 조건에 충족하면 발생합니다:

  • v-if를 통한 조건부 렌더링
  • v-show를 통한 조건부 표시
  • 스페셜 엘리먼트 <component>를 통해 전환되는 동적 컴포넌트
<button @click="show = !show">토글</button>
<Transition>
  <p v-if="show">안녕</p>
</Transition>
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}

<Transition>은 슬롯 컨텐츠로 단일 엘리먼트 또는 컴포넌트만 지원합니다. 컨텐츠가 컴포넌트인 경우, 컴포넌트에는 단일 루트 엘리먼트만 있어야 합니

CSS 기반 트랜지션

  1. v-enter-from: 진입 시작 상태. 엘리먼트가 삽입되기 전에 추가되고, 엘리먼트가 삽입되고 1 프레임 후 제거됩니다.
  2. v-enter-active: 진입 활성 상태. 모든 진입 상태에 적용됩니다. 엘리먼트가 삽입되기 전에 추가되고, 트랜지션/애니메이션이 완료되면 제거됩니다. 이 클래스는 진입 트랜지션에 대한 지속 시간, 딜레이 및 이징(easing) 곡선을 정의하는 데 사용할 수 있습니다.
  3. v-enter-to: 진입 종료 상태. 엘리먼트가 삽입된 후 1 프레임 후 추가되고(동시에 v-enter-from이 제거됨), 트랜지션/애니메이션이 완료되면 제거됩니다.
  4. v-leave-from: 진출 시작 상태. 진출 트랜지션이 트리거되면 즉시 추가되고 1 프레임 후 제거됩니다.
  5. v-leave-active: 진출 활성 상태. 모든 진출 상태에 적용됩니다. 진출 트랜지션이 트리거되면 즉시 추가되고, 트랜지션/애니메이션이 완료되면 제거됩니다. 이 클래스는 진출 트랜지션에 대한 지속 시간, 딜레이 및 이징 곡선을 정의하는 데 사용할 수 있습니다.
  6. v-leave-to: 진출 종료 상태. 진출 트랜지션이 트리거된 후 1 프레임이 추가되고(동시에 v-leave-from이 제거됨), 트랜지션/애니메이션이 완료되면 제거됩니다.

트랜지션 이름 지정하기

<Transition name="fade">
.fade-enter-active,

CSS 애니메이션

네이티브 CSS 애니메이션은 CSS 트랜지션과 동일한 방식으로 적용되지만, 엘리먼트가 삽입된 직후에 *-enter-from이 제거되지 않고, animationend 이벤트에서 제거된다는 차이점이 있습니다.

커스텀 트랜지션 클래스

<!-- Animate.css가 페이지에 포함되어 있다고 가정합니다. -->
<Transition
  name="custom-classes"
  enter-active-class="animate__animated animate__tada"
  leave-active-class="animate__animated animate__bounceOutRight"
>
  <p v-if="show">안녕</p>
</Transition>

트랜지션과 애니메이션을 같이 사용하기

transitionend 또는 animationend가 될 수 있습니다. 둘 중 하나만 사용하는 경우
<Transition type="animation">...</Transition>

성능 고려사항

  • 애니메이션 진행중에 문서 레이아웃에 영향을 미치지 않으므로 모든 애니메이션 프레임에서 값비싼 CSS 레이아웃 계산을 트리거하지 않습니다. (height 또는 margin)
  • 대부분의 최신 브라우저는 transform에 애니메이션을 적용할 때 GPU 하드웨어 가속을 활용할 수 있습니다.

JavaScript 훅

<Transition
  :css="false"
  @before-enter="onBeforeEnter"
  @enter="onEnter"
  @after-enter="onAfterEnter"
  @enter-cancelled="onEnterCancelled"
  @before-leave="onBeforeLeave"
  @leave="onLeave"
  @after-leave="onAfterLeave"
  @leave-cancelled="onLeaveCancelled"
>

JavaScript 전용 트랜지션을 사용할 때 일반적으로 :css="false" prop을 추가하는 것이 좋습니다.

재사용 가능한 트랜지션

  <Transition
    name="my-transition"
    @enter="onEnter"
    @leave="onLeave">
    <slot></slot> <!-- 슬롯 컨텐츠 전달 -->
  </Transition>

등장 트랜지션

노드의 초기 렌더링에도 트랜지션을 적용 <Transition appear>

엘리먼트 간 트랜지션

<Transition>
  <button v-if="docState === 'saved'">수정</button>
  <button v-else-if="docState === 'edited'">저장</button>
  <button v-else-if="docState === 'editing'">취소</button>
</Transition>

트랜지션 모드

진출 시 엘리먼트가 먼저 애니메이션 처리되고 진입 시 엘리먼트가 진출 애니메이션이 완료된 이후에 삽입되기를 원할 수 있습니다. <Transition mode="out-in">
<Transition>은 자주 사용되지는 않지만 mode="in-out"도 지원합니다.

컴포넌트 간 트랜지션

<Transition>은 동적 컴포넌트에서도 사용할 수 있습니다:

<Transition name="fade" mode="out-in">
  <component :is="activeComponent"></component>
</Transition>

동적 트랜지션

<Transition :name="transitionName">

트랜지션 그룹

목록에서 렌더링되는 엘리먼트 또는 컴포넌트의 삽입, 제거 및 순서 변경을 애니메이션으로 만들기 위해 설계된 빌트인 컴포넌트

<Transition>과의 차이점

  • 기본적으로 래퍼 엘리먼트를 렌더링하지 않습니다. 그러나 tag prop으로 렌더링할 엘리먼트를 지정할 수 있습니다.
  • 트랜지션 모드는 더 이상 상호 배타적인 엘리먼트를 사용하지 않기 때문에 사용할 수 없습니다.
  • 내부 엘리먼트는 고유한 key 속성을 필수로 가져야 합니다.
  • CSS 트랜지션 클래스는 그룹/컨테이너 자체가 아닌 목록의 개별 엘리먼트에 적용됩니다.
<TransitionGroup name="list" tag="ul">
  <li v-for="item in items" :key="item">
    {{ item }}
  </li>
</TransitionGroup>

이동 트랜지션

.list-move, /* 움직이는 엘리먼트에 트랜지션 적용 */
.list-enter-active,
.list-leave-active {
  transition: all 0.5s ease;
}

.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

/* 이동 애니메이션을 올바르게 계산할 수 있도록
   레이아웃 흐름에서 나머지 항목을 꺼내기. */
.list-leave-active {
  position: absolute;
}

시차가 있는 목록 트랜지션

GreenSock 라이브러리를 사용

function onEnter(el, done) {
  gsap.to(el, {
    opacity: 1,
    height: '1.6em',
    delay: el.dataset.index * 0.15,
    onComplete: done
  })
}

KeepAlive

<KeepAlive>는 여러 컴포넌트 간에 동적으로 전환될 때, 컴포넌트 인스턴스를 조건부로 캐시할 수 있는 빌트인 컴포넌트입니다.

기본 사용법

인스턴스가 비활성 상태인 경우에도 상태가 보존되기를 원합니다

<!-- 비활성 컴포넌트가 캐시됩니다! -->
<KeepAlive>
  <component :is="activeComponent" />
</KeepAlive>

Include / Exclude

내부의 모든 컴포넌트 인스턴스를 캐시, include 및 exclude props를 통해 이 동작을 사용자 정의할 수 있습니다.
props 모두 쉼표로 구분된 문자열, RegExp(정규식) 또는 이 두 유형 중 하나를 포함하는 배열이 될 수 있습니다:

최대 캐시 인스턴스

max props를 통해 캐시할 수 있는 컴포넌트 인스턴스의 최대 수를 제한할 수 있습니다.
max가 지정되면 <KeepAlive>는 LRU(Least recently used) 캐시처럼 작동합니다.

캐시된 인스턴스의 수명주기

마운트 해제되는 대신 비활성화됨 상태가 됩니다
kept-alive 컴포넌트는 onActivated()onDeactivated()를 사용하여 이 두 가지 상태에 대한 수명 주기 훅을 등록할 수 있습니다:

텔레포트

컴포넌트 템플릿의 일부를 해당 컴포넌트의 DOM 계층 외부의 DOM 노드로 "이동"할 수 있게 해주는 빌트인 컴포넌트입니다.

템플릿의 일부가 논리적으로 여기에 속하지만, 시각적인 관점에서 Vue 앱 외부의 다른 DOM 어딘가에 표시되어야 합니다.

<button @click="open = true">모달 열기</button>

<Teleport to="body">
  <div v-if="open" class="modal">
    <p>짜자잔~ 모달입니다!</p>
    <button @click="open = false">닫기</button>
  </div>
</Teleport>

컴포넌트와 함께 사용하기

<Teleport>에 컴포넌트가 포함되어 있으면, 해당 컴포넌트는 논리적으로 <Teleport>를 포함하는 부모 컴포넌트의 자식으로 유지됩니다. Props 전달() 및 이벤트 발신(emit)은 계속 동일한 방식으로 작동합니다.

텔레포트 비활성화

<Teleport :disabled="isMobile">

같은 대상에 여러 텔레포트 사용

<Teleport to="#modals">
  <div>A</div>
</Teleport>
<Teleport to="#modals">
  <div>B</div>
</Teleport>

Suspense

<Suspense> 는 실험적인 기능입니다. 아직 개발이 완료된 안정된 상태가 아니며 추후 AP가 변경 될 수 있습니다.

<Suspense> 컴포넌트는 이러한 중첩된 비동기 의존성이 해결될 때까지 최상위에서 로드/에러 상태를 표시할 수 있는 기능을 제공합니다.

비동기 setup()

export default {
  async setup() {
    const res = await fetch(...)
    const posts = await res.json()
    return {
      posts
    }
  }
}

로딩 상태

#default#fallback 이라는 두 개의 슬롯이 있습니다.

<Suspense>
  <!-- 컴포넌트와 중첩된 비동기 의존성 -->
  <Dashboard />

  <!-- #fallback 슬롯을 통한 로딩 상태 -->
  <template #fallback>
    로딩중...
  </template>
</Suspense>

이벤트

pending 이벤트는 보류 상태에 들어갈 때 발생합니다. resolve 이벤트는 새 컨텐츠가 default 슬롯에서 해결되면 발생합니다. fallback 슬롯의 컨텐츠가 표시될 때 fallback 이벤트가 시작됩니다.

에러 처리

<Suspense> 는 현재 컴포넌트 자체를 통해 에러를 처리하지 않습니다. 그러나 errorCaptured 옵션 또는 onErrorCaptured() 훅을 사용하여 <Suspense> 의 부모 컴포넌트에서 비동기 에러를 캡처하고 처리할 수 있습니다.

다른 컴포넌트와 결합

<RouterView v-slot="{ Component }">
  <template v-if="Component">
    <Transition mode="out-in">
      <KeepAlive>
        <Suspense>
          <!-- 메인 컨텐츠 -->
          <component :is="Component"></component>

          <!-- 로딩 상태 -->
          <template #fallback>
            로딩중...
          </template>
        </Suspense>
      </KeepAlive>
    </Transition>
  </template>
</RouterView>

확장하기

싱글 파일 컴포넌트

하나의 파일에서 컴포넌트의 뷰, 로직 및 스타일을 캡슐화하고 배치합니다

Vue CLI

Vue CLI는 웹팩 기반의 Vue 공식 툴체인입니다
특정 웹팩 전용 기능에 의존하는 유지보수 상태가 아니라면, Vite로 새 프로젝트를 시작하는 것이 좋습니다.

브라우저 내 템플릿 편집에 대한 참고 사항

클라이언트 번들 크기를 줄이기 위해 Vue는 다양한 사용 사례에 최적화된 다른 "빌드"를 제공합니다.

  • vue.runtime.*으로 시작하는 빌드 파일은 런타임 전용 빌드입니다. 컴파일러를 포함하지 않습니다. 이러한 빌드를 사용할 때 모든 템플릿은 빌드 과정을 통해 사전 컴파일되어야 합니다.
  • .runtime을 포함하지 않는 빌드 파일은 전체 빌드입니다. 컴파일러를 포함하고 브라우저에서 직접 템플릿 컴파일을 지원합니다. 그러나 페이로드가 ~14kb 증가합니다.

IDE 지원

권장 IDE 설정은 VSCode + Volar입니다.

타입스크립트

Volar는 템플릿 표현식 및 교차 컴포넌트 props 유효성 검사를 포함하여 <script lang="ts"> 블록을 사용하여 SFC에 대한 유형 검사를 제공합니다.

라우팅

대부분의 싱글 페이지 앱(SPA)의 경우 공식적으로 지원되는 vue-router 라이브러리를 사용하는 것이 좋습니다.
브라우저 hashchange 이벤트를 수신하거나 History API를 사용하여 현재 컴포넌트 상태를 업데이트할 수 있습니다.

<script setup>
import { ref, computed } from 'vue'
import Home from './Home.vue'
import About from './About.vue'
import NotFound from './NotFound.vue'

const routes = {
  '/': Home,
  '/about': About
}

const currentPath = ref(window.location.hash)

window.addEventListener('hashchange', () => {
  currentPath.value = window.location.hash
})

const currentView = computed(() => {
  return routes[currentPath.value.slice(1) || '/'] || NotFound
})
</script>

<template>
  <a href="#/">Home</a> |
  <a href="#/about">About</a> |
  <a href="#/non-existent-path">잘못된 링크</a>
  <component :is="currentView" />
</template>

상태 관리

반응형 API를 통한 간단한 상태 관리

여러 인스턴스에서 공유해야 하는 상태가 있는 경우, reactive()를 사용하여 반응형 객체를 만든 다음 여러 컴포넌트에서 가져갈 수 있습니다.

// store.js
import { reactive } from 'vue'

export const store = reactive({
  count: 0,
  increment() {
    this.count++
  }
})
<!-- ComponentA.vue -->
<script setup>
import { store } from './store.js'
</script>

<template>
  <button @click="store.increment()">
    B 컴포넌트에서: {{ store.count }}
  </button>
</template>

SSR 고려 사항

서버 사이드 렌더링(SSR)을 활용하는 앱을 구축하는 경우, 위 패턴은 store가 여러 요청에서 공유되는 싱글톤이기 때문에 문제를 일으킬 수 있습니다. 이것에 대한 자세한 내용은 SSR 가이드에서 설명합니다.

Pinia(피니아: 공식 상태관리 라이브러리)

Vuex와 비해 Pinia는 더 간단한 API를 제공하고, Composition-API 스타일의 API를 제공하며, 가장 중요한 것은 TypeScript와 함께 사용할 때 견고한 유형 추론을 지원합니다.

테스트

테스트 유형

  • 단위: 주어진 함수, 클래스 또는 컴포저블에 제공된 입력 정보가 의도하는 출력 또는 사이드 이팩트를 생성하는지 확인합니다.
  • 컴포넌트: 컴포넌트가 마운트, 렌더링, 상호 작용이 의도대로 작동하는지 확인합니다. 이러한 테스트는 단위 테스트보다 더 많은 코드를 가져오고 더 복잡하며 실행하는 데 더 많은 시간이 필요합니다.
  • End-to-end: 여러 페이지에 걸쳐 있는 기능을 확인하고, 프로덕션으로 빌드되는 Vue 앱처럼 실제 네트워크 요청을 합니다. 이러한 테스트에는 종종 데이터베이스 또는 기타 백엔드를 구축하는 작업이 포함됩니다.

단위 테스트

단위 테스트는 일반적으로 단일 함수, 클래스, 컴포저블 또는 모듈을 다룹니다.
단위 테스트는 일반적으로 UI 렌더링, 네트워크 요청 또는 기타 환경 문제를 포함하지 않는 자체적으로 해야 할 일에 대한 논리, 컴포넌트, 클래스, 모듈 또는 함수에 적용됩니다.
Vue 관련 기능을 단위 테스트하는 두 가지 경우가 있습니다.
1. 컴포저블
1. 컴포넌트
예를 들어, 컴포넌트 테스트는 프로그래밍 방식으로 컴포넌트와 상호 작용하는 대신 사용자가 엘리먼트를 클릭하는 것과 같아야 합니다.

Vitest와 브라우저 기반 러너의 주요 차이점은 속도와 실행 컨텍스트입니다. 간단히 말해서, Cypress와 같은 브라우저 기반 러너는 Vitest와 같은 노드 기반 러너가 포착할 수 없는 문제(예: 스타일 문제, 실제 네이티브 DOM 이벤트, 쿠키, 로컬 스토리지 및 네트워크 에러)를 포착할 수 있지만, 브라우저 기반 러너는 브라우저를 열고 스타일시트를 컴파일하는 등의 작업을 수행하기 때문에 Vitest보다 훨씬 느립니다. Cypress는 컴포넌트 테스트를 지원하는 브라우저 기반 러너입니다.

마운팅 라이브러리

앱의 테스트 우선 순위와 초점이 더 잘 맞기 때문에 앱의 컴포넌트를 테스트할 때 @testing-library/vue를 사용하는 것이 좋습니다. Vue 전용 내부 테스트가 필요한 고급 컴포넌트를 빌드하는 경우에만 @vue/test-utils를 사용하세요.

E2E 테스트

E2E 테스트는 프로덕션으로 빌드된 Vue 앱에 대해 네트워크 요청을 필요로하는 다중 페이지 앱 동작에 중점을 둡니다.

브라우저 간 테스트

E2E 테스트의 주요 이점 중 하나는 여러 브라우저에서 앱을 테스트할 수 있다는 것입니다.
전반적으로 우리는 Cypress가 유익한 UI, 뛰어난 디버깅 가능성, 빌트인 검증 및 stubs, 내결함성, 병렬화 및 스냅샷과 같은 기능을 갖춘 가장 완벽한 E2E 솔루션을 제공한다고 믿습니다.

서버 사이드 렌더링 (SSR)

왜 SSR 일까요?

  • 컨텐츠에 도달하는 시간 단축: 인터넷 속도가 느리거나 기기가 느린 경우 더 두드러집니다. 서버 렌더링 마크업은 모든 JavaScript가 다운로드 및 실행되어 표시될 때까지 기다릴 필요가 없으므로 사용자가 완전히 렌더링된 페이지를 더 빨리 볼 수 있습니다. 또한 데이터 가져오기는 초기 방문을 위해 서버 측에서 수행되므로 클라이언트보다 데이터베이스에 더 빠르게 연결할 수 있습니다. 이는 일반적으로 개선된 Core Web Vitals 측정항목과 더 나은 UX를 가져오며, 컨텐츠에 도달하는 시간이 전환율과 직접적으로 관련된 앱에 중요할 수 있습니다.

  • 통합 유지보수 모델: 백엔드 템플릿 시스템과 프론트엔드 프레임워크 사이를 왔다 갔다 하는 대신, 전체 앱을 개발하기 위해 동일한 언어와 선언적 컴포넌트 지향의 유지보수 모델을 사용할 수 있습니다.

  • 더 나은 SEO: 검색 엔진 크롤러는 완전히 렌더링된 페이지를 직접 볼 수 있습니다.

제약사항

  • 개발 제약 사항. 브라우저별 코드는 특정 수명주기 훅 내에서만 사용할 수 있습니다. 일부 외부 라이브러리는 서버 렌더링 앱에서 실행할 수 있도록 특별한 처리가 필요할 수 있습니다.

  • 더 복잡한 빌드 설정 및 배포 요구 사항. 모든 정적 파일 서버에 배포할 수 있는 완전 정적 SPA와 달리 서버 렌더링 앱에는 Node.js 서버를 실행할 수 있는 환경이 필요합니다.

  • 더 많은 서버 측 부하. Node.js에서 전체 앱을 렌더링하는 것은 정적 파일을 제공하는 것보다 CPU를 더 많이 사용하므로, 트래픽이 많을 것으로 예상되는 경우, 해당 서버 로드에 대비하고 캐싱 전략을 현명하게 사용하세요.

SSR vs. SSG (SSG: Static-Site Generation)

미리 렌더링된 페이지가 생성되어 정적 HTML 파일로 제공됩니다.
SSG는 정적 데이터, 즉 빌드 시 결정되어 배포 간에 변경되지 않는 데이터를 소비하는 페이지에만 적용할 수 있습니다. 하지만 데이터가 변경될 때마다 새로운 배포가 필요합니다.
소수의 마케팅 페이지(예: /, /about, /contact 등)의 SEO를 개선하기 위해 SSR만 고려하고 있다면, SSG를 대안으로 추천합니다.

클라이언트 하이드레이트

클라이언트 측 앱을 대화형으로 만들기 위해 Vue는 하이드레이트 단계를 수행해야 합니다.

교차 요청 상태 오염(Cross-Request State Pollution)

SSR 컨텍스트에서 앱 모듈은 일반적으로 서버가 부팅될 때 서버에서 한 번만 초기화됩니다. 동일한 모듈 인스턴스가 여러 서버 요청에서 재사용되고 싱글톤 상태 객체도 재사용됩니다.

profile
developer

0개의 댓글