FrameWorks/Vue

VueRouter의 Navigation Guard 파헤치기

ABCD 2024. 6. 27.

네비게이션 가드

주로 탐색을 리다이렉션 or 취소하여 탐색을 막는데 사용하게 됩니다.
가드를 연결하는 방법은 다양한데 다음 세 가지 종류가 있습니다.

  • 전역
  • 라우트별
  • 컴포넌트 내부

BeforeGuard

BeforeGurad는 탐색 이전에 실행되게 됩니다.
router Instance의 beforeEach()메서드에 콜백 함수를 전달하여 등록합니다.
콜백함수에서 사용하는 parameter에는 다음과 같은 종류가 있습니다.

  • to : 탐색 될 라우트 위치 객체
  • from : 탐색 전 현재 라우트 위치 객체

만약 라우트 작업을 취소하고 싶을 경우엔 return false로 취소 할 수 있습니다.
또한, return { name: ${route_name} }을 통해 리다이렉션를 명시할 수 있습니다.

router.beforeEach(async (to, from) => {
    if(
    // 유저 로그인 여부 확인
    !isAuthenticated &&
    // 무한 리다이렉션 방지
    to.name != 'login'
    ) {
        // 로그인 페이지로 이동
        return { name: 'login' }
    }
})

 

예외 상황에 대해서도 Error를 던질 수도 있습니다.

이 경우에 진행하던 탐색은 취소되며, router.onError()메서드에 전달돼 등록된 모든 콜백함수를 호출하게 됩니다.

 

기본적으로 tofrom인자를 사용하지만, 선택적으로 세번째 인자 next를 사용 할 수 있습니다.
next가 일반적인 실수의 원인이었으며, RFC를 이유로 제거되었습니다.

 

하지만, 여전히 지원은 되므로 참고해 봅시다.
next의 경우 트리거 되는 탐색 가드별 정확히 한 번만 next를 호출해야 합니다.
그 이유는 무한루프의 늪에 빠질 수 있기 때문입니다.

 

다음 예제를 보면서 나쁜 예와 좋은 예를 보겠습니다.

// Bad Case
router.beforeEach((to, from, next) => {
    if (to.name !== 'login' && !isAuthenticated) next({ name: 'login'})
    // 사용자가 인증되지 않은 경우, `next`가 두번 호출됨
    next()
})
// Good Case
router.beforeEach((to, from, next) => {
    if (to.name !== 'login' && !isAuthenticated) next({ name: 'login'})
    else next()
})

 

위와 같은 경우 두번 next()를 호출하는 이유는 if절에서 한번 그리고 로직의 흐름대로 마지막의 next()이렇게 두번을 호출하게 됩니다. return을 중간에 삽입해 해결 할 수 있겠지만, 보기 숭하겠죠???

beforeResolve Guard

router.beforeResolve로 전역 가드를 등록할 수 있습니다.
router.beforeEach와 유사하지만, 컴포넌트 내 가드 및 비동기 라우트 컴포넌트가 모두 해결된 후, 최종적으로 탐색을 진행할 것인가를 결정하기 위한 목적으로 호출합니다.
라고... 공식문서에 작성되어 있지만, 무슨말인지 당최 못알아먹겠죠??(저는 그랬음...)

 

router.beforeResolve의 경우에는 탐색되기 가장 직전에 동작하는 로직을 정의해 준다고 생각하면 되겠습니다.

 

예제를 살펴보겠습니다.

 

meta에 정의한 속성인 requiresCamera가 있는 라우트에 대해, 사용자가 카메라에 접근 권한을 부여 했는지에 대해 확인하는 예제입니다.

router.beforeResolve(async to => {
    if(to.meta.requiresCamera) {
        try {
            await askForCameraPermission()
        } catch (error) {
            if(error instanceof NotAllowedError) {
                // ... 오류를 처리한 다음 탐색을 취소
                return false
            } else {
                // 예기치 않은 오류, 탐색을 취소하고 오류를 전역 핸들러에 전달
                throw error
            }
        }
    }
})

afterEach Hook

전역으로 탐색 후 훅을 등록할 수도 있지만, Guard와 달리 next인자를 전달 받지 않으며 탐색에는 영향을 끼치지 않습니다.

router.afterEach((to, from) => {
    sendToAnalytics(to.fullPath)
})

 

애널리스틱, 페이지 <title> 변경, 페이지 정보를 알리는 접근성 기능 및 기타 여러 작업에 유용합니다.

 

세 번째 인자로 failure을 받게 되는데, 탐색 실패인자입니다.
차후 자세하게 다룰 것이므로 일단은 있다는 것만 파악하고 넘어가도록 합시다.

router.afterEach((to, from, failure) => {
    if (!failure) sendToAnalytics(to.fullPath)
})

Guard에서 전역 Injection 사용하기

Vue 3.3부터는 Navigation Guard 내에서 inject()를 사용할 수 있습니다.
Pinia Store와 같은 전역 속성을 Injection하는 데 유용합니다.

// main.js
const app = createApp(App)
app.provide('global', '안녕 인젝션!')

// /src/router/index.js or main.js
router.beforeEach((to, from) => {
    const global = inject('global')
    // Pinia Store
    const userStore = useAuthStore()
})

beforeEnter

라우트를 구성하는 객체에 직접 beforeEnter 가드를 정의할 수 있습니다.
이는 beforeEach가 실행된 후 beforeRouteUpdate이후 동작하게 됩니다.

 

유의사항으로는 라우트에 진입할 때만 가드를 실행하며, 내부적으로 params, query, hash가 변경될 때 트리거하지 않습니다.

const routes = [
    {
        path: '/users/:id',
        name: 'profile',
        component: UserDetails,
        beforeEnter: (to, from) => {
            // 라우트 진입 거부
            return false
        },
    },
]

 

함수를 배열로 만들어 전달할 수도 있습니다.
다른 라우트에 재사용을 할경우 용이하겠죠??

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],
    },
]

 

Nested Route의 경우에도 사용 할 수 있습니다.
하지만, 유의할 점이 있습니다.

 

부모, 자식 각각에 beforeEnter를 작성할 수 있지만, 부모에 선언된 beforeEnter의 경우 자식들간에 이동시 트리거되지 않습니다.

 

예를 들어보겠습니다.

 

아래와 같이 /user/list에서 /user/details으로의 화면 전환시에는 beforeEnter를 선언했지만 트리거되지 않습니다. 대신 children에 각각의 route에 선언하면 라우트간 이동시 동작할 것입니다.

const routes = [
    {
        path: '/user',
        beforeEnter: (to, from) => {
            // ...
        },
        children: [
            { path: 'list', component:UserList },
            { path: 'details', component:UserDetails },
        ],
    },
]

In-Component Guard

라우트를 구성하는 객체에 전달되는 라우트 컴포넌트내의 <script> or <script setup>태그에서 라우트 탐색 가드를 직접 정의할 수 있습니다.

beforeRouteEnter(to, from, next)

컴포넌트를 렌더링하는 라우트가 결정 되기 전에 호출됩니다.
beforeEnter이전에 동작하는 Guard입니다.


이 시점에는 컴포넌트 인스턴스는 아직 생성 전이므로 this를 통한 컴포넌트 인스턴스에 접근할 수 없습니다.


하지만, 3번째 인자인 next에 콜백을 전달하여 인스턴스에 접근이 가능합니다.

// Composition API에서는 사용할 수 없음
beforeRouteEnter (to, from, next) {
    next(vm => {
        // `vm`을 통해 컴포넌트 공개 인스턴스에 접근
    })
}

 

beforeRouteEnter는 Hook의 콜백함수에 next인자가 전달되는 유일한 가드입니다.
beforeRouteUpdatebeforeRouteLeave의 경우 this가 이미 사용가능하므로, 콜백을 전달할 필요가 없는 것이죠.

beforeRouteUpdate(to, from)

컴포넌트가 새로 렌더링되지 않고, 해당 컴포넌트를 재사용하고 데이터만 변경될 경우에만 동작합니다. /pages/1에서 /pages/2로 페이징 정보를 변경할 경우처럼 말이죠.
이 경우 this의 사용이 가능해집니다.

// Options API
beforeRouteUpdate (to, from) {
    // 컴포넌트 인스턴스에 접근하기 위해 `this`를 사용.
    this.name = to.params.name
}

//Composition API

beforeRouteLeave(to, from)

beforeRouteLeave의 경우, 페이지를 떠날 때 동작하게됩니다.

상황 예시를 들자면...

 

우리가 1시간을 공들여 작성한 게시글이 있습니다.
그러다가 실수로 페이지의 나가기 버튼이나 뒤로 가기버튼을 눌렀다고 가정해 봅시다.
우리가 공들여 작업한 데이터들이 한순간에 증발해 버리는 것이죠.

이럴경우 우리는 재차 확인을 통해 방지할 수 있는 것이죠.

// Options API
beforeRouteLeave (to, from) {
    const answer = window.confirm('정말 떠나시겠습니까? 저장되지 않는 변경사항이 있습니다!')
    if (!answer) return false
}

// Composition API
onBeforeRouteLeave (to, from) {
    const answer = window.confirm('정말 떠나시겠습니까? 저장되지 않는 변경사항이 있습니다!')
    if (!answer) return false
}

참고사항

전체적인 탐색 흐름은 아래와 같습니다.

Vue Router LifeCycle Hook

  1. 탐색이 트리거됨.
  2. 비활성화된 컴포넌트에서 beforeRouteLeave 가드 호출.
  3. 전역 beforeEach 가드 호출.
  4. 재사용된 컴포넌트에서 beforeRouteUpdate 가드 호출.
  5. 라우트를 구성하면서 beforeEnter 호출.
  6. 비동기 라우트 컴포넌트를 해결(resolve).
  7. 활성화된 컴포넌트에서 beforeRouteEnter 호출.
  8. 전역 beforeResolve 가드 호출.
  9. 탐색이 승인됨.
  10. 전역 afterEach 훅 호출.
  11. DOM 업데이트가 트리거됨.
  12. 인스턴스화 된 인스턴스 내부 beforeRouteEnter 가드에서 전달된 next 콜백 호출.
728x90
반응형

댓글

💲 추천 글