들어가며
업무에서 프론트엔드 개발에 Vue.js 프레임워크를 활용하고 있다. 기존 소스코드를 빠르게 파악하기 위해 공식 문서를 읽으면서 필요한 개념들을 공부하였다. SPA의 정의와 Vue.js 프레임워크 소개, 가상 DOM을 활용하는 렌더링 메커니즘, 반응성, 컴포넌트, 라우팅까지 “당장 필요한 만큼만” 이해해보고자 한다. 그 밖에 생명 주기, 상태 관리 등 세부적인 내용은 공식 문서에 잘 정리가 되어있으므로 추후 개발하면서 필요할 때마다 참고할 수 있다.
SPA(Single Page Application)
SPA를 용어 그대로 풀이하면, 하나의 페이지로 이루어진 어플리케이션이다. 많은 경우에 웹 어플리케이션은 사용자의 복잡한 상호작용을 지원할 필요가 있다. 하나의 화면 안에서도 사용자의 권한 여부에 따라 컨텐츠를 다르게 보여준다거나, 사용자의 상호작용(입력)에 따른 데이터 및 화면 변경이 복잡하게 수행될 수 있다. SPA에서는 하나의 페이지가 여러 개의 컴포넌트로 구성되어 있으며, 각 컴포넌트는 자식 컴포넌트 여러 개로 구성될 수 있고 상태에 따라 그 내용(구성,데이터,…)이 바뀔 수 있다. 라우팅(경로 이동) 시에도 새로고침을 하지 않고, 그 경로에 맞는 컴포넌트를 화면에 보여주어 높은 사용자 경험을 제공한다(화면 깜빡임 방지).
Vue.js?
Vue는 사용자 인터페이스(UI)를 구축하기 위한 Javascript 프레임워크이다. 컴포넌트 기반의 프로그래밍 모델을 제공하여 효율적으로 UI를 개발할 수 있다. Single Page Application으로써 널리 활용되며, 상황에 따라 SSR 프레임워크(Nuxt.js)로도 활용할 수도 있고, 별도의 빌드 없이 정적 HTML 파일을 꾸며주기도 하는 등 다양한 용도로 활용할 수 있다.
공식 문서에서는 핵심 기능으로 선언적 렌더링과 반응성을 제시하고 있다. 선언적 렌더링이란 Javascript 상태에 따라 HTML 출력을 선언적으로 기술할 수 있는 특성을 의미한다. HTML을 확장한 템플릿 문법을 활용하여 쉽게 기술할 수 있다. 한편, 반응성을 제공한다는 것은 JavaScript 상태 변화를 자동으로 추적하고, 변화가 발생하면 DOM을 효율적으로 업데이트함을 의미한다.
가상 DOM을 활용한 렌더링 메커니즘
그러면 Vue.js는 어떻게 화면을 그릴까(렌더링할까)? Vue의 각 컴포넌트는 템플릿(template)으로 작성되며 이러한 템플릿들은 HTML의 DOM으로 변환된다. 참고로 DOM이란 Document Object Model의 줄임말로, 브라우저가 HTML 문서를 메모리에 올려놓은 “트리 구조 객체 모델”을 의미한다. 웹 페이지의 구성 요소들을 프로그래밍 할 수 있는 인터페이스라고 보면 된다. Vue.js는 이러한 템플릿을 DOM으로 변환할 때, 가상의 DOM을 만들어서 메모리에 보관하고, 실제 DOM으로 변환해주게 된다.
이러한 렌더링 과정 - 렌더 파이프라인 - 을 좀 더 상세히 살펴보자. 크게 컴파일 → 마운트 → 패치 3단계로 구성되며, 각 단계에 대한 설명은 공식 문서 설명이 잘 되어 있어서 그대로 가져왔다.

높은 수준에서, Vue 컴포넌트가 마운트될 때 다음과 같은 일이 일어납니다:
- 컴파일: Vue 템플릿은 렌더 함수로 컴파일됩니다. 렌더 함수는 가상 DOM 트리를 반환하는 함수입니다. 이 단계는 빌드 단계에서 미리 수행할 수도 있고, 런타임 컴파일러를 사용해 실시간으로 수행할 수도 있습니다.
- 마운트: 런타임 렌더러가 렌더 함수를 호출하여 반환된 가상 DOM 트리를 순회하고, 이를 기반으로 실제 DOM 노드를 생성합니다. 이 단계는 반응형 효과로 수행되므로, 사용된 모든 반응형 의존성을 추적합니다.
- 패치: 마운트 시 사용된 의존성이 변경되면, 효과가 다시 실행됩니다. 이때 새로운, 업데이트된 가상 DOM 트리가 생성됩니다. 런타임 렌더러는 새 트리를 순회하며 이전 트리와 비교하고, 실제 DOM에 필요한 업데이트를 적용합니다.
반응성
사용자의 동작에 의해 변수의 값(상태)이 바뀌었을 경우, 이를 화면에 반영해 줄 수 있어야 한다. Vue.js에서는 data() 옵션을 활용해서 변수를 반응형으로 바꿔준다. 해당 변수가 변경되었을 때, 화면에도 반영된다. 한편, methods에 메서드를 선언할 수 있다. 주의할 점은, 화살표 함수는 사용할 수 없다. 일반 함수와 달리 this가 컴포넌트의 인스턴스로 바운드 될 수 없기 때문이다.
예제 코드 - data()와 methods
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
// 메서드는 라이프사이클 훅이나 다른 메서드에서 호출할 수 있습니다!
this.increment()
}
}
예제 코드 - 메서드를 사용하는 template 코드
<button @click="increment">{{ count }}</button>
컴포넌트
개념
Vue.js 어플리케이션은 컴포넌트라는 요소들로 구성되어 있다. 컴포넌트란 UI를 독립적이고 재사용 가능하게 분리한 조각으로, Vue를 활용해서 만드는 앱은 중첩된 컴포넌트의 트리 구조로 이루어져있다.

정의
다음과 같이 .vue 파일로 정의하며, <script>, <template> 블록으로 구성된다. 각각의 .vue 파일을 **싱글 파일 컴포넌트(SFC)**라고 한다.
<script>
export default {
data() {
return {
count: 0
}
}
}
</script>
<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>
사용법
기본적으로는 .vue 파일 이름과 동일하게 내보내기가 된다. import 해와서 사용할 수 있고, export default 블록 하위에 components로 작성해주어야 한다. 그렇게 하면 </component>와 같이 부모 컴포넌트에서 넣어서 사용할 수 있다.
<script>
// 이름과 동일하게 내보내기
import ButtonCounter from './ButtonCounter.vue'
export default {
components: {
ButtonCounter
}
}
</script>
<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
</template>
컴포넌트 간의 통신
Vue.js는 컴포넌트 간의 통신을 지원한다. 부모 컴포넌트에서 자식 컴포넌트로 데이터(값)를 전파하고, 자식 컴포넌트에서 부모 컴포넌트로 이벤트를 전파한다.
prop : 부모 컴포넌트 → 자식 컴포넌트
자식 컴포넌트에서 전달 받을 변수를 선언하고, 부모 컴포넌트에서 해당 변수에 데이터(값)를 넘겨줄 수 있다. v-bind(:) 문법을 활용하여 데이터를 동적으로 전달할 수 있다.
BlogPost.vue(자식 컴포넌트)
<script>
export default {
props: ['title']
}
</script>
<template>
<h4>{{ title }}</h4>
</template>
App.vue(부모 컴포넌트)
<script>
export default {
// ...
data() {
return {
posts: [
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
]
}
}
}
</script>
<template>
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
</template>
emit: 자식 컴포넌트 → 부모 컴포넌트
자식에서 부모로 이벤트를 전달한다. $emit으로 전파하며, @혹은 v-on으로 이벤트를 리스닝한다.
BlogPost.vue(자식 컴포넌트)
<script>
export default {
props: ['title'],
// emits 명시는 필수 사항은 아니나 권장됨
emits: ['enlarge-text']
}
</script>
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlarge-text')">Enlarge text</button>
</div>
</template>
App.vue(부모 컴포넌트)
...
<template>
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
@enlarge-text="postFontSize += 0.1"
/>
</template>
동적 컴포넌트
<component>와 :is라는 특별한 속성을 활용해서, 상황에 맞게 동적으로 컴포넌트를 넣을 수 있다. 탭 화면을 구현하는 데 유용하게 활용될 수 있다. is에 전달되는 값은 등록된 컴포넌트의 이름 문자열, 또는 실제 import 한 컴포넌트 객체가 된다.
<component :is= "currentTab"></component>
라우팅
사용자가 URL을 업데이트하면 해당하는 컨텐츠로 연결해주는데 이를 라우팅이라 한다. SPA에서는 클라이언트 사이드 라우팅 방식을 채택하여, 라우팅이 될 때마다 페이지를 서버에서 다시 로드하지 않고 페이지 내에서 바로 전환해준다. 공식적으로 vue-router 라이브러리를 지원하며, 크게 보아 라우터 객체를 만들고, 루트 컴포넌트(App.vue)에 등록한 뒤, RouterLink에 해당하는 RouterView를 보여준다. 동적 라우팅, 중첩 라우팅, 프로그래밍 방식 라우팅, 네비게이션 가드 등 다양한 개념들에 대해서도 설명하고 있어, 보다 섬세한 조작이 필요하다면 공식 문서를 참고할 수 있다.
라우터 객체 만들기
어떤 경로(path)에서 어떤 컴포넌트를 보여줄 지 명시한다. 참고로 history는 URL과 라우트가 어떻게 매핑되는 지 결정하는 방법을 정의하는 옵션이다. 상세한 설명은 공식 문서의 히스토리 모드 섹션에서 다룬다.
import { createMemoryHistory, createRouter } from 'vue-router'
import HomeView from './HomeView.vue'
import AboutView from './AboutView.vue'
const routes = [
{ path: '/', component: HomeView },
{ path: '/about', component: AboutView },
]
const router = createRouter({
history: createMemoryHistory(),
routes,
})
루트 컴포넌트(App.vue)에 등록하기
App을 마운트하기 이전에, .use를 활용하여 router를 등록한다.
createApp(App)
.use(router)
.mount('#app')
RouterLink-RouterView 작성
RouterLink를 통해 해당 경로로 이동하는 링크를 만든다. 해당 경로로 이동했을 때, RouterView 에 라우터에 등록해 둔 컴포넌트가 보이게 된다.
<template>
<h1>Hello App!</h1>
<p>
<strong>현재 라우트 경로:</strong> {{ $route.fullPath }}
</p>
<nav>
<RouterLink to="/">홈으로 이동</RouterLink>
<RouterLink to="/about">소개로 이동</RouterLink>
</nav>
<main>
<RouterView />
</main>
</template>
참고 자료
각 항목의 내용은 공식 문서를 참고하여 작성
SPA
Vue.js
Vue.js - 프로그래시브 자바스트립트 프레임워크
ko.vuejs.org
Vue.js?
Vue.js
Vue.js - 프로그래시브 자바스트립트 프레임워크
ko.vuejs.org
반응성
Vue.js
Vue.js - 프로그래시브 자바스트립트 프레임워크
ko.vuejs.org
컴포넌트
Vue.js
Vue.js - 프로그래시브 자바스트립트 프레임워크
ko.vuejs.org
라우팅
Vue Router | Vue.js를 위한 공식 라우터
Vue.js 공식 라우터
router.vuejs.kr