지금은 React를 주로 사용하고, 좋아하지만 예전에 웹개발을 처음 배우기 시작하면서 Vue2를 먼저 배우고 사용했었다. 2023년을 마지막으로 Vue2 지원이 종료되는 가운데 React를 계속하던 개발자의 관점에서 React와 Vue.js와 비교하고 React와는 비슷하면서도 색다른 Vue3의 매력을 정리해보려고한다
Frontend Frameworks 사용 비율: State of JS
Vue.js는 React, Angular와 더불어 굉장히 많은 사람들이 사용하고 있으며 npm 기준으로
Vue가 React 다음으로 굉장히 많은 다운로드 수를 보이고 있다.
여전히 많은 사람들이 관심갖고 이미 사용하고 있는 Vue.js의 3버전을 최근에 공부하면서 Composition API를 사용하고, 타입스크립트를 적용해 보면서 내가 느낀점과리액트와의 차이점을 비교해 보려고한다
Vue.js
는 어플리케이션 레벨의 개발을 지원하는 자바스크립트 프레임워크이다.
아무래도 Javascript
에 대한 지식이 많이 요구되는 React
랑 비교했을 때 진입장벽이 더 낮고 초기 개발 생산성이 높아서 처음개발을 시작할 때 리액트와 더불어 많이 선호된다.
vue3에서는 vue2에서 플러그인 형태로 지원되던 Composition API가 vue3에서 공식 API로 채택되었고 아무래도 Composition API가 React hooks의 영향을 받아서 인지 Composition API는 굉장히 React hooks와 유사하며 이를통해 컴포넌트의 재사용성을 높일 수 있게 되었다
/* Counter.tsx */
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
return <div>{count}</div>;
};
<!-- Counter.vue -->
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<div>{{ count }}</div>
</template>
<style lang="css" scoped></style>
하나의 컴포넌트와 관련된 코드(HTML, CSS, JS)
를 하나의 .vue
파일에서 관리하는 방법을 싱글파일 컴포넌트 라고한다. 다른 프레임워크와 다른 Vue.js
의 특징 중 하나이고 이러한 .vue
파일은 웹팩 로더의 한 종류인 vue-loader
에 의해서 HTML, CSS, JS
로 분리된다
React 문서를 보면 Reconciliation(재조정)
라는 단어가 유독 많이 나오는데
Vue.js 문서에서는 Reactivity(반응성)
라는 용어를 많이 강조한다.
Vue.js의 핵심 컨셉은 reactivity
를 통해서 상태변화에 반응하는 것인데 vue3
에서는 자바스크립트의 Proxy
객체를 활용하여 reactivity
를 구현 하고있다. 쉽게말해 ref
나 reactive
와 같은 Vue의 반응성 API를 사용하면 변수에 reactivity
가 주입되어 해당 변수에 대해서 변경내용을 추적하고 변화를 바탕으로 Proxy
객체가 반응해서 UI를 변경한다
// app.component.ts
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div>{{ count }}</div>
`,
styleUrls: [],
})
export class Counter implements OnInit {
count: number = 0;
constructor() {}
ngOnInit(): void {}
}
여담으로 재밌는점은 최근에 Angular
를 잠깐 해봤는데 Vue.js
랑 디테일한 문법이 진짜 유사하고, 확실히 Vue.js
가 Angular
의 영향을 많이 받은 것 같다.
<script lang="ts">
export default {
props: [],
data() {
return {};
},
methods: {},
computed: {},
/* .... */
};
</script>
기존의 Vue2에서 기본적으로 사용되는 API로 컴포넌트 옵션들이 하나의 객체에 몰려 있는 수직적인 구조이다. 사실 이러한 Options API도 충분히 훌륭하고 잘 동작하지만 어플리케이션 규모가 커지고 복잡해지면서 유지보수성, 타입 안정성 등에서 어려운 점이 있었다
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
setup() {
const count = ref(0);
return { count };
},
});
</script>
<template>
<div>{{ count }}</div>
</template>
Composition API는 Vue 3에서 추가된 API로, 코드를 더 모듈화하고 재사용성을 높이기 위한 목적으로 도입되었다. 확실히 좀 더 자바스크립트 스러워 졌다라고 볼 수 있다
또한 vue3에서 타입스크립트로 API를 작성해서 타입추론이 용이해졌고 리액트의 커스훅처럼 커스텀 Composition을 만들어서 재사용성이 높아졌다
둘 다 Virtual DOM
을 사용한다지만 디테일하게 보면 렌더링의 큰 차이가있다
React
의 함수형 컴포넌트는 그 자체로 render()
함수이기떄문에 state, props와 같은 반응형값이 바뀌었을 때 함수 자체가 다시 실행되면서 리렌더링이 발생하고 변경된 부분만을 업데이트하는 Reconciliation
이 일어나서 실제 DOM에 반영한다
Vue
에서는 ref
, reactive
와 같이 Reactivty
가 주입된 데이터를 추적하고 해당 변경 사항을 감지하여 변경된 부분만을 가상 DOM에 반영한다
실제로 vue3
에서는 자바스크립트의 Proxy
객체로 데이터를 래핑해서 변경을 추적한다
즉,
react
는 렌더링간에 컴포넌트가 다시 실행되면서 이전 렌더링과 현재렌더링간의 차이를 계산해서 DOM에 업데이트하고vue
는 데이터의 변경을 추적하고 변경된 데이터의 부분만 실제 DOM에 업데이트하는 것
/* App.jsx */
const App = () => {
const [firstName, setFirstName] = useState("jiheon");
const [lastName, setLastName] = useState("kim");
const fullName = firstName + lastName;
return <div>{fullName}</div>;
};
React를 하던 사람이라면 어떤 변수가 기존의 state를 의존하고 있을 때 이 값은 새로운 state로 만들지 않고 그냥 컴포넌트 안에서 로컬변수로 사용할 것이다 왜냐하면 state가 변경되면 다시 컴포넌트 렌더링이 일어나면서 함수가 실행되기 때문에 fullName 변수가 다시 정의되기 때문
<script setup lang="ts">
import { ref } from "vue";
const firstName = ref("jiheon");
const lastName = ref("kim");
const fullName = firstName.value + lastName.value;
</script>
<template>
<div>{{ fullName }}</div>
</template>
하지만 이건 어떨까? 처음 마운트될 때는 화면에는 잘 나오겠지만 fullName
이라는 변수는 Reactivty
가 주입되지 않아 이후에 name이 변경됨에 따라 반응하지 않는다.
const fullName = computed(() => firstName.value + lastName.value);
따라서 computed
함수를 통해서 데이터에 Reactivty
를 주입해줘야한다
⭐ vue의 computed
가 vue와 react의 렌더링 방식을 비교하는 좋은 예시인 것같다
앞서도 얘기했지만 ref
함수는 리액트의 useState()
와 매우 유사하며, 리액트 useRef()
훅에서 데이터에 ref.current
로 접근하는 것처럼 vue3의 ref
는 ref.value
로 접근한다.
아래 예시에서 myCounter
데이터에 접근하려면 myCounter.value
로 접근 해야하는데 이건 script
태그 안에서만 적용되고 template
에서는 그냥 myCounter
만 적어도 된다
<script setup lang="ts">
import { ref } from "vue";
const myCounter = ref({ count: 0 });
const handleIncrement = () => {
myCounter.value.count++;
};
</script>
<template>
<div>{{ myCounter.count }}</div>
<button @click="handleIncrement">++</button>
</template>
리액트는 이전 상태값과 이후 상태값을 비교해서 다른 경우에만 업데이트를 한다. 따라서 리액트의 state
는 불변은 지켜야하고 state
가 배열이나 객체의 참조 타입의 경우 상태 변경은 기존값의 수정이 아닌 새로운 객체를 생성 해야한다.
반면 Vue.js
는 객체의 속성을 직접 수정하는 것이 가능한데 이는 객체의 속성 변경을 감지하고 리렌더링을 트리거하는 reactivity
시스템을 갖고 있기 때문이다
// useCounter.ts
import { ref } from "vue";
export const useCounter = () => {
const counter = ref(0);
const increment = () => counter.value++;
const decrement = () => counter.value--;
return { counter, increment, decrement };
};
<!-- Counter.vue -->
<script setup lang="ts">
import { useCounter } from "../hooks/useCounter";
const { counter, increment, decrement } = useCounter();
</script>
<template>
<div>{{ counter }}</div>
<button @click="increment">++</button>
<button @click="decrement">--</button>
</template>
리액트의 커스텀 hook
처럼 Vue에서도 커스텀 composition
을 만들 수 있는데 보다시피 커스텀 훅을 사용해 봤다면 어렵지 않게 사용할 수 있고 이름또한 use
라는 접두사를 보편 적으로 사용하고 있어서 거부감이 없이 사용했던 것 같다. 이러한 커스텀 컴포지션을 이용해
컴포넌트에서 상태 관리, 사이드 이펙트 처리 등의 로직을 분리하고 재사용할 수 있다.
Vue3
는 옵션 API
와 컴포지션 API
는 서로 호환될뿐더러 동시에 같이 사용이 가능하다
그래서 간단한 어플리케이션에서는 간단 명료한 옵션API
를 사용하고 어플리케이션이 충분히 복잡해졌을 때 컴포지션 API
을 도입해서 로직을 분리하고 코드의 재활용을 고려해 보면 된다.
Vue3에서 컴포지션 API
가 표준으로 추가되었지만 어디까지나 컴포지션 스타일은 필수가 아니기떄문에 상황에 따라서 옵션 API
와 컴포지션 API
또는 섞어서 적절하게 사용할 수 있는데 이러한 점이 Composition API
의 매력인 것 같다
<script lang="ts">
import { ref, defineComponent } from "vue";
export default defineComponent({
setup() {
const count = ref(0);
return { count };
},
methods: {
increment() {
this.count++;
},
decrement() {
this.count--;
},
},
});
</script>
<template>
<div>{{ count }}</div>
<button @click="increment">++</button>
<button @click="decrement">--</button>
</template>
Vue 3의 컴포지션 API는 Vue 2의 옵션 API와 굉장히 높은 수준의 호환성을 갖고 있고
심지어 Vue3로 마이그레이션 한다고한들 기존 옵션 API에서 혼합해서 사용할 수 있기 때문에 굉장히 유연하다고 느꼈다.
반면 리액트는 훅이 도입되었지만 함수형 컴포넌트와 클래스 컴포넌트는 완전 대체되기 어려운 경우가 있어서 이런 점에서 볼 때 리액트보다 더 유연하다는 생각이 조금 들었다
Vue3
의 Composition API
에서 watch
는 reactivty
가 주입된 변수를 추적하고 변경되었을 때 추가적인 로직을 실행하는 주는 함수이다 리액트의 useEffect
훅과 매개변수 순서만 다르고 사용법은 똑같다
<script setup lang="ts">
import { ref, watch, watchEffect } from "vue";
const count = ref(0);
const increment = () => count.value++;
watch([count], () => {
const div = document.createElement("div");
div.textContent = count.value.toString();
document.body.appendChild(div);
});
watchEffect(() => {
//...
});
</script>
<template>
<div>{{ count }}</div>
<button @click="increment">++</button>
</template>
재미있는 건 watch
말고 watchEffect
라는 메소드도 존재하는데 watchEffect
는 watch
와 매우 유사한데 의존성 배열을 넘기지 않으며 watchEffect
내부에 reactivity
가 주입된 값들 전부를 추적하고 그 값들중 하나라도 변하면 effect 함수가 실행된다.
또한 useEffect
와 마찬가지로 두 메소드 다 처음 마운트될 때 한번은 무조건 실행된다.
확실히 React는 JSX
를 사용하여 마크업을 작성하기 때문에 훨씬 자바스크립트스러운 반면 Vue는 앵귤러와 유사하게 Template
을 기반으로 마크업을 작성하기 때문에 좀 더 HTML
스럽다
하지만 그렇기 때문에 Vue에서 제공하는 템플릿 문법들은 굉장히 재미있고 흥미로운데 리액트와는 색다른 매력이 있다
<template>
<div v-if="isLoading">loading...</div>
<div v-else="isLoading">contents</div>
<ul v-for="item in items">
<li v-bind:key="item.id">{{ item.name }}</li>
</ul>
<input type="text" v-model="input">
</template>
Directives
는 Vue.js에서 v-
접두사가 있는 특수 속성으로 <template>
에서 사용가능하다
v-if, v-else, v-for, v-bind, v-on:[evnet]
등의 다양한 디렉티브들이 존재한다
:
@
#
React
에서는 JSX
안에서 표현식만을 사용해야하므로 배열 순회 및 분기 처리 시에는 JavaScript
의 배열 메소드나 삼항 연산자 등을 사용해야한다 하지만 Vue
에서는 템플릿에서 사용 가능한 다양한 디렉티브를 제공하여, 추가적인 JavaScript 문법을 작성하지 않아도 편리하게 배열 순회나 분기를 처리 등을 간편하게 작성하게 도와준다
<template>
<a @click.stop="doThis"></a>
<form @submit.prevent="onSubmit"></form>
<input @keyup.enter="submit" />
<input @keyup.alt.enter="clear" />
</template>
이벤트에서 preventDefault
혹은 stopPropagation
등을 호출하는 것은 흔하기 때문에
Vue
는 이벤트에서 이러한 함수들을 처리하기 위한 다양한 이벤트 수식어를 제공한다
리액트의 children
같은 개념으로 <FancyButton>
사이에 들어가는 모든값들을 children
으로 내려주고 자식 컴포넌트에서 props.children
으로 받았던 것 처럼
Vue에서는 자식컴포넌트에서 <slot>
태그로 children
을 받을 수 있다
React에서는 children
을 자유롭게 다루려면 Children API
를 사용해야하는데
Vue에는 v-slot
이라는 디렉티브가 존재하여 slot
에 이름을 부여하고 여러 개의 slot
을 간단하게 사용할 수 있다
// React
<MyContext.Consumer>
{value => (
<p>{value}</p>
)}
</MyContext.Consumer>
// Vue
<Child v-slot="slotProps">
{{ slotProps.text }}
{{ slotProps.count }}
</Child>
또한 리액트에서는 자식 컴포넌트의 데이터를 부모컴포넌트의 children에서 사용하려면
Context API
의 Consumer
와 같은 Render Props
패턴을 사용해야하는데
Vue에서는 scoped slot
을 제공하여 자식 컴포넌트에서 <slot>
태그에 바인딩 한 값을 부모 컴포넌트의 contents 내부에서 사용이 가능하다
Vue.js 자체에서 제공하는 몇 가지 특별한 빌트인 컴포넌트가 존재한다
예를 들어 트랜지션 및 애니메이션 작업에 도움이 되는 빌트인 컴포넌트인 <Transition>
컴포넌트를 제공하는데 <Transition>
태그안에서 v-show 혹은 v-if를 통해 조건부 렌더링을할때 간단하게 트랜지션을 제공한다. (추가적인 스타일 코드가 필요하다)
<Transition>
<p v-if="show">hello</p>
</Transition>
재밌는 건 <Teleport>
라는 빌트인 컴포넌트를 제공하는데 컴포넌트를 DOM트리 내 다른 위치에서 렌더링할 수 있다 리액트의 portal
과 동일한개념이며 빌트인 컴포넌트라서 그런지 사용방법이 훨씬 간단했다
이러한 Teleport
은 다른위치에서 렌더링하기 때문에 컴포넌트 구조상으로는 원래위치 이지만 DOM 트리상에서는 root 컴포넌트 밖으로 뺄 수 있으며 중요한 건 원래 컴포넌트에 있던 스타일의 영향을 받지 않는다는 특징이있다 왜냐하면 root 밖으로 텔레포트 시켰기 때문
<Teleport to="body">
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
이번에 Vue3에 관심이 생겨서 Composition API에 대해서 공부하면서 Vue3에 대한 새로운 매력을 느꼈고 이 블로그에 내용을 정리하면서도 즐겁게 Vue.js를 했던 것같다 비슷하면서도 다른 이 두 개를 비교하면서 내 시야가 좀 더 넓어진것같고 나중에 기회가 되면 충분히 Vue.js를 고려해 볼 것같다