네비게이션 상 2개의 링크 (Teams, Users)가 존재한다 하자.
그러나 네비게이션의 링크를 클릭하면 화면에 표시되는 스크린은 바뀌지만, URL은 바뀌지 않는다.
Vue를 이용해서 Javascript 기반 클라이언트 사이드 웹 애플리케이션을 구축할 수 있는데, 이때 사용자는 브라우저에서 실행되는 JavaScript가 제어하는 것만 볼 수 있다. (이것이 Vue의 역할이기도 하다.)
또한, 애플리케이션이 패칭되는 단일 HTML 파일에 작성하는 모든 JavaScript 코드 스크립트가 import 되어있는 싱글 페이지 애플리케이션(SPA)를 구축하고 있다. 단순히 teams, users 폴더에 작성한 코드가 아니라, 해당 코드에 대한 변형 및 최적화 버전 코드가 포함된다. 이는 CLI가 모든 것을 백그라운드에 관리하기에 가능한 일이다. 결국, 하나의 HTML을 가지고 JavaScript 코드가 브라우저에서 실행되는, 화면에 표시되는 모든 것을 관장한다. 그러나 이 방식에는 바로 연상되는 하나의 문제점이 있는데, 페이지의 모든 영역에 있어 동일한 URL을 사용한다는 것이다.
만약 내가 www.도메인.com
라는 주소를 타인과 공유하더라도, 이는 시작 페이지만 공유 가능함을 의미한다. 버튼을 클릭하면 페이지가 달라지는 것은 JavaScript 기반 동작으로 URL과 아무런 관련이 없기 때문이다.
URL의 변화 없이 JavaScript 코드 만으로 잘 작동하는 애플리케이션을 개발하더라도, 동시에 URL과 대화하는(interact) 상태여야만 위치한 페이지를 나타내는 URL을 다른 사용자와 공유할 수 있다.
🧐 그냥 여러 HTML 파일을 만들어서 서로 가르키도록 하면 안되나?
www.도메인.com/Users.html
처럼
Vue 애플리케이션을 그런식으로 구축하는 것은 의미가 없다. 각 HTML파일이 각자 다른 스크립트 코드와 서로 관계가 없는 것으로 인식될테니까.
대신, 이런 문제를 해결하기 위해 Vue에서 Routing(라우팅)이라는 기능을 제공하고 있다.
라우팅에 대해 자세히 알아보자!
npm install --save vue-router
router.js
파일을 추가하는것 외에도, main.js
에 router를 사용할 것을 알려줘야 한다.
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
const router = createRouter ({
history: createWebHistory(),
routes: []
});
cons tapp = createApp(App)
app.mount('#app');
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter ({history: , routes: []});
history: createWebHistory()
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import TeamsList from './components/teams/TeamsList.vue';
import UsersList from './components/users/UsersList.vue';
const router = createRouter ({
history: createWebHistory(),
routes: [
{ path: '/teams', component: TemasList },
// 도메인.com/temas => TeamsList 컴포넌트
{ paht: '/users', component: UsersList },
// 도메인.com/users => UsersList 컴포넌트
]
});
cons tapp = createApp(App)
app.mount('#app');
app.use(router);
use()
: vue 내장 메서드.<template>
<header>
<nav>
<ul>
<li>
<button @click="setActivePage('teams-list')">Teams</button>
</li>
<li>
<button @click="setActivePage('users-list')">Users</button>
</li>
</ul>
</nav>
</header>
</template>
<template>
<header>
<nav>
<ul>
<li>
<router-link to="/teams">Teams</router-link>
</li>
<li>
<router-link to="/users">Users</router-link>
</li>
</ul>
</nav>
</header>
</template>
<router-link to="/이동할경로">
<a>
를 랜더링한다.<a>
태그에 적용해주면 된다. (위의 예시에서는 <button>
에 적용되던 css를 <a/>
태그로 옮겨주면 된다.)Q.
<a></a>
태그와 유사해 보이는데, 어떤 차이점이 있나요?
A. 다른 페이지를 불러와 전체 새로고침하여 현재 상태를 잃는 앵커 태그<a/>
와 다르게,<router-link/>
는 다른 페이지로 로드되는 것을 막고 사용자가 클릭한 링크를 분석하여 알맞은 컴포넌트를 로드하며 URL을 업데이트 합니다.
button:hover,
button:active
a.router-link-active {
color: #f1a80a;
border-color: #f1a80a;
background-color: #1a037e;
}
이후에 배우게 될 '중첩 라우팅' 사용 시, 두 속성을 구분해서 적용할 수 있다.
아래와 같은 요구사항이 있다고 생각해보자.
Confirm이란 버튼이 있고, 해당 버튼을 클릭하면 사용자 정보를 저장한다. 이후 'Users' 페이지로 이동한다.
위 요구사항에선 '양식 제출'이라는 특정 작업을 수행하고, 수행 이후 사용자를 다른 곳으로 이동시켜야 한다. 실제로 개발 상황에 빈번하게 요구될 수 있는 사항이다. 이를 구현하기 위해서 router를 어떻게 적용해야 할까?
<template>
<button @click="confirmInput">Confirm<button>
<ul>
<user-item v-for="user in users" :key="user.id" />
</ul>
</template>
특정 작업을 수행해야 하므로,
<router-link/>
태그를 사용하지 못하고,<button/>
을 통해confrimInput
메서드를 트리깅하고 있다.
<script>
input UserItem from './UserItem.vue';
export default {
components: {
UserItem,
},
inject: ['users'],
methods: {
confirmInput() {
// 양식 제출 코드
// 양식 제출 이후 다른 페이지로 이동!
}
}
</script>
<script>
//...
methods: {
confirmInput() {
// 양식 제출 코드
this.$router.push('/teams');
this.$router.forward(); // 앞으로 가기
this.$router.back(); // 뒤로 가기
}
}
</script>
this
키워드로 접근 가능push
메서드를 통해 라우팅 히스토리에 새 라우트(이동할 경로)를 추가(푸시)할 수 있다.📌 결론
이동 전 실행해야 할 로직(코드)가 있는 경우 $router를 통해 이동하자!
// ...
const router = createRouter ({
history: createWebHistory(),
routes: [
{ path: '/teams', component: TeamsList },
{ paht: '/users', component: UsersList },
]
});
// ...
아래와 같은 요구사항이 있다고 생각해보자.
경로가
'/teams/t1'
이면 팀1 사람들의 정보를 보여주고,'/teams/t2'
이면 팀2 사람들의 정보를 보여주고 싶어.
위 요구사항에 따르면, t1, t2와 같은 매개변수를 통해 다른 데이터를 컴포넌트에 로드해야 하는 상황이다. 즉, 라우트 매개변수가 있는 동적 라우트가 필요하다.
routes: [
{ path: '/teams/new' }
{ path: '/teams/:teamId', component: TeamMembers }
]
{ path: '/teams/:teamId'}
/teams
뒤에 /무언가
를 입력하면 이 라우트가 활성화된다.cf.
{ path: '/teams/new' }
new라는 하위 경로가 있는 경우, 순서가 동적 매개변수를 사용하는 경로보다 코드상 위에 위치해야 한다. 더 아래에 있는 경우, new는 매개변수 취급되어 해당 경로가 활성화되지 않게 된다. (new도 teamId로 취급하게 되는 것)
export default {
create() {
const teamId = this.$route.params.teamId;
}
}
<template>
<li>
<h3> {{ name }} </h3>
<div class="team-member"> {{ memberCount }} Members </div>
<router-link :to="'/teams/' + id"> View Members <router-link>
</li>
</template>
<script>
export default {
props: ['id', 'name', 'memberCount'],
};
</script>
<a href="">
를 이용한 url의 변화가 아닌 라우트 링크를 통해서도 새로운 경로로 이동할 수 있다.
앞서 확인했던 <router-link to="/이동할경로">
는 정적인 링크 생성을 위해 사용된다. 그러나 v-bind
를 to 앞에 붙여주어 동적으로 변하는 링크를 생성할 수 있다.
이제 TeamItem.vue
컴포넌트마다 부모로 부터 전달받은(props
를 통해) id에 따른 링크를 동적으로 생성할 것이다.
<template>
의 로직을 줄여주기 위해 해당 코드를 연산(Computed) 프로퍼티에 옮길수도 있다.
<template>
<li>
<h3> {{ name }} </h3>
<div class="team-member"> {{ memberCount }} Members </div>
<router-link :to=teamMembersLink> View Members <router-link>
</li>
</template>
<script>
export default {
props: ['id', 'name', 'memberCount'],
computed: {
teamMembersLink() {
return '/teams/' + this.id;
}
};
</script>
export default {
// ...
created() {
const teamId = this.$route.params.teamId;
const this.members = 팀원 호출 로직(teamId);
},
}
동적 매개변수로 로드된 페이지에서 다른 값을 가진 페이지로 간다면 화면이 업로드 되지 않는다.
/teams/t1
에 /teams/t2
로 이동하는 버튼이 있다. (매개변수의 값이 변한 상황) 이 버튼을 클릭하면 URL은 바뀌지만 화면에 표시되는 데이터는 변하지 않는다.
이는 페이지 탐색 시 Vue 라우터가 로드된 컴포넌트를 파기하고 새로 구축하지 않기 때문에 발생한다. Vue 라우터는 URL이 바뀔때마다 새롭게 컴포넌트를 만들지 않고 캐시에 저장해두고 사용한다.
즉, create()의 호출로 컴포넌트가 생성된(created) 이후 URL이 변경된다고 해서 컴포넌트를 파기하고 다시 created()를 실행하여 컴포넌트를 생성하지 않는다.
따라서 새로운 매개변수를 통해 다른 데이터를 가진 페이지를 로드하고 싶어도 Vue 라우터는 기본 설정상 반응하지 않는다.
어떻게 반응하도록 고칠 수 있을까?
URL 변화 시 $route
프로퍼티도 변한다. 로드한 라우트에 대한 최신 정보를 담고 있으므로 URL 변경시 최신 매개변수로 업데이트된다.
따라서 감시자(Watcher)를 추가하여 내장 프로퍼티인 $route
의 변화를추적할 수 있다.
export default {
// ...
methods: {
loadTeamMember() {
const teamId = this.$route.params.teamId;
// teamLoading 로직
}
},
created() {
this.loadTeamMembers();
},
watch: {
$route() {
this.loadTeamMembers();
}
}
기존에 라우트로부터 '팀ID'를 찾아 팀 정보를 로딩하는 로직을 loadTeamMember()
로 메서드화 했다. 그리고 이 메서드를 created()
와 watch
에서 호출함으로써 컴포넌트 생성될 때만이 아니라, 라우터 정보가 바뀌었을 때에도 팀 정보 로딩을 실행하여 업데이트할 수 있다.
loadTeamMember()
에서 직접 this.$route
에서 인수를 조회하는 대신에, created()
와 watch
에서 인수를 전달하도록 수정한다.
export default {
// ...
methods: {
loadTeamMember(route) {
const teamId = route.params.teamId;
// teamLoading 로직
}
},
created() {
this.loadTeamMembers(this.$route);
},
watch: {
$route(newRoute) {
this.loadTeamMembers(newRoute);
}
}
created()
$route
를 전달한다.watch
export default {
// ...
methods: {
loadTeamMember(route) {
const teamId = route.params.teamId;
// teamLoading 로직 (teamId를 통해)
}
},
created() {
this.loadTeamMembers(this.$route);
},
watch: {
$route(newRoute) {
this.loadTeamMembers(newRoute);
}
}
코드상 팀 정보를 로드하기 위해 필요한 정보를 라우트를 의존 ($route의 param 메서드를 통해 호출)하고 있다.
Vue 라우팅을 사용하면 URL을 기반으로 한 경로에 따라 컴포넌트가 로드되고, $route
를 이용해 라우팅된 정보를 활용한다. 그러나 이같은 접근 방식은 다른 컴포넌트나 템플릿에서 TeamMembers.vue
를 포함하고자 할 때 제한적일 수 있다.
예를 들어, 라우팅 하지 않고 다른 컴포넌트에 포함하거나, 한 번은 라우팅을 통하고 다른 한번은 템플릿의 태그를 통해 로드해서 다른 위치에서 사용하고 싶은 경우 문제가 될 수 있다. (라우트를 사용하지 않는 다른 방식에서 문제가 발생한다.)
이같은 라우트 의존 문제는 로드할 때 프로퍼티(prop)을 활용하는 방법으로 해결할 수 있다. 즉, teamId를 프로퍼티로 가지고, 이 데이터가 필요한 곳에 this.$route
대신 해당 프로퍼티를 참조하는 것이다.
기존에 라우터를 통해 호출되던 teamId를 props의 teamId로 변경한다.
<script>
export default {
props: ['teamId'],
methods: {
loadTeamMember(teamId) {
const teamId =
// teamLoading 로직 (teamId를 통해)
}
},
created() {
this.loadTeamMembers(this.teamId);
},
watch: {
teamId(newId) {
this.loadTeamMembers(newId);
}
}
라우트는 기본 설정상 데이터를 프로퍼티에 업로드하지 않는다. Vue 라우터에 변수가 $route에만 전달되는 것이 아니라 프로퍼티로서 컴포넌트에 전달되어야 한다고 알려줘야 한다.
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/teams', component: TeamList },
{ path: '/users', component: UserList },
{ path: '/teams/:teamId', component: TeamMembers, props: true },
],
});
props: true
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/teams', component: TeamList },
{ path: '/users', component: UserList },
{ path: '/teams/:teamId', component: TeamMembers, props: true },
],
});
일반적으로 사용자는 도메인주소만을 가지고 사이트에 접속한다. (예를 들어, 네이버에 접속할 경우 www.naver.com
으로 사이트에 접속할 것이다.) 이 경우 라우터는 경로(path)가 /
인 라우트를 찾는다. 그러나 route의 경로 중 '/'
와 일치하는 경로가 없기 때문에 화면을 로드하지 못한다. 이 문제를 해결하기 위해서는 라우트에 /
경로를 추가해줘야 한다.
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: TeamList },
{ path: '/teams', component: TeamList },
{ path: '/users', component: UserList },
{ path: '/teams/:teamId', component: TeamMembers, props: true },
],
});
{ path: '/', component: TeamList },
TeamList
컴포넌트를 제공하고 싶어 새로운 경로로 해당 컴포넌트를 연결해주었다.만약 단순히
TeamList
컴포넌트를 보여주는 것 뿐만이 아니라 주소자체를도메인주소/teams
로 변경하고 싶으면 어떻게 해야할까?
이럴때 redirect 혹은 alias 프로퍼티를 활용할 수 있다.
routes: [
{ path: '/', redirect: '/teams' },
{ path: '/teams', component: TeamList },
],
도메인
으로 접속하더라도 도메인/teams
로 URL이 변경된다. routes: [
{ path: '/teams', component: TeamList, alias: '/' },
{ path: '/users', component: UserList },
{ path: '/teams/:teamId', component: TeamMembers, props: true },
],
/teams
경로에 /
라는 별칭을 부여한다.사용자가 지원하지 않는 라우트를 접속하는 경우 (ex. route에 없는 localhost:8080/something
으로 접속하는 경우) 사용자가 입력 가능한 모든 URL에 라우트를 지정할 순 없다. 이럴 때 catchAll 라우트를 사용할 수 있다.
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: TeamList },
{ path: '/teams', component: TeamList },
{ path: '/users', component: UserList },
{ path: '/teams/:teamId', component: TeamMembers, props: true },
{ path: '/:notFound(.*)', component: NotFound },
],
});
path: '/:notFound(.*)', component: NotFound
/:
)로 /
경로 뒤 무언가 특정 값이(notFound
에 해당하는) 입력되는 경우를 지정한다.(.*)
: Vue라우터가 지원하는 정규 표현식. notFound
위치에 어떠 ㄴ값이든 모두 올 수 있음을 의미.NotFound.vue
컴포넌트를 연결할 수 있다.path: '/:notFound(.*)', redirect: '/teams'
라우터 안에 또 다른 라우터를 설정하고 싶은 경우 중첩 라우팅을 통해 구현할 수 있다.
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: TeamList },
{ path: '/teams', component: TeamList },
{ path: '/users', component: UserList },
{ path: '/teams/:teamId', component: TeamMembers, props: true },
{ path: '/:notFound(.*)', component: NotFound },
],
});
현재의 router.js에서는 모든 라우트들이 루트 레벨에 속해 서로 형제 관계이다. 중첩 라우트 구성은 내부에 라우트를 중첩하려는 라우트로 이동하여 children 옵션을 추가하는 방법으로 설정한다.
/temas
라우트는 /teams/:teamId
라우트를 취하게 되므로 /teams
라우트의 children으로 포함시킬 수 있다.
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: TeamList },
{
path: '/teams',
component: TeamList
children: [
{ path: ':teamId', component: TeamMembers, props: true }
]
},
{ path: '/users', component: UserList },
{ path: '/:notFound(.*)', component: NotFound },
],
});
{
path: '/teams',
component: TeamList
children: [
{ path: ':teamId', component: TeamMembers, props: true }
]
},
'/teams'
경로는 중복작성하지 않아도 괜찮다.App.vue 파일에 사용한 router-view
컴포넌트는 앱 전체에 사용되는 최상단 컴포넌트로 오직 루트 라우트(routes 배열에 직접 등록한 라우트)만 다룬다. 따라서 자식 라우트는 router-view
에 렌더링되지 않는다. 대신 children을 가지는 부모 컴포넌트에 router-view
를 추가해줘야 한다.(이 경우 TeamList
)
<template>
<router-view></router-view>
<ul>
<teams-item
v-for="team in teams"
:key="team.id"
:id="team.id"
//...
></teams-item>
</ul>
</template>
부모 컴포넌트에 추가된 <router-view>
부분에 자식 컴포넌트들이 렌더링되게 된다.
Vue Router에서는 'router-link' 요소의 활성 상태를 나타내기 위해 router-link-active와 router-link-exact-active 클래스를 제공한다.
router-link-active
클래스: 현재 URL이 링크의 'to' 속성과 일치하면 적용된다.router-link-exact-active
클래스: 현재 URL이 링크의 'to'속성과 정확하게 일치하면 적용된다.만약 아래와 같은 Vue 템플릿과 Css 설정이 있다고 생각해보자.
<router-link to="/teams" class="my-link" exact>Teams</router-link>
.router-link-active {
font-weight: bold;
}
.router-link-exact-active {
color: #ff0000; /* 빨간색으로 변경 예시 */
}
router-link-exact-active
css 속성인 경우, 정확히 경로가 /teams
인 경우에만 해당 클래스의 css 설정이 적용된다.router-link-active
css 속성인 경우, /temas/123
과 같이루트 경로가 일치하는 경우 해당 css가 적용된다. 따라서, 중첩 라우트 구성의 경우 router-link-active
의 css가 적용되게 된다.
규모가 큰 Vue 애플리케이션의 경우 수십, 수백개의 다양한 라우트와 중첩 라우트가 있을 수 있다. 중첩 라우트를 여러 레벨로 나누고, 그 중첩 라우트에 또 자식 라우트를 만들 수 있으니 링크를 생성하는 것이 매우 복잡해질 수 있다.
<router-link :to="teamMembersLink"> View Members </router-link>
computed: {
teamMemberLink() {
return '/teams/' + this.id;
}
}
항상 위와 같이 이동할 모든 경로를 문자열로 조합하는 것은 번거롭고 어려운 일이다. 이런 경우를 위해 Vue는 경로 이동에 유용한 기능을 제공하고 있다.
router-link 요소에 적용되는 to 프로퍼티는 (경로를 다루는) 문자열 링크뿐만 아니라 객체를 사용할 수도 있다. 이 경우 반드시 to 프로퍼티와 :
을 동적으로 바인딩해야 한다. (<router-link :to=>
)
computed: {
teamMemberLink() {
return { path: '/teams' + this.id };
}
}
문자열 대신 객체를 전달했다. 하지만 이는 객체 내부에 작성했을 뿐 앞서 작성한 것과 별반 다를 것이 없다.
라우트 구성에 name
프로퍼티를 추가함으로써 작성한 모든 라우트에 이름을 할당할 수 있다.
routes: [
{
name: 'teams',
path: '/teams',
component: TeamsList,
children: [
{name: 'team-members', path: 'teamId', component: TeamMembers, props: True }
]
],