Skip to content

Complete guide to

Mastering Pinia

written by its creator

Vue 2에서 Vue 3로 마이그레이션하기

Vue Router API의 대부분은 v3(Vue 2용)에서 v4(Vue 3용)로의 재작성 과정에서 변경되지 않았지만, 여전히 일부 중단된 변경 사항들이 있습니다. 이 가이드는 이러한 변경 사항들이 발생한 이유를 이해하고 Vue Router 4에서 작동하도록 애플리케이션을 적응시키는 방법을 돕기 위해 제공됩니다.

중단된 변경 사항들

변경 사항들은 사용량에 따라 정렬되었습니다. 따라서 이 목록을 순서대로 따르는 것이 좋습니다.

new RoutercreateRouter로 대체되었습니다

Vue Router는 이제 클래스가 아닌 함수들의 집합입니다. 따라서 new Router()를 작성하는 대신에 이제 createRouter를 호출해야 합니다:

js
// 이전에는
// import Router from 'vue-router'
import { createRouter } from 'vue-router'

const router = createRouter({
  // ...
})

새로운 history 옵션이 mode를 대체했습니다

mode: 'history' 옵션이 더 유연한 history로 대체되었습니다. 사용하던 모드에 따라 적절한 함수로 교체해야 합니다:

  • "history": createWebHistory()
  • "hash": createWebHashHistory()
  • "abstract": createMemoryHistory()

전체 코드 예시는 다음과 같습니다:

js
import { createRouter, createWebHistory } from 'vue-router'
// createWebHashHistory 및 createMemoryHistory도 있습니다.

createRouter({
  history: createWebHistory(),
  routes: [],
})

SSR의 경우, 적절한 history를 수동으로 전달해야 합니다:

js
// router.js
let history = isServer ? createMemoryHistory() : createWebHistory()
let router = createRouter({ routes, history })
// 서버 진입점(server-entry.js) 어딘가에서
router.push(req.url) // 요청된 url
router.isReady().then(() => {
  // 요청 처리
})

이유: 사용하지 않는 history에 대해 트리 쉐이킹(tree shaking)을 가능하게 하며, 네이티브 솔루션과 같은 고급 사용 사례에 대해 사용자 정의 history를 구현할 수 있게 합니다.

base 옵션이 이동되었습니다

base 옵션은 이제 createWebHistory(및 다른 histories)의 첫 번째 인자로 전달됩니다:

js
import { createRouter, createWebHistory } from 'vue-router'
createRouter({
  history: createWebHistory('/base-directory/'),
  routes: [],
})

fallback 옵션이 제거되었습니다

fallback 옵션은 더 이상 라우터 생성 시 지원되지 않습니다:

diff
-new VueRouter({
+createRouter({
-  fallback: false,
// 다른 옵션들...
})

이유: Vue가 지원하는 모든 브라우저가 HTML5 History API를 지원하므로 location.hash를 수정하는 해킹 방법을 피하고 직접 history.pushState()를 사용할 수 있습니다.

* (별표 또는 캐치 올) 라우트가 제거되었습니다

Catch all 라우트 (*, /*)는 이제 사용자 정의 정규식을 사용하는 매개변수로 정의해야 합니다:

js
const routes = [
  // pathMatch는 매개변수의 이름입니다. 예를 들어, /not/found로 이동하면
  // { params: { pathMatch: ['not', 'found'] }}가 생성됩니다.
  // 이것은 마지막 * 덕분에 반복된 매개변수가 생성되며,
  // 이것은 not-found 라우트를 직접 이름으로 사용하여 해당 라우트로 직접 이동하는 경우에 필요합니다.
  { path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound },
  // 마지막 `*`을 생략하면 params 내의 `/` 문자가 해석될 때 인코딩됩니다.
  { path: '/:pathMatch(.*)', name: 'bad-not-found', component: NotFound },
]
// 명명된 라우트(named routes)를 사용하는 경우 나쁜 예시:
router.resolve({
  name: 'bad-not-found',
  params: { pathMatch: 'not/found' },
}).href // '/not%2Ffound'
// 좋은 예시:
router.resolve({
  name: 'not-found',
  params: { pathMatch: ['not', 'found'] },
}).href // '/not/found'

TIP

라우트의 이름을 사용하여 직접 not-found 라우트로 이동하지 않는 경우에는 반복된 매개변수를 위해 *을 추가할 필요가 없습니다. router.push('/not/found/url')과 같이 호출하면 올바른 pathMatch 매개변수가 제공됩니다.

이유: Vue Router는 더 이상 path-to-regexp을 사용하지 않으며, 대신 라우트 순위 지정과 동적 라우팅을 가능하게 하는 고유한 구문 분석 시스템을 구현합니다. 일반적으로 하나의 단일 catch-all 라우트를 프로젝트에 추가하므로 *에 대한 특별한 구문을 지원하는 것에 큰 이점이 없습니다. 매개변수의 인코딩은 예측하기 쉽도록 라우트 간에 인코딩됩니다.

currentRoute 속성은 이제 ref()입니다

이전에는 라우터 인스턴스의 currentRoute 객체의 속성에 직접 접근할 수 있었습니다.

vue-router v4의 도입으로 라우터 인스턴스의 currentRoute 객체의 기본 유형이 Vue 3에서 소개된 새로운 반응성 기본 사항에서 가져온 Ref<RouteLocationNormalizedLoaded>으로 변경되었습니다.

useRoute() 또는 this.$route로 라우트를 읽는 경우에는 아무 것도 변경되지 않지만 라우터 인스턴스에서 직접 접근하는 경우 currentRoute.value를 통해 실제 라우트 객체에 접근해야 합니다:

ts
const { page } = router.currentRoute.query
const { page } = router.currentRoute.value.query

onReadyisReady로 대체했습니다

기존의 router.onReady() 함수가 인자 없이 반환하는 Promise인 router.isReady()로 대체되었습니다:

js
// 이전 방식
router.onReady(onSuccess, onError)
// 새로운 방식
router.isReady().then(onSuccess).catch(onError)
// 또는 await을 사용합니다:
try {
  await router.isReady()
  // onSuccess
} catch (err) {
  // onError
}

scrollBehavior 변경사항

scrollBehavior에서 반환되는 객체가 이제 ScrollToOptions와 유사합니다. xleft로, ytop으로 이름이 변경되었습니다. RFC를 참조하세요.

이유: 객체를 ScrollToOptions와 비슷하게 만들어서 원래의 JS 네이티브 API와 더 친숙하게 만들고, 미래에 새로운 옵션을 추가하는 것을 가능하게 하기 위함입니다.

<router-view>, <keep-alive>, 그리고 <transition>

transitionkeep-alive는 이제 RouterView 내부에서 v-slot API를 통해 사용되어야 합니다:

template
<router-view v-slot="{ Component }">
<transition>
  <keep-alive>
    <component :is="Component" />
  </keep-alive>
</transition>
</router-view>

이유: 이 변경은 필요한 변경으로, 관련된 RFC를 참조하세요.

<router-link>에서 append 속성이 제거되었습니다. 이제 기존의 path에 값을 수동으로 연결해야 합니다:

template
이전 방식
<router-link to="child-route" append>to relative child</router-link>
새로운 방식
<router-link :to="append($route.path, 'child-route')">
  to relative child
</router-link>

App 인스턴스 전역에서 append 함수를 정의해야 합니다:

js
app.config.globalProperties.append = (path, pathToAppend) =>
  path + (path.endsWith('/') ? '' : '/') + pathToAppend

이유: append는 매우 자주 사용되지 않으며, 사용자 정의 코드로 쉽게 대체할 수 있습니다.

<router-link>eventtag 속성이 제거되었습니다. 이제 v-slot API를 사용하여 <router-link>를 완전히 커스터마이징할 수 있습니다:

template
이전 방식
<router-link to="/about" tag="span" event="dblclick">About Us</router-link>
새로운 방식
<router-link to="/about" custom v-slot="{ navigate }">
  <span @click="navigate" @keypress.enter="navigate" role="link">About Us</span>
</router-link>

이유: 이러한 속성들은 <a> 태그와 다른 요소를 사용하는 데 자주 함께 사용되었지만, v-slot API 이전에 도입되었으며 모든 사용자를 위해 번들 크기를 증가시키는 것에 충분히 사용되지 않았기 때문에 제거되었습니다.

exact 속성은 더 이상 필요하지 않으므로 제거되었습니다. 이제 두 가지 사항을 알아두어야 합니다:

  • 라우트는 더 이상 생성된 라우트 위치 객체 및 해당 path, query, hash 속성에 기반하여 활성화됩니다.
  • path 섹션만 일치하고 queryhash는 더 이상 고려되지 않습니다.

만약 hash 섹션도 고려하고자 한다면 v-slot API를 사용하여 <router-link>를 확장해야 합니다.

이유: 더 이상 필요하지 않은 기능을 수정하고, 라우터의 무게를 늘리지 않으면서 더 간단한 방법으로 처리하기 위해서입니다. 자세한 내용은 active matching에 관한 RFC을 참조하십시오.

현재 믹스인의 네비게이션 가드는 지원되지 않습니다. vue-router#454에서 이 지원에 대한 추적을 할 수 있습니다.

router.match 제거 및 router.resolve 변경

router.matchrouter.resolve 모두 router.resolve로 통합되었으며 약간 다른 형식으로 변경되었습니다. 자세한 내용은 API를 참조하십시오.

이유: 동일한 목적으로 사용되던 여러 메서드를 통합하기 위해서입니다.

router.getMatchedComponents() 제거

router.getMatchedComponents 메서드가 이제 제거되었습니다. 대신 매치된 컴포넌트들은 router.currentRoute.value.matched에서 가져올 수 있습니다:

js
router.currentRoute.value.matched.flatMap(record =>
  Object.values(record.components)
)

이유: 이 메서드는 SSR 시에만 사용되었으며 사용자가 한 줄로 처리할 수 있는 간단한 메서드였기 때문에 제거되었습니다.

리다이렉트 레코드에서 특수 라우트 사용 불가능

이전에는 비공개로 설정된 기능으로 리다이렉트 레코드를 /events/:id와 같은 특수 라우트로 설정할 수 있었고, 기존의 id 매개변수를 재사용할 수 있었습니다. 이제 이 기능은 더 이상 지원되지 않으며 두 가지 옵션이 있습니다:

  • 매개변수 없이 라우트의 이름을 사용하기: redirect: { name: 'events' }. 단, :id 매개변수가 선택적인 경우에는 작동하지 않습니다.
  • 함수를 사용하여 대상에 기반한 새 위치를 재생성하기: redirect: to => ({ name: 'events', params: to.params })

이유: 이 구문은 거의 사용되지 않았으며 다른 방법들과 비교하여 짧지 않았을 뿐더러, 라우터를 무겁게 만드는 복잡성을 도입했습니다.

모든 네비게이션은 이제 항상 비동기

첫 번째 네비게이션을 포함하여 모든 네비게이션은 이제 비동기로 처리됩니다. 따라서 transition을 사용하는 경우 앱을 마운트하기 전에 라우터가 "준비"되어야 할 수도 있습니다:

js
app.use(router)
// 참고: 서버 측에서는 초기 위치를 수동으로 푸시해야 합니다
router.isReady().then(() => app.mount('#app'))

그렇지 않으면 appear prop을 transition에 제공한 것처럼 처음에 초기 전환이 발생하게 됩니다. 이것은 라우터가 초기 위치(아무것도 없는 상태)를 표시한 후에 첫 번째 위치를 표시하기 때문입니다.

참고: 초기 네비게이션에서 네비게이션 가드가 있는 경우, 서버 측 렌더링을 수행하지 않는 한 앱 렌더를 차단하고 해결되기 전까지 기다리지 않는 것이 좋습니다. 이 경우 라우터를 준비되기 전에 앱을 마운트하는 것은 Vue 2와 동일한 결과를 얻을 수 있습니다.

router.app 제거

router.app은 이제 더 이상 마지막 루트 컴포넌트(Vue 인스턴스)를 나타내지 않습니다. 이제 Vue Router는 동시에 여러 Vue 애플리케이션에서 안전하게 사용할 수 있습니다. 라우터를 사용하는 경우 여전히 app을 추가할 수 있습니다:

js
app.use(router)
router.app = app

또한 Router 인터페이스의 TypeScript 정의를 확장하여 app 속성을 추가할 수도 있습니다.

이유: Vue 3 애플리케이션이 Vue 2에서 존재하지 않고 이제 같은 라우터 인스턴스를 사용하는 여러 애플리케이션을 올바르게 지원합니다. app 속성이 있으면 루트 인스턴스가 아닌 애플리케이션을 나타내므로 혼란스러울 수 있습니다.

라우트 컴포넌트의 <slot>에 콘텐츠 전달

이전에는 라우트 컴포넌트의 <slot>에 직접 템플릿을 전달할 수 있었습니다. 이제는 <router-view> 컴포넌트 아래에 중첩하여 <slot>에 전달해야 합니다:

template
<router-view>
  <p>In Vue Router 3, I render inside the route component</p>
</router-view>

<router-view>에 대한 v-slot API를 사용하여 <component>에 전달해야 합니다:

template
<router-view v-slot="{ Component }">
  <component :is="Component">
    <p>In Vue Router 3, I render inside the route component</p>
  </component>
</router-view>

라우트 위치에서 parent 제거

parent 속성은 정규화된 라우트 위치(this.$routerouter.resolve에 의해 반환되는 객체)에서 제거되었습니다. 그러나 여전히 matched 배열을 통해 접근할 수 있습니다:

js
const parent = this.$route.matched[this.$route.matched.length - 2]

이유: parentchildren 속성이 중복되는 것은 불필요한 순환 참조를 만들어 냅니다. 이미 matched를 통해 속성을 얻을 수 있기 때문에 이러한 변경이 이루어졌습니다.

pathToRegexpOptions 제거

라우트 레코드의 pathToRegexpOptionscaseSensitive 속성이 createRouter()sensitivestrict 옵션으로 대체되었습니다. 이제 createRouter()를 사용하여 직접 pathToRegexpOptions를 전달할 수 있습니다. path-to-regexp에 특정한 다른 옵션은 더 이상 사용되지 않습니다.

무명 매개변수 제거

path-to-regexp의 제거로 인해 무명 매개변수가 더 이상 지원되지 않습니다:

  • /foo(/foo)?/suffix는 이제 /foo/:_(foo)?/suffix가 됩니다.
  • /foo(foo)?는 이제 /foo:_(foo)?가 됩니다.
  • /foo/(.*)는 이제 /foo/:_(.*)가 됩니다.

TIP

매개변수에 _ 대신 다른 이름을 사용할 수 있습니다. 중요한 것은 하나를 제공하는 것입니다.

history.state 사용

Vue Router는 history.state에 정보를 저장합니다. 수동으로 history.pushState()를 호출하는 코드가 있다면, 이를 피하거나 정상적인 router.push()history.replaceState()로 리팩터링해야 합니다:

js
// 이전 방식
history.pushState(myState, '', url)
// 새로운 방식
await router.push(url)
history.replaceState({ ...history.state, ...myState }, '')

마찬가지로, 현재 상태를 보존하지 않고 history.replaceState()를 호출하고 있다면 현재 history.state를 전달해야 합니다:

js
// 이전 방식
history.replaceState({}, '', url)
// 새로운 방식
history.replaceState(history.state, '', url)

이유: 라우터는 스크롤 위치, 이전 위치 등과 같은 네비게이션에 대한 정보를 history state에 저장하기 때문에 이러한 변경이 이루어졌습니다.

options에서 routes 옵션 필수화

options에서 routes 속성은 이제 필수적으로 요구됩니다.

js
createRouter({ routes: [] })

이유: 라우터는 라우트와 함께 생성되도록 설계되었습니다. 대부분의 경우 최소한 하나의 라우트가 필요하며, 대개 앱 당 한 번 작성되므로 이렇게 변경되었습니다.

존재하지 않는 이름을 가진 라우트

존재하지 않는 이름의 라우트를 푸시하거나 해결하려고 하면 오류가 발생합니다:

js
// 이름을 잘못 입력했을 경우
router.push({ name: 'homee' }) // 오류 발생
router.resolve({ name: 'homee' }) // 오류 발생

이유: 이전에 라우터는 /로 이동하지만 아무 것도 표시하지 않았습니다(홈 페이지 대신). 오류를 발생시키는 것이 더 타당하며, 유효한 URL로 이동할 수 없기 때문에 더 이상 /로 이동하지 않습니다.

필요한 매개변수가 누락된 이름 있는 라우트

필요한 매개변수 없이 이름 있는 라우트를 푸시하거나 해결하려고 하면 오류가 발생합니다:

js
// 다음 라우트를 가정하면:
const routes = [{ path: '/users/:id', name: 'user', component: UserDetails }]

// `id` 매개변수가 누락된 경우 오류 발생
router.push({ name: 'user' })
router.resolve({ name: 'user' })

이유: 위와 동일합니다.

path를 가진 이름 있는 하위 라우트의 슬래시 제거

path를 가진 중첩된 이름 있는 라우트는 이제 슬래시를 포함하지 않습니다:

js
const routes = [
  {
    path: '/dashboard',
    name: 'dashboard-parent',
    component: DashboardParent,
    children: [
      { path: '', name: 'dashboard', component: DashboardDefault },
      {
        path: 'settings',
        name: 'dashboard-settings',
        component: DashboardSettings,
      },
    ],
  },
]

이름 있는 라우트 dashboard로 이동하면 이제 슬래시 없이 URL이 생성됩니다:

js
router.resolve({ name: 'dashboard' }).href // '/dashboard'

이로 인해 다음과 같은 결과가 발생합니다:

js
const routes = [
  {
    path: '/parent',
    component: Parent,
    children: [
      // 이제 `/parent/home` 대신 `/home`로 리디렉션됩니다.
      { path: '', redirect: 'home' },
      { path: 'home', component: Home },
    ],
  },
]

주의: path/parent/인 경우에는 여전히 /parent/home으로의 상대적 위치는 /parent/home이며 home의 상대적 위치는 /home이기 때문에 작동합니다.

이유: 이로 인해 후행 슬래시 동작이 일관되게 유지되며, 기본적으로 모든 라우트가 후행 슬래시를 허용하게 됩니다. 이는 strict 옵션을 사용하여 비활성화할 수 있으며, 라우트에 직접 슬래시를 추가하거나 추가하지 않을 수도 있습니다.

$route 속성 인코딩

params, query, 그리고 hash에서 디코딩된 값들은 이제 어디서 네비게이션을 시작하더라도 일관성을 가집니다 (구버전 브라우저는 여전히 인코딩되지 않은 pathfullPath를 생성할 것입니다). 초기 네비게이션은 앱 내에서 발생하는 네비게이션과 동일한 결과를 가져야 합니다.

아래는 정규화된 라우트 위치를 기준으로 합니다:

  • path, fullPath의 값은 이제 더 이상 디코딩되지 않습니다. 브라우저가 제공하는대로 표시됩니다(대부분의 브라우저에서는 인코딩된 상태로 제공됩니다). 예를 들어, 주소 창에 직접 https://example.com/hello world를 입력하면 인코딩된 버전인 https://example.com/hello%20world를 얻게 되며, pathfullPath는 모두 /hello%20world가 됩니다.
  • hash는 이제 디코딩되며, 이로 인해 다음과 같이 복사하여 사용할 수 있습니다: router.push({ hash: $route.hash }) 그리고 scrollBehaviorel 옵션에서 직접 사용할 수 있습니다.
  • push, resolve, 그리고 replace를 사용하고 string 위치나 객체의 path 속성을 제공할 때, 반드시 인코딩해야 합니다(이전 버전과 같습니다). 반면에 params, query, 그리고 hash는 인코딩되지 않은 상태로 제공되어야 합니다.
  • 슬래시 문자 (/)는 이제 params 내에서 적절히 디코딩되지만 URL에서는 인코딩된 버전인 %2F로 생성됩니다.

이유: 이로 인해 router.push()router.resolve()를 호출할 때 존재하는 위치의 속성을 쉽게 복사할 수 있으며, 결과적으로 브라우저 간 일관된 라우트 위치를 만들 수 있습니다. router.push()는 이제 멱등성을 가지며, 즉 router.push(route.fullPath), router.push({ hash: route.hash }), router.push({ query: route.query }), 그리고 router.push({ params: route.params })를 호출하여 추가적인 인코딩을 생성하지 않습니다.

TypeScript 변경 사항

더 일관적이고 표현력 있는 타이핑을 위해 몇 가지 타입이 이름을 바꾸었습니다:

vue-router@3vue-router@4
RouteConfigRouteRecordRaw
LocationRouteLocation
RouteRouteLocationNormalized

새로운 기능

Vue Router 4에서 주목해야 할 일부 새로운 기능들은 다음과 같습니다:

Translated by router.vuejs.kr