Home 꼭-알아야-하는-vue3-변경사항-톺아보기
Post
Cancel

꼭-알아야-하는-vue3-변경사항-톺아보기

1. v-model의 변화

Vue 3에서는 혼동을 줄이고 개발자가 v-model 디렉티브를 보다 유연하게 사용할 수 있도록, 양방향 데이터 바인딩을 위한 API가 표준화되었다.

공식문서

1) 컴포넌트에 사용되는 v-model의 prop 및 event 사용법이 변경

2.x3.x
valuemodelValue
inputupdate:modelValue
model 옵션전달인자 사용


(1) 2.x

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- 부모 컴포넌트 --> 
<template>
   <div>
    <!-- 변수 inputData를 속성 value에 바인딩하고 input이벤트를 수신하여 변수 inputData를 업데이트 한다. --> 
    <my-input-component v-model="inputData"></my-input-component>
  </div>
</template>

<script>
import MyInputComponent from "./MyInputComponent.vue";
export default {
  name: "MyComponent",
  components: {
    MyInputComponent,
  },
  data() {
    return {
      inputData: "",
    };
  },
};
</script>
  • 2.x에서 컴포넌트에 v-model을 사용하는 것은 value를 prop으로 전달하고 input 이벤트를 emit 하는 것과 같다.
  • 즉, input 이벤트가 발생했을 때, value값이 변경되는 것과 같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 자식 컴포넌트 --> 
<template>
  <div>
    <input type="text" :value="value" @input="$emit('input', $event.target.value)">
  </div>
</template>

<script>
  export default {
    name: "MyInputComponent",
    props: [
      "value"
    ],
  }
</script>


prop이나 이벤트 명을 변경하려면?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 자식 컴포넌트 --> 
<template>
  <div>
    <input type="text" :textData="textData" @input="$emit('custom-change', $event.target.value)">
  </div>
</template>

<script>
  export default {
    name: "MyInputComponent",
    model: {
        props: "textData"
        event: "custom-change"
    }
    props: {
      textData
  	}
  }
</script>
  • 만약 prop 또는 이벤트 명을 다른 이름으로 변경하려면 model 옵션의 추가가 필요하다.


🤔 잠깐! vue 2.x에 있던 .sync수식어란?

props에 양방향 바인딩이 필요한 경우 (v-model외에 추가적으로 다른 props에 양방향 바인딩을 사용해야 하는 경우), 다음과 같이 코드를 작성한다. 자식 컴포넌트에서 부모 컴포넌트의 값을 수정하기에, 부모 컴포넌트에서는 어떠한 자식 컴포넌트가 값을 수정한 것인지 파악하기 어렵다. 그래서 자식 컴포넌트에서 update:propName을 이용하여 이벤트를 부모 컴포넌트에 전달한다.

자식 컴포넌트에서 this.$emit('update:propName', newValue)를 통해 새로운 값이 할당되었음을 부모에게 알린다. 그때 부모는 아래와 같은 코드로 해당 이벤트를 듣는다.

1
2
3
4
5
6
<!-- 변수 inputData를 속성 propName에 바인딩하고 update:PropName 이벤트를 수신하여 변수 inputData를 업데이트한다. -->
<my-input-component :propName="inputData" @update:propName="inputData = $event"/>

<!-- 축약된 방식은 아래와 같다 -->

<my-input-component :propName.sync="inputData"/>
  • v-model과의 차이점
    • sync를 사용할 때는 자식 컴포넌트에는 valueProp이 필요하지 않다. 대신 부모와 동일한 prop 이름을 사용한다.
    • 또한, prop을 업데이트 하기위해 input 이벤트를 emit하는 것이 아니라 update:propName을 emit한다.
    • 하나의 컴포넌트에 다중으로 사용이 가능하다.


(2) 3.x

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- 부모 컴포넌트 --> 
<template>
   <div>
    <h1>3.x</h1>
    <my-input-component  v-model="inputData3"></my-input-component>
  </div>
</template>

<script>
import MyInputComponent from "./MyInputComponent.vue";
export default {
  name: "MyComponent",
  components: {
    MyInputComponent,
  },
  data() {
    return {
      inputData3: "",
    };
  },
};
</script>
  • 컴포넌트에 v-model을 사용하는 것은 modelValue prop를 전달하고 update:modelValue 이벤트를 emit 하는 것과 같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 자식 컴포넌트 --> 
<template>
  <input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>

<script>
export default {
  name: "MyInputComponent",
  props: {
    modelValue: {
      type: String
    }
  },
}
</script>


prop이나 이벤트 명을 변경하려면?

1
2
3
4
5
<my-input-component v-model:content="inputData3" />

<!-- 축약된 방식은 아래와 같다 -->

<my-input-component :content="inputData3" @update:content="inputData3 = $event" />
  • 3.x에서 모델명을 변경하려면 model 옵션 대신에 전달인자를 v-model에 전달할 수 있다. (props를 통한 부모와 자식의 양방향 통신)
  • 이는 .sync수식어를 대체하는 역할을 한다.


2) 다중 v-model 바인딩

다중으로 v-model을 바인딩 할 수 있다.

한 컴포넌트에 다중으로 사용할 수 있는.sync수식어를 완벽하게 대체하기 위해 v-model에도 적용되게 되었다.

이제는 단일 컴포넌트 인스턴스에 대해 다중 v-model 바인딩이 가능해졌다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- 부모 컴포넌트 --> 
<template>
   <div>
    <h1>3.x</h1>
    <my-input-component  v-model:"firstName" v-model:"lastName" ></my-input-component>
  </div>
</template>

<script>
import MyInputComponent from "./MyInputComponent.vue";
export default {
  name: "MyComponent",
  components: {
    MyInputComponent,
  },
  data() {
    return {
      firstName: "",
      lastName:""
    };
  },
};
</script>


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 자식 컴포넌트 --> 
<template>
  <input type="text" :value="firstName" @input="$emit('update:firstName', $event.target.value)">
  <input type="text" :value="lastName" @input="$emit('update:lastName', $event.target.value)">
</template>

<script>
export default {
  name: "MyInputComponent",
  props: {
    firstName: {
      type: String
    },
    lastName: {
      type: String
    }
  },
}
</script>


3) 사용자 수식어 생성 기능 추가

커스텀 수식어를 추가할 수 있다.

빌트인 수식어설명예시
.lazyinput대신 change 이벤트 이후에 동기화 할 수 있다.<input v-model.lazy="msg">
.number사용자 입력이 자동으로 숫자로 형변환 된다.<input v-model.number="age" type="number" />
.trim사용자 입력 중 양쪽 공백을 제거한다.<input v-model.trim="msg" />

v-model은 위의 표에 정리된 것처럼 빌트인 수식어를 가지고 있다. 이제는 상황에 따라서 커스텀 수식어를 만들 수 있다.

공식문서에 예제가 있으니 참고하면 된다.


2. CompositionAPI

이전의 vue는 optionAPI기반으로 컴포넌트를 구성했다. optionAPI는 컴포넌트의 옵션들 (data, computed, methods, watch)로 논리에 따라 구분하여 컴포넌트를 구성하게 된다. 하지만, 컴포넌트가 커지면 커질 수록 해당 옵션들 또한 커지게된다. 기능별로 묶여있는 것이 아닌 옵션 별(논리적 구분)로 묶여있기 때문에 코드가 길어질 수록 코드 가독성이 떨어질 수 밖에 없다. 즉, 하나의 기능을 논리적인 분류에 따라 분산하여 작성하기 때문에 코드가 분산되어 작성될 수 밖에 없다.

그래서 이를 보완하기 위해 compositionAPI가 등장하게 되었다. 기능별 + 논리별로 코드를 배치하게 한다. 흩어져있는 데이터를 한 곳에 모아서 관리를 함으로 재사용성을 높일 수 있다.


1) setup

새로운 setup 컴포넌트 옵션은 컴포넌트가 생성되기 에, props가 한번 resolved될 때 실행되며 composition API의 진입점 역할을 한다.

이곳에 논리적인 흐름에 따라서 구현하고 싶은 기능을 넣어준다.

  • setup은 컴포넌트 인스턴스가 생성되지 않았기 때문에 props, context를 제외하고 컴포넌트 내부 데이터에 접근할 수 없다.
    • 즉, this 접근이 불가능하다.


  • setup 함수의 첫번째 전달인자는 props이고 두번째 전달 인자는 context이다.
    • props는 반응성이 있다. (ES6 구조 분해 할당을 하면 반응성을 잃는다. )
    • context는 3가지 컴포넌트 프로퍼티를 가지는 일반 JS 객체로 반응성이 없다.
      • context.attrs
      • context.slots
      • context.emit

attrsslots은 컴포넌트 자체가 업데이트될 때, 항상 업데이트되는 상태 저장 객체이다. 즉, attrs와 slots에 구조분해할당을 피하고, 항상 속성을 attrs.xslots.x의 형태로 참조해야한다. 그래서 attrsslots의 변경으로 인한 사이드이펙트를 의도하려면, onUpdated 라이프사이클 훅 안에서 수행해야한다.

1
2
3
4
5
6
7
<script>
  export default {
      setup(props, {attrs, slots, emit}){
          ...
      }
  }
</script>


(1) ref

원시자료형 데이터를 반응형으로 만드는 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>
  <div> 9</div>
  <button @click="countUp"> 카운트 업!</button>
</template>

<script>
  import { ref } from "vue"
   
  export default {
    setup(){
      const count = ref(0);
      const countUp = ()=> {
        count.value++
      } 
      return {
        count,
        countUp
      };   
    };
  };
</script>

<!--======= optionAPI =======-->
<script>
  export default {
    data(){
      return {
        count: 0
      };
    },
    methods: {
      count(){
         this.count++
      }
    };
  };
</script>
  • ref: 전달인자를 받고 반응형 변수의 값에 접근하거나 변경할 수 있는 value속성을 가진 객체를 반환한다. (값에 반응형 참조를 만든다 )
  • ref객체는 단 하나의 속성을 가지는데, 내부 값을 가리키는 .value이다.
  • ref 가 retrun을 통해 반환되고 템플릿에서 접근되면, 자동적으로 내부 값을 풀어냅니다. 즉, 템플릿에서 .value 를 추가할 필요가 없다.


관련 함수설명
unref주어진 인자가 ref라면 내부 값을 반환하고, 아니라면 주어진 인자를 반환한다.
toRef소스가 되는 reacitve 객체의 속성을 가져와 ref를 만들 수 있다.
이 ref는 여기저기 인자로 전달할수 있으면서, 소스 객체에 대해 리액티브 연결을 유지할수 있습니다.
toRefsreactive 객체를 일반 객체로 변환하여 반환하지만, 반환되는 객체의 각 속성들이 ref로 원래의 reactive 객체 속성으로 연결된다.
반응성을 잃지 않고 반환된 값을 구조 분해 할당 할 수 있다.
isRef주어진 값이 ref 객체인지 확인한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 2.toRef --> 
<script>
  import {toRef, reactive} from "vue";
  
  const state = reactive({
    foo: 1,
    bar: 2
  })
  const fooRef = toRef(state, 'foo')
  
  fooRef.value++
  console.log(state.foo) // 2

  // 연결이 유지되어있다. 
  state.foo++
  console.log(fooRef.value) // 3
    
</script>


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- 3.toRefs --> 

<script>
  import {toRefs, reactive} from "vue";
    
  function useFeatureX() {
    const state = reactive({
      foo: 1,
      bar: 2
    })
    return toRefs(state)
  };
    
  export default {
    setup(props) {
      const { baz } = toRefs(props);
      const { foo, bar } = useFeatureX();
      return {
        foo,
        bar
      };
    };
  };
<script>
  • props의 구조분해할당이 필요한 경우, setup펑션의 toRefs를 사용하여 반응성을 유지할 수 있습니다.
    • props는 반응성이 있다. 그러다 단순하게, ES6의 구조분해할당을 사용한다면 props의 반응성이 제거된다.
  • toRefs는 소스 객체에 포함 된 속성에 대한 ref만 생성합니다. 특정 속성에 대한 참조를 만들려면 대신 toRef를 사용하면 된다.


🤔 반응형…?

반응형 API


(2) reactive

객체 형태의 데이터를 반응형 상태로 생성하기 위해 사용하는 메서드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
  <div> 9</div>
</template>
<script>
  import { ref, reactive } from "vue"
   
  export default {
    setup(){
      const age = ref(20);
	  const state = reative({
        name: "박싸피",
        age
      });
      return {
        state
      };   
    };
  };
</script>
  • reactive: 객체의 반응형 복사본을 반환한다.
  • 반응형이 깊게 적용 된다. 모든 중첩된 속성의 변화를 감지한다.
  • reactive객체의 속성에 ref를 할당하면, 자동적으로 내부 값으로 벗겨내서 사용된다.


관련 함수설명
readonly객체(반응형 또는 일반 객체) 또는 ref를 가져와서 원본에 대한 읽기전용으로 변경한다. (수정을 가하지 못하는 복사본)
isReactive객체가 반응형(reactive)로 생성된 것인지 아닌지 확인한다.
shallowReadonly고유한 속성을 읽기 전용으로 만들지만 중첩된 객체의 경우 수정이 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 1. readonly -->
<script>
  import { reactive, readonly } from 'vue'
  const original = reactive({ count: 0 })
  const copy = readonly(original)

  // 원본이 변이되면 복사본인 copy의 값도 변한다.
  original.count++

  // 복사본을 변이하려고 하면 경고와 함께 실패한다.
  copy.count++ // warning: "Set operation on key 'count' failed: target is readonly."  
</script>


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 3. shallowReadonly --> 
<script>
  import { shallowReadonly } from 'vue'
  const state = shallowReadonly({
      foo: 1,
      nested: {
      bar: 2
    }
  })

  // 객체의 자체적인 속성 변경이 안된다. 
  state.foo++
  
  // 하지만 중첩된 객체에서는 수정이 가능하다. 
  isReadonly(state.nested) // false
  state.nested.bar++ // 반응
</script>


(3) watch

Vue에서 import한 watch함수를 사용하여 동일한 작업을 수행할 수 있다.

3가지의 전달인자를 허용한다.

  • 감시를 원하는 반응성 참조나 getter function
  • 콜백 ((value, oldValue, onInvalidate) => void형태의 콜백)
  • 선택적인 구성 옵션 (immediate나 deep과 같은 wathchOptions)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<template>
  <div> 9</div>
  <button @click="countUp"> 카운트 업!</button>
</template>

<script>
  import { ref, watch } from "vue"
   
  export default {
    setup(){
      const count = ref(0);
      const countUp = ()=> {
        counter.value ++
      }
      watch(count, (newValue, oldValue) => {
          console.log(`${oldValue}=> ${newValue}`)
      }) 
      return {
        count,
        countUp
      };   
    };
  };
</script>

<!--======= optionAPI =======-->
<script>
  export default {
    data(){
      return {
        count: 0
      };
    },
    methods: {
      count(){
         this.count++
      };
    },
    watch: {
      count(newValue, oldValue){
          console.log(`${oldValue}=> ${newValue}`);
      };
    };
  };
</script>


(4) computed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
  <div> </div>
</template>

<script>
  import { ref, computed } from "vue"
   
  export default {
    setup(){
      const count = ref(0);
      
      const twiceTheCount = 
         computed(()=> counter.value ++);
        
      // 값에 접근하기 위해서는 value속성에 접근해야 한다. 
      console.log(twiceTheCount.value)
        
      return {
        count,
        twiceTheCount
      };   
    };
  };
</script>
  • computed 함수는computed의 첫번째 인자를 전달된 게터와 같은 콜백의 결과에 대한 읽기 전용 반응성 참조를 리턴한다.

  • 새로 생성된 computed 변수의 value에 접근하려면, ref와 마찬가지로 .value 속성을 사용해야 한다.


2) 라이프 사이클 훅

Options API와 비교하여 Composition API 형태를 완벽하게 만들기 위해서, setup 안에 라이프사이클 훅을 등록하는 방법도 필요하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
  <div> </div>
</template>

<script>
  import { ref, onMounted } from "vue"
   
  export default {
    setup(){
      const count = ref(0);
      
      const countUp = ()=> {
        counter.value ++
      } 
        
      onMounted(countUp) // mounted에서 countUp함수 호출
 
      return {
        count,
        countUp
      };   
    };
  };
</script>
  • Composition API의 라이프사이클 훅은 Options API의 라이프사이클 훅의 이름과 동일하지만, 접두사 on이 붙는다. 예) mounted -> onMounted
  • 컴포넌트에 의해 훅이 호출될 때 실행될 콜백함수를 인자로 받는다.


📌 Composition API의 라이프 사이클 훅

Options APIsetup 내부의 훅
beforeCreate필요하지 않음*
created필요하지 않음*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered

setupbeforeCreate, created 라이프사이클 훅 사이에 실행되는 시점이므로, 명시적으로 정의할 필요가 없다.


3. Teleport

Vue는 UI 및 관련 동작을 컴포넌트로 캡슐화하여 UI를 구축하도록 권장한다. 그러나, 논리적으로 컴포넌트에 속하는 템플릿의 일부라도 기술적인 관점에서 보면 Vue 앱 범위를 벗어나 DOM의 다른 곳으로 템플릿의 일부를 옮기는 게 더 바람직할 때도 있다. ex) 모달 창


처음 vue로 프로젝트를 시작하면 index.html의 <div id="app"></div>태그에 모든 화면을 넣는다.

1
2
3
4
5
6
7
<html>
  <body>
    <div id="app">
      <!-- 이곳에 vue로 만드는 모든 화면이 들어간다. -->
    </div>
  </body>
</html>

만약 모달창을 구현하면 현재 보이는 화면을 뚫고 최상위에 올라와야 하는데, 이는 프로젝트가 커질 수록 번거롭다. 그러나 teleport를 활용하면, 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링 할 수 있게 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
  <button @click="toggleModal"> 모달 열기! </button>
  
  <!-- 이 HTML을 "body" 태그로 teleport 해라!! --> 
  <teleport to="body">
      <div v-if="modal" class="modal">
         <button @click="toggleModal"> 모달 닫기! </button>
      </div>
  </teleport> 
</template>

<script>
  import {ref} from "vue";
    
  export default {
    setup(){
      const modal = ref(false);
      
      const toggleModal = ()=> {
        modal.value = !modal.value;
      };
        
      return {
        modal, 
        toggleModal
      }
    }
  }
</script>


[ modal off ]


[ modal on ]


📖 참고문서

공식문서

vue teleport 관련 블로그 글


This post is licensed under CC BY 4.0 by the author.