Skip to content

데이터 로더 정의하기

데이터 로더를 사용하려면 먼저 정의해야 합니다. 데이터 로더 자체는 서로 다른 defineLoader 함수가 반환하는 composable입니다. 각 로더 정의는 사용한 defineLoader 함수에 따라 달라집니다. 예를 들어 defineBasicLoader는 첫 번째 인자로 async 함수를 기대하는 반면, defineColadaLoaderquery 함수를 가진 객체를 기대합니다. 모든 로더는 오류를 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하지 않음으로써 구현할 수 있습니다.

모두를 위한 문서 한글화