우리는 한다, 개발을.

Vue2와 Vue3의 차이점

⌛ 15 mins

목차


최근에 Vue 2 버전이 2.7 버전을 끝으로 업데이트가 종료된다는 소식을 듣게 되어 Vue 3 버전과 어떤 차이점이 있는지 제가 알고 있는 선에서 공유 드리고자 이 주제로 글을 작성하였습니다.

3버전으로 올라가면서 라이프사이클에도 약간의 변화가 생기긴 했지만 이번 포스팅에서는 사용하는 문법 위주로 소개해보려고 합니다.

Vue 2 에서의 스크립트 작성 방식은 Options API 방식과 Class Component 방식 2 가지가 있습니다.

Options API 가 OOP 를 배경을 두고 만들어졌기 때문에 Class 기반으로 잘 맞는다고 합니다.

그래서 TypeScript를 사용하는 저희는 Class Component 기반으로 프로젝트를 구성하고 있습니다.

1. Options API 방식

Options API 방식은 data, methods, mounted 와 같은 속성을 Vue 객체 내에서 사용하여 컴포넌트의 로직을 정의하는 방법을 뜻 합니다.

// Options API
export default {
	name: "SampleComponent",
	data: () => { return { sample: ''} },
	methods: {},
	mounted: {},
}

// Class 형 Component
export default class SampleComponent extends Vue {
	sample = '';
    mounted() {}
}

Vue 객체를 extends 받아서 새로운 Component 객체를 생성하는 방식

2. Composition API 방식 with setup()

Vue 3로 버전이 올라가면서 새롭게 추가된 방식으로 Composition API 방식과 setup() 가 추가되었습니다.

Composition API 는 컴포넌트 내에서 사용하는 특정 기능을 갖는 코드를 유연하게 구성하여 사용할 수 있도록 Vue3 에서 추가된 함수 기반의 API를 의미압니다.

(Vue2에서 사용했던 data(), methods, computed 와 같은 객체 형태에 얽매이지 않음)

Composition API 를 사용하면 setup() 라는 메서드 안에서 자유롭게 코드를 작성할 수 있습니다.

script 내에 setup 지시어를 추가하거나 setup() { } 메서드를 사용하여 코드를 작성하는 방법으로 두 가지가 있습니다.

기존 Vue2에서 사용했던 문법에 setup() 함수만 추가해주면 됩니다.

다만, 이렇게 사용할 경우 반드시 사용된 변수 또는 함수에 대해서 return 으로 내보내줘야 template 에서 사용할 수 있습니다.

<template>
  <div>
    <p></p>
    <button type="button" @click="onClickHandler">Button</button>
  </div>
</template>

<script lang="ts">
import {defineComponent} from "vue";

export default defineComponent({
  components: {},
  setup() {
    // 변수 선언
    const sample = ref('sample');

    // 함수 선언
    const onClickHandler = () => {
      console.log(`click`);
    }

    return { // return 필수!!!!!!!!
      sample, onClickHandler
    }
  }
})
</script>

이 방법을 보완하고자 나온게 script setup 문법입니다.

<template>
  <div>
    <p></p>
    <button type="button" @click="onClickHandler">Button</button>
  </div>
</template>

<script lang='ts' setup>
import {ref, Ref} from "vue";
// 필요한 컴포넌트는 단순 import 만 해주면 사용할 수 있다.

const sample: Ref<string> = ref('');

const onClickHandler = () => {
  console.log(`click`);
}

// return 이 필요없다.

</script>

script 안에 setup 만 넣어주면 return 문 없이 바로 template 에서 사용할 수 있습니다.

setup 함수 내에서 React 처럼 render 함수JSX를 지원하고있어 template 없이 컴포넌트를 구성할 수 있다고 하는데 이 방법은 아직 안해봤습니다..ㅎㅎ

컴포지션 API가 추가 로직 재사용 및 코드 구성 이점과 함께 뛰어난 TypeScript 통합을 제공한다는 점을 감안할 때 Vue 3에서 Class API를 더 이상 사용하지 않는 것이 좋다. 라고 합니다.

(CompositionAPI 방식은 Vue2.7 버전에서는 사용 가능)


v-model 선언 방식 차이


Vue2 에서의 방법

Vue 2에서 Data 변수를 선언할 때 data 함수 내부에 선언해주거나 Class 내부에 선언만 해주면 변수 선언이 끝이나게 됩니다.

export default {
    data() {
        return {
            sampleBoolean: true,
            sampleId: '',
            sampleLists: [],
        }
    }
}

@Component({})
export default class SampleComponent extends Vue {
    sampleBoolean = true;
    sampleId = '';
    sampleLists = [];
}


Vue3 에서의 방법

1. Ref 를 사용한 방법

Vue3 이전의 Ref는 뷰 템플릿의 DOM 또는 컴포넌트를 가리키는 속성이었지만, Vue3에서는 reactive reference를 의미합니다.

<script setup lang='ts'>
import { ref, Ref } from "vue";

interface Sample {
  id: string;
  password: string;
}

const sampleBoolean: Ref<boolean> = ref(true);
const sampleId: Ref<string> = ref('');
const sampleLists: Ref<Sample[]> | Sample[] = ref([]);
</script>


원시형 데이터는 ref로 이용하여 반응형 데이터를 관리 (Object 와 같은 타입도 선언 가능합니다. <타입의 영향을="" 받지="" 않음="">)

데이터 접근 방식 : isLoading.value, recipeId.value 와 같이 .value를 통하여 접근할 수 있습니다.

value 값을 변경하거나 가져올 때도 동일하게 .value로 접근하면 됩니다.

💡 원시형 데이터

boolean, number, string, undefined, null, symbol


2. Reactive를 사용한 방법

<script setup lang='ts'>
import { reactive } from "vue";

interface Sample {
  id: string;
  password: string;
}

interface State {
  sampleBoolean: boolean;
  sampleId: string;
  sampleLists: Sample[];
}

const state: State = reactive({
  sampleBoolean: true,
  sampleId: '',
  sampleLists: [],
});
// state 객체에서 꺼내서 사용 가능
const { sampleBoolean, sampleId, sampleLists } = state;

// ref 와 다르게 .value로 값을 꺼낼 필요가 없음
const onClickHandler = () => {
  alert(`${sampleBoolean} - ${sampleId} - ${sampleLists}`);
}
</script>

참조형 데이터는 Reactive 를 사용

Vue2에서 사용하는 data() 내에 선언하는 방식과 비슷합니다.

💡 참조형 데이터

Array, Object, Map, Set 등


Computed 선언 방식 차이


Vue2 에서의 방법

<script lang="ts">
export default {
  computed: {
    isLogin: function() {
      return this.$store.getters["utilModule/isLogin"];
    }
  }
}
</script>

<script lang="ts">
import {Component, Vue} from "vue-property-decorator";

@Component({})
export default class SampleClass extends Vue {
  get isLogin() {
    return this.$store.getters["utilModule/isLogin"];
  }  
}
</script>


Vue3 에서의 방법

<script lang="ts" setup>
import { computed, ComputedRef } from "vue";
import store from "@coponents/store";

const isLogin: ComputedRef<boolean> = computed(() => store.getters["utilModule/isLogin"]);
</script>

vue 에서 computed 를 import 받아 사용합니다.
또한 ComputedRef Type을 지원하고 있어 타입을 지정해 줄 수 있습니다.


Watch / Props 선언 방식 차이


Vue2 에서의 방법

<script lang="ts">
import { Prop, Watch, Vue, Components } from 'vue-property-decorator';

@Components({})
export default class ClassComponent extends Vue {
  @Prop({ default: false }) readonly isOpen: boolean;
  
  sampleText = '';
  
  @Watch('route')
  watchProps() {
    this.sampleText = '';
  }

 
}
</script>


Vue3 에서의 방법

<script lang="ts" setup>
import { ref, Ref, defineProps, withDefaults, watch } from "vue";
import {useRoute} from 'vue-router';

const route = useRoute();
const sampleText: Ref<string> = ref('');

const props1 = defineProps({
  isOpen: {
    type: boolean,
    required: true,
  }
}) // default 값 없음

const propsDefault = withDefaults(defineProps<{ isOpen: boolean }>(), { isOpen: false })

watch(route, () => sampleText.value = '', { deep: true, immediate: false })
</script>

vue에서 필요한 함수를 import 하여 사용합니다.

defineProps 함수를 사용하여 Props를 선언하고,
withDefaults 함수를 사용하여 Prop의 기본 값을 설정할 수 있습니다.

💡 **deep이나 immediate이란?**    

deep: 지정한 속성안에 있으면, 그 속성 또한 감시 대상으로 지정
immediate: 페이지를 읽어들일 때에도 watch를 실행

기본적으로 watch는 지정한 속성만 감시하는데 deep : true를 줬을 경우 지정한 값의 내부안의 값 또한 감시 대상으로 올립니다.

페이지가 실행되는 시점에는 watch가 동작하지 않습니다.


Emit 선언 방식 차이


Vue2 에서의 방법

<script lang="ts">
import { Vue, Emit, Component } from 'vue-property-decorator';

@Component({})
export default class SampleComponent extends Vue {
  isOpen = false;
  
  // 1. Decorator 사용
  @Emit('closeModal')
  closeMenu() {
    return !this.isOpen;
  }

// 2. Vue 객체 이용 
  closeMenu() {
    return this.$emit('closeModal', !this.isOpen);
  }
}
</script>

Vue3 에서의 방법

<script lang="ts" setup>
import { defineEmits } from "vue";

// emit Event 정의
const emit = defineEmits(['closeModal']);

// 정의한 이벤트 호출
const sendDataToParentComponent = (isOpen: boolean) => {
  emit('closeModal', !isOpen);
};
</script>


Router / Route 선언 방식 차이


Vue2 에서의 방법

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator';

@Component({})
export default class SampleComponent {
  
  sampleRouterFunction() {
    //router
    this.$router.push('/');
    
    // route
    const { title } = this.$route.query as { title: string };
  }
}
</script>


Vue3 에서의 방법

<script lang="ts" setup>
import { useRouter, useRoute } from "vue-router";

const router = useRouter();
const route = useRoute();

const redirect = (): void => {
  router.push('/');
}

const { title } = route.query as { title: string };
</script>

useRouter() useRoute() 훅을 호출하여 라우터 기능을 사용할 수 있습니다.


Store 선언 방식 차이


Vue2 에서의 방법

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator';

@Component({})
export default class SampleComponent extends Vue {
  sampleText = '';
  
  mounted() {
    this.$store.dispatch('sampleStore/saveText', this.sampleText)
  }
}
</script>

Vue3 에서의 방법

<script lang="ts" setup>
import {onMounted, ref, Ref} from "vue";
import store from '@/store';

const sampleText: Ref<string> = ref('');

//mount hook import
onMounted(() => {
  store.dispatch("sampleStore/saveText", sampleText.value);
});

</script>


Store import 시 주의할 점

<script lang="ts" setup>
import { useStore } from "vuex";

const store = useStore();

store.dispatch("sampleStore/saveText", 0);
</script>

setup 함수 내에 useStore() 함수를 호출할 경우 콘솔에서 아래와 같은 경고 문구를 볼 수 있습니다.

❗ vue warn inject() can only be used inside setup() or functional components

Vuex 에서 store 객체를 바로 호출하기 보다는 createStore() 이용하여 만들어진 store를 호출하는 방식으로 변경하면 경고 문구는 사라지게 됩니다.

// src > stroe > index.ts
import { createStore } from "vuex";
import createPersistedState from "vuex-persistedstate";

export default createStore({
  plugins: [
    createPersistedState({
      paths: [],
    }),
  ],
  modules: {},
});