Vue.js 3.0 무엇이 달라졌는가?

Vue.js 3.0 무엇이 달라졌는가?

2014년 첫 출시된 Vue.js가 2020년 9월, 버전 3.0으로 업그레이드되었습니다. Vue.js 3.0의 개발이 시작된 지 약 2년 만이었습니다. Vue.js는 개발자에게 더 쉽고, 가볍고, 누구나 빨리 배울 수 있는 접근성이 뛰어난 프레임워크라는 방향성을 가지고 개발됐습니다. Vue.js는 기존 HTML 마크업 기반의 템플릿을 그대로 활용하며 CSS를 작성하는 스타일도 기존 문법을 그대로 따릅니다. 이 때문에 프레임워크를 처음 접하는 사용자들이 진입하기에 부담스럽지 않다는 장점이 있습니다.

또한 확장성도 빼놓을 수 없습니다. 제이쿼리(jQuery)처럼 스크립트 태그로 CDN(Content Delivery Network)을 추가할 수 있고 프로그램에 따라 점진적으로 라이브러리를 채택할 수 있습니다. 아울러 Vue.js는 라우팅∙상태 관리∙빌드 도구 등 공식적으로 지원하는 라이브러리와 패키지를 통해 배포합니다. 이러한 확장성은 웹 개발을 더욱 단순하고 쉽게 만들어 줍니다.

[그림 1] Vue.js의 공식 로고

본 아티클에서는 Vue.js 3.0이 기존 버전의 한계점을 어떤 방식으로 해결했으며 개발자 측면에서 어떤 점이 달라졌는지 가장 중요한 변화를 기준으로 소개해보겠습니다.

성능 향상

가상 DOM 최적화
기존 Vue.js의 렌더링을 위한 가상 DOM(Virtual Document Object Model) 설계는 HTML 기반의 템플릿을 제공하고 이 템플릿 구문을 가상 DOM 트리로 반환한 후 실제로 DOM의 어떤 영역이 업데이트되어야 하는지 재귀적으로 탐색하는 방식이었습니다. 이 작업은 불필요한 탐색이 많이 포함될 수밖에 없었습니다. 그 이유는 무엇일까요? 변경 사항을 파악하기 위해 전체 DOM 트리를 재귀적으로 탐색해야만 하는 상황이 있다고 가정해보겠습니다. 변경이 필요한 부분만 확인하는 것이 아니라 매번 전체 트리를 모두 확인해야 하는 동작은 비효율적입니다. 만약 템플릿 구문에서 정적인(Static) 구문이 대부분을 차지하고 동적 바인딩은 적은 경우라면 그 비효율성은 더욱 커집니다. Vue.js는 이같이 불필요한 탐색을 위한 코드를 제거하여 렌더링 성능을 향상하고자 다음과 같이 가상 DOM의 최적화 작업을 진행했습니다.

첫 번째로 템플릿 구문에서 정적 요소와 동적 요소를 구분하여 트리를 순환할 때 동적 요소만 순환할 수 있도록 했습니다. 미리 구문 내의 정적인 영역을 블록으로 구분합니다. 렌더링할 때 동적 요소가 있는 코드가 영향을 받게 되면 정적인 영역으로 구분해 둔 블록에는 접근하지 않고 동적 요소가 있는 코드에만 접근하여 렌더링 트리의 탐색을 최적화하는 방식입니다.

두 번째로 렌더링 시 객체가 여러 번 생성되는 것을 방지하기 위해 컴파일러가 미리 템플릿 구문 내에서 정적 요소∙서브 트리∙데이터 객체 등을 탐지해 렌더러(Renderer) 함수 밖으로 호이스팅(Hoisting) 하는 것입니다. 이를 통해 렌더링 시 렌더러마다 객체를 다시 생성하는 것을 방지하여 메모리 사용량을 낮추었습니다.

세 번째로 컴파일러가 미리 템플릿 구문 내에서 동적 바인딩 요소에 대해 플래그를 생성합니다. 예를 들면, 특정 요소가 동적 클래스 바인딩을 가지고 있고 정적인 값이 지정된 속성을 갖고 있다면 클래스만 처리하면 됩니다. 따라서 컴파일러가 미리 생성해 둔 플래그로 필요한 부분만 처리하여 렌더링 속도를 향상할 수 있었습니다.

트리쉐이킹 강화
트리쉐이킹(Tree shaking)이란 나무를 흔들어 잎을 떨어트리듯 모듈을 번들링하는 과정에서 사용하지 않는 코드를 제거하여 파일 크기를 줄이는 최적화 방안을 의미합니다. Vue.js 3.0은 템플릿 컴파일러가 실제 사용하는 코드만 임포트(Import) 하도록 했습니다. 양방향 데이터 바인딩을 지원하는 v-모델 디렉티브와 같은 대부분의 사용자 정의 기능에서 트리쉐이킹이 가능했기 때문에 Vue.js 3.0에서 이를 강화하여 번들 크기를 절반 이상으로 대폭 줄일 수 있었습니다.

Chrome mount update, Chrome update update best, Chrome memory Vu2tempbte+wtii 93.46 23.17 1644 11,9 Vue3Ienate.wth 77.43 9.18 169 4,5 lmprov.m.i over v2 (in-bro*w compited) 2070% 152(0% 113.78% 42.18% Vu. 2 r.nderfn (manual h. no this access) 909 1575 818 102 VueS rendefn (manual h, no this access) 76.46 7.56 5.33 7.4 lrnproyern.i ovar v2 (manual rendar *ia1cton) 18.89% 10633% 53.47% -27,45% Vu 2 templat. no with 97.44 13.18 829 9.9 WeStemtenowth 62.82 564 378 4.5 lmprov.mardovarv2(pre-compNd) 55.11% 13369% 119.31% .54.56% Vue 2mw (no resctv state) 88.67 1227 199 8.6 We 3 raw (no rescv, 1*5ta) 47.6 3.37 238 3.7 lnWroverner ovar v2 (raw, no resct? state) 86.28% 26409% 235.71% -56.98%
[표 1] Vue 2 vs Vue 3 in Chrome 성능 비교 (출처: https://geckodynamics.com)

Vue.js 3.0 개발팀은 릴리즈 노트에서 가상 DOM 최적화, 트리쉐이킹 강화 등을 통해 이전 버전에 비해 번들 크기가 최대 41% 감소하였고, 초기 렌더링은 최대 55% 빠르며 업데이트와 메모리 사용량은 최대 133% 향상되었다고 밝혔습니다. [표 1]은 실제로 2.0 버전과 3.0 버전을 비교하여 크롬(Chrome) 환경에서 성능 테스트를 해 본 사용자들의 리포트입니다. Vue.js 개발팀이 밝힌 바와 같이 이전 버전에 비해 눈에 띄는 성능 향상을 보이고 있습니다. 성능이 필수적인 프론트엔드 프레임워크에서 더 빠르고 더 작은 프레임워크로 나아가고자 하는 방향성이 돋보이는 업데이트라고 생각됩니다.

컴포지션 API의 등장

컴포지션 API(Composition API)는 함수 기반의 API로 Vue.js 3.0의 핵심이라고 할 수 있습니다. 기존 Vue.js의 한계점에 대해 먼저 알아보고, 이를 컴포지션 API를 활용하여 어떻게 해결할 수 있는지 살펴보겠습니다.

① 프로젝트 규모에 따라 커지는 코드의 복잡성
Vue.js의 사용자층이 늘어감에 따라 대규모 프로젝트에 대한 지원이 필요해졌습니다. 여러 개발자들이 하나의 프로젝트에 오랜 기간 매달리고 유지보수하는 경우 이전 버전의 Vue.js는 한계점이 분명히 드러났습니다. 하나의 컴포넌트를 개발하는 데 여러 기능이 함께 들어가야 하는 경우를 생각해보겠습니다. 하나의 기능에 대해 Vue 인스턴스의 데이터 영역에 변수 선언이 들어갈 것이고, 메소드 영역(Methods)에는 함수가, 컴퓨티드 영역(Computed)에는 계산 속성이 들어갈 것입니다. 이 외에도 라이프사이클 훅(Hook)이나 와치(Watch) 등 더 많은 부분으로 로직이 분리됩니다. 이렇게 여러 개의 기능이 나누어져 코드 안에 뒤섞이게 되면 컴포넌트의 크기가 커질수록 복잡성이 증가합니다. 복잡한 코드는 가독성이 좋지 않고 논리적으로도 읽기가 힘들어집니다.

Options API와 Composition API의 코드 샘플로 Options API의 코드가 훨씬 복잡하다 [그림 2] Options API vs Composition API (출처 : https://v3.vuejs.org)

컴포지션 API(Composition API)는 모든 코드를 독립적으로 정의할 수 있습니다. 각 기능을 함수로 묶어 처리하기 때문에 특정 기능의 유지보수를 위해 해당 함수만 확인하면 됩니다. 이전 버전에서 제공하던 옵션 API(Options API)는 자바스크립트 프레임워크를 처음 접하는 사람이라면 더 직관적으로 와닿을 수도 있습니다. 그러나 컴포넌트의 크기가 커질 경우 하나의 기능에 대한 코드가 여러 부분에서 작성되어 파편화되면서 유지보수가 어려워집니다. 따라서 컴포지션 API를 통해 기능을 모듈화하여 구성하는 것이 더 나은 선택이 될 수 있습니다.

② 코드 재사용이 어려움
이전 버전의 Vue.js에서도 믹스인(Mixins)이나 슬롯(Slots) 등으로 컴포넌트 코드를 재사용할 수 있었습니다. 컴포넌트 로직을 재사용하기 위해 주로 이용되었던 믹스인은 한계점이 존재했습니다. 프로젝트가 커지고 믹스인을 사용해 다중으로 상속하게 되면 컴포넌트 관리가 어려워졌습니다. 예를 들어, 각 기능의 속성이 병합될 때 이름 충돌이 발생하기 쉬워 개발자 측면에서 네이밍에 대한 명확한 컨벤션이 필요했습니다. 또한 매개변수를 믹스인을 통해 전달할 수 없어 코드 재사용 시 유연성이 떨어졌습니다. 여기서 컴포지션 API를 사용하면 인스턴스의 특정 기능 단위로 모듈화된 로직을 여러 컴포넌트에서 재사용할 수 있게 됩니다. 이는 대규모 프로젝트에서 로직의 유연성을 높여줍니다.

③ 제한된 타입스크립트 지원
Vue.js 3.0은 타입스크립트를 더욱 적극적으로 지원하기 위해 코드베이스를 타입스크립트로 작성했습니다. 이를 통해 개발자는 Vue CLI로 타입스크립트 또는 자바스크립트를 사용하여 추가 도구 없이 Vue 앱을 생성할 수 있습니다. 물론 이전 버전의 Vue도 타입스크립트를 지원했습니다. 데코레이터를 이용하여 클래스 기반의 API를 선언하거나 Vue.extend로 컴포넌트를 정의해 기존 객체 구조 방식대로 사용할 수 있었습니다. 다만 Vue.js가 기존에 옵션 API를 중심으로 객체 구조 방식을 사용하고 있었기 때문에 타입스크립트를 온전히 사용하기에는 한계가 있었습니다. 타입스크립트의 타입 추론 방식은 타입을 명시적으로 선언하지 않아도 추론이 가능해야 하는데 객체 구조 특성상 개발자가 일일이 타입을 정의해 줘야 하는 상황이 많았기 때문입니다. Vue.js 3.0은 컴포지션 API와 함께 타입스크립트를 사용하기 위해서 컴포지션 API 내부의 setup() 함수에서 자동으로 타입을 추론하기 때문에 사용하기가 훨씬 수월해졌습니다.

그 외 주목할 만한 변화

텔레포트
텔레포트(Teleport)는 리액트(React)에서 기본으로 제공하는 포털(Portals)과 유사한 기능입니다. Vue.js가 기존에 Portal-Vue라는 플러그인을 통해 제공하고 있던 기능이기도 합니다. 다음과 같은 상황에서 텔레포트가 유용하게 사용됩니다. 모달이나 알림 등과 같은 요소를 렌더링하려는 위치가 템플릿 구문이 속하는 컴포넌트와 다른 컴포넌트에 있을 때는 보통 모달이 포함된 컴포넌트를 하나 더 만들어 컴포넌트의 구조를 변경해야 합니다. 다른 태그 위치로 모달의 위치를 조정하는 것을 CSS를 통해 해결하기가 까다로웠기 때문입니다. Vue.js 3.0은 텔레포트를 사용하여 모달 컴포넌트를 분리하지 않고도 "teleport" 태그 내부의 HTML을 특정 태그로 옮겨 렌더링할 수 있게 되었습니다.

프래그먼트
이전 버전의 Vue는 템플릿 구문에 단 하나의 "div" 태그만 허용했습니다. 그 이유는 Vue 인스턴스를 단일 DOM 요소로 바인딩해야 했기 때문입니다. 프래그먼트(Fragment)를 사용하면 다중루트노드를 가질 수 있습니다. 프래그먼트는 실제 DOM 트리에서는 렌더링되지 않는 컴포넌트이기 때문에 중복 DOM 노드가 생기지 않으면서도 다중루트노드를 가질 수 있게 됩니다.

서스펜스
서스펜스 컴포넌트(Suspense component)는 리액트가 지원하던 컴포넌트 종류 중 하나입니다. 서스펜스는 컴포넌트 내에 있는 조건인 Async 구문이 충족되지 않으면 조건이 충족될 때까지 템플릿 내에 Fallback 구문을 렌더링합니다. 컴포지션 API를 통해 Setup() 함수 내에서 외부 API에 접근해 데이터를 가져오는 비동기 작업을 수행하면 데이터를 모두 가져올 때까지 로딩 표시를 해야 할 수 있습니다. 이럴 때 서스펜스를 사용해 컴포넌트를 감싸면 대체할 템플릿 구문을 렌더링할 수 있습니다. 그렇다면 데이터를 가져오는 도중 오류가 발생하면 어떻게 될까요? Vue.js 3.0은 새로운 라이프사이클 훅(Lifecycle Hooks)인 OnErrorCaptured를 제공합니다. 오류가 발생한 경우 OnErrorCaptured를 사용하여 에러를 잡아낼 수 있고 해당 에러에 대한 처리 구문을 Fallback 구문 대신 표시할 수 있습니다.

리액티비티 API
이전 버전의 Vue.js는 인스턴스 내부에 오브젝트를 선언하고 새로운 속성을 추가하는 것을 감지할 수 없었습니다. 그래서 기존에는 Vue.set 메소드를 사용하여 기존 객체에 반응성을 부여했습니다. Vue.js 3.0은 이러한 데이터 반응성을 해결하기 위해 리액티비티 API(Reactivity API)를 지원합니다. 객체에 반응성을 추가하기 위해서는 리액티브 메소드(Reactive method)를 사용하면 됩니다. 단순값이라면 Ref 메소드를 사용합니다. 이 외에도 Readonly, ToRef 등 반응성을 지원하는 여러 API가 추가되었습니다. 세부사항은 Vue.js 3.0 공식 사이트(https://v3.vuejs.org/guide)에서 확인할 수 있습니다.

마치며

지금까지 보다 다양한 개발자 경험을 제공하려는 Vue.js 3.0의 개선 사항을 엿볼 수 있었습니다. 기존 Vue.js의 한계점을 극복하기 위한 노력도 돋보였지만 타사의 라이브러리나 프레임워크에서 제공하는 기능을 탑재한 것이 의미심장 했습니다. 그 중 가장 큰 변화로 다가오는 것은 역시 컴포지션 API의 등장입니다. 이 글을 읽는 누군가는 그럼 이제 옵션 API로 개발할 수 없게 된 것인지 당황해 할 수 있을 것입니다. 그러나 Vue.js 3.0에서도 옵션 API를 여전히 사용할 수 있으니 큰 걱정은 하지 않아도 됩니다.

현재 진행 중인 프로젝트의 문제와 특징을 파악하고 이를 해결하기 위해 어떤 솔루션이 적합할지 고민해 보는 시간을 가져 보는 것은 어떨까요? Vue.js 3.0과 함께 보다 효율적이고 유지보수가 쉬운 프로젝트를 완성할 수 있을 것입니다.

# References
[1] https://github.com/vuejs/vue-next/releases/tag/v3.0.0
[2] https://increment.com/frontend/making-vue-3/
[3] https://v3.vuejs.org/guide/
[4] https://vuejs.org/v2/guide/
[5] https://vueschool.io/articles/vuejs-tutorials/exciting-new-features-in-vue-3/
[6] https://geckodynamics.com/blog/vue2-vs-vue3



▶  해당 콘텐츠는 저작권법에 의하여 보호받는 저작물로 기고자에게 저작권이 있습니다.
▶  해당 콘텐츠는 사전 동의 없이 2차 가공 및 영리적인 이용을 금하고 있습니다.


공유하기 열기
백은제
백은제 IT 테크놀로지 전문가

에스코어㈜ 소프트웨어사업부 개발플랫폼그룹

에스코어에서 UI Dev 플랫폼 개발 및 유지보수를 담당하고 있습니다.