프레임워크 있는 프론트 개발 #2 todo on any framework - Vue 편

봉승우·2023년 3월 29일
0
post-thumbnail

앵귤러 투두에 이은 Vuejs 를 활용한, 투두 만들기 프로젝트입니다.

해당 포스팅도 앵귤러와 동일한 목차로 정리해보고자 합니다.
1. Vuejs의 특징을 알아봅니다.
2. Vuejs의 기초적인 활용법을 알아봅니다.
3. Vuejs를 활용하여 TODO 앱을 제작해봅니다.
4. 타 프레임워크와 차이점을 비교해봅니다.

📌 Vuejs의 특징

vuejs 공식문서

간략 소개

vuejs는 본인들 스스로 "웹 사용자 인터페이스를 만들기 위한 쉽고 강력하며 다재다능한 프레임워크" 라고 소개하고 있습니다.
리액트와는 달리 프레임워크라고 소개하는 부분이 인상적인데,
어떤 차이점으로 이렇게 소개하는지 알아보면 좋을 것 같습니다.

그리고 vuejs의 개발 목적은 다음과 같다고 합니다.

  • 개발자에게 더 쉽고 가볍고 누구나 빨리 배울수 있는 프레임워크

위와 같은 방향성을 토대로 만들어졌기 때문인지,
전에 살펴봤던 앵귤러와는 다르게 기존 웹 개발자들의 DX가 크게 달라지는 것 같지는 않았습니다.
위와 같이 느낀 이유는 하기와 같습니다.

  • 일반 스크립트 태그로 CDN을 통해 사용 가능
  • 기존 HTML 마크업 템플릿을 거의 그대로 사용
  • css를 작성하는 스타일 문법 동일

또한 vuejs는 컴포넌트 기반 프로그래밍 모델을 제공하는데,
이는 앵귤러 리액트등 모던 프론트엔드들과 크게 다르지 않습니다.
더욱이, 선언적 렌더링과 반응성 이라는 키워드도 vuejs에서 빼먹을 수 없는데,
해당 개념도 다른 도구들과 크게 다르지 않은 모습입니다.

Vuejs 3.0에서의 변화

얼핏 듣기로 vue3.0이 세상에 나오기까지 예정된 일정보다 많이 늦어져서,
"수 많은 vuejs 개발자 분들이 react로 이민을 갔었다" 라는 이야기를 들었습니다.

vue3 이전의 버전에는 어떤 문제가 있었길래 위와 같은 말들이 나왔을까요?

우선 컴포넌트 코드의 재사용이 어려웠습니다.
주로 로직을 재사용하기 위해 믹스인 방식을 많이 활용했는데,
하나의 컴포넌트에서 믹스인을 2개 이상 활용하면,
로직의 흐름을 따라가기 어렵습니다.

또한 더 심각했던 문제는 타입스크립트에 대한 지원이라고 생각합니다.
이전 버전에서도 ts를 활용할 수는 있었지만,
vuejs의 철학과도 맞지 않게,
굉장히 앵귤러 스럽게 vuejs 프로젝트를 구성해야 합니다.

이런 문제를 가지고 있던 vuejs는 하기와 내용에 대해 업데이트를 제공합니다.

  • 로직 재활용성 개선
    위에서 기존 버전의 문제로 언급했던, 로직의 재활용성을 개선했습니다.
    대체로 리액트에서 함수형 컴포넌트를 사용하면서 경험할 수 있는 재사용성과 비슷한 개념이라 보입니다.

  • 타입스크립트 지원
    기존 버전은 객체 방식으로 컴포넌트를 구성했습니다.
    그렇기 때문에 타입스크립트를 100% 활용하는데 어려움이 있었지만,
    업데이트를 통해서 타입 추론등의 기능을 더 잘 활용할 수 있게 되었습니다.

객체 구조 방식에서 타입스크립트를 사용하기 어려운 이유
TS의 타입 추론은 명시적으로 타입을 지정하지 않아도, TS가 타입을 추론해준다 라는 것인데,
객체 구조에서는 개발자가 타입을 정의해야하는 경우가 많다고 합니다.
이와 관련된 내용에 대해 더 잘 아시는 분이 있다면, 피드백 부탁드립니다...🙏

  • 가상 DOM 비교 개선
    가상 돔과 관련되어 있으며, 기존의 전체 트리에 대해 diff를 확인하는 것이 아니라,
    이제는 정적 요소와 동적요소를 구분하여, 동적요소만 diff를 비교 한다고 합니다.

  • 중복 객체 생성 억제
    리렌더링 등의 이유로, 한 객체가 여러 번 생성되는 것을 방지하고자,
    컴파일러가 관련 내용을 탐지하여,
    렌더링 함수 밖으로 호이스팅을 제공합니다.

  • 트리쉐이킹
    해당 개념은 번들링시에 필요한 코드만 가져온다는 개념으로 파일의 크기를 크게 줄일 수 있습니다.

  • 컴포지션 API
    기존의 옵션 API와는 달리 함수 기반의 API를 제공합니다.
    리액트가 클래스형 컴포넌트에서 함수형 컴포넌트로 넘어가던 과정과 굉장히 유사하네요.
    해당 API에서는 모든 코드를 독립적으로 정의할 수 있고,
    각각의 기능을 함수로 묶어서 처리할 수 있어 유지보수에 장점이 있습니다.

  • 기타 변경 사항
    텔레포트 (React의 Portals)
    프래그먼트 (React의 <></>)
    서스펜스 (React의 <Suspense fallback={<></>}/>)
    리액티비티API (React의 useState)

📌 Vuejs 활용 기초

기본적으로 Vuejs는 React와 상당히 유사한 부분이 많이 있는 것 같습니다.
코드를 구성하는 방법이나 API를 활용하는 세세한 부분에서는 차이가 있겠지만,,,,

그래서 간략하게 Vuejs에서 많이 사용되는 문법을 기록해보고자 합니다.

디렉티브

해당 개념은 리액트에서는 생소할 수 있지만,
전에 다루었던 앵귤러에서 이미 한번 확인했던 문법입니다.

<a v-bind:href="imageUrl">과 같이 동적 인자 등의 값을 세팅하거나,
해당 컴포넌트 혹은 DOM에게 어떤 작업을 지시하는데 사용할 수 있습니다.

v-if, v-bind, v-on 등 다양한 기본 디렉티브가 제공되고 있습니다.

해당 디렉티브들은 다음과 같은 구조를 가집니다.

당연하게도 앵귤러와 마찬가지로, 커스텀 디렉티브도 생성할 수 있습니다.

이벤트 핸들러

vue에서는 디렉티브를 적극적으로 활용하기 때문인지,
리액트와 사용법이 살짝 다른 모습을 확인할 수 있었습니다.

v-on:이벤트 명 혹은 @이벤트 명 과 같은 방식으로 사용할 수 있습니다.

<button v-on:click="클릭했어요">클릭</button>
<button @click="클릭했어요($event)">클릭</button>

여기에 더해, 이벤트 관련 디렉티브에서는 수식어를 활용하여,
리액트에 비해 더 간결한 코드를 작성할 수 있습니다.

<input @keydown.enter="엔터가_눌렸어요">
위 코드에서 보듯이,
enter라는 키가 눌리면 실행될 함수를 쉽게 붙일 수 있습니다.

컴포넌트간 이벤트 전달

리액트에서는 컴포넌트에서 함수를 props로 내려서 그냥 이벤트가 발생하는 곳에 연결하였습니다.
vue도 크게 다르지는 않지만,
Evnet emit에 대한 추가적인 문법을 제공합니다.
그래서 함수를 내리는게 아니라,
이벤트를 상위 컴포넌트로 던져서 로직을 실행시키는 구조로 구성이 가능합니다.

// 최하위 컴포넌트 (이벤트 발생)
<button @click="$emit('delete', 10)">삭제</button>
// 중간 컴포넌트 (이벤트 전달)
<todo-item @delete="$emit('remove', $event)"></todo-item>
// 최상위 컴포넌트 (이벤트 소비)
<todo-list @delete="deleteTodo"></todo-list>
~
deleteTodo(num) {
  axios.delete('/todo/' + num);
}
~

컴포넌트간 데이터 전달

해당 부분도 리액트와 유사한 방법을 활용합니다.
다만 차이점이라면, 문자열은 그냥 내리면 되지만,
그 외의 값은 디렉티브를 활용해야합니다.

문자열을 전달하는 경우,
<velog title="VUE 투두"></velog>

그 외의 값을 전달하는 경우,


<template>
	<velog :data="데이터"></velog>
</template>

<script>
export default {
  data() {
    return {
      데이터: '벨로그 안녕!'
    }
  }
}
</script>

데이터 바인딩

해당 내용은 앵귤러에서도 다루었던 내용입니다.

앵귤러와 마찬가지로 input에 입력되는 값과 데이터를 동기화 시킬 수 있습니다.

<input v-model="text">

위의 코드를 풀어서 쓰면 다음과 같이 작성할 수 있습니다.

<input
  :value="text"
  @input="event => text = event.target.value">

📌 Vue Todo 코드잼

먼저, 완성된 Todo 앱의 화면입니다.
vue-todo

옵션 API를 활용하여 프로젝트를 진행하였습니다.

초기 세팅

전체적인 프로젝트 구조는 하기 사진과 같이 App.vue에서 todoList에 대한 데이터와 관련 로직을 전부 담당하도록 할 예정입니다.

이후 필요에 따라서,
해당 데이터와 로직을 props로 통해서 내려주는 과정을 거칠 것입니다.

이렇게 진행한 이유는,
해당 프로젝트의 특성상 많은 데이터를 다루는 것이 아니라 todoList 하나의 데이터에 대해서만 다루고 있으며,
props로 모든 값을 내린다고 하여도,
단계가 3단계 뿐이기 때문에 props drilling으로 발생하는 문제는 크게 없어 보였습니다.

그 대신 데이터와 관련된 로직을 최상단에서 관리하면서 얻게될 유지보수성 및 가독성이 더 크다고 판단하였습니다.

모델

우선 관련 App.vue에 포함될 todoList의 로직에 대해서 모두 구현해야합니다.

추가로 고려해야하는 사항은 localStorage에 값이 저장되어야 한다는 것입니다.

ang-todo에서 진행한 바와 같이 localStorage에 값이 저장될 때에는,
문자열 값만 들어갈 수 있기 때문에,
데이터를 저장하거나 읽어올 때 추가적인 작업이 항상 필요합니다.

그래서 해당 작업을 대신할 수 있도록 유틸함수를 만들었습니다.
(localStorage의 인스턴스를 만들고 데이터 입출력 함수를 수정하여 내보내는 것도 가능했겠지만, 오히려 가독성과 블랙박스를 키울 수 있을 것 같다는 생각이 들었습니다.)

// utils/storage
export const getData = (key: string) => {
  const savedData = localStorage.getItem(key);
  return savedData && JSON.parse(savedData);
};

export const saveData = (value: unknown, key: string) => {
  const toJson = JSON.stringify(value);
  localStorage.setItem(key, toJson);
};

위의 localStorage의 util 함수를 이용하여 다음과 같은 함수를 구성하였습니다.

methods: {
    addTodoItem(todo: string) {
      if (todo) {
        const newTodoData = {
          id: this.todoList[this.todoList.length - 1]?.id + 1 || 1, // 마지막 요소의 id값 + 1
          completed: false,
          todo,
        };
        this.todoList.push(newTodoData);

        // LocalStorage 업데이트
        saveData(this.todoList, TODO_STORAGE_KEY);
      }
    },
    toggleTodoItem(targetId: number) {
      this.todoList.forEach((todoItem) => {
        if (todoItem.id === targetId) {
          todoItem.completed = !todoItem.completed; // toggle
        }
      });

      // LocalStorage 업데이트
      saveData(this.todoList, TODO_STORAGE_KEY);
    },
    removeTodoItem(targetId: number) {
      this.todoList = this.todoList.filter(
        (todoItem) => todoItem.id !== targetId
      );

      // LocalStorage 업데이트
      saveData(this.todoList, TODO_STORAGE_KEY);
    },
    resetTodoList() {
      this.todoList = [];

      // LocalStorage 업데이트
      saveData(this.todoList, TODO_STORAGE_KEY);
    },
  },

그리고 해당 todoList는 앱이 실행되면 항상 불러져와야 합니다.
컴포넌트가 생성되고 DOM에 마운트되기 전에 불려지는 훅인 created에 관련 로직을 작성해주면 됩니다.

라이프사이클에 대한 자세한 내용은 재그지그님의 블로그에 확인할 수 있습니다.

  created() {
    const storageData = getData(TODO_STORAGE_KEY);
    this.todoList = storageData;
  },

template와 모델 연결하기

우선 해당 과정에서 필요한 내용은 크게 다음과 같았습니다.
1. props로 데이터 / 함수 전달하기
2. 데이터 / 함수를 사용하기

만들어진 App구조에 따르면, App.vue의 데이터와 함수를 props로 전달해야합니다.

단순히 데이터를 전달하는 경우에는 정적인 데이터와, 동적인 데이터를 내릴 수 있습니다.
공식문서-props

todoList는 CRUD가 가능한 모델이기에 동적 데이터로 취급하였으며,
다음과 같이 데이터를 전달할 수 있습니다.

  <MainWrapper :todoList="todoList" />

하위 컴포넌트에서는 위 데이터를 다음과 같이 받을 수 있습니다.

<script lang="ts">
// ...

  export default {
    // todoList 라고 하는 props를 받아온다.
    props: ["todoList"],
  }
  
</script>

<template>
  <TodoList :todoList="todoList" />
</teamplate>

자식 컴포넌트에서 발생한 이벤트를 받아서,
부모 컴포넌트에서 실행시키는 방법은 다음과 같습니다.
공식문서-이벤트

내장 메서드 $emit을 사용하여 템플릿 표현식에서 직접 사용자 정의 이벤트를 발신할 수 있습니다

  methods: {
    addTodoItem(todo: string) {
      this.$emit("addTodoItem", todo);
    },
  }

$emit에서 첫 번째 인자는 사용자 정의 이벤트를,
두 번째 이후부터는 해당 이벤트의 인자 값들을 전달할 수 있습니다.

부모 컴포넌트에서는 해당 이벤트를 다음과 같이 전달받을 수 있습니다.

	// 화살표 함수
	<MyButton @increase-by="(n) => count += n" />
    // 혹은 이벤트 핸들러
    <MyButton @increase-by="increaseCount" />
	<MyButton v-on:increase-by="increaseCount" />

이렇게 기본적으로 데이터와 이벤트 함수를 다룰 수 있습니다.
해당 Todo App프로젝트 또한 위와 같이 방식으로 구성되어 있습니다.

또한 Vue에서는 양방향 데이터 바인딩을 지원하여,
리액트에 비해서 편리하게 form을 다룰 수 있습니다.

양방향 데이터 바인딩이란 다음과 같습니다.

데이터의 변경사항이 발생할 때 자동으로 화면에 반영되는 기능입니다.
일반적인 데이터 바인딩 방식에서는, 데이터의 변경사항이 발생하면 해당 데이터를 사용하는 화면 요소를 갱신하는 일반적인 방식을 따릅니다.
즉, 화면의 변경사항은 데이터를 업데이트할 수 있지만,
데이터의 변경사항은 화면을 갱신할 수는 없습니다.
이러한 상황에서 사용하는 것이 양방향 데이터 바인딩입니다.
양방향 데이터 바인딩은 데이터의 변경사항이 발생하면 해당 데이터를 사용하는 화면 요소를 업데이트하고, 화면 요소의 변경사항도 데이터를 업데이트하는 방식으로 작동합니다. 이를 위해 프레임워크에서는 일반적으로 v-model이라는 디렉티브를 제공합니다.

즉, 일반적으로는 "데이터 변경 -> 화면 요소 갱신" 이며,
양방향 데이터 바인딩에서는 "데이터 변경 <-> 화면 요소 갱신" 으로 작용할 수 있습니다.

그래서 실제로 폼을 다룰때에도 단방향 바인딩만을 지원하는 리액트와는 달리,
더 간결하게 form데이터를 다룰 수 있습니다.

// vueJS
<input
  type="text"
  v-model="newTodoItem"
  @keyup.enter="addTodo"
  placeholder="add what you have to do"
/>
    
// 리액트  
<input
  type="text"
  value={todoItem}
  onChange={(e) => {
	if(엔터){ addTodo() }
    setTodoItem(e.target.value)
  }}
  placeholder="add what you have to do"
/>

위와 같이 기본적인 Vue 활용을 기반으로 TODO 앱을 구현할 수 있습니다.
모든 코드는 깃허브 링크에 있습니다.

📌 타 프레임워크와의 차이 및 DX

vs 리액트 및 특징

직전에 앵귤러를 보고와서 그런지,
체감상으로 리액트와는 유사한 점이 굉장히 많았다고 느껴졌습니다.

우선 template 문법이 기존의 html, css, js 와 크게 다르지 않아서, 리액트의 JSX 문법보다는 쉽게 접근해볼 수 있겠다고 느꼈습니다.
(하지만, 리액트에 익숙한 상태에서 vue를 보니 다양한 디렉티브가 오히려 거추장스럽다라는 생각도 듭니다.)

한 동안 "리액트를 왜 써요?" 의 질문에,
"리액트가 ~ 가상 돔 ~ 빨라요"라고 답했던 시기가 있었던 것 같은데,
사실 뷰도 가상 돔을 지원하기 때문에 적절한 답변은 아닌었던 것 같습니다.
(뷰도 가상돔을 품고 있다는 뜻입니다.)

또한 리액트에서는 본인들을 UI 라이브러리 라고 칭하며,
뷰는 프로그레시브 프레임워크 라고 칭합니다.

프로그레시브 프레임워크는 일반적인 프레임워크에 비해 자유도가 높으며,
시스템과 개발자에 의해 프레임워크의 활용도를 결정할 수 있다고 합니다.

그 만큼 vue는 개발자의 선택에 따라 프레임워크 적인 특성을 가질 수도 있으며, 혹은 반대로 라이브러리 적인 특성을 가질 수 있습니다.

그리고 이 둘의 차이는 코드의 형태에서 많이 드러납니다.
vue를 활용할 때에는 html, css, js 영역을 분리하여 작성하는 반면, 리액트는 JSX(JavaScript XML)를 활용하여 모든 영역을 자바스크립트로 구현합니다.

양방향 데이터 바인딩

양방향 데이터 바인딩의 지원으로,
간단한 form을 다룰 때에는 리액트보다는 공수가 덜 든다는 느낌을 받았습니다.

물론 양방향 데이터 바인딩에 대해서는 단점도 존재한다고 합니다.
우선 복잡성, 성능 저하, 디버깅 어려움 등이 존재한다고 알려져 있습니다.
하지만 해당 프로젝트를 진행하면서 저의 생각은 다음과 같습니다.
(양방향 바인딩을 폼에서 활용활 때)

  • 복잡성: 복잡성은 전혀 느끼지 못했고, 오히려 간편했습니다.
    양방향 바인딩으로 인해 onChange 이벤트를 신경쓰지 않아도 되어서 편했습니다.
  • 성능저하: 일반적으로 onChange 이벤트에 setState를 붙이기 때문에,
    리렌더링이 발생하는 것을 동일할 것으로 보입니다. 물론 나름의 최적화 방법이 있다고는 하지만, 이것이 양방향 데이터 바인딩의 큰 단점일지는 모르겠습니다.
  • 디버깅 어려움: 위 두가지 단점보다는 가장 설득력있는 단점으로 생각됩니다. 리액트의 useEffect과 유사하게, 어쩌면 뷰의 변화가 사이드 effect을 불러와서 값이 변하는 과정이 디버깅의 어려움을 야기할 수 있다고는 느껴집니다.

📌 Vuejs의 개발 경험

앞서 언급한 바와 같이 많은 부분이 리액트와 유사하다고 느꼈습니다.

다만 일부 form이 다수 존재하는 프로젝트나,
혹은 단순하게 CDN과 SFC를 활용해서 가볍고 빠른 앱을 구현해야하는 상황에서는 vue를 고민해볼 수 있겠다라는 생각이 들었습니다.

물론 개발 규모에 대해 확장 가능성을 고려해야하고,
복잡성이 가늠이 안될때에는 참고자료가 많으며 트렌드에 뒤쳐지지 않는 리액트를 선택할 것 같습니다.

📌 참고자료들

profile
안녕하세요🙂

0개의 댓글