한컴테크를 통해 한컴의 기술을 공유합니다. 한컴의 프로그래밍, 프레임워크, 라이브러리 및 도구 등 다양한 기술을 만나보세요. 한컴 개발자들의 다양한 지식을 회사라는 울타리를 넘어 여러분과 공유합니다. 한컴이 제공하는 기술블로그에서 새로운 아이디어와 도전을 마주하고, 개발자가 꿈꾸는 미래를 실현하세요.

한컴테크

프론트엔드 도구의 전략적 선택: 경량 프레임워크 Vue.js 탐구와 Vue3 적용기


요약

프론트엔드 프레임워크인 Vue.js를 소개하고 실무 적용 경험을 공유합니다.

1. 프론트엔드 도구 다각화의 필요성


현대에는 수많은 서비스들이 웹으로 구현되고 있고, 그만큼 기반 기술과 도구도 빠르게 발전하고 탄생합니다. 하지만 대부분의 사람들은 리스크를 줄이고 싶어 하기에 생태계가 잘 꾸려져 문제 해결과 확장이 쉬운 도구가 각광받습니다. 이렇게 선택받은 도구는 또다시 커지고 대중화됩니다. 그렇게 오랜 기간 인기 프론트엔드 도구 순위 최상위를 달리고 있는 것이 바로 React입니다.

React는 컴포넌트 기반 아키텍처로 UI 재사용과 모듈화에 용이하다는 장점이 있습니다. 또한 방대한 라이브러리 생태계로 고성능, 대규모 애애플리케이션 운용에 합리적인 도구들을 찾아볼 수 있어 편리합니다. 많은 개발자가 React를 경험했다는 것도 매우 큰 장점입니다. 그러나 새로운 시작을 할 때면 ‘이번에도 적합한가’에 대한 고민을 하게 됩니다.

1.1 The right tool for the job

React는 렌더링을 위한 Virtual DOM을 메모리에 유지하는데, 복잡한 화면일수록 커지며 최적화가 미비하다면 이는 초기 로딩 시간과 메모리 점유율 증가에 영향을 줍니다. 또한 상태 관리, 라우팅 등을 위해서는 또 다른 라이브러리를 도입해야 하며, 독특한 리렌더링 매커니즘 또한 익혀야 합니다. 이러한 초기 구축 리소스 학습 곡선 문제는 늘 이야기되어 왔습니다. 이런 것들은 React가 ‘무겁다’는 인상을 줍니다. 따라서 가벼운 프로젝트에까지 대형 프레임워크를 적용할 필요는 없으며 다방면의 검토로 적합한 선택을 해야 합니다.

1.2 Vue.js

한컴싸인의 백오피스는 적은 리소스 투입으로 빠른 구축이 필요했습니다. 서비스를 관리하는 역할이기에 큰 기능을 요하지 않았으며, 최초에는 사내 서비스가 목표였습니다. 이러한 조건에 아래와 같은 전략으로 Vue.js를 채택하였습니다.

경량화와 효율화

  • TreeShaking에 친화적으로, 빌드 후 매우 작은 크기를 가집니다.
    (최소 크기 16KB, Vue 제공 기능 모두 사용 시 27KB)
  • 점진적 프레임워크 지향: 단순하게 시작 후 필요에 따른 기능의 점진적 도입이 용이
  • 초기 구축 리소스를 줄여 빠른 개발 착수에 용이
  • 성숙한 라이브러리 생태계의 확장 용이성

고성능

  • js-framework-benchmark의 성능 비교 결과에 따르면 React, Angular 등 다른 virtualDOM 프레임워크에 비해 빠르며, 비 virtualDOM 프레임워크에 비교하여도 준수한 성능을 보입니다.

유지보수성

  • 직관적인 코드 구조로 HTML, CSS, JavaScript 와 친숙하다면 쉽게 개발에 참여할 수 있습니다.
  • 대중적인 도구로 프로젝트의 장기 유지/보수를 꾀할 수 있습니다.

2. Vue.js


Vue는 사용자 인터페이스 구현을 위한 자바스크립트 프레임워크입니다. 표준 HTML, CSS, JavaScript 위에 구축되며, 선언적이고 컴포넌트 기반의 프로그래밍 모델을 제공하여 효율적으로 사용자 인터페이스를 개발할 수 있습니다. Vue의 대표적인 특징은 아래와 같습니다.

2.1 선언적 렌더링과 반응성

<template>
  <button @click="count++">Count is: {{ count }}</button>
</template>
<script setup>
  import { ref } from 'vue';
  const count = ref(0);
</script>
<style scoped>
button {
  font-weight: bold;
}
</style>

위는 Vue로 작성한 간단한 예시입니다. 여기서 두 가지 핵심을 볼 수 있습니다.

선언적 렌더링

  • 순수 HTML과 JavaScript로 작성한 코드의 경우, HTML의 상태를 변경하기 위해 취해야 할 모든 단계와 과정을 직접 명시합니다. 예시의 경우 “버튼의 텍스트를 찾아 count와 같은 숫자로 변경해라”라는 구체적인 DOM 조작 명령을 내려야 합니다. 이를 ‘명령적 렌더링’이라고 합니다.
  • Vue는 표준 HTML을 확장한 템플릿 문법을 제공하여, JavaScript 상태에 따라 HTML의 출력을 선언적으로 기술할 수 있게 해줍니다. 예시의 경우 “버튼 텍스트로 표시되는 것은 count야”라는 선언만으로 출력이 가능합니다.

반응성

  • Vue는 JavaScript 상태 변화를 자동으로 추적하고, 변화가 발생하면 가장 효율적인 조작 방법을 찾아 DOM을 업데이트합니다. 개발자는 Element를 어떻게 찾아 어떻게 바꿀 것인지 고민하지 않아도 됩니다.

이러한 특성들은 Vue와 같은 프론트엔드 도구의 가장 큰 이점입니다.

2.2 싱글 파일 컴포넌트 (SFC)

대부분의 빌드 도구 기반 Vue 프로젝트에서는 싱글 파일 컴포넌트(일명 *.vue 파일, SFC로 약칭)라는 HTML 유사 파일 형식을 사용하여 Vue 컴포넌트를 작성합니다. Vue SFC는 이름 그대로 컴포넌트의 로직(JavaScript), 템플릿(HTML), 스타일(CSS)을 하나의 파일에 캡슐화합니다.

<!-- Counter.vue -->
<!-- 템플릿 (HTML) -->
<template>
  <button @click="count++">Count is: {{ count }}</button>
</template>
<!-- 로직 (JavaScript) -->
<script setup>
  import { ref } from 'vue';
  const count = ref(0);
</script>
<!-- 스타일 (CSS) -->
<style scoped>
button {
  font-weight: bold;
}
</style>

SFC는 Vue의 대표적인 기능이며, 프레임워크 전용 파일 형식이기 때문에 반드시 빌드 단계가 필요하다는 조건이 있으나, Vue 컴포넌트를 작성하는 권장 방식입니다.

프레임워크에 종속적인 파일 형식을 써야 한다는 것은 부담스러운 일이나, SFC 사용에는 많은 이점이 있습니다.

  • 익숙한 HTML, JavaScript, CSS 문법으로 모듈화된 컴포넌트 작성
  • 본질적으로 연결된 관심사끼리의 배치A)
  • 런타임 컴파일 비용 없는 사전 컴파일된 템플릿
  • 컴포넌트 범위의 CSS 정의
  • Vue가 제공하는 Composition API 사용 가능
  • 컴파일 타임 최적화
  • 템플릿 표현식에 대한 자동 완성 및 IDE 지원
  • 기본적으로 핫 모듈 교체(HMR) 지원B)

A) 기존 웹 개발 환경에서는 HTML/JS/CSS를 분리하는 엔지니어링 원칙인 관심사 분리(Separation of Concerns, SOC)를 중요시하나, Vue는 이에 대하여 관심사의 분리는 파일 형식 분리와 동일하지 않다고 말합니다. 엔지니어링 원칙의 궁극적 목표는 코드베이스의 유지보수성을 높이는 것이기에 파일의 맹목적인 분리는 점점 더 복잡해져가는 프론트엔드 애플리케이션에서는 그 목표 달성에 도움이 되지 않는다는 것입니다.

B) HMR (Hot Module Replacement)
Webpack, Vite와 같은 번들러에서 제공하는 기능으로, 개발 과정에서 코드의 변경이 발생했을 시 상태의 손실 없이 변경된 코드만 주입하여 화면에 즉시 반영합니다. 이는 변경 사항을 거의 즉각적으로 확인할 수 있고 새로고침이 없으므로 개발자가 상태를 재현하지 않아도 되어 생산성 증대에 도움을 줍니다.

하지만 이는 권장사항일 뿐이며, 순수 JavaScript로도 사용할 수 있습니다.

2.3 API 스타일

Vue 컴포넌트는 Options API와 Composition API라는 두 가지 다른 API 스타일로 작성할 수 있습니다.

Options API

  • Options API에서는 data, method, Lifecycle Hook과 같은 역할별(Options) 객체를 사용하여 로직을 정의합니다.
  • 정의된 객체들은 코드 상에서 this를 통해 사용할 수 있으며, 이는 컴포넌트 인스턴스를 가리킵니다.
<script>
export default {
  // data()에서 반환된 속성들은 반응형 상태가 되며
  // `this`로 노출됩니다.
  data() {
    return {
      count: 0
    }
  },
  // methods는 상태를 변경하고 업데이트를 트리거하는 함수입니다.
  // 템플릿에서 이벤트 핸들러로 바인딩할 수 있습니다.
  methods: {
    increment() {
      this.count++;
    }
  },
  // LifeCycle Hook은 컴포넌트의 생명주기 각 단계에서 호출됩니다.
  // 이 함수는 컴포넌트가 마운트될 때 호출됩니다.
  mounted() {
    console.log(`The initial count is ${this.count}.`);
  }
}
</script>
<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

Composition API

  • Vue3 등장과 함께 권장되는 방식입니다.
  • Composition API에서는 ref, reactive 등 API 함수를 사용하여 로직을 정의합니다.
  • SPC에서는 Composition API 사용 시 <script setup>를 함께 사용하는 것이 일반적입니다.
<script setup>
import { ref, onMounted } from 'vue'
// 반응형 상태
const count = ref(0);
// 상태를 변경하고 업데이트를 트리거하는 함수
function increment() {
  count.value++;
}
// 라이프사이클 훅
onMounted(() => {
  console.log(`The initial count is ${count.value}.`);
})
</script>
<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>
<script setup>

<script setup>은 싱글 파일 컴포넌트(SFC) 내에서 Composition API를 사용할 때의 컴파일 타임 문법 설탕(Syntactic Sugar)입니다. SFC와 Composition API를 모두 사용하는 경우 권장되는데, setup 속성은 Vue가 컴파일 타임 변환을 수행하도록 힌트를 주어, 보일러 플레이트를 줄여줍니다.
아래는 동일한 코드를 <script setup>의 사용 여부를 비교하여 보여줍니다. 작은 차이로 보이지만, 코드 작성이 많아질수록 큰 차이로 느껴집니다.

<!--------- not use 'script setup' --------->
<script>
import { ref } from 'vue'
export default {
  setup() {
    const count = ref(0);
    // 템플릿과 다른 옵션 API 훅에 노출
    return {
      count;
    }
  },
  mounted() {
    console.log(this.count);
  }
}
</script>
<!----------- use script setup ----------->
<script setup>
  import { ref } from 'vue'
  const count = ref(0);
  onMounted(() => {
    console.log(count.value);
  })
</script>

그래서, 무얼 써야 할까?

두 방식 모두 일반적인 사용 사례를 지원합니다. 따라서 사용자의 취향과 프로젝트의 상태에 따라 선택할 수 있습니다.

  • Options API는 “컴포넌트 인스턴스”(this, 예시에서 볼 수 있듯이) 개념을 중심으로 하며, OOP 언어 배경을 가진 사용자에게는 클래스 기반의 사고방식과 더 잘 맞을 수 있습니다. 또한 반응성의 세부 사항을 추상화하고 옵션 그룹을 통한 코드 구조화를 강제하여 초보자에게 더 친숙합니다.
  • Composition API는 함수 스코프 내에서 반응형 상태 변수를 직접 선언하고, 여러 함수에서 상태를 조합하여 복잡성을 다루는 데 중점을 둡니다. 더 자유로운 형태이며, 효과적으로 사용하려면 Vue의 반응성 동작에 대한 이해가 필요합니다. 그 대신, 더 강력한 로직 구성 및 재사용 패턴을 가능하게 합니다.

3. Vue의 문법


Vue 문법은 HTML 기반 템플릿 문법을 사용하여 친숙하면서도 v- 접두사가 붙는 디렉티브 같은 경우 Vue로 작성된 코드라는 인상을 확연히 줍니다. 이러한 특성을 보이는 몇 가지 예제를 소개합니다.

3.1 데이터 바인딩 (텍스트 보간)

  • 이중 중괄호 {{ }}를 사용하여 데이터를 출력합니다.
  • 원시 HTML을 해석하고 출력하고 싶다면 v-html을 사용합니다.
const msg = 'hello!';
const rawHtml = '<span style="color: red">이것은 빨간색이어야 합니다.</span>';
<p>텍스트 보간 사용: {{ msg }}</p>
<p>v-html 디렉티브 사용: <span v-html="rawHtml"></span></p>

3.2 속성 바인딩

  • v-bind 디렉티브를 사용하여 HTML 속성 값을 데이터와 연결합니다. 축약하여 :로 표기합니다.
<!-- 아래 두 코드는 동일합니다. -->
<div v-bind:id="dynamicId"></div>
<div :id="dynamicId"></div>
  • 여러 속성을 가지는 객체가 있다면, v-bind를 사용하여 한 번에 바인딩 할 수 있습니다.
const objectOfAttrs = {
  id: 'container',
  class: 'wrapper',
  style: 'background-color:green'
};
<div v-bind="objectOfAttrs"></div>

3.3 이벤트 처리

  • v-on 디렉티브를 사용하여 DOM 이벤트를 핸들링 합니다. 축약하여 @로 표기합니다.
<!-- 아래 두 코드는 동일합니다. -->
<button v-on:click="doSomething">CLICK!</button>
<button @click="doSomething">CLICK!</button>

3.4 조건부 렌더링

  • v-if, v-else-if, v-else를 사용하여 조건에 따라 블록을 렌더링 합니다.
<div v-if="type === 'A'">
  A
</div>
<div v-else-if="type === 'B'">
  B
</div>
<div v-else-if="type === 'C'">
  C
</div>
<div v-else>
  A/B/C가 아님
</div>
  • v-show로 블록을 조건부로 표시할 수도 있습니다. 다른 점은 v-show가 적용된 요소는 항상 렌더링 되어 DOM에 남아있다는 것입니다.
<!-- isShowReady는 컴포넌트를 보여줄 준비가 되면 true로 변경됩니다. -->
<h1 v-show="isShowReady">Hello Vue!</h1>
  • v-if vs v-show
    • v-if는 “진짜” 조건부 렌더링입니다. 조건부 블록 내의 이벤트 리스너와 자식 컴포넌트가 토글에 따라 적절히 파괴되고 다시 생성됩니다.
    • v-show는 초기 조건과 관계없이 항상 렌더링 되며, CSS 기반 토글만 수행합니다.
    • 일반적으로 v-if가 토글 비용이 더 높고, v-show는 초기 렌더 비용이 더 높습니다. 용도에 따라 적절히 사용해야 합니다.

3.5 리스트 렌더링 (반복 렌더링)

  • v-for 디렉티브를 사용하여 배열을 기반으로 항목을 렌더링 할 수 있습니다. item in items와 같은 특별한 형태의 문법을 필요로 하며, items는 소스 데이터 배열, item은 반복되는 배열 요소입니다.
  • 현재 항목의 index를 받아올 수도 있습니다.
const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
<li v-for="item in items">
  {{ item.message }}
</li>
<li v-for="(item, index) in items">
  {{ index }} - {{ item.message }}
</li>
  • 객체에 대한 v-for도 지원하며, 순서는 객체에 대해 Object.values()를 호출한 결과를 기반으로 합니다.
const myObject = reactive({
  title: 'Vue.js에 대해 알아보자',
  author: '홍길동',
  publishedAt: '2025-11-19'
});
<li v-for="(value, key) in myObject">
  {{ key }}: {{ value }}
</li>
  • 정수도 지원하여 특정 범위만큼 렌더링을 반복할 수도 있습니다.
<li v-for="n in 10">{{ n }}번째 목록</li>

3.6 폼 입력 바인딩 (양방향 바인딩)

  • v-model 디렉티브를 사용하여 input, textarea, select와 같은 입력 요소와 데이터를 연결합니다. 데이터가 변경되면 입력 필드가 업데이트 되고, 값이 입력되면 데이터가 업데이트 됩니다.
<!-- 이렇게 써야했던 것을 -->
<input
  :value="text"
  @input="event => text = event.target.value">
<!-- 이렇게 쓸 수 있습니다. -->
<input v-model="text">
<!-- textarea -->
<textarea v-model="message" placeholder="여러 줄을 추가하세요"></textarea>
<!-- checkbox -->
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>
<!-- select -->
<div>선택됨: {{ selected }}</div>
<select v-model="selected">
  <option disabled value="">하나를 선택하세요</option>
  <option>A</option>
  <option>B</option>
  <option>C</option>
</select>
  • 컴포넌트에서의 v-model
    • 커스텀 컴포넌트에서도 동일한 방식으로 부모와 자식을 양방향 바인딩 할 수 있습니다.
<!-- 자식 컴포넌트 (ChildComponent.vue) -->
<template>
  <input
    type="text"
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)" 
    />
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
// 1. `modelValue` 이름의 prop을 정의 (필수!)
const props = defineProps({
  modelValue: String
});
// 2. 이벤트를 정의
const emit = defineEmits(['update:modelValue']);
// ...
</script>
<!-- 부모 컴포넌트 -->
<template>
  <ChildComponent v-model="parentSearchText" />
  <p>현재 검색어: {{ parentSearchText }}</p>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const parentSearchText = ref('');
</script>

4. Vue3.0의 등장


Vue3.0 Onepiece의 공식 이미지(그 Onepiece 맞습니다.)
출처 – Announcing Vue 3.0 “One Piece” | The Vue Point

Vue3.0은 2020년 9월 공식 출시되었습니다. 새로운 버전의 개발이 시작된 지 2년 만이었습니다. Vue3는 향상된 성능더 작은 번들 크기, 코드 베이스의 TypeScript로의 전환으로 TypeScript 친화적이라는 강점을 내세웁니다. 그로부터 몇 번의 안정화 버전 출시 이후 2022년 2월, Vue 프로젝트의 기본 템플릿이 Vue3로 변경되었습니다.

Vue2 LTS 관련 공지사항, 출처 – https://v2.vuejs.org/lts/

그리고 Vue2는 2023년 12월 말에 지원 종료(EOL)를 맞이합니다. 한컴싸인의 백오피스는 2021년부터 개발하였기에 당시의 기본 버전인 Vue2 기반으로 설계되었습니다. 부득이한 경우를 위하여 Vue2의 보안 업데이트를 제공하긴 하나 이는 일정 금액을 지불해야 하며, 서비스의 장기 지원과 원활한 유지 보수, 보안 취약점 업데이트를 위해서는 Vue3로의 전환은 필수불가결했기에 Vue3로의 전환에 착수하기로 하였습니다.

아래는 Vue3로 전환에서의 주요 사항을 공유합니다.

5. Composition API


Vue3로 버전을 올린 후, 각종 라이브러리를 호환되는 버전으로 변경하는 작업 외에 가장 많은 비중을 차지한 작업이 Composition API로의 변경입니다. 물론 Vue3에서도 Options API를 사용할 수 있습니다만, 더 나은 유지/보수를 위하여 권장되는 방법을 선택하였습니다.

Vue2에서 주로 사용하였던 Options API으로 작성한 코드는 data, method 등 역할(Options) 기반으로 구분되어 있어 Vue를 처음 접하는 개발자도 코드를 직관적으로 이해할 수 있으며, this를 이용하여 옵션에 쉽게 접근할 수 있습니다. 또한 믹스인(Mixin)이라는 로직 재사용 메커니즘을 이용하여, 컴포넌트 간에 로직을 공유하여 사용합니다. 그러나 이에는 서비스의 규모가 커질수록 아래와 같은 단점이 발생합니다.

Options API의 단점

  1. 관심사 로직의 분산
    • 하나의 기능(Feature)과 관련된 data, method, hook 등이 컴포넌트 파일 내의 여러 옵션에 흩어져 존재하게 됩니다. 이는 여러 기능을 다루어야 하는 컴포넌트에서 특히 두드러집니다.
  2. 로직 재사용의 어려움 (믹스인의 단점)
    • 컴포넌트 간 로직의 재사용을 위하여 믹스인을 주로 사용하는데, 주입된 로직이 어떤 믹스인에 의해 주입되었는지 출처가 불분명하여 추적이 어렵습니다.
    • 글로벌 믹스인을 이용하거나, 믹스인을 이용하여 또다시 믹스인을 만드는 등 로직이 복잡해질수록 의존성이 강해집니다.
    • 여러 믹스인이 동일한 속성 키를 사용하면 충돌을 일으킬 수 있습니다.
  3. 타입 추론의 한계
    • 초기부터 타입 추론을 염두에 두고 설계된 방식이 아니기에, Typescript 사용 시 타입 추론이 복잡해지고 불명확한 경우도 발생합니다.

이러한 단점을 Composition API를 적절히 사용하면 극복할 수 있습니다.

Composition API는 옵션을 선언하는 제공되는 함수를 사용하여 Vue 컴포넌트를 작성할 수 있게 해주는 API 집합입니다. 반응성 API, 생명주기 훅(Hook), 의존성 주입 API과 같은 API를 포괄하는 상위 개념입니다.

Composition api의 장점

  1. 더 유연한 코드 구성

코드를 기능 별, 즉 논리적 관심사 별로 배치할 수 있어 복잡한 비즈니스 로직의 응집도를 높입니다. 아래는 각각 Options API와 Composition API로 구현한 폴더 탐색기 코드에 논리적 관심사 별로 색깔을 입힌 모습입니다.

출처 – https://ko.vuejs.org/guide/extras/composition-api-faq

Options API의 경우 동일한 논리적 관심사의 코드가 서로 다른 옵션으로 분류되어 파일 여러 부분에 배치될 수밖에 없습니다. 따라서 수백 줄에 달하는 컴포넌트에서 하나의 논리적 관심사를 파악하려면 파일을 계속 위아래로 스크롤 하며 탐색해야 하므로 이해가 어렵습니다. 또한 로직 재사용을 위한 분리 등 리팩토링 시에도 더 많은 수고가 듭니다.

반면 Composition API의 경우 코드를 유연하게 배치할 수 있기 때문에, 동일한 논리적 관심사의 코드를 그룹화할 수 있습니다. 이렇게 배치하면 특정 기능에 대한 작업을 할 때, 더 이상 코드를 이리저리 오갈 필요가 없으며 코드를 외부로 분리하는 작업도 최소한의 노력으로 가능합니다. 이러한 리팩토링 마찰 감소는 대규모 코드 베이스의 장기적 유지 보수성의 핵심입니다.

  1. 더 나은 로직 재사용
    • Composition API의 주요 장점은 컴포저블(Composables) 함수 형태로 명료하고 효율적인 로직 재사용이 가능하다는 것입니다. 이는 Options API에서 주로 사용된 메커니즘인 믹스인의 단점을 해결합니다.
컴포저블(Composables)

Vue 애플리케이션에서의 “컴포저블” 이란 Composition API를 활용하여 상태를 가진 로직을 캡슐화 및 재사용하는 함수입니다.

아래는 컴포저블 함수 형태로 구현된 마우스 트래커 예제입니다.

import { ref, onMounted, onUnmounted } from 'vue'
// 관례상, 컴포저블 함수 이름은 "use"로 시작합니다.
export function useMouse() {
  // 컴포저블이 캡슐화하고 관리하는 상태
  const x = ref(0);
  const y = ref(0);
  // 컴포저블은 시간이 지남에 따라 관리하는 상태를 업데이트할 수 있습니다.
  function update(event) {
    x.value = event.pageX;
    y.value = event.pageY;
  }
  // 컴포저블은 소유 컴포넌트의
  // 생명주기에 훅을 걸어 부수 효과를 설정 및 해제할 수 있습니다.
  onMounted(() => window.addEventListener('mousemove', update));
  onUnmounted(() => window.removeEventListener('mousemove', update));
  // 관리하는 상태를 반환값으로 노출
  return { x, y };
}

컴포넌트에서 다음과 같이 사용할 수 있습니다.

<script setup>
import { useMouse } from './mouse.js';
const { x, y } = useMouse();
</script>
<template>마우스 커서 위치: {{ x }}, {{ y }}</template>

  1. 더 나은 타입 추론

최근 많은 사용자들이 프론트엔드 개발에 TypeScript를 채택하고 있습니다. 이는 코드의 안정성을 높이며 IDE 지원을 통하여 좋은 개발 경험을 제공합니다. 하지만 Options API는 2013년에 고안될 당시 타입 추론을 염두에 두지 않았기에, TypeScript 지원을 위하여 타입 추론이 동작하도록 매우 복잡한 Type Gymnastics (타입 체조)를 구현해야 했습니다. 그럼에도 불구하고 믹스인과 의존성 주입에서는 타입 추론이 불분명할 수 있습니다.

이에 비하여 Composition API에서의 반응형 상태와 함수는 평범한 변수와 함수처럼 정의되므로 별도의 힌트나 과정 없이 자연스럽고 완전하게 타입 추론이 가능합니다.

Type Gymnastics

주로 TypeScript 환경에서 사용되는 용어로, 컴파일러를 만족시키고 복잡한 로직을 타입 시스템 자체에서 구현하기 위해 고급 타입 기능을 극한으로 활용하는 관행을 의미합니다.

이는 실제 런타임 코드(JavaScript)가 아닌, 컴파일 타임(타입 레벨)에서 복잡한 연산, 변환, 검증 등을 수행하기 위해 TypeScript의 타입 시스템을 마치 프로그래밍 언어처럼 사용하는 것을 비유적으로 표현한 것입니다.

  1. 더 작은 번들 및 오버헤드 감소

Composition API와 <script setup>으로 작성된 코드는 더 효율적이고 난독화(파일 최소화)에 적합합니다. <script setup>에 작성된 코드는 인라인 함수로 컴파일 되기 때문입니다. 또한 this를 통하여 프로퍼티에 접근하는 것과 달리, 인스턴스 프록시 없이 내부에 선언된 변수에 직접 접근이 가능합니다.

💭경험

  1. 유연한 코드 배치
    • Composition API에서는 유연한 코드 배치로 동일한 논리적 관심사 간의 응집이 가능하다는 점을 메인 장점으로 소개하고 있습니다.
    • 하지만 코드 상단에는 변수 정의를, 하단에는 함수나 로직을 정의하는 관행적 코드 습관과 Options API로 작성했던 코드를 단순 리팩토링 한 부분이 많아, 논리적 관심사 이슈가 아직까지 큰 이점으로 느껴지진 않았습니다. 이는 여러 사용자들이 이야기하는 부분입니다.
    • 그러나 유연하게 배치가 가능하여 로직의 복잡도에 따라 다양한 방향으로 적용할 수 있어, 확장 가능성에 이점을 보았습니다.
  2. 보일러 플레이트 코드 감소
    • Composition API를 import 하는 구문이 생겼지만, data, method 등 직접 작성하는 보일러 플레이트 코드가 감소하여 새로운 컴포넌트 작성 시 초기 세팅 시간이 줄어들었습니다.
    • 소량이지만, 코드 라인 수가 대부분의 컴포넌트에서 감소하였습니다.

6. Provide / Inject, 그리고 Plugin


Vue3로 버전을 업그레이드하며 이전에 사용하던 라이브러리를 호환하는 것으로 교체하거나 없애는 작업이 필요했습니다. 그중 하나가 대화상자 라이브러리였고, 대화상자 동작 메커니즘을 새로 만들어야 했습니다.

대화상자는 어디서나 보여주고 닫을 수 있는 전역적 상태 관리가 필요합니다. Vue에서는 Pinia나 Vuex와 같은 공식 상태 관리 라이브러리를 제공합니다. 그러나 백오피스는 비교적 간결한 기능을 제공하는 서비스이기에 상태 관리 라이브러리를 사용하지 않는 상황이었고, 대화상자만을 위한 새로운 라이브러리의 도입은 부담이었습니다.

그래서 Composition API인 ProvideInject를 활용하여 간단한 상태 관리 플러그인(Plugin)을 만들기로 하였습니다.

Prop Drilling

일반적으로 부모에서 자식 컴포넌트로 데이터 전달 시 props를 사용합니다. 하지만 깊이 중첩된 컴포넌트에서 먼 조상 컴포넌트의 데이터를 필요로 한다면 props만으로는 동일한 prop을 모든 부모에 걸쳐 전달해야 합니다.

Prop Drilling, 출처 – https://vuejs.org/guide/components/provide-inject.html

<Footer> 컴포넌트는 이 props를 사용하지 않더라도 <DeepChild> 컴포넌트를 위하여 props를 선언하고 전달해야 합니다. 부모 체인이 더 길어진다면 더 많은 컴포넌트가 영향을 받게 됩니다. 이를 “props drilling”이라고 합니다.

provideinject를 이용하면 이러한 props drilling 문제를 해결할 수 있습니다. provide를 사용하여 모든 부모 컴포넌트는 자손 컴포넌트를 위한 의존성(데이터, 함수)을 제공할 수 있습니다. 깊이와 관계없이 모든 자손은 부모 트리 상단의 컴포넌트가 제공하는 의존성을 주입할 수 있습니다.

provide/injdex 흐름
출처 – https://vuejs.org/guide/components/provide-inject.html

Provide

컴포넌트의 자손에게 데이터를 제공하기 위해 provide() 함수를 사용합니다.

<script setup>
  import { ref, provide } from 'vue';
  // message 라는 이름의 key로 'hello!'를 제공합니다.
  provide(/* key */ 'message', /* value */ 'hello!');
  // 반응형 값을 제공하면, 사용하는 자손 컴포넌트가
  // 제공자인 부모 컴포넌트와 반응형 연결을 맺게 됩니다.
  const count = ref(0);
  provide('key', count);
</script>

Inject

데이터를 주입받고자 하는 자손 컴포넌트에서는 inject() 함수를 사용합니다.

<script setup>
import { inject } from 'vue';
const message = inject('message');
</script>

Plugin

Vue에서의 플러그인은 앱 수준의 기능을 추가할 수 있는 독립적인 코드를 말합니다. 플러그인을 정의하고 설치하는 방법은 아래와 같습니다.

// ./plugins/myplugin.js
export default {
  install: (app, options) => {
    // 플러그인 코드는 여기에 작성합니다
  }
}
// main.js
import { createApp } from 'vue';
import myPlugin from './plugins/myplugin';
const app = createApp({});
app.use(myPlugin, {
  /* 선택적 옵션 */
});

플러그인에서 Provide / Inject 사용하기

플러그인에서는 Provide를 통해 플러그인 사용자에게 함수 및 데이터에 접근할 수 있도록 제공할 수 있습니다. 위에 기술한 Provide, Inject, 플러그인을 종합하여 구현한 대화상자 상태 관리의 간단한 예시입니다.

플러그인 예시

// ./plugins/modal.js
export default {
  install(app) {
    const dataset = {
      // 대화상자별 정의
      CommonDialog: { isShow: ref(false), props: {} },
      UserAddDlg: { isShow: ref(false), props: {} },
      UserInfoDetailDlg: { isShow: ref(false), props: {} },
      ...
    };
    const show = (id, params) => {
      dataset[id].isShow.value = true;
      dataset[id].props = params;
    };
    const hide = (id) => {
      dataset[id].isShow.value = false;
      dataset[id].props = {};
    };
    const getIsShow = (id) => {
      return dataset[id].isShow.value;
    };
    ...
    // 필요한 것들만 provide
    const modal = {
      show,
      hide,
      getShow
    };
    app.provide('modal', modal);      // 공급!
  }
};

App level에서의 연결

// main.js
import { createApp } from 'vue';
import modal from '@/plugins/modal.js';
...
const app = createApp(App);
app.use(modal);           // plugin 연결

이제 어느 위치의 컴포넌트에서든 아래와 같이 호출하여 특정 대화상자를 띄울 수 있고,

<script setup>
  const modal = inject('modal');    // 주입!
  // 회원 추가
  const onAddUser = (type) => {
    modal.show('UserAddDlg');
  };
</script>

대화상자 내에서는 플러그인 내에 정의된 활성 관련 반응성 플래그(isShow)를 참조하여 활성화 여부를 결정하며, 플러그인 내의 함수(hide)를 참조하여 닫을 수도 있습니다.

<template>
  <!-- v-로 시작하는 컴포넌트는 vuetify 라이브러리 제공 컴포넌트입니다. -->
  <v-dialog
    v-model="isShow"
    name="UserAddDlg">
    ...
    <v-btn @click="hideDlg">
      close
    </v-btn>
  </v-dialog>
</template>
<script setup>
  const modal = inject('modal');
  const isShow = computed({
    get() {
      return modal.getIsShow('UserAddDlg');
    }
  });
  const hideDlg = () => {
    modal.hide('UserAddDlg');
  }
</script>

이렇게 Vue3의 Composition API와 플러그인 구조를 활용하여 간결하고 기능에 충실한 상태 관리 로직을 구현할 수 있습니다.

💭경험

  1. 가볍고 간결하다
    • 별도의 상태 관리 라이브러리를 사용해도 좋지만, 이는 설치 및 설정, 또 다른 보일러 플레이트 코드 작성이 필요합니다. 위 예시처럼 단순하고 독립적인 전역 상태 몇 가지의 관리만 필요하다면 Vue 내장 기능인 provide, inject만으로도 충분하고 가볍고 빠르게 개발이 가능합니다.
  2. 그래도, Pinia?
    • 서비스가 확대됨에 따라 요구하는 스펙도 점점 까다로워지고 있습니다. Vue 공식 상태 관리 라이브러리인 Pinia는 디버깅 툴, 스토어/컴포넌트 단위 테스트 기능, 데이터의 mock 처리 등 개발에 편리한 기능을 제공하기에 프로젝트가 더 복잡해진다면 도입을 고민해 보아야 합니다.
Pinia 소개 페이지, 출처 – Pinia 🍍

7. Vite


Vue2 시절에는 Vue 프로젝트의 빠른 설정 및 개발 환경 구축을 돕는 표준 툴인 Vue CLI 사용이 권장되었습니다. 또한 Webpack 기반으로 구성되었기에 빌드 도구로도 사용되었습니다. 그러나, Vue 3.3의 발표와 함께 Vue CLI는 유지 보수 모드로 돌입하였고, Vue3에서는 Vite가 공식적으로 권장 빌드 도구가 되었습니다.

Vite 로고, 출처 – https://github.com/vitejs/vite

Vite는 Vue3와 함께 등장한 빌드 도구로, Vue의 핵심 개발자인 Evan You에 의해 개발되었습니다. Vite는 프랑스어로 “빠르다(Quick)”를 의미하며, 그 뜻과 같이 모던 웹 프로젝트 개발 환경에 초점을 맞춘 빠르고 간결한 빌드 툴입니다.

브라우저에서 ES Module을 지원하기 전까지 JavaScript 모듈화를 네이티브 레벨에서 진행할 수 없었습니다. 그래서 소스 모듈을 브라우저에서 실행할 수 있는 파일로 만들어주는 “번들링”이라는 과정을 거쳐야 했고, Webpack, Rollup, Parcel 같은 도구들이 이를 해결함으로써 개발 생산성이 매우 향상되었습니다. 그러나 애플리케이션이 점점 더 거대해지며 모듈 개수가 극적으로 증가하고, 이러한 상황에서 JavaScript 기반의 도구들은 성능 병목 현상이 나타나게 됩니다.

Vite는 이러한 상황에 초점을 맞춰, 브라우저에서 지원하는 ES Module 및 네이티브 언어로 작성된 JavaScript 도구 등을 활용해 문제를 해결하고자 하였습니다.

Native ES Module (ESM)

ECMAScript 2015 (ES6) 표준에 도입된 자바스크립트 모듈 시스템을 의미합니다. 이 시스템은 파일을 모듈로 취급하여 importexport 구문을 사용해 코드를 가져오고 내보내는 것을 가능하게 하며, 특히 웹 브라우저가 별도의 도구 없이 이 모듈을 직접 해석하고 로드할 수 있게 합니다.

Vite는 두 가지의 주요 역할을 합니다.

  • 개발 서버 (Dev Server) : Native ES Module을 활용하여 매우 빠르고 즉각적인 HMR (Hot Module Replacement)을 제공합니다. 이는 코드 수정 시 브라우저가 거의 즉시 업데이트되어 개발 편의성이 좋아집니다.
  • 번들러 (Bundler) : 프로덕션 환경으로 배포 시, Rollup(https://rollupjs.org/)을 사용해 코드를 번들링 하여 최적화된 정적 자원(Static Asset)을 제공합니다.

개발 서버에서의 차이

Cold Start 방식으로 개발 서버 구동 시, 번들러 기반의 도구들은 모든 소스 코드에 대해 빌드 작업을 마쳐야지만 페이지를 제공할 수 있습니다.

기존 번들러에서의 개발 서버 제공 방법. 모든 소스코드를 번들링 한 후 제공한다.
출처 – https://ko.vite.dev/guide/why.html

그에 반해 Vite는 애플리케이션의 모듈을

  • 개발 시 그 내용이 바뀌지 않을 일반적인 JavaScript 소스 코드인 디펜던시
  • JSX, Vue와 같이 컴파일이 필요하거나 수정이 잦은 Non-Plain JavaScript 소스 코드

두 가지로 나눠 처리하여 개발 서버의 시작 시간을 개선합니다.

Vite에서는 디펜던시를 사전 번들링 할 때 Esbuild를 사용하는데, 이는 Webpack과 Parcel 등과 같은 기존 번들러 대비 10배 이상 빠른 속도를 보입니다.

또한 소스 코드는 Native ESM을 이용하여 브라우저가 직접 import 구문을 해석하고 요청하는 모듈에 대해서만 최소한의 처리만을 거쳐 제공하여 화면의 즉각적인 변경을 볼 수 있습니다.

Native ESM을 기반으로 한 Vite의 개발 서버 제공 방법.
import 시 소스 코드를 제공하여 브라우저와 역할을 나눈다.
출처 – https://ko.vite.dev/guide/why.html

Vue와 Vite

Vite는 특정 프레임워크에 종속적이지 않으나, Vue SFC, 특히 Vue3의 Composition API와 <script setup> 문법을 사용하는 프로젝트에 최적화되어 있습니다.

아래 명령은 Vite 기반으로 Vue3 프로젝트를 생성합니다.

npm init vue@latest

💭경험

  1. 빠르다
    • Vite로 빌드 툴을 변경한 후, 개발 서버의 최초 실행 시간이 매우 줄어들었고, 개발 시의 소스 코드 즉각 반영도 로딩 시간이 느껴지지 않을 정도로 빠르게 반영됩니다. 이런 경험은 Vue.js 때문은 아니지만, 빠르고 가볍다는 인상을 더해주었습니다.
  2. 하지만, 정답은 아니다
    • Vite는 여러 프론트엔드 프레임워크를 지원하며 React도 포함됩니다. 그럼 React를 사용하는 서비스에도 적용하면 좋겠다 싶지만, 이미 Next.js 같은 도구를 사용하고 있는 경우 기능적 이점을 포기하고 구조를 재설계 해야 하기 때문에 단순 교체는 어렵습니다.

Conclusion


가볍고 빠른 프레임워크라는 타이틀로 비교적 ‘덜’ 사용되던 Vue.js를 소개해 드렸지만, Vue가 탄생한 지 벌써 10년이 흐르면서 생태계는 더욱 탄탄해지고 안정적인 궤도에 올랐습니다. 그리고 프론트엔드 환경은 여전히 빠르게 변화하며 새로운 도구들이 계속 등장하고 있습니다. 각 도구가 해결하고자 하는 새로운 문제나 추구하는 가치가 있기 때문입니다.

가장 대중화된 도구를 선택하는 것은 협업을 쉽게 하고 시행착오를 줄이는 길입니다. 하지만 개발 경험이 쌓일수록 프로젝트의 용도와 목적을 고려하여 최적의 도구를 선택할 수 있게 됩니다.

짧은 개발 기간과 기능적 특수성에 Vue.js를 도입했던 것처럼 다양한 서비스를 개발하며 그 목적에 맞는 도구를 현명하게 선택한다면, 우리는 더 나은 개발 퍼포먼스와 만족스러운 제품 퀄리티를 추구할 수 있을 것입니다.

References


  1. https://vuejs.org/
  2. https://blog.vuejs.org/
  3. https://blog.vuejs.org/posts/vue-3-one-piece
  4. https://v2.vuejs.org/lts/
  5. https://ko.vuejs.org/guide/extras/composition-api-faq
  6. https://vuejs.org/guide/components/provide-inject.html
  7. https://pinia.vuejs.org/
  8. https://ko.vite.dev/
  9. https://github.com/vitejs/vite
  10. https://ko.vite.dev/guide/why.html
Scroll to Top