directory structure

.nuxt

개발 모드에서 vue application을 만들기 위한 directory

.output

배포 모드에서 vue application을 만들기 위한 directory

assets

프로젝트에 사용되는 모든 assets을 포함하며 빌드 도구인 webpack, vite의 영향을 받는다.

  • Stylesheets (CSS, SASS, etc.)
  • Fonts
  • Images that won’t be served from the public/ directory

만약 assets을 서버에서 가져오고 싶다면 public 루트에 넣는 것이 좋음.

components

컴포넌트들을 넣어두는 디렉토리. 컴포넌트는 pages 안에 있는 vue 페이지나 component에서 import 될 수 있음. Nuxt는 components 디렉토리에 있는 파일을 자동으로 import 해줌.

Component Names

| components/
--| base/
----| foo/
------| Button.vue

만약 다음과 같이 중첩되는 디렉토리를 가졌다면

컴포넌트 이름은 <BaseFooButton /> 가 된다.

Dynamic components

resolveComponent 를 사용해서 동적으로 빌트인 컴포넌트를 이용하여 component를 import 할 수 있다.

<template>
	<component :is="clickable ? MyButton : 'div'" />
</template>
<script setup>
	const MyButton = resolveComponent('MyButton')
</script>

Dynamic imports

컴포넌트를 동적으로 import하기 위해서는 component이름 앞에 Lazy를 붙임

컴포넌트가 항상 필요하지 않을 경우에는 동적으로 맞는 타이밍에 import 시킴으로써 번들 사이즈를 최적화 할 수 있음.

Library Authors

사용할 라이브러리의 자동 import와 tree-shaking이 쉬움

node_modules안에 있는 각 라이브러리 디렉토리에 nuxt.js파일을 만들고 components:dirs 훅에서 control 할 수 있음.

nuxt.config에서

export default {
  modules: ['awesome-ui/nuxt']
}

다음과 같이 modules로 라이브러리를 추가 할 수 있음.

composables

Nuxt는 vue의 composable을 auto import 함.

예를들어

composables
 | - useFoo.ts // scanned
 | - useBar
 | --- supportingFile.ts // not scanned
 | --- index.ts // scanned

위와 같은 디렉토리 구조에서 useFoo.ts 와 useBar/index.ts 는 자동으로 import된다. 특히 useBar/index.ts 는 index로 등록 되지 않고 userBar로 등록된다.

export const useBar = () => {
}

// Enables auto import for this export
export { useBaz } from './supportingFile'

만약 useBar/supportingFile.ts 를 자동으로 import하고 싶을 때는 위와 같이 useBar/index.ts에서 userBar/supportingFile.ts를 다시 한번 export 시켜줘야 한다.

layouts

Nuxt는 커스텀가능한 layout을 제공하는데

만약 예를들어 pages 디렉토리가 아닌 app.vue 하나에서 사용 한다면

-| layouts/
---| custom.vue
-| app.vue

라는 디렉토리 구조에서

custom.vue는

<template>
	<div>
    Some shared layout content:
    <slot />
	</div>
</template>

다음과 같이 보여줄 페이지 자리에 <slot/>을 입력함

<template><NuxtLayout name="custom">
    Hello world!
  </NuxtLayout></template>

app.vue에서 layout을 다음과 같이 사용함

만약 /pages 디렉토리를 사용한다면

-| layouts/
---| custom.vue
-| pages/
---| index.vue

다음과 같은 디렉토리 구조에서

<script>
// This will also work in `<script setup>`
definePageMeta({
  layout: "custom",
});
</script>

index.vue 파일에 definePageMeta 메서드를 이용하여 layout을 지정해 사용할 수 있다.

middleware

Nuxt는 커스텀 가능한 route middleware를 제공함. 이는 global하게 앱 전역에서 사용이 가능함

지정한 route에 navigate 할 때 code를 작성할 수 있음.

pages

Usage

Nuxt는 vue router를 이용해 file-based 라우팅을 제공함

page와 component는 .vue .js .jsx .ts .tsx 확장자를 가질 수 있음

pages/index.vue

<template><h1>Index page</h1></template>

pages/index.ts

// <https://vuejs.org/guide/extras/render-function.html>
export default defineComponent({
  render () {
    return h('h1', 'Index page')
  }
})

pages/index.tsx

// <https://vuejs.org/guide/extras/render-function.html#jsx-tsx>
export default defineComponent({
  render () {
    return

Index page

  }
})

pages 디렉토리 안 index.vue 는 / route와 매핑됨

transition을 이용하려면 무조건 하나의 root element를 가져야 함

⇒ server-rendered 된 페이지는 불가능

definePageMeta({
  title,  // This will create an error
  someData
})

으로 router의 meta data를 지정할 수 있음

특별한 Metadata로는 keepalive, key, layout, middleware, layoutTransition, pageTransition, alias 등이 있음

Navigation

두 페이지 간 전환이 필요할 때 <NuxtLink> 컴포넌트를 이용함.

NuxtLink 컴포넌트는 Nuxt에 기본으로 포함되어 있어 import할 필요 없음

<template><NuxtLink to="/">Home page</NuxtLink></template>

위와 같이 to=”/path” 형식으로 라우팅함

Programmatic Navigation

navigateTo() 메서드를 이용하여 프로그래밍적으로 라우팅이 가능함

<script setup>
const router = useRouter();
const name = ref('');
const type = ref(1);

function navigate(){
  return navigateTo({
    path: '/search',
    query: {
      name: name.value,
      type: type.value
    }
  })
}
</script>

plugins

Nuxt는 plugins 디렉토리의 plugin 파일들을 자동으로 읽음

.server .client 같은 파일 뒤 suffix로 서버사이드 렌더링이나 클라이언트 사이드 렌더링에서 사용되는 플러그인을 구분 할 수 있음

plugins/ directory의 가장 상위 레벨의 파일들이 plugin으로 등록됨

예를들어

plugins
 | - myPlugin.ts
 | - myOtherPlugin
 | --- supportingFile.ts
 | --- componentToRegister.vue
 | --- index.ts

에서 myPlugin.ts 와 myOtherPlugin/index.ts 가 등록됨

Creating plugins

export default defineNuxtPlugin(nuxtApp => {
  // Doing something with nuxtApp
})

plugin을 정의 할때 받는 매개변수는 nuxtApp 하나임

Automatically providing helpers

export default defineNuxtPlugin(() => {
  return {
    provide: {
      hello: () => 'world'
    }
  }
})

위와 같이 Nuxtapp 인스턴스에 helper를 제공하기 위해서는 provide key 밑 property로 설정 후 provide를 return 함

<template><div>
    {{ $hello() }}
  </div></template><script setup lang="ts">
// alternatively, you can also use it here
const { $hello } = useNuxtApp()
</script>

vue 파일에선 위와 같이 사용함

해결해야 하는 문제점

1. 당시 우리 서비스는 쇼핑몰 아이템을 클릭 후 뒤로가기 시 다시 서버에 요청을 보내 데이터를 받아와 화면을 갱신해야하는 요구사항이 있었기 때문에 앞서 소개했던 keep-alive라던가, web storage에 모든 데이터를 넣는 방법이라던가, scrollBehavior을 통해 스크롤 위치를 return하는 방법을 그대로 사용하기 어려웠다.

 

2. 또, 알 수 없이 페이지를 렌더링 할 때 스크롤이 맨 아래로 향해있는 이슈가 있어 window.scrollTo(0,0) 코드를 넣어 가장 상위로 스크롤을 강제로 위치시키고 있었다.

 

어떻게 해결했는가?

1. Vue Router meta 정보에 savePosition, preservePosition 이라는 속성(boolean)을 할당하여, 페이지가 넘어갈 시 스크롤 위치를 저장하는 페이지와 다시 돌아올 때 저장했던 스크롤 위치를 불러오는 페이지를 설정할 수 있게 했다.

 

2. scrollBehavior에서 뒤로가기를 할 시, 이전 scroll위치를 받아와 sessionStorage에 저장하였고, 뒤로가기가 아닐시에는 (0,0)을 반환하여 처음 위치로 가게 하였다.

 

3. 라이프사이클 updated 훅에서 nextTick을 이용하여 모든 화면(자식 컴포넌트 포함)이 렌더링 된 후 window.scrollTo를 이용하여 sessionStorage에서 저장한 이전 scroll위치로 스크롤을 위치시켰다.

*자식 컴포넌트를 포함한 모든 화면이 update가 된 이후에 메서드를 호출하려면 mounted가 아닌 updated의 nextTick 안에서 해야한다*

 

4. 알 수 없이 페이지의 스크롤이 맨 아래로 향해있었던 이슈는 DOM에서 computed로 계산된 유저의 이름을 바로 사용하고 있었는데, 그 부분을 data의 변수에 할당하고 computed가 아닌 data안의 변수를 DOM에서 사용하는 방식으로 변경하니 이슈가 해결되었다. 그런데 정확한 원인은 아직 제대로 파악하지 못하였다..

 

 

쇼핑몰 프로젝트를 진행하면서, 뒤로가기 시 스크롤 유지 관련한 이슈 해결 히스토리를 기록해보고자 이 글을 작성하게 되었다. 현재 프로젝트에서는 Vue + Vue Router + Vuex 를 사용한다.

 

 

개요

현재 진행하던 프로젝트에서는 기존에 sessionStorage에 스크롤 위치를 저장하고, updated 훅에서 모든 화면 update가 끝난 후 sessionStorage에 있는 scroll값을 가져와 scroll을 이동시키는 스크롤을 컨트롤 하고 있었다. 이과정에서 서버 통신 후 데이터를 렌더링하는 시점과, 스크롤을 이동시키는 시점에 오류가 생겨 스크롤을 잘 잡지 못하는경우가 발생하여 로직을 수정하게 되었다.

 

고려해본 방법들 

1. keep-alive 적용

2. web storage 사용

3. router savedPosition 사용

 

Keep-alive 적용

keep-alive는 컴포넌트를 전환할 때, 성능상의 이유로 상태를 유지하거나, 재 렌더링을 피할 수 있게 해주는 element입니다. 재 렌더링을 피할 수 있기 때문에, 뒤로가기 시 별다른 장치 없이 그대로 화면을 유지할 수 있는 장점이 있다. 

<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>

 

사용은 이런식으로 원하는 컴포넌트를 <keep-alive> 태그로 감싸주면 된다.

여기서 유의할점은 화면 전체의 재 렌더링을 피하고 싶다면 router-view 전체를 keep-alive 태그로 감싸야 한다는 것이다. (이것때문에 여러번 실패를 했었네요..) 

뒤로가기를 누를때만 적용 되야 하기 때문에 동적으로 할당 해야한다. 또한 재렌더링이 안되기 때문에 신중히 사용해야한다.

 

Web Storage 사용

화면 데이터와 스크롤 위치를 localStorage나 sessionStorage에 담아놓고, 돌아올 때 그대로 다시 불러오는 방법. 새롭게 서버통신을 해서 다시 리스트를 불러오지 않기 때문에 infinity scroll 사용 시 스크롤 위치를 잡기가 용이하다. 또 라이프사이클 훅에서 이루어지는 모든 것들이 정상 동작하므로 keep-alive를 사용할 수 없는 경우(라이프사이클을 거쳐야 하는 경우) 사용하기 좋다.

 

router savedPosition 사용

vue router는 scroll을 컨트롤 하기 위한 scrollBehavior 라는 함수를 제공하고 있다. 이는 HTML 히스토리 모드에서만 작동하는데, 라우터 객체를 만들때

const router = new VueRouter({
  routes: [...],
  scrollBehavior (to, from, savedPosition) {
    // 원하는 위치로 돌아가기
  }
})

이 안에 함수를 넣어서 사용하면 된다.

 

함수 안에서 리턴 값에 따라 스크롤의 위치가 변경되는데, 세번째 전달인자 (여기서는 savedPosition)은 브라우저가 앞/뒤 버튼으로 트리거 될때만 사용 가능하다

scrollBehavior (to, from, savedPosition) {
  if (savedPosition) {
    return savedPosition
  } else {
    return { x: 0, y: 0 }
  }
}

이런식으로 사용하면 되는데, 무조건 맨 위로 올리고 싶을 때는 

scrollBehavior (to, from, savedPosition) {
  return { x: 0, y: 0 }
}

이렇게 항상 0을 리턴하면 된다.

 

또 부드럽게 스크롤을 하도록 동작을 컨트롤 할 수도 있는데,

scrollBehavior (to, from, savedPosition) {
  if (to.hash) {
    return {
      selector: to.hash,
      behavior: 'smooth',
    }
  }
}

 이렇게 behavior에 스크롤 옵션을 입력 하면 된다. (selector는 위에서 설명했던 스크롤 위치이다)

 

 

이 이슈를 어떻게 해결하게 되었는지 전체적인 플로우는 다음장에서 설명하겠다.

+ Recent posts