Front-End/vue

[nuxt3] plugin 기능과 definePageMeta를 활용한 웹 내 페이지 검색 기능 구현

찐코딩 2023. 8. 7. 15:32

🎯목표

이렇게 페이지의 타이틀을 검색하면 검색 결과가 나오고, 클릭하면 routing이 되는 기능을 구현해 볼 것이다.


 

 

1.  먼저 plugins 폴더가 없다면 폴더를 생성하고 하위 파일을 생성한다.

공식문서에 따르면 "Nuxt는 자동으로 plugins 디렉토리의 파일을 읽고 Vue 애플리케이션 생성 시 로드합니다." 라고 되어있다.

이제 이 기능을 활용해서 웹페이지가 로드될 때 useRouter로 앱에 있는 모든 router 정보를 긁어올 것이다.

https://nuxt.com/docs/guide/directory-structure/plugins

 

plugins/ · Nuxt Directory Structure

Nuxt automatically reads the files in your plugins directory and loads them at the creation of the Vue application. You can use .server or .client suffix in the file name to load a plugin only on the server or client side. All plugins in your plugins/ dire

nuxt.com

 

이렇게 폴더를 생성 후 파일을 만들고 난 뒤, nuxt.config.ts 내의 plugins 키에 파일을 추가한다.

 

nuxt.config.ts

export default defineNuxtConfig({
  ...생략...
  // plugins
  plugins: ['~/plugins/pages.ts'],
  ...생략...
})

 

2. page가 load 될 때, 앱 내 모든 page의 meta 정보를 긁고 저장(plugin)

/plugins/pages.ts

import { useSearchStore, IPageMata } from '../stores/search'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('page:finish', () => {
    const router = useRouter()
    const searchStore = useSearchStore()
    // 검색 인덱스 선언
    const searchIndex: IPageMata[] = []
    router.getRoutes().forEach((route) => {
      if (route.meta && route.meta.title) {
        const metadata: IPageMata = {
          url: route.path,
          title: route.meta.title.toString(),
        }
        searchIndex.push(metadata)
      }
    })
    // pinia에 저장
    searchStore.setSearchIndex(searchIndex)
  })
})

먼저, PageMeta 인터페이스로 구성된 배열을 search index라고 명하고, 여기서 검색을 할 수 있게끔 할 것이다.

 

 PageMeta 인터페이스는 아래와 같이 구성되어있다.

interface IPageMata {
  url: string
  title: string
}

url: 페이지의 route path

title: 페이지의 제목. 각 페이지별로 구분되는 text가 들어갈 곳이다.

나는 페이지별로 기능을 나눠놔서 페이지 제일 상단부에 들어가는 기능을 작성했다.

이런식으로 페이지가 구성되어 있다면 '관리자 설정' 이 해당 페이지의 title이 되는 것이다.

그렇게 meta 정보를 불러오고, pinia에 검색 인덱스(searchIndex)를 저장한다.

 

공식문서에  보면  useRouter에는 getRoutes 라고 모든 경로 기록을 가져오는 함수가 있다.

router.getRouter() 를 로그로 찍어보면,

이렇게 path와 meta 등 다른 여러 route 정보가 있는 배열을 반환한다.

이중에 path와 meta 객체를 활용하여 search Index를 생성하고, 저장할 것이다.

 

참고로 저 meta 정보는 vue 파일의 <script setup> 구문 내에서

definePageMeta라고 페이지의 meta정보를 저장하는 곳에서 정의한 값이다.

definePageMeta({
  layout: 'global-console',
  title: '관리자 현황',
})

보통은 definePageMeta에 layout나 middleware를 설정하기 위해 사용하는데, 공식 문서에 보면

이렇게 사용자 마음대로 key: value 형식으로만 주입하면 된다니,

나는 이걸 활용해서 key값은 'title'로 명명하고 검색 옵션을 줘봤다.

(검색 시 나오지 말아야 할 페이지에는 title 키를 주입하지 않으면 된다.)

 

3. 검색 기능 (pinia)

import { defineStore } from 'pinia'

export interface IPageMata {
  url: string
  title: string
}
export const searchIndex: IPageMata[] = []
export const useSearchStore = defineStore('search', {
  state: () => ({
    searchIndex,
  }),
  actions: {
    setSearchIndex(searchIndex: IPageMata[]) {
      this.searchIndex = searchIndex
    },
    search(query: string) {
      const results = Array.from(this.searchIndex).filter((page) =>
        Object.assign({}, page)
          .title.toLowerCase()
          .includes(query.toLowerCase())
      )
      return results
    },
  },
})

따로 Array와 Object로 감싸준 것은 searchIndex가 proxy 객체여서 type error떠서 잡아줬다

setSearchIndex는 plugins에서 검색 인덱스를 저장하기 위해서 선언한 함수이고,

검색 기능은 search 함수를 보면 되는데,

query라는 string 인자를 받으면, 검색 인덱스 배열 중에 query를 포함한 title이 있는지 반환하는 함수이다.

기능 자체는 매우 간단

 

4. 검색 컴포넌트 작성(vue)

searchPages.vue

<script setup lang="ts">
  import { useSearchStore, IPageMata } from '~~/stores/search'
  import { useUserInfo } from '~~/stores/userInfo'
  const s = useSearchStore()
  const result = ref<IPageMata[]>([])
  const query = ref('')
  const noResult = ref(false)
  watch(query, () => {
    noResult.value = false
    result.value = s.search(query.value)
    if (result.value.length === 0 && query.value.length > 0) noResult.value = true
  })
  </script>
  <template>
    <div>
      <div class="pb-5 border-b-1">
        <input 
          v-model.trim="query" 
          type="text"
          placeholder="검색어를 입력하세요."
        />
      </div>
      <div class="h-50 overflow-x-auto">
        <div v-show="query.length > 0" class="flex flex-col gap-3">
          <NuxtLink
            v-for="(item, i) in result"
            :key="i"
            class="flex flex-row gap-2 text-sm items-center"
            :to="item.url"
          >
            <span>{{ item.title }}</span>
          </NuxtLink>
        </div>
        <div v-show="noResult" class="flex justify-center">
          <span class="text-sm text-red-500"
            >"{{ query }}"로 검색한 결과가 없습니다!</span
          >
        </div>
      </div>
    </div>
  </template>