[Vue] React 관점에서 Vue3 바라보기

Jiheon Kim·2024년 1월 7일
4

Vue.js

목록 보기
1/1
post-thumbnail

✨시작에 앞서

지금은 React를 주로 사용하고, 좋아하지만 예전에 웹개발을 처음 배우기 시작하면서 Vue2를 먼저 배우고 사용했었다. 2023년을 마지막으로 Vue2 지원이 종료되는 가운데 React를 계속하던 개발자의 관점에서 React와 Vue.js와 비교하고 React와는 비슷하면서도 색다른 Vue3의 매력을 정리해보려고한다

State of JS

Frontend Frameworks 사용 비율: State of JS

Vue.js는 React, Angular와 더불어 굉장히 많은 사람들이 사용하고 있으며 npm 기준으로
Vue가 React 다음으로 굉장히 많은 다운로드 수를 보이고 있다.
여전히 많은 사람들이 관심갖고 이미 사용하고 있는 Vue.js의 3버전을 최근에 공부하면서 Composition API를 사용하고, 타입스크립트를 적용해 보면서 내가 느낀점과리액트와의 차이점을 비교해 보려고한다


💡 Vue.js란

Vue.js 는 어플리케이션 레벨의 개발을 지원하는 자바스크립트 프레임워크이다.
아무래도 Javascript 에 대한 지식이 많이 요구되는 React랑 비교했을 때 진입장벽이 더 낮고 초기 개발 생산성이 높아서 처음개발을 시작할 때 리액트와 더불어 많이 선호된다.
vue3에서는 vue2에서 플러그인 형태로 지원되던 Composition API가 vue3에서 공식 API로 채택되었고 아무래도 Composition API가 React hooks의 영향을 받아서 인지 Composition API는 굉장히 React hooks와 유사하며 이를통해 컴포넌트의 재사용성을 높일 수 있게 되었다

⭐ React

/* Counter.tsx */
import React, { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);
  return <div>{count}</div>;
};

⭐Vue.js

<!-- 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>

✔️ 1) 싱글 파일 컴포넌트(Single-File Component)

하나의 컴포넌트와 관련된 코드(HTML, CSS, JS)를 하나의 .vue파일에서 관리하는 방법을 싱글파일 컴포넌트 라고한다. 다른 프레임워크와 다른 Vue.js의 특징 중 하나이고 이러한 .vue파일은 웹팩 로더의 한 종류인 vue-loader에 의해서 HTML, CSS, JS로 분리된다

✔️ 2) reactivity

React 문서를 보면 Reconciliation(재조정)라는 단어가 유독 많이 나오는데
Vue.js 문서에서는 Reactivity(반응성) 라는 용어를 많이 강조한다.

Vue.js의 핵심 컨셉은 reactivity를 통해서 상태변화에 반응하는 것인데 vue3에서는 자바스크립트의 Proxy 객체를 활용하여 reactivity를 구현 하고있다. 쉽게말해 refreactive와 같은 Vue의 반응성 API를 사용하면 변수에 reactivity가 주입되어 해당 변수에 대해서 변경내용을 추적하고 변화를 바탕으로 Proxy객체가 반응해서 UI를 변경한다

⭐Angular

// 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.jsAngular의 영향을 많이 받은 것 같다.


💡Option API와 Compositoin API

1) Option API

<script lang="ts">
export default {
  props: [],
  data() {
    return {};
  },
  methods: {},
  computed: {},
  /*  ....  */
};
</script>

기존의 Vue2에서 기본적으로 사용되는 API로 컴포넌트 옵션들이 하나의 객체에 몰려 있는 수직적인 구조이다. 사실 이러한 Options API도 충분히 훌륭하고 잘 동작하지만 어플리케이션 규모가 커지고 복잡해지면서 유지보수성, 타입 안정성 등에서 어려운 점이 있었다

2) Composition 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을 사용한다지만 디테일하게 보면 렌더링의 큰 차이가있다

1) React

React의 함수형 컴포넌트는 그 자체로 render() 함수이기떄문에 state, props와 같은 반응형값이 바뀌었을 때 함수 자체가 다시 실행되면서 리렌더링이 발생하고 변경된 부분만을 업데이트하는 Reconciliation이 일어나서 실제 DOM에 반영한다

2) Vue

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의 렌더링 방식을 비교하는 좋은 예시인 것같다


💡 like React

✔️ 1) 리액트 훅과의 유사성

앞서도 얘기했지만 ref함수는 리액트의 useState() 와 매우 유사하며, 리액트 useRef()훅에서 데이터에 ref.current로 접근하는 것처럼 vue3의 refref.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 시스템을 갖고 있기 때문이다

✔️ 2. Custom Composition

// 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라는 접두사를 보편 적으로 사용하고 있어서 거부감이 없이 사용했던 것 같다. 이러한 커스텀 컴포지션을 이용해
컴포넌트에서 상태 관리, 사이드 이펙트 처리 등의 로직을 분리하고 재사용할 수 있다.

✔️ 3. 호환성과 유연성

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에서 혼합해서 사용할 수 있기 때문에 굉장히 유연하다고 느꼈다.
반면 리액트는 훅이 도입되었지만 함수형 컴포넌트와 클래스 컴포넌트는 완전 대체되기 어려운 경우가 있어서 이런 점에서 볼 때 리액트보다 더 유연하다는 생각이 조금 들었다

✔️ 4. Side Effect

Vue3Composition API에서 watchreactivty가 주입된 변수를 추적하고 변경되었을 때 추가적인 로직을 실행하는 주는 함수이다 리액트의 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라는 메소드도 존재하는데 watchEffectwatch와 매우 유사한데 의존성 배열을 넘기지 않으며 watchEffect 내부에 reactivity가 주입된 값들 전부를 추적하고 그 값들중 하나라도 변하면 effect 함수가 실행된다.
또한 useEffect와 마찬가지로 두 메소드 다 처음 마운트될 때 한번은 무조건 실행된다.

✔️ 5. 다양한 템플릿 문법

확실히 React는 JSX를 사용하여 마크업을 작성하기 때문에 훨씬 자바스크립트스러운 반면 Vue는 앵귤러와 유사하게 Template을 기반으로 마크업을 작성하기 때문에 좀 더 HTML스럽다
하지만 그렇기 때문에 Vue에서 제공하는 템플릿 문법들은 굉장히 재미있고 흥미로운데 리액트와는 색다른 매력이 있다

⭐ 1) 디렉티브(Directives)

<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] 등의 다양한 디렉티브들이 존재한다

  • v-bind: 축약형태 :
  • v-on: 축약형태 @
  • v-slot: 축약형태 #

React에서는 JSX안에서 표현식만을 사용해야하므로 배열 순회 및 분기 처리 시에는 JavaScript의 배열 메소드나 삼항 연산자 등을 사용해야한다 하지만 Vue에서는 템플릿에서 사용 가능한 다양한 디렉티브를 제공하여, 추가적인 JavaScript 문법을 작성하지 않아도 편리하게 배열 순회나 분기를 처리 등을 간편하게 작성하게 도와준다

⭐ 2) event modifier

<template>
	<a @click.stop="doThis"></a>
	<form @submit.prevent="onSubmit"></form>
    <input @keyup.enter="submit" />
    <input @keyup.alt.enter="clear" />
</template>

이벤트에서 preventDefault 혹은 stopPropagation 등을 호출하는 것은 흔하기 때문에
Vue는 이벤트에서 이러한 함수들을 처리하기 위한 다양한 이벤트 수식어를 제공한다

✔️ 6. Slot

vuejs-slot

리액트의 children 같은 개념으로 <FancyButton> 사이에 들어가는 모든값들을 children으로 내려주고 자식 컴포넌트에서 props.children으로 받았던 것 처럼
Vue에서는 자식컴포넌트에서 <slot> 태그로 children을 받을 수 있다

vuejs-slot

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 APIConsumer와 같은 Render Props 패턴을 사용해야하는데
Vue에서는 scoped slot을 제공하여 자식 컴포넌트에서 <slot>태그에 바인딩 한 값을 부모 컴포넌트의 contents 내부에서 사용이 가능하다

✔️ 7. Built-in Components

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를 고려해 볼 것같다

📕참고 자료

vuejs 공식문서
캡틴판교 Vue 3 가볍게 훑어보기

profile
누군가는 해야하잖아

0개의 댓글