SPA(Single Page Application)에서 페이지를 새로고침하지 않고 화면을 전환하기 위한 Vue 공식 라우팅 라이브러리 정리
웹사이트에 Home과 About 페이지를 만든다고 할 때, SPA에서는 어떻게 페이지를 나눌 수 있을까? Vue Router는 이 문제를 해결하는 공식 도구다.
Vue Router는 크게 두 가지 핵심 컴포넌트를 제공한다.
<RouterLink><RouterView>즉, 페이지를 바꾸지 않고(새로고침 없이) 링크를 설정하는 방법을 다룬다.
Routing = 네트워크에서 경로를 선택하는 프로세스
라우팅은 사용자가 접속한 URL 주소에 따라 적절한 페이지(컴포넌트)를 보여주는 기능이다.
/home 주소는 Home 컴포넌트로, /about 주소는 About 컴포넌트로 연결하는 등 미리 정의된 경로에 따라 어떤 내용을 보여줄지 결정한다.
SSR: 서버에서 완성된 HTML 페이지를 만들어, 브라우저에 보내는 방식
CSR: 서버는 뼈대만 주고, 브라우저가 직접 페이지를 그리는 방식
결론: 페이지는 1개이지만, 주소에 따라 여러 컴포넌트를 새로 렌더링하여 마치 여러 페이지를 사용하는 것처럼 보이도록 해야 함
SPA: 하나의 페이지 안에서, 내용만 바꿔가며 보여주는 웹 앱
Vue Router = Vue 공식 라우터
Vue.js의 공식 라우팅 라이브러리로, Vue로 만든 SPA에서 페이지 이동 기능을 구현할 때 사용된다.
<router-link><router-view>라는 핵심 컴포넌트를 제공한다. 어떤 URL 경로에 어떤 컴포넌트를 보여줄지 정의하기만 하면, Vue Router가 연결해준다.
$ npm create vue@latest
프로젝트 생성 과정에서 Select features to include in your project 단계에서 space 키로 Router (SPA development) 옵션을 선택한다.
□ TypeScript
□ JSX Support
■ Router (SPA development) ← space로 선택
□ Pinia (state management)
□ Vitest (unit testing)
□ End-to-End Testing
□ ESLint (error prevention)
□ Prettier (code formatting)
Home, About 링크에 따라 변경되는 URL과 새로 렌더링되는 화면을 확인한다.
TIP — Vue 서버 실행하는 방법
1.npm create vue@latest
2. 생성된 프로젝트 폴더로 이동 ($ cd [프로젝트명])
3. 패키지 설치 ($ npm install)
4. 서버 실행 ($ npm run dev)
Router를 추가하면 프로젝트 구조에 다음 변화가 생긴다.
App.vue 코드 변화router 폴더 신규 생성 (router/index.js)views 폴더 신규 생성 (HomeView.vue, AboutView.vue)src/
├── assets/
├── components/
├── router/
│ └── index.js
├── views/
│ ├── AboutView.vue
│ └── HomeView.vue
├── App.vue
└── main.js
<a> 태그를 렌더링<!-- App.vue -->
<template>
<header>
<div class="wrapper">
<HelloWorld msg="You did it!" />
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</div>
</header>
<RouterView />
</template>
RouterLink: 페이지 이동 링크 (네비게이션 부분)RouterView: 현재 URL에 맞는 컴포넌트가 렌더링되는 위치/, home, …)Django에서
urls.py의 역할을 해준다.
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue'),
},
],
})
export default router
TIP
- 프로젝트 규모가 작을 때는
index.js하나로 관리해도 충분하다.- 프로젝트 규모가 커질 때는 기능별로 경로 관리를 하고,
index.js에서 합치는 방식으로 운영한다.
RouterView 위치에 렌더링할 컴포넌트를 배치components 폴더와 기능적으로 다른 것은 없으며 단순 분류의 의미로 구성됨일반 컴포넌트와 구분하기 위해 컴포넌트 이름을
View로 끝나도록 작성하는 것을 권장
| 구분 | 의미 |
|---|---|
| 일반 컴포넌트 | 조각 |
| views | 기능별 page |
index.js에 라우터 관련 설정 작성RouterLink에 index에 정의한 주소 값 작성RouterLink 클릭 시 경로와 일치하는 컴포넌트가 RouterView에서 렌더링// index.js
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
}, // …
]
})
to 속성으로 index.js에서 정의한 주소 값(path)을 사용<!-- App.vue -->
<template>
<header>
<div class="wrapper">
<HelloWorld msg="You did it!" />
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</div>
</header>
<RouterView />
</template>
to는<a>링크로 변환되어 웹에서 실행될 것임.
<RouterView />가 위치한 자리에 경로에 맞는 컴포넌트가 그려진다.
index.js에서 입력한 path 경로를 그대로 RouterLink에 사용하고 있음// index.js
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [ {
path: '/', // ← 이 경로를
name: 'home',
component: HomeView,
}, {
path: '/about', // ← 직접 사용하면
name: 'about',
component: () => import('../views/AboutView.vue'),
}]
})
<!-- App.vue -->
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
해결책: name과 바인딩(v-bind)을 통해 문제점을 해결한다.
name 속성 값에 경로에 대한 이름을 지정RouterLink에 v-bind를 사용해 to에 props 객체로 전달 가능// index.js
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [ {
path: '/',
name: 'home',
component: HomeView,
}, {
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue'),
}]
})
<!-- App.vue -->
<RouterLink :to="{ name: 'home' }">Home</RouterLink>
<RouterLink :to="{ name: 'about' }">About</RouterLink>
v-bind: 콜론(
:)을 사용해, HTML 속성을 데이터와 실시간으로 연동
props 객체: 부모 컴포넌트가 자식 컴포넌트에게 데이터를 전달하는 통로
Django에서의 Variable Routing
Dynamic Route Matching = URL의 일부를 변수로 사용하여 경로를 동적으로 매칭
동적 라우트 매칭은 /user/1, /user/2처럼 패턴은 같지만 ID 값만 다른 여러 URL을 하나의 라우트 설정으로 처리하는 기능이다. 이를 통해 수백 개의 상품 상세 페이지나 사용자 프로필 페이지를 각각 만들 필요 없이, 하나의 컴포넌트를 재사용하여 효율적으로 개발할 수 있다.
주어진 패턴의 여러 경로를 하나의 컴포넌트에 매핑해야 하는 경우 (예: 모든 사용자의 ID를 활용한 프로필 페이지 URL)
user/1
user/2
user/3
→ 일정한 패턴의 URL 작성을 반복해야 함. 이럴 때 사용하는 것이 Dynamic Route Matching.
views 폴더 내 UserView 컴포넌트 작성
<!-- UserView.vue -->
<template>
<div>
<h1>UserView</h1>
</div>
</template>
매개변수는 콜론(:)으로 표기하고, UserView 컴포넌트 라우터를 등록
// index.js
import UserView from '../views/UserView.vue'
const router = createRouter({
routes: [
{
path: '/user/:id',
name: 'user',
component: UserView
},
]
})
params 속성의 객체 타입으로 전달⚠️ 단, 객체의 key 이름과 index.js에서 지정한 매개변수 이름이 같아야 함 (
:id↔'id')
<!-- App.vue -->
<script setup>
import { ref } from 'vue'
const userId = ref(1)
</script>
<!-- App.vue -->
<RouterLink :to="{ name: 'user', params: { 'id' : userId } }">User</RouterLink>
경로가 일치하면 라우트의 매개변수는 컴포넌트에서 $route.params로 참조 가능
<!-- UserView.vue -->
<template>
<div>
<h1>UserView</h1>
<h2>{{ $route.params }}번 User 페이지</h2>
<h2>{{ $route.params.id }}번 User 페이지</h2>
</div>
</template>
출력 결과 예시:
UserView
{ "id": "1" }번 User 페이지
1번 User 페이지
$route.params로 template에 바로 작성하는 것보다는useRoute() 함수를 사용해 스크립트 내에서 반응형 변수에 할당 후 템플릿에 출력하는 것을 권장$route를 사용하는 것과 동일<!-- UserView.vue -->
<script setup>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const userId = ref(route.params.id)
</script>
<!-- UserView.vue -->
<template>
<div>
<h2>{{ userId }}번 User 페이지</h2>
</div>
</template>
Nested Routes = 중첩된 라우팅
애플리케이션의 UI는 여러 레벨 깊이로 중첩된 컴포넌트로 구성되기도 한다. 이 경우 URL 또한 중첩된 컴포넌트 구조에 맞춰 표현할 수 있으며, 이를 Nested Routes라 부른다.
/user/:id/profile /user/:id/posts
┌─ User ──────────┐ ┌─ User ──────────┐
│ ┌─ Profile ─┐ │ ⟷ │ ┌─ Posts ───┐ │
│ └───────────┘ │ │ └───────────┘ │
└─────────────────┘ └─────────────────┘
중첩 라우트는 특정 페이지(부모)의 레이아웃은 유지한 채, 그 안의 일부 영역만 다른 내용으로 교체하는 라우팅 방식이다. '사용자' 페이지의 사이드바는 그대로 두고, 주된 내용 영역만 '프로필', '게시글 목록' 등으로 바꿔 보여주는 대시보드 형태의 UI를 만들 때 매우 유용하다.
- URL마다 보여주고 싶은 페이지가 다르다!
- Django의
include()와 같은 개념- 라우터 안에 라우터를 만들어서 구현

components 폴더에 UserProfile, UserPosts 컴포넌트 작성 후 라우터 등록
<!-- UserProfile.vue -->
<template>
<div>
<h1>UserProfile</h1>
</div>
</template>
<!-- UserPosts.vue -->
<template>
<div>
<h1>UserPosts</h1>
</div>
</template>
// index.js
import UserProfile from '@/components/UserProfile.vue'
import UserPosts from '@/components/UserPosts.vue'
children 옵션을 사용해 중첩된 라우터에 컴포넌트를 등록children 옵션은 배열 형태로 필요한 만큼 중첩 관계를 표현할 수 있음// index.js
{
path: '/user/:id',
component: UserView,
name: 'user',
children: [
// UserProfile은 UserView의 <RouterView> 내부에 렌더링
{ path: 'profile', name: 'userProfile', component: UserProfile },
// UserPosts는 UserView의 <RouterView> 내부에 렌더링
{ path: 'posts', name: 'userPosts', component: UserPosts }
],
},
두 컴포넌트(userProfile, userPosts)에 대한 RouterLink 및 RouterView 작성 후 클릭하여 렌더링 결과 확인
<!-- UserView.vue -->
<script setup>
import { useRoute, RouterLink, RouterView } from 'vue-router'
</script>
<template>
<div>
<RouterLink :to="{ name: 'userProfile' }">Profile</RouterLink>
<RouterLink :to="{ name: 'userPosts' }">Posts</RouterLink>
<h1>UserView</h1>
<h2>{{ userId }}번 User 페이지</h2>
<hr>
<RouterView />
</div>
</template>
/user/:id로 이동했을 때 항상 중첩된 경로를 바로 렌더링 가능/user/:id 접속 시 바로 중첩된 경로 표시(UserProfile)// index.js
{
path: '/user/:id',
// name: 'user', // ← 상위 경로의 이름 제거
component: UserView,
children: [
{ path: '', name: 'user', component: UserProfile }, // 빈 path
{ path: 'profile', name: 'userProfile', component: UserProfile },
{ path: 'posts', name: 'userPosts', component: UserPosts }
],
},
⚠️ 단, 이전처럼 상위 경로의 이름을 유지하는 구조로 작성해도 무방함
path는 / 없이 작성해야, 부모 경로 뒤에 자동으로 연결됨:id)는 자식 컴포넌트에서도 바로 접근해서 사용할 수 있음Programmatic Navigation =
<RouterLink>를 사용하는 대신, JavaScript 코드를 사용해 페이지를 이동시키는 것
Programmatic Navigation은 사용자가 <router-link>를 클릭하는 대신, JavaScript 코드(로직)를 통해 특정 URL로 이동시키는 기능이다. router.push()와 같은 메서드를 호출하여, 원하는 경로로 강제 이동시킬 수 있다.
Django에서 CREATE 하고 나면 Detail 페이지로 redirect 해줬던 것.
<a> 태그를 만드는 것처럼 프로그래밍으로 네비게이션 관련 작업을 수행할 수 있음router.push() — 다른 위치로 이동하기router.replace() — 현재 위치 바꾸기router.push()를 호출하는 것과 같음| 선언적 표현 | 프로그래밍적 표현 |
|---|---|
<RouterLink :to="…"> | router.push(…) |
활용 — UserView에서 HomeView로 이동하는 버튼 만들기
<!-- UserView.vue -->
<script setup>
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
const goHome = function () {
router.push({ name: 'home' })
}
</script>
<!-- UserView.vue -->
<button @click="goHome">홈으로!</button>
| 선언적 표현 | 프로그래밍적 표현 |
|---|---|
<RouterLink :to="…" replace> | router.replace(…) |
활용 — UserView에서 HomeView로 이동하는 버튼 만들기
<!-- UserView.vue -->
<script setup>
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
const goHome = function () {
router.replace({ name: 'home' })
}
</script>
공식 문서: https://router.vuejs.org/guide/essentials/navigation.html
// literal string path
router.push('/users/alice')
// object with path
router.push({ path: '/users/alice' })
// named route with params to let the router build the url
router.push({ name: 'user', params: { username: 'alice' } })
// with query, resulting in /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })
useRoute()는 컴포넌트의 setup 함수나 <script setup> 최상단에서만 호출해야 함<!-- UserView.vue -->
<script setup>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const userId = ref(route.params.id)
</script>
route 객체의 특징
route.params), 쿼리(route.query), name(route.name) 등을 통해 현재 페이지 상태를 알 수 있음route.params.id를 참조하고 있다면, URL이 바뀌어 id가 변경될 때 해당 값이 자동으로 반영됨useRouter는 페이지 이동 등 액션용, useRoute는 경로 정보 읽기용으로 역할이 다름<!-- UserView.vue -->
<script setup>
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
const goHome = function () {
router.push({ name: 'home' })
}
</script>
| 구분 | useRoute()로 얻은 객체 (route) | useRouter()로 얻은 객체 (router) |
|---|---|---|
| 개념 | 현재 경로(페이지) 정보 읽기용 | 전체 라우팅 관리용 인스턴스 |
| 역할 | 현재 상태 확인 | 경로 변경, 이동 |
| 주용도 | params, query, name 등 현재 라우트 정보 확인 | push, replace 등으로 라우트 변경 (네비게이션) |
| 예제 | route.params.id 확인 | router.push('/home')로 페이지 이동 |
Navigation Guard = Vue router를 통해 특정 URL에 접근할 때 다른 URL로 redirect를 하거나 취소하여 내비게이션을 보호하는 기능
내비게이션 가드는 특정 URL로 이동하기 전이나 후에 실행되는 기능이다. 라우트 전환 전/후에 자동으로 실행되며 사용자의 로그인 상태나 권한을 확인하여, 내비게이션을 허용하거나, 취소하거나, 다른 페이지로 리다이렉트시킬 수 있다. 주로 로그인하지 않은 사용자가 '마이페이지'에 접근하는 것을 막고 로그인 페이지로 보내는 등, 인증 기반의 라우팅 로직을 구현할 때 사용된다.
| 종류 | 적용 범위 | 작성 위치 |
|---|---|---|
| Globally (전역 가드) | 애플리케이션 전역에서 모든 라우트 전환에 적용 | index.js |
| Per-route (라우터 가드) | 특정 라우트에만 적용 | index.js의 각 routes |
| In-component (컴포넌트 가드) | 컴포넌트 내에서만 적용 | 각 컴포넌트의 <script> 내부 |
index.js)beforeEach()beforeResolve()afterEach()다른 URL로 이동하기 직전에 실행되는 함수 (Global Before Guards)
router.beforeEach((to, from) => {
…
return false // 또는 return { name: 'About' }
})
to: 이동할 URL 정보가 담긴 Route 객체 (목적지)from: 현재 URL 정보가 담긴 Route 객체 (출발지)falsefrom 경로의 URL로 재설정router.push()를 호출하는 것처럼 경로 위치를 전달하여 다른 위치로 redirectto URL Route 객체로 이동HomeView에서 UserView로 이동했을 때 각 인자 값 출력 확인하기. to에는 이동할 URL인 user 라우트에 대한 정보가, from에는 현재 URL인 home 라우트에 대한 정보가 들어있음.
// index.js
router.beforeEach((to, from) => {
console.log(to)
console.log(from)
})
export default router
to → { fullPath: '/user/1', hash: '', query: {…}, name: 'user', path: '/user/1', …}
from → { fullPath: '/', path: '/', query: {…}, hash: '', name: 'home', …}
"Login이 되어있지 않다면 페이지 진입을 막고 Login 페이지로 이동시키기"
만약 로그인이 되어있지 않고(1), 이동하는 주소 이름이 login이 아니라면(2) login 페이지로 redirect
<!-- LoginView.vue -->
<template>
<div>
<h1>Login View</h1>
</div>
</template>
// index.js
router.beforeEach((to, from) => {
const isAuthenticated = false
if (!isAuthenticated && to.name !== 'login') {
console.log('로그인이 필요합니다.')
return { name: 'login' }
}
})
<!-- App.vue -->
<RouterLink :to="{ name: 'login' }">Login</RouterLink>
beforeEach와 모든 컴포넌트 단위 가드가 실행된 후, 내비게이션이 확정되기 직전에 호출beforeEach보다 낮음document.title)을 변경하는 등 후처리 작업에 적합index.js의 각 routes)beforeEnter 가드를 많이 사용특정 route에 진입했을 때만 실행되는 함수
routes: [
{
path: '/user/:id',
name: 'user',
component: UserView,
beforeEnter: (to, from) => {
...,
return false
}
},
…
]
HomeView에서 LoginView로 이동 후 각 인자 값 출력 확인. to에는 이동할 URL인 login 라우트에 대한 정보, from에는 현재 URL인 home 라우트에 대한 정보가 있음. 다른 경로에서 login 라우트를 탐색했을 때 실행되는 것.
// index.js
{
path: '/login',
name: 'login',
component: LoginView,
beforeEnter: (to, from) => {
if (isAuthenticated === true) {
console.log('이미 로그인 상태입니다.')
return { name: 'home' }
}
}
}
to → { fullPath: '/login', hash: '', query: {…}, name: 'login', path: '/login', …}
from → { fullPath: '/', path: '/', query: {…}, hash: '', name: 'home', …}
이미 로그인 한 상태라면 LoginView 진입을 막고 HomeView로 이동시키기.
로그인 상태라면 HomeView로 이동, 로그인 상태가 아니라면 LoginView로 이동.
⚠️ 전역 가드
beforeEach관련 코드는 주석 처리 후 진행
// index.js
const isAuthenticated = true
const router = createRouter({
routes: [
{
path: '/login',
name: 'login',
component: LoginView,
beforeEnter: (to, from) => {
if (isAuthenticated === true) {
console.log('이미 로그인 상태입니다.')
return { name: 'home' }
}
}
}, … ]
})
<script> 내부)onBeforeRouteLeave()onBeforeRouteUpdate()<!-- UserView.vue -->
<script setup>
import { onBeforeRouteLeave } from 'vue-router'
onBeforeRouteLeave((to, from) => {
const answer = window.confirm('정말 떠나실 건가요?')
if (answer === false) {
return false
}
})
</script>
UserView 페이지에서 다른 id를 가진 User의 UserView 페이지로 이동하기.
같은 라우트 내에서 업데이트 되는 경우 (/user/1 → /user/100)
<!-- UserView.vue -->
<button @click="routeUpdate">100번 유저 페이지</button>
<!-- UserView.vue -->
<script setup>
import { onBeforeRouteUpdate } from 'vue-router'
const routeUpdate = function () {
router.push({ name: 'user', params: { id: 100 } })
}
onBeforeRouteUpdate((to, from) => {
userId.value = to.params.id
})
</script>
만약 onBeforeRouteUpdate를 사용하지 않았다면?
onBeforeRouteUpdate에서userId를 변경하지 않으면 userId는 갱신되지 않음 (같은 컴포넌트가 재사용되기 때문)
<!-- UserView.vue -->
<script setup>
// onBeforeRouteUpdate((to, from) => {
// userId.value = to.params.id
// })
</script>
→ URL은 /user/100으로 바뀌어도 화면에는 여전히 1번 User 페이지가 표시됨.
| 종류 | 동작 범위 | 작성 위치 |
|---|---|---|
| Globally (전역 가드) | 애플리케이션 전역에서 동작 | index.js |
| Per-route (라우터 가드) | 특정 route에서만 동작 | index.js의 각 routes |
| In-component (컴포넌트 가드) | 특정 컴포넌트 내에서만 동작 | 각 컴포넌트의 <script> |
https://router.vuejs.org/guide/advanced/navigation-guards.html
// index.js
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue')
},
urls.py 역할.name으로 경로 참조 → 유지보수 용이, 오타 방지.:id 매개변수로 하나의 컴포넌트 재사용. route.params로 참조.children 옵션으로 부모 레이아웃 유지 + 일부 영역만 교체.router.push()(스택 쌓음) / router.replace()(스택 안 쌓음).useRoute(읽기 전용 상태) vs useRouter(이동 액션).beforeEach/beforeResolve/afterEach) / 라우터(beforeEnter) / 컴포넌트(onBeforeRouteLeave/onBeforeRouteUpdate).