/** * 공개 라우트 이동 후 새 화면 기준으로 스크롤 위치를 정리한다. * * Vue Router 기본 scrollBehavior는 전환 중 이전 화면을 먼저 움직일 수 있어 * `app/router.options.js`에서 끄고, 여기서는 Nuxt 페이지 렌더링이 끝난 뒤 * 새 화면에서만 최상단 또는 해시 위치로 이동한다. 전환 중에는 짧은 베일을 * 씌워 이전 화면이 상단으로 튀는 순간이 사용자에게 보이지 않게 한다. */ const ROUTE_VEIL_CLASS = 'site-route-veil' const ROUTE_VEIL_TIMEOUT_MS = 8000 export default defineNuxtPlugin((nuxtApp) => { const router = nuxtApp.$router const root = document.documentElement /** @type {{ id: number, to: import('vue-router').RouteLocationNormalizedLoaded, from: import('vue-router').RouteLocationNormalizedLoaded } | null} */ let pendingRouteScroll = null let routeScrollId = 0 /** @type {ReturnType | null} */ let veilTimer = null /** * 브라우저 렌더 프레임을 기다린다. * @returns {Promise} 프레임 대기 Promise */ const waitForFrame = () => new Promise((resolve) => { requestAnimationFrame(() => { requestAnimationFrame(resolve) }) }) /** * 페이지 전환 베일을 표시한다. * @returns {void} */ const showRouteVeil = () => { root.classList.add(ROUTE_VEIL_CLASS) clearTimeout(veilTimer) veilTimer = setTimeout(() => { root.classList.remove(ROUTE_VEIL_CLASS) }, ROUTE_VEIL_TIMEOUT_MS) } /** * 페이지 전환 베일을 숨긴다. * @returns {void} */ const hideRouteVeil = () => { clearTimeout(veilTimer) veilTimer = null root.classList.remove(ROUTE_VEIL_CLASS) } /** * 이동 완료 후 적용할 스크롤 정보를 저장한다. * @param {import('vue-router').RouteLocationNormalizedLoaded} to - 이동 대상 라우트 * @param {import('vue-router').RouteLocationNormalizedLoaded} from - 이전 라우트 * @returns {void} */ const queueRouteScroll = (to, from) => { if (to.fullPath === from.fullPath) { return } routeScrollId += 1 pendingRouteScroll = { id: routeScrollId, to, from } } /** * 새 페이지 렌더링 이후 스크롤 위치를 적용한다. * @returns {Promise} */ const applyPendingRouteScroll = async () => { const routeScroll = pendingRouteScroll if (!routeScroll) { hideRouteVeil() return } await waitForFrame() if (!pendingRouteScroll || pendingRouteScroll.id !== routeScroll.id) { return } pendingRouteScroll = null const { to } = routeScroll if (to.hash) { const targetId = decodeURIComponent(to.hash.slice(1)) const target = document.getElementById(targetId) if (target) { target.scrollIntoView({ block: 'start' }) await waitForFrame() hideRouteVeil() return } } window.scrollTo({ top: 0, left: 0, behavior: 'auto' }) await waitForFrame() hideRouteVeil() } router.afterEach((to, from) => { queueRouteScroll(to, from) }) nuxtApp.hook('page:loading:start', showRouteVeil) nuxtApp.hook('page:finish', applyPendingRouteScroll) nuxtApp.hook('app:error', hideRouteVeil) })