[vue3-router] 05. 네비게이션 가드(navigation guard)
- -
이번 포스트에서는 내비게이션 가드에 대해 살펴보자.
navigation guard
navigation guard란?
navigation guard는 router를 이용한 경로 탐색 과정에서 라우터가 동작하기 전/후에 추가적인 동작을 실행하게 해 준다.
예를 들면 로그인하지 않은 사용자에게 로그인을 유도한다든가, 권한이 없는 사용자의 접근을 차단하거나 경로 탐색 이력을 로그로 작성하는 일등이 가능하다.
여기까지 개념을 backend에서 생각해 보면 servlet의 filter나 spring의 interceptor를 떠올릴 수 있는데 딱 들어맞는다. navigation guard는 router에 적용하는 filter 또는 interceptor이다.
테스트 시나리오
navigation guard 예제를 위한 시나리오와 기본 라우터 구성을 살펴보자.
먼저 아주 중요한 내용을 가지고 있는 ImportantView라는 컴포넌트가 있다고 하자. 이 컴포넌트를 보려면 반드시 로그인 상태여야 한다.
만약 이미 login 한 상태라면 ImportantView를 서비스하고 만약 그렇지 않다면 LoginView를 이용해서 login을 시도한다. 여기서 login에 성공하면 애초에 원했던 ImportantView로 이동하고 그렇지 않은 경우 login을 재시도한다.
로그인 정보는 Pinia 등을 이용할 수도 있지만 여기서는 app 차원의 provide와 inject를 활용해 보자.
기본 코드 작성
로그인 정보 관리
전역의 app에 privide를 이용해 login 정보를 유지해 보자. main.js에 다음과 같이 추가한다.
app.use(router)
// mount 하기 전에 provide 처리
app.provide("user",reactive({}))
app.mount('#app')
전역의 beforeEach guard
navigation guard를 작성하는 위치는 통상 전역, 경로 별, 컴포넌트 내부로 구분해서 생각할 수 있는데 기본적인 동작 개념만 이해하면 아주 비슷하게 사용할 수 있다. 먼저 전역에 설정하는 형태로 동작 방식을 이해해 보자.
전역의 guard를 설정하기 위해서는 router 객체에 직접 guard를 설정한다. beforeEach() 함수는 router에서 모든 탐색을 처리하기 전에 언제나 호출되는 가드이다.
beforeEach(guard)는 guard라는 callback을 파라미터로 받는데 guard는 다음과 같이 작성한다.
- 파라미터: to(탐색될 경로)와 from(현재 경로) 두 개의 파라미터가 선언된다.
- 이 객체들은 RouteLocationNormalized 타입으로 route에 대한 다양한 정보를 갖는데 path 속성은 url path를 의미한다.
- 세번째로 optional한 next가 선언될 수 있으나 호환성 유지를 위해 추가되었을 뿐 사용하지 않는다.
- 반환 값: boolean 또는 탐색할 경로 정보가 반환된다.
- 논리값: false를 반환할 때는 이동 명령은 무시됨(undefined, 0 등 falsy 값 포함)
- 공식 문서에는 undefined, true 또는 아무것도 반환하지 않는 경우 to로 진행한다고 되어있지만 undefined는 false로 평가됨. 명시적으로 true/false를 사용하자.
- 경로 객체: router.push()를 통해 이동할 때처럼 route 객체를 구성해서 전달하면 redirect 처리됨
- 논리값: false를 반환할 때는 이동 명령은 무시됨(undefined, 0 등 falsy 값 포함)
// 보호 대상 경로 배열
const needLogin = ['/guard/important']
router.beforeEach((to, from) => {
// route logging
console.log(`router: from: ${from.path} ==> to: ${to.path}`)
if (needLogin.includes(to.path) && !inject('user').id) {
// 지정 페이지로 redirection
// return `/guard/login?to=${to.path}`
return {path:'/guard/login', query:{to:to.path}}
}
})
위의 예를 살펴보면 먼저 needLogin 배열에서 로그인이 필요한 경로들을 관리하고 있다.
router에는 beforeEach 가 설정되어 있고 내부에서는 navigation guard의 전형적인 기능인 logging과 filtering을 하고 있다. 만약 다음 경로(to)가 needLogin에 속해있고 inject("user").id 값이 없다면 아직 login 하지 않은 상태이므로 /guard/login으로 이동해서 로그인을 하도록 유도한다. query로 전달한 to는 로그인 성공 후 다시 돌아오기 위한 경로이다.
beforeEach() 내부에서 error를 throw 할 수도 있는데 이 경우는 router.onError() 함수에 error가 전달된다. onError는 beforeEach 뿐 아니라 모든 예외 상황에서 동작한다.
router.beforeEach((to, from) => {
. . .
// throw 내용은 onError에 전달되는 콜백의 파라미터가 됨
throw "navation fail"
})
// error 발생 시 호출되는 함수
router.onError((e)=>{
console.log("error? ", e)
});
route meta field
route meta field는 경로에 meta라는 속성을 추가해서 부가적으로 필요한 데이터를 전달하기 위해 사용한다. 위의 예에서 needLogin이라는 배열을 login 이 필요한 경로를 관리하기 위해서 사용되었다.
const needLogin = ['/guard/important']
이 정보는 실제 route와 떨어져서 관리되기 때문에 관리하기가 쉽지 않다. 또한 login이 필요한 경로가 새로 생길 때마다 이 배열을 찾아서 추가한다는 것도 일이다. 이런 경우 route meta field를 사용할 수 있다.
route meta field는 route 상에 meta라는 속성으로 객체를 추가하고 이 객체에 필요한 사용자 정의 속성들을 선언하는 방식을 말한다.
{
path: '/guard/important',
component: ImportantView,
// 경로상 추가적인 meta 정보 설정 가능
meta:{requiresAuth: true},
}
이 속성은 route 내에 작성되기 때문에 경로와 따로 놀일이 없어 관리가 용이하다. 그럼 이 속성을 어디서 사용할까? meta 속성들은 guard에 전달되는 to나 from에서 meta라는 속성으로 참조가 가능하다.
router.beforeEach((to, from) => {
console.log(`router: from: ${from.path} ==> to: ${to.path}`)
// if (needLogin.includes(to.path) && !inject('user').id) {
if(to.meta.requiresAuth && !inject('user').id){
return { path: '/guard/login', query: { to: to.path } }
}
})
이로써 기존의 neeedLogin을 사용하는 형태보다 훨씬 깔끔한 형태의 작업이 가능해졌다.
기타 전역레벨의 navigation guard
beforeEach 외의 전역 guard에 대해 살펴보자.
beforeResolve
beforeResolve는 beforeEach와 모든 면에서 유사하지만 호출 시점만 다르다. beforeResolve는 navigation guard 작업을 완료하기 바로 직전에 호출되는 guard로 component 변경에 최종 도장을 찍는 부분이다.
예를 들어 사용자가 카메라 엡을 사용하는 동작을 생각했을 때 로그인 되어있는지 아닌지에 따라서 페이지로 이동할지 못할지는 beforeEach에서 처리한다. 이후 로그인은 되었고 이동하기로 했는데 카메라를 사용하기 위한 권한을 얻어야 한다면 그 작업은 beforeResolve에서 처리한다. 이 함수의 사용법은 beforeEach와 동일하다.
router.beforeResolve((to, from) =>{
console.log(`before resolve: 이동하기로 했으니 데이터를 가져올까?`);
console.log(`예를 들면 로그인은 했는데 그 기능을 쓰려면 카메라 제어권이 있어야해. 권한을 받자.`);
console.log(`before resolve: from: ${from.path} ==> to: ${to.path}`)
});
afterEach
afterEach는 navigation이 종료된 후 호출되는 것으로 이미 이동이 종료되었기 때문에 페이지 방문 여부를 결정하지는 않는다. 그래서 guard라고 하지 않고 단순 hook이라고 하기도 한다. 이 함수의 콜백에는 3번째 파라미터로 만약 탐색이 실패되었을 때 실패 원인에 해당하는 failure를 전달하는데 이를 통해서 통계 분석을 위한 정보로 사용될 수 있다.
router.afterEach((to, from, failuer) =>{
console.log(`after each: navigation 작업이 완료된 직후. 탐색에 영향을 주지는 못함`);
if(failuer){
console.log("실패 원인은: ", failuer)
}
console.log(`after each: from: ${from.path} ==> to: ${to.path}`)
});
실패의 예로 현재 route와 동일한 경로로 이동하라고 하면 이동 실패가 된다.
경로별 가드
이제까지 살펴본 가드가 전역 레벨로 router에게 설정한 내용이었다면 개별 route에 설정할 수 있는 life cycle이 존재한다.
beforeEnter
beforeEnter는 경로(route)에 진입하기 전에 실행되는 guard이다.
{
path: '/guard/important',
component: ImportantView,
beforeEnter: (to, from) => {
console.log(`before enter: 이동 대상 route에 진입하기 전`)
console.log(`before enter: from: ${from.path} ==> to: ${to.path}`)
}
}
이 녀석의 재밌는 점은 beforeEnter를 정의할 때 함수는 물론 배열을 선언할 수 있다는 점이다. 여러 route에서 경로에 들어섰을 때 처리할 일들이 동일하다면 별도의 함수로 분리 후 함수를 등록해서 재사용할 수 있다.
다음은 공식 문서에 있는 예제이다.
function removeQueryParams(to) {
if (Object.keys(to.query).length)
return { path: to.path, query: {}, hash: to.hash }
}
function removeHash(to) {
if (to.hash) return { path: to.path, query: to.query, hash: '' }
}
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: [removeQueryParams, removeHash],
},
{
path: '/about',
component: UserDetails,
beforeEnter: [removeQueryParams],
},
]
컴포넌트 내부 가드
마지막으로 컴포넌트 별로도 가드를 작성할 수 있다.
beforeRouteLeave
현재 컴포넌트가 다른 컴포넌트로 대체되기 전에 호출되며 composition api에서는 onBeforeRouteLeave 함수를 이용해서 정의한다.
import { onBeforeRouteLeave, onBeforeRouteUpdate} from 'vue-router';
onBeforeRouteLeave((to, from) => {
console.log(`before router leave: 기존 컴포넌트가 제거되기 전에 호출`)
console.log(`before router leave: from: ${from.path} ==> to: ${to.path}`)
})
beforeRouteUpdate
현재 rendering 된 component가 업데이트될 때 호출된다. 예를 들어 params를 이용한 dynamic route에서는 새로운 링크가 동작하더라도 컴포넌트가 새로 만들어지지 않고 update만 이뤄진다. 이전의 코드에서는 watch속성으로 route의 params, query를 감지했지만 beforeRouteUpdate에서 처리할 수도 있다.
composition api에서는 onBeforeRouteUpdate 함수를 이용해서 정의한다.
onBeforeRouteUpdate((to, from) =>{
console.log(`before router update: 기존 컴포넌트를 재사용할 때 호출(params로 dynamic route 처리한 경우 등)`);
console.log(`before router update: from: ${from.path} ==> to: ${to.path}`)
});
beforeRouteEnter
아직 경로에 들어가지 않았으므로 컴포넌트가 생성되기 전의 시점에 호출된다. 그런데 composition api에서는 setup이 시작점인데 이 시점은 이미 routing이 된 시점이라 beforeRouteEnter가 불필요하다. 그래서 위의 두 함수와 달리 onXX 함수가 제공되지 않는다.
전체적인 흐름
이제까지 살펴본 navigation guard의 전체적인 흐름을 살펴보고 정리해 보자.
'Vue 3.0 > 05.vuerouter' 카테고리의 다른 글
[vue3-router] 07. 지연 로딩 경로 (2) | 2023.11.04 |
---|---|
[vue3-router] 06. route meta field와 multiple layout (0) | 2023.11.03 |
[vue3-router] 04. 기타 라우팅 기법 (0) | 2023.11.02 |
[vue3-router] 03. nested route와 named view (1) | 2023.11.01 |
[vue3-router] 02. dynamic route matching (0) | 2023.11.01 |
소중한 공감 감사합니다