Vue3의 출시일은 2020년 9월 18일입니다. 2020년에 SSAFY를 진행하면서 Vue2기반의 프론트엔드 개발을 맡아 진행했었습니다. 그 후에 스타트업에 입사하게 되면서, 런칭 전인 하이브리드 앱의 개발을 맡게 되었습니다. 당시 회사에서는 Vue3를 사용해 로그인 페이지와 기본 틀들이 구현이 되어 있어서 자연스럽게 Vue3 기반으로 개발하게 되었습니다. 당시에는 안정화 버전이 나오지 않아서 Vue2를 사용하는게 더 좋았을 것 같기도 합니다😂
이번 게시글은 Vue3와 Vue2의 차이점에 대해 정리해보려 합니다.
현재는 Vue3가 기준 버전이 되면서 공식문서의 주소도 변화했다고 합니다!
1. 사라진 eventBus
스타트업 기술면접에서 Vuex가 아닌 다른 데이터 전송 방법이 있었는데 왜 사용하지 않았는가에 대한 질문을 받은 적이 있습니다. 이는 Vue2의 Event Bus에 관한 내용이었고 당시 Vue3에 Event Bus가 아직 있는줄 알았습니다. 그런데 Vue3부터는 event bus를 공식적으로 지원하지 않고 있습니다
Event Bus란, 자식에서 부모로 또는 그 반대로 이벤트를 손쉽게 발생시키기 위해 사용하는 것입니다.
하지만 Vue에서도 Event Bus를 공식적으로 추천하지 않습니다. 공식문서에 Event bus에 관한 언급이나 예제가 별로 없습니다. 공식문서에서는 Vuex를 사용하는 방법을 추천하고 있습니다.
[Event Bus의 장단점]
1. 부모-자식 사이가 아니어도 사용이 가능하다.
2. 서로 떨어져있는 컴포넌트 간 통신을 가능하게 하지만 컴포넌트에서 매번 listener등록과 listener제거를 해줘야한다
3. 어디서 호출하는지 파악하기 쉽지 않기 때문에 구조 파악이 어렵다.
[Vuex의 장점]
1. Vue.js의 상태관리 패턴이자 라이브러리
2. 직관적이어서 구조 파악이 쉽다
3. Redux와 같이 Flux 패턴을 적용해 단방향 데이터 흐름을 채택한 구조다
3-1. actions => mutations => state 구조를 가지기 때문에 어디서든 actions를 이용해 call하고 mutations로 data를 저장,각 컴포넌트에서 data를 꺼내 쓰는 구조다.
3-2. 이 패턴만 알고있으면 로직의 흐름을 파악하기 쉽다
[Event Bus와 Vuex 특징]
1. Vuex - store를 사용하든 지역 컴포넌트에서 데이터를 당겨와 EventBus로 작업하든 속도적인 측면에서는 별차이가 없다. (더미데이터로 속도 측정)
2. Event Bus를 사용하면 가독성과 데이터 흐름을 파악하기 어려워져 유지보수가 어려워진다
3. Vue가 무거워질 것을 대비해 사용하지 않을 때는 항상 메모리를 해제할 수 있도록 객체를 초기화해준다.
[Vue2에서 Vue3 마이그레이션시 주의점]
1. $on / $once / $off API를 사용하지 않는다 (events api change : RFC문서)
2. 이벤트 버스 개념은 발행-구독(publish-subscribe) 패턴이기 때문에 Vue3에서는 mitt와 같은 다른 라이브러리를 이용해 이 개념을 사용해도 무관하다
* mitt : 상위의 상위의 상위로 커스텀 이벤트 전송해야 할 경우 유용 (이벤트 버스와 동일 기능) / emitter의 약자
// 이벤트 버스 예시 (Vue 2)
import Vue from 'vue';
const eventBus = new Vue();
// 구독
eventBus.$on('sandwich-made', () => console.log('sandwich made!'));
// 발행
eventBus.$emit('sandwich-made');
// 써드 파티 라이브러리(ex. mitt)를 사용하여 리팩토링
import mitt from 'mitt';
const eventBus = mitt();
// 구독
eventBus.on('sandwich-made', () => console.log('sandwich made!'));
// 발행
eventBus.emit('sandwich-made');
2. 성능향상
[가상 DOM 최적화]
Vue2 렌더링의 가상 DOM설계는 HTML기반 템플릿을 제공하고, 이를 가상 DOM Tree로 반환 후 실제 DOM의 어떤 영역이 업데이트 되어야하는지 재귀적으로 탐색하는 방식이었다.
=> 매 변경을 파악하기 위해 모든 트리를 확인하는 비효율성이 존재했다
Vue는 이같이 불필요한 탐색 제거하고 렌더링 성능 향상시키고자 가상DOM 최적화 작업을 진행했다.
- 템플릿 구문에서 정적요소와 동적요소를 구분, 트리를 순환할 때 동적인 요소만 순환해서 탐색의 최적화를 반영했다.
- 렌더링 시 객체가 여러 번 생성되는 것을 방지하기 위해 컴파일러가 미리 템플릿 구문 내 정적요소, 서브트리, 데이터 객체 등을 탐지해 Renderer 함수 밖으로 호이스팅시켜 객체의 복수 생성을 방지한다.
- 컴파일러가 템플릿 내 동적 바인딩 요소에 플래그를 생성한다.미리 생성해둔 플래그로 필요한 부분만 처리하여 렌더링 속도를 향상시켰다.
[Tree Shaking 강화]
트리쉐이킹은 나무를 흔들어 잎을 떨어뜨리듯 모듈을 번들링하는 과정에서 사용하지 않는 코드를 제거해 파일 크기를 줄이고 로딩 성능을 향상시키는 최적화 방안입니다.
Vue3에서 컴파일러가 실제 사용하는 코드만 import하여, v-model과 같은 양방향 바인딩에서 트리 쉐이킹을 적용해 번들 크기를 절반 이상으로 대폭 줄일 수 있었습니다.
3. Composition API
Vue3에서 Composition API가 등장함에 따라 함수형 프로그래밍 기반의 코드 템플릿 변화가 일어났다.
Composition API는 컴포넌트 내에서 사용하는 코드 구조를 유연하게 구성하여 사용할 수 있도록 Vue3버전에서 추가된 함수기반의 API이다.
나는 Vue3를 사용하지만 따로 Composition APi방식으로 코드를 작성하지 않고 Options API 방식으로 작성했다
[Option API 사용한 이유]
1. 당시에는 Composition API에 대한 질문/답변 글들이 많이 없었다. 그래서 대중적으로 사용하고 있던 Option API 방식을 사용했었다.
2. 일정이 급박하다 보니 이전에 계속 사용했던 코드 방식을 적용해 빠르게 개발하는 것을 우선으로 두었다.
3. 많은 Vue 개발자들이 Composition API보다 Options API로 인해 Vue를 좋아한다는 글을 봤고, 공식문서에서도 이러한 이유로 Options API를 없애지 않겠다는 글을 봤습니다. Vue3에서도 사용해도 무관하고, Typescript가 아닌 Javascript를 사용하고 있었기 때문에 Options API를 사용하기로 했었다.
당시에는 StackOverflow에도 Composition API 해결방법에 대한 명확한 답을 많이 찾아볼 수 없었다. 현재는 Composition API방식을 이전보다는 많이 사용하는 것 같다. 그래서 다음에 Vue3를 사용하게 된다면 적용해보고 싶다.
[Composition API 장단점]
- options에서 data, computed(), methods등 데이터의 변화에 관련된 로직이 각각 흩어져 있다면, Composition API는 setup()이라는 메서드 안에서 이들을 그룹핑하므로 데이터의 흐름을 쉽게 파악하고 유지보수가 용이해진다.
- setup() 메서드는 기존 Vue2 LifeCycle의 beforeCreate, created에 대응하는 일종의 훅 초기화 메서드
- 반복되는 코드들을 별도의 hooks로 모듈화하고 컴포넌트 내에서 import해서 사용할 수 있기 때문에 코드 재사용성 면에서도 유리한 문법이다
- Vue2의 mixin으로 컴포넌트 로직을 어느정도 재사용할 수 있었지만 오버라이딩이나 다중 믹스인을 상속하면 컴포넌트 관리가 어려워지는 단점들도 존재했다.
- Typescript를 택하는 프론트엔드 개발자들에게는 코드를 해석해 나가는 타입 추론이 필요하다. Options API는 타입 추론을 염두해두지 않고 설계했고, 타입 추론을 작동하게 하려면 매우 복잡하게 구현해야한다. 이에 반해 Composition API는 대부분 일반 변수와 함수를 사용하여 타입 친화적이다. Composition API코드는 Typescript와 Javascript에서 거의 동일하게 보여진다.
- Vue- Typescript를 사용하는 사람들은 Composition API를 사용하는 것이 적합하다.
- 더 적은 생산 번들과 더 적은 오버헤드를 가진다. Composition API로 작성된 코드 <script setup>은 Options API에 상응하는 것보다 더 효율적이고 최소화하기 쉽다. 템플릿이 <script setup>의 동일한 범위에 인라인된 함수로 컴파일되기 때문이다.
- Composition API가 더 이상 코드를 각 버킷에 넣도록 안내하는 "가드 레일"을 제공하지 않는 것은 사실이다. 하지만 일반 JavaScript를 작성하는 것과 같이 구성 요소 코드를 작성할 수 있다는 것이 장점이다.
- Options API는 코드를 작성할 때 "생각을 적게" 할 수 있어 많은 사용자가 좋아한다. 그러나 오버헤드를 줄이는데 있어 규정된 코드 구성패턴에 갇히게 되어 대규모 프로젝트에서 코드 품질을 개선하거나 리팩토링하기 어려울 수 있다. 그그에 반해 Composition API는 더 나은 장기 확장성을 제공한다.
[Options 문법 vs Composition API 문법]
이전 게시글 참고 (업데이트 예정)
4. Fragment
Vue2에서는 <template> 내에 단일 태그로 랩핑을 필수적으로 해야했다
Vue 인스턴스를 단일 DOM요소로 바인딩 되어야 하고 여러 DOM노드가 있는 구성 요소를 만들 수 있는 유일한 방법은 기본 Vue 인스턴스가 없는 깆능 구성 요소를 만드는 것이다.
리액트에서도 같은 문제를 확인했고 해결하기 위해 나온 솔루션이 Fragment라는 가상요소이다.
class Columns extends React.Component {
render() {
return (
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
);
}
}
Fragment는 일반 DOM요소처럼 보이지만 가상이며 DOM트리에서 전혀 렌더링되지 않는다. 이렇게 하면 중복 DOM노드를 만들지 않고 구성요소 기능을 단일 요소에 바인딩할 수 있다.
Vue3에서도 <Fragment> 태그를 지원하여 이를 통해 다중루트 노트를 작성할 수 있게 됐다.
<!-- Vue2 -->
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>
<!-- Vue3 -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
5. Teleport
React의 Teleport와 유사한 기능으로 모달이나 알림 등과 같이 특정 컴포넌트가 부모에 상속되어 있으면서도 렌더링되는 위치는 제 2의 루트인게 유리한 경우 Teleport 기능이 유용하다.
이를 통해 기존에 CSS로 조정하거나 인위적으로 엘리먼트 주입하던 방식에서 <teleport> 태그 내부의 HTML요소를 특정 태그로 옮겨 렌더링 할 수 있게 되었다
아래 예시와 같이 다른 곳에 렌더링할 요소를 <teleport>로 감싸고, to 속성에 렌더링할 위치의 id(혹은 class)명을 설정하면 된다. (id면 #. class면 . 붙이기)
<button @click="open = true">Open Modal</button>
<Teleport to="body">
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
당시에는 해당 내용에 대해 알지 못했는데 다음에 개발할 때 참고해서 사용해보고 싶다
6. Suspense
<Suspense> 컴포넌트 역시 React에서 지원하는 컴포넌트의 일종이다. 로드 상태를 렌더링할 수 있다.
컴포넌트 내 비동기 호출이 종료될 때까지 템플릿 내 fallback으로 노출할 구문, 그리고 에러가 발생했을 때 노출할 구문 등을 설정하기 위한 기능이다
<template>
<div v-if="error">
// error report...
</div>
<Suspense v-else>
<template #default>
<UserProfile />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<script>
import { onErrorCaptured } from 'vue'
setup() {
const error = ref(null)
onErrorCaptured(e => {
error.value = e
return true
})
return { error }
}
</script>
이처럼, <Suspense> 컴포넌트로 #default(기본 노출 엘리먼트), #fallback(대체 노출 엘리먼트) 를 설정할 수 있다.
또한, onErrorCaptured() 라이프사이클을 통해 error가 발생하는 경우 노출할 엘리먼트 역시 설정하곤 한다.
[출처]
- https://abangpa1ace.tistory.com/m/243
- https://abangpa1ace.tistory.com/239?category=948651
- https://ui.toast.com/weekly-pick/ko_20200804
- https://github.com/vuejs/composition-api
- https://stackoverflow.com/questions/68611657/do-i-have-to-use-the-composition-api-in-vue-3-or-can-i-still-do-things-the-vue%EF%BB%BF
- https://vuejs.org/guide/extras/composition-api-faq.html
- https://pozafly.github.io/tripllo/(13)Vuex-and-eventBus/
- https://hannut91.github.io/blogs/flux
- https://kdinner.tistory.com/53
- https://joshua1988.github.io/web-development/vuejs/vuex-start/
- https://gobae.tistory.com/122
- https://vueframework.com/docs/v3/ko/ko-KR/guide/migration/render-function-api.html#%E1%84%85%E1%85%A6%E1%86%AB%E1%84%83%E1%85%A5-%E1%84%92%E1%85%A1%E1%86%B7%E1%84%89%E1%85%AE-%E1%84%8C%E1%85%A5%E1%86%AB%E1%84%83%E1%85%A1%E1%86%AF%E1%84%8B%E1%85%B5%E1%86%AB%E1%84%8C%E1%85%A1
- https://tech.osci.kr/2022/05/17/%EC%9E%91%EA%B3%A0-%EC%86%8C%EC%A4%91%ED%95%9C-vue%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90%EA%B0%9C%EC%9A%94/
- https://www.webmound.com/composition-api-vs-options-api-in-vue-3/
- https://k1005.github.io/2021/08/29/Vue-3%EC%9D%98-%ED%9D%A5%EB%AF%B8%EB%A1%9C%EC%9A%B4-%EC%83%88-%EA%B8%B0%EB%8A%A5/#fragments
- https://vuejs.org/guide/built-ins/teleport.html