Skip to content

데이터 로더 정의하기 ​

데이터 로더를 사용하려면 먼저 정의해야 합니다. 데이터 로더 자체는 서로 다른 defineLoader 함수가 반환하는 composable입니다. 각 로더 정의는 사용한 defineLoader 함수에 따라 달라집니다. 예를 들어 defineBasicLoader는 첫 번째 인자로 async 함수를 기대하는 반면, defineColadaLoader는 query 함수를 가진 객체를 기대합니다. 모든 로더는 오류를 throw할 수 있는 async 함수를 전달할 수 있어야 하며, reroute()를 호출해 내비게이션을 제어할 수 있어야 합니다.

어떤 defineLoader 함수가 반환하든 composable은 공통된 시그니처를 가집니다:

vue
<script lang="ts">
import 'vue-router/auto-routes'
// ---cut---
import { defineBasicLoader } from 'vue-router/experimental'
import { getUserById } from '../api'

export const useUserData = defineBasicLoader('/users/[id]', async to => {
  return getUserById(to.params.id)
})
</script>

<script setup lang="ts">
const {
  data: user, // 로더가 반환한 데이터
  isLoading, // 로더가 데이터를 가져오는 중인지 나타내는 불리언 값
  error, // 로더가 실패했을 때의 오류 객체
  reload, // 내비게이션 없이 데이터를 다시 가져오는 함수
} = useUserData()
</script>

하지만 여기에만 제한되지는 않습니다! 예를 들어 defineColadaLoader 함수는 status, refresh 같은 추가 속성을 가진 composable을 반환합니다. 따라서 사용하는 특정 로더의 문서를 참고하는 것이 중요합니다.

이 페이지에서는 구현이 무엇이든 데이터 로더를 정의하는 기초 를 안내합니다.

로더 함수 ​

로더 함수는 데이터 로더의 핵심 입니다. 이는 반환된 composable의 data 속성으로 노출할 데이터를 반환하는 비동기 함수입니다.

to 인자 ​

to 인자는 우리가 이동하려는 location 객체를 나타냅니다. 데이터 가져오기에 필요한 모든 매개변수의 source of truth로 사용해야 합니다.

ts
import 'vue-router/auto-routes'
import { defineBasicLoader } from 'vue-router/experimental'
import { getUserById } from '../api'
// ---cut---
export const useUserData = defineBasicLoader('/users/[id]', async to => {
  const user = await getUserById(to.params.id)
  // 여기에서 반환하기 전에 데이터를 수정할 수 있습니다
  return user
})

데이터를 가져올 때 라우트 location을 사용하면 데이터와 URL 사이의 일관된 관계를 보장할 수 있어 사용자 경험이 향상됩니다.

부작용 ​

로더 함수에서는 부작용을 피하는 것이 중요합니다. watch를 호출하거나 ref, toRefs(), computed 같은 반응형 effect를 만들지 마세요.

전역 속성 ​

로더 함수 안에서는 라우터 인스턴스, store 같은 전역 속성에 접근할 수 있습니다. 이는 내비게이션 가드 내부와 마찬가지로 로더 함수 안에서도 inject()를 사용할 수 있기 때문입니다. 로더는 비동기이므로, inject 함수는 반드시 어떤 await보다도 먼저 사용해야 합니다:

ts
import 'vue-router/auto-routes'
import { defineBasicLoader } from 'vue-router/experimental'
import { getUserById } from '../api'
// ---cut---
import { inject } from 'vue'
import { useSomeStore, useOtherStore } from '@/stores'

export const useUserData = defineBasicLoader('/users/[id]', async to => {
  // ✅ 이것은 동작합니다
  const injectedValue = inject('key') 
  const store = useSomeStore() 

  const user = await getUserById(to.params.id)
  // ❌ 이것들은 동작하지 않습니다
  const injectedValue2 = inject('key-2') 
  const store2 = useOtherStore() 
  // ...
  return user
})

로더는 내비게이션 컨텍스트 안에서 실행되므로 reroute()를 호출해 내비게이션을 제어할 수 있습니다. 이는 내비게이션 가드에서 값을 반환하는 것과 비슷합니다. 내부적으로 throw하므로 실행이 즉시 중단됩니다.

ts
import { reroute } from 'vue-router/experimental'

const useDashboardStats = defineBasicLoader('/admin', async (to) => {
  try {
    return await getDashboardStats()
  } catch (err) {
    if (err.code === 401) {
      // 내비게이션 가드에서 '/login'을 반환하는 것과 같습니다
      reroute('/login')
    }
    throw err // 예상하지 못한 오류
  }
})

TIP

lazy loader는 내비게이션을 막지 않으므로 제어할 수 없다는 점에 유의하세요.

내비게이션 인식 섹션에서 더 자세히 읽어보세요.

오류 ​

throw된 모든 Error는 내비게이션 가드와 마찬가지로 내비게이션을 중단시킵니다. 정의되어 있다면 router.onError 핸들러를 트리거합니다.

TIP

lazy loader는 내비게이션을 막지 않으므로 제어할 수 없으며, throw된 오류는 error 속성에 나타날 뿐 내비게이션을 중단시키거나 router.onError 핸들러에 나타나지 않는다는 점에 유의하세요.

내비게이션을 중단시키지 않도록 예상된 오류를 정의할 수 있습니다. 자세한 내용은 오류 처리 섹션에서 확인하세요.

옵션 ​

데이터 로더는 유연하고 사용자 정의가 가능하도록 설계되었습니다. 내비게이션 중심이지만 내비게이션 밖에서도 사용할 수 있으며, 이러한 유연성이 설계의 핵심입니다.

lazy를 사용한 non-blocking 로더 ​

기본적으로 로더는 non-lazy 이며, 데이터가 가져와질 때까지 내비게이션을 막습니다. 하지만 lazy 옵션을 true로 설정하면 이 동작을 바꿀 수 있습니다.

vue
<script lang="ts">
// ---cut-start---
import 'vue-router/auto-routes'
import { defineBasicLoader } from 'vue-router/experimental'
// ---cut-end---
import { getUserById } from '../api'

export const useUserData = defineBasicLoader(
  '/users/[id]',
  async (to) => {
    const user = await getUserById(to.params.id)
    return user
  },
  { lazy: true } // 👈 lazy로 표시합니다
)
</script>

<script setup>
// 위 예제와 달리 `user.value`는 처음에 `undefined`일 수 있고 실제로도 그렇습니다
const { data: user, isLoading, error } = useUserData()
//            ^?
</script>

<!-- ... -->

이 패턴은 중요하지 않은 데이터 를 가져오는 동안 내비게이션을 막지 않기 위해 유용합니다. lazy loader가 계속 로딩 중이어도 페이지를 더 일찍 표시할 수 있고, isLoading 속성 덕분에 로더 표시기도 보여 줄 수 있습니다.

lazy loader는 내비게이션을 막지 않으므로 throw된 오류가 내비게이션을 중단시키거나 router.onError 핸들러에 나타나지 않습니다. 대신 오류는 error 속성에서 사용할 수 있습니다.

이 방식은 SSR과 클라이언트 사이드 내비게이션에서 서로 다른 동작을 갖게 하는 것도 허용합니다. 예를 들어 SSR에서는 로더를 기다리되 클라이언트 사이드 내비게이션에서는 기다리지 않게 할 수 있습니다:

ts
export const useUserData = defineBasicLoader(
  async (to) => {
    // ...
  },
  {
    lazy: !import.meta.env.SSR, // Vite specific
  }
)

각 로드/내비게이션에 따라 로더를 lazy로 만들지 결정하기 위해 lazy에 함수를 전달할 수도 있습니다:

ts
export const useSearchResults = defineBasicLoader(
  async (to) => {
    // ...
  },
  {
    // 같은 라우트에 머무르는 경우 lazy 처리
    lazy: (to, from) => to.name === from.name,
  }
)

이 방식은 새 데이터를 가져오는 동안 이전 데이터를 표시할 수 있고, 검색 결과나 페이지네이션 버튼처럼 페이지 일부에서 라우트 업데이트가 필요한 경우에 특히 유용합니다. 라우트가 바뀔 때만 lazy loader를 사용하면 검색 결과를 가져오는 동안에도 페이지네이션을 즉시 갱신할 수 있어, 사용자는 검색 결과를 기다리지 않고 페이지네이션 버튼을 여러 번 클릭할 수 있습니다.

commit으로 데이터 업데이트 지연하기 ​

기본적으로 데이터는 모든 로더가 해결된 뒤 한 번만 업데이트됩니다. 이는 부분적으로만 로드된 데이터나, 더 나쁘게는 일관되지 않은 데이터 집계를 표시하는 일을 피하는 데 유용합니다.

다른 로더가 아직 pending 상태이더라도, 데이터가 준비되는 즉시 업데이트하고 싶을 때가 있습니다. 이는 commit 옵션을 변경하여 구현할 수 있습니다:

ts
import { defineBasicLoader } from 'vue-router/experimental'
interface Book {
  title: string
  isbn: string
  description: string
}
function fetchBookCollection(): Promise<Book[]> {
  return {} as any
}
// ---cut---
export const useBookCollection = defineBasicLoader(fetchBookCollection, {
  commit: 'immediate',
})

lazy loader의 경우 기본값은 commit: 'after-load'입니다. 가능하다면 다른 모든 non-lazy loader 뒤에 commit하지만, await되지 않기 때문에 그렇게 하지 못할 수도 있습니다. 이 경우 데이터는 로딩이 끝났을 때 사용할 수 있게 되며, 이는 내비게이션 완료보다 훨씬 늦을 수 있습니다.

server로 서버 최적화 ​

SSR 중에는 초기 렌더링에 중요하지 않은 데이터를 로드하지 않는 편이 더 성능상 유리할 수 있습니다. server 옵션을 false로 설정하면 이를 구현할 수 있습니다. 그러면 SSR 동안 해당 로더를 완전히 건너뜁니다.

ts
import { defineBasicLoader } from 'vue-router/experimental'
interface Book {
  title: string
  isbn: string
  description: string
}
function fetchRelatedBooks(id: string | string[]): Promise<Book[]> {
  return {} as any
}
// ---cut---
export const useRelatedBooks = defineBasicLoader(
  (to) => fetchRelatedBooks(to.params.id),
  { server: false }
)

SSR 섹션에서 서버 사이드 렌더링에 대해 더 자세히 읽어볼 수 있습니다.

로더를 페이지에 연결하기 ​

라우터는 어떤 페이지에서 어떤 로더를 실행해야 하는지 알아야 합니다. 이는 두 가지 방법으로 이뤄집니다:

  • 자동으로: lazy load되는 페이지 컴포넌트에서 로더를 export하면 해당 로더가 페이지에 자동으로 연결됩니다

    ts
    import { createRouter, createWebHistory } from 'vue-router'
    
    export const router = createRouter({
      history: createWebHistory(),
      routes: [
        {
          path: '/settings',
          component: () => import('./settings.vue'),
        },
      ],
    })
    vue
    <script lang="ts">
    import { getSettings } from './api'
    export const useSettings = defineBasicLoader('/settings', async (to) =>
      getSettings()
    )
    </script>
    
    <script lang="ts" setup>
    const { data: settings } = useSettings()
    </script>
    <!-- ...컴포넌트의 나머지 부분 -->
  • 수동으로: 정의한 로더를 meta.loaders 속성에 전달합니다:

    ts
    import { createRouter, createWebHistory } from 'vue-router'
    import Settings, { useSettings } from './settings.vue'
    
    export const router = createRouter({
      history: createWebHistory(),
      routes: [
        {
          path: '/settings',
          component: Settings,
          meta: {
            loaders: [useSettings],
          },
        }
      ],
    })
    vue
    <script lang="ts">
    import { getSettings } from './api'
    export const useSettings = defineBasicLoader('/settings', async (to) =>
      getSettings()
    )
    </script>
    
    <script lang="ts" setup>
    const { data: settings } = useSettings()
    </script>
    <!-- ...컴포넌트의 나머지 부분 -->

페이지에서 로더 연결 해제하기 ​

로더를 페이지에 연결하지 않는 것도 가능합니다. 이렇게 하면 컴포넌트가 마운트될 때까지 로딩을 지연할 수 있습니다. 보통은 가능한 한 빨리 데이터 로딩을 시작하고 싶겠지만, 경우에 따라서는 컴포넌트가 마운트될 때까지 기다리는 편이 더 나을 수 있습니다. 이는 페이지 컴포넌트에서 로더를 export하지 않음으로써 구현할 수 있습니다.

모두를 위한 문서 한글화