환경 변수 란 ?

어떠한 프로세스가 실행될 때 영향을 미치는 동적인 값

OS 단에 선언되어있는 배포 환경에 따라 값을 동적으로 변경하기 위한 변수

내가 하고 싶었던 건?

환경변수인 NODE_ENV 값을 local, development, production으로 각각 나누어 의도한 바에 따라 .env파일을 분기하여 사용하고 싶었다

시행착오

  1. process.env 환경변수를 vue.파일의 script 태그 안에서 콘솔로 로그를 찍으니 계속 undefined를 얻었다.. → client side에서는 process.env를 사용 할 수 없다는 사실을 모르고 있었다. 알고보니 process.env는 server side에서만 사용이 가능했고, clientside에서도 활용이 가능하게 하기 위해 nuxt에서는 nuxt.config.ts 파일 안에 config 설정을 따로 두고 있었다.
  2. cmd 에서 환경변수를 set하는 방법을 몰라서 한참을 헤맸다. package.json 의 scripts 부분 안에 있는 명령어들을 지정할 때이런 방식으로 환경변수를 셋팅 하니 잘 동작했다.
  3. SET 환경변수명=값 & nuxt build
  4. SET NODE_ENV를 변경해도 저절로 .env 파일을 찾아가지 못했다. 예전 Vue CLI를 이용해서 프로젝트를 생성했을때는 뒤에 --mode local 이런 방식으로 지정하면 .env.local 파일의 변수값을 바라보게 자동으로 셋팅이 되었는데, Nuxt3는 그렇게 되지 않았고const phase = process.env.PHASE!!주의할 점!!따라서 nuxt.config.ts 안에서 process.env값을 runtimeConfig로 정의하여 컴포넌트 안에서 사용해야한다.
  5. nuxt에서는 process.env 값을 component안에서 console로 찍어 확인하면 nuxt.config.ts 안에서의 process.env 값과 다르다.
  6. require(’dotenv’).config({path:./.env${phase}})
  7. 해결방법으로 스크립트에 PHASE라는 환경변수값을 셋팅하고, 그값을 찾아와 nuxt.config.ts 파일 에서 dotenv의 파일 path값을 동적으로 바꿔주었다. ex) SET PHASE=local&&nuxi dev

CSR (Client-side only rendering)

과정

  1. 브라우저가 빈 HTML Document를 다운받는다
  2. 브라우저가 JS를 다운받고 실행시킨다
  3. 앱이 렌더링 되고 interactive 해진다

장점

  1. 서버와의 호환을 생각할 필요가 없기 때문에 개발 속도가 빠르다.
  2. SSR은 자바스크립트를 지원하는 플랫폼에서 서버를 가동하는데 비용이 들고, CSR은 정적인 HTML, CSS, JS파일들만 호스팅 하는 서버만 필요하기 때문에 상대적으로 비용이 저렴하다.
  3. 전체적으로 코드가 브라우저에서 실행되기 때문에 인터넷이 가능하지 않은 환경에서도 계속 실행이 가능하다.

단점

  1. 브라우저가 다운로드, 파싱, 자바스크립트를 실행 시킬 때까지 기다려야 하기 때문에, 다운로드하는 네트워크와 사용자의 디바이스에 따라 시간이 오래 걸릴 수 있음
  2. search engine이 첫시도에 페이지가 완전히 렌더링 될 때까지 기다릴 수 없기 때문에 SEO가 어렵다.

Universal Rendering

브라우저가 URL을 요청하면 서버는 완전히 렌더링 된 HTML페이지를 브라우저에게 보낸다. 페이지가 미리 생성되었던 캐싱되었던 Nuxt는 서버 환경에서 자바스크립트를 실행 시키고 HTML을 만들어낸다. CSR과 다르게 사용자는 애플리케이션의 내용을 즉각적으로 볼 수 있다. 이 단계는 정통적인 server-side rendering 방식과 비슷하다

CSR의 장점도 함께 가져가기 위해 HTML document가 다운로드 되었을 때 Client는 자바스크립트 코드를 백그라운드에서 실행 시킨다. 브라우저는 코드를 다시 해석하고 vue.js는 애플리케이션을 interactive하게 만든다.

정적 페이지를 브라우저에서 interactive하게 만드는 것을 Hydration이라고 한다.

Universal rendering은 nuxt 애플리케이션이 빠르게 페이지를 다운로드하는 동시에 CSR의 장점도 함께 보존한다. 게다가 애플리케이션의 content가 HTML document에 이미 존재한 채로 브라우저에 전달 되기 때문에 크롤러가 overhead없이 그것을 스캔할 수 있다.

과정

  1. 완전한 HTML이 브라우저로 전달되고 렌더링 된다
  2. 브라우저가 백그라운드에서 자바스크립트를 다운로드하고 실행 시킨다
  3. Hydration step이 완전히 끝나고 애플리케이션이 완전히 interactive해진다

장점

  1. 사용자가 바로 페이지에 접근이 가능하기 때문에 브라우저가 정적 컨텐츠를 더 빨리 보여줄 수 있다.
  2. Universal rendering은 완전한 HTML을 전달 하기 때문에 웹 크롤러가 페이지의 내용을 바로 찾을 수 있기 때문에 SEO가 가능하다.

단점

  1. 서버와 브라우저 환경이 같은 API를 제공하지 않거나, 서버와 브라우저 간 이질감 없이 실행되기 위해 코드를 짜는것이 까다로울 수 있다.
  2. 화면 렌더링을 위해 서버는 가동 되어야 한다. 이는 전통적인 서버와 같이 비용이 들어간다. 그러나 client side rendering navigation을 통해 비용이 많이 감소된다.

features

Assets

  • public : server root에서 파일 그대로 브라우저로 전달함.
  • assets : build tool에 의해 처리가 됨

Vite나 Webpack의 주기능은 자바스크리트 파일들을 처리하는 것임. 그러나 plugins(Vite)나 loaders(Webpack) 을 통해 다른 형태의 assets들도 처리 함( ex) stylesheets, fonts, SVG 등)

이 작업은 원래의 파일을 주로 성능이나 캐싱을 위한 절차 임( style파일을 최소화, 브라우저 캐시무효화)

Head Management

useHead Composable

ex)

<script setup>
useHead({
  titleTemplate: 'My App - %s',
  // or, instead:
  // titleTemplate: (title) => `My App - ${title}`,
  viewport: 'width=device-width, initial-scale=1, maximum-scale=1',
  charset: 'utf-8',
  meta: [
    { name: 'description', content: 'My amazing site.' }
  ],
  bodyAttrs: {
    class: 'test'
  }
})
</script>

setup function 안에서 useHead메서드를 사용해서 meta속성들(title, titleTemplate, base, script, style, meta, link, htmlAttrs, bodyAttrs)을 커스터마이징 해 사용할 수 있음. charset, viewport속성은 예시와 같이 chartset, viewport key값에 value를 설정해 쉽게 커스텀 할 수 있음

Meta Components

Nuxt는 metadata를 vue component template에서 사용할 수 있게 <Title>, <Base>, <Script>, <Style>, <Meta>, <Link>, <Body>, <Html>and <Head> 컴포넌트를 제공함

<script setup>
const title = ref('Hello World')
</script>
<template>
	<div>
		<Head>
			<Title>{{ title }}</Title>
			<Meta name="description" :content="title" />
			<Style type="text/css" children="body { background-color: green; }" />
		</Head>
		<h1>{{ title }}</h1>
	</div>
</template>

usage with definePageMeta

현재의 route의 metadata를 설정하기 위해 useHead 메서드와 definePageMeta메서드는 함께 사용가능함

<script setup>
definePageMeta({
  title: 'Some Page'
})
</script>

위의 예시는 page에 해당하는 vue파일임

<script setup>
const route = useRoute()

useHead({
  meta: [{ name: 'og:title', content: `App Name - ${route.meta.title}` }]
})
</script>

위의 예시는 layout파일에 해당함.

각각 page에 해당하는 파일에 meta 속성을 설정 해놓고, layout 파일에서 route의 현재 route의 메타데이터를이용해 html 데이터를 설정 할 수 있음.

Data Fetching

Nuxt는 데이터를 핸들링 하기 위해 useFetch, useLazyFetch, useAsyncData, useLazyAsyncData, 를 제공함 ⇒ 이 메서드들은 setup이나, lifecycle hook에서만 사용 가능함.****

useAsyncData

page, component, plugin에서 비동기적으로 데이터를 받을 때 useAsyncData를 사용할 수 있음

ex)

let counter = 0
export default () => {
  counter++
  return JSON.stringify(counter)
}

위의 예시는 server/api/count.ts 파일

<script setup>
const { data } = await useAsyncData('count', () => $fetch('/api/count'))
</script><template>
  Page visits: {{ data }}
</template>

count의 data를 받아 올때는 위와 같이 사용함

useLazyAsyncData

useAsyncData에 lazy:true 옵션을 추가한 것과 동일함.

async function 이 페이지 전환을 막지않음. data가 null일때도 data를 핸들링 할 수 있음

<template>
	<div>
    {{ pending ? 'Loading' : count }}
  </div>
</template>
<script setup>
const { pending, data: count } = useLazyAsyncData('count', () => $fetch('/api/count'))
watch(count, (newCount) => {
  // Because count starts out null, you won't have access
  // to its contents immediately, but you can watch it.
})
</script>

useFetch

page, component, plugin 파일 안에서 useFetch 메서드를 사용 할 수 있음

useFetch는 어느 URL이든 접근하여 데이터를 fetch 할 수 있게 하는 메서드임.

이 메서드는 useAsyncData와 $fetch를 함께 포함하고 있는 개념이기 때문에 사용하기 편리함.

<script setup>
const { data } = await useFetch('/api/count')
</script><template>
  Page visits: {{ data.count }}
</template>

useLazyFetch

useFetch에 lazy:true 옵션을 추가한 것과 동일함.

async function이 페이지 전환을 막지 않음

data가 null일때도 핸들링이 가능함.

<template><!-- you'll need to handle a loading state -->
  <div v-if="pending">
    Loading ...
  </div>
	<div v-else>
		<div v-for="post in posts">
			<!-- do something -->
    </div>
	</div>
</template>
<script setup>
const { pending, data: posts } = useLazyFetch('/api/posts')
watch(posts, (newPosts) => {
  // Because posts starts out null, you won't have access
  // to its contents immediately, but you can watch it.
})
</script>

Refreshing Data

data 요청을 다시 보내 data를 새로 load해야 하는 겨우. refresh API를 사용할 수 있음.

useAsyncData 를 이용해 refresh 메서드를 받아 사용함

<script setup>
const page = ref(1);

const { data:users, loading, refresh, error } = await useFetch(() => `users?page=${page.value}&take=6`, { baseURL: config.API_BASE_URL }
);

function previous(){
  page.value--;
  refresh();
}

function next() {
  page.value++;
  refresh();
}
</script>

refreshNuxtData

useAsyncData, useLazyAsyncData, useFetch, useLazyFetch 의 캐시를 무효화 하고, refetch하기 위한 메서드.

현재 페이지의 모든 data를 새롭게 refresh 할때 사용

<template><div>
    {{ pending ? 'Loading' : count }}
  </div><button @click="refresh">Refresh</button></template><script setup>
const { pending, data: count } = useLazyAsyncData('count', () => $fetch('/api/count'))

const refresh = () => refreshNuxtData('count')
</script>

Isomorphic fetch and $fetch

브라우저에서 fetch를 사용할때, user의 헤더(ex)cookie)는 바로 API로 보내진다. 그러나 server-side-rendering에서 fetch 요청은 서버 안에서 발생하게 되기 때문에 이는 브라우저의 cookie를 전송 받지 못하고, 응답에도 cookie를 보낼 수 없다.

Pass client headers to the API

이 문제는 useRequestHeaders를 사용해서 server-side의 API에서 cookie를 접근할 수 있음

<script setup>
const { data } = useFetch('/api/me', {
  headers: useRequestHeaders(['cookie'])
})
</script>

위는 브라우저에서 cookie를 받아 사용하는 예시

Pass on cookies from server-side API calls on SSR response

만약 cookie를 반대로 응답에 포함하여 전달하고 싶을 때는 아래 예시와 같이 사용

export const fetchWithCookie = async (url: string, cookieName: string) => {
  const response = await $fetch.raw(url)
  if (process.server) {
    const cookies = Object.fromEntries(
      response.headers.get('set-cookie')?.split(',').map((a) => a.split('='))
    )
    if (cookieName in cookies) {
      useCookie(cookieName).value = cookies[cookieName]
    }
  }
  return response._data
}
<script setup lang="ts">
// This composable will automatically pass on a cookie of our choice.
const result = await fetchWithCookie("/api/with-cookie", "test")
onMounted(() => console.log(document.cookie))
</script>

Using async setup

async setup() composable에는 처음 await를 사용하고 나서 현재 component의 instance가 손실됨.

따라서 useFetch같은 메서드를 여러번 호출 하기 위해서는 <script setup>을 사용하거나 함께 묶어서 await를 걸어야 함

State Management

useState

SSR-frendly한 반응형을 만들기 위해 Nuxt는 useState라는 composable을 제공함.

이 메서드를 이용해 만든 value는 server-side 렌더링 이후에도 보존됨.

모든 컴포넌트들에서 공유됨.

Basic usage

<script setup>
const counter = useState('counter', () => Math.round(Math.random() * 1000))
</script>
<template>
	<div>
    Counter: {{ counter }}
    <button @click="counter++">
      +
    </button><button @click="counter--">
      -
    </button>
	</div>
</template>

위 예시는 component-local counter state를 사용하고 있음. 하지만 다른 컴포넌트들 역시 useState(’counter’)로 같은 반응형 state를 공유한다.

Error Handling

Nuxt3는 full-stack framework로 예측불허한 다양한 런타임 에러들이 다양한 context에서 발생할 수 있음.

  • Errors during the Vue rendering lifecycle(SSR + SPA)
  • Errors during API or Nitro server lifecycle
  • Server and client startup errors(SSR + SPA)

Errors during the Vue rendering lifecycle (SSR + SPA)

Vue 에러를 onErrorCaptured 에서 핸들링 할수 있음

또한 Nuxt는 에러가 발생하면 상위 레벨로 에러를 전파 시켜 vue:error 훅이 호출 됨

vueApp.config.errorHandler에서 핸들링 가능한 모든 Vue에러를 받을 수 있음

Server and client startup errors (SSR + SPA)

Nuxt는 Nuxt 애플리케이션이 시작할 때 에러가 발생하면 app:error 훅을 호출한다.

이는

  • plugin을 실행할 때
  • app:created, app:beforeMount 훅을 처리할 때
  • app이 mount될때의 에러는 onErrorCaptured 나 vue:error 훅에서 처리해야함
  • app:mounted 훅을 처리할 때

호출된다.

Errors during API or Nitro server lifecycle

server-side에서 handler를 지정 할 수는 없지만, 에러 페이지를 띄울 수 있음.

Rendering an error page

Nuxt가 렌더링 시 client side나 server side lifecycle에서 에러를 만나면 JSON 응답 또는 HTML 에러 페이지를 렌더링 함(요청 헤더에 Accept:application/json 이 있다면)

이 에러 페이지는 app.vue와 같은 레벨의 source directory의 ~/error.vue로 만들어서 커스터마이징 할 수 있다. 만약 error 페이지를 지우고 싶다면 clearError helper 함수로 다른 페이지로 에러 발생시 리다이렉트 시킬 수 있다.

<template>
	<button @click="handleError">Clear errors</button>
</template>
<script setup>
const props = defineProps({
  error: Object
})

const handleError = () => clearError({ redirect: '/' })
</script>

Error helper methods

  • useError

→ 전역으로 발생하는 Nuxt 에러를 핸들링 할 수 있음

  • throwError

→ 이 함수는 client side, server side에서 middleware, plugin, setup함수에서 언제든지 사용 가능함. 이 함수는 트리거가 될때마다 에러페이지를 반환함. 이 에러페이지는 clearError 함수로 clear시킬 수 있음

  • clearError

→ 이 함수는 핸들링 된 Nuxt error를 clear하며 다른 페이지로 redirect 할 수 있음

Rendering errors within your app

<NuxtErrorBoundary> 컴포넌트는 다른 에러 페이지를 생성하지 않고도 에러페이지를 렌더링 하게 해줌.

이 컴포넌트는 반응형으로 에러를 핸들링 할 수 있음.

<template><!-- some content -->
  <NuxtErrorBoundary @error="someErrorLogger">
<!-- You use the default slot to render your content -->
    <template #error="{ error }">
      You can display the error locally here.
      <button @click="error = null">
        This will clear the error.
      </button>
		</template>
	</NuxtErrorBoundary>
</template>

Server Routes

Nuxt는 자동으로 /server/api, /server/routes, /server/middleware 디렉토리에 들어있는 파일들을 스캔하고 API와 서버 핸들러에 등록함

이 핸들러는 JSON 데이터를 리턴하며, Promise, event.res.end() 를 사용하여 응답을 보낸다.

ex) server/api/hello.ts 파일을

export default defineEventHandler((event) => {
  return {
    api: 'works'
  }
})

전역적으로 await $fetch(’/api/hello’)로 API를 호출 할 수 있음

Server Routes

~/server/api 안의 파일들은 prefix로 /api라는 path를 자동으로 갖게 되며, /api라는 prefix가 붙지 않고 사용하려면 ~/server/routes 디렉토리에 추가 해야함.

예를들어 ~/server/routes/hello.ts 라는 파일을 아래와 같이 작성하면

export default defineEventHandler(() => 'Hello World!')

http://localhost:3000/hello 로 접근 할 수 있음

Server Middleware

Nuxt는 ~/server/middleware 디렉토리 안에 있는 모든 파일들을 자동으로 읽고 server middleware를 생성함.

middleware handler는 모든 요청에 server의 route를 거치기 전 헤더를 체크하거나 로직을 추가 하는데 사용 함.

ex) /server/middleware/log.ts

export default defineEventHandler((event) => {
  console.log('New request: ' + event.req.url)
})

ex) /server/middleware/auth.ts

export default defineEventHandler((event) => {
  event.context.auth = { user: 123 }
})

Matching Route Parameters

server routes는 동적으로 파라미터를 가져올 수 있다.

예를들어 /api/hello/[name].ts 파일의 이름을 event.context.params.name으로 찾아올 수 있음

ex) /server/api/hello/[name].ts

export default defineEventHandler(event => Hello, ${event.context.params.name}!)

await $fetch(’/api/hello/nuxt’)를 호출하면 Hello, nuxt!를 얻을 수 있음

Matching HTTP Method

파일 이름 뒤에 HTTP Method(.get, .post, .put, .delete ...)를 붙여서 핸들링 할 수 있음.

ex) /server/api/test.get.ts

export default defineEventHandler(() => 'Test get handler')

ex) /server/api/test.post.ts

export default defineEventHandler(() => 'Test post handler')

위의 예시들은 각각 get 핸들러 , post 핸들러를 리턴함

Catch-all route

Catch-all route는 루트 path 이후에 catch-all route가 정해져 있지 않으면 해당 path를 포함하는 모든 요청에 대한 핸들러를 작성 할 수 있음

ex) /server/api/foo/[...].ts

export default defineEventHandler(() => Default foo handler)

ex) /server/api/[...].ts

export default defineEventHandler(() => Default api handler)

Handling Requests with Body

ex) /server/api/submit.post.ts

export default defineEventHandler(async (event) => {
    const body = await useBody(event)
    return { body }
})

예시에서 $fetch(’/api/submit’, {method: ‘post’, body :{test:123}})로 호출 할 수 있음

.post.ts 파일은 useBody를 사용할 수 있음, 만약 .get.ts 파일을 핸들링 하려고 useBody를 쓰면 405 에러가 남

Handling Requests with query parameters

ex) /server/api/query.get.ts

export default defineEventHandler((event) => {
  const query = useQuery(event)
  return { a: query.param1, b: query.param2 }
})

Get access to the runtimeConfig

ex) /server/api/foo.ts

export default defineEventHandler((event) => {
  const config = useRuntimeConfig()
  return { key: config.KEY }
})

Access Request Cookies

ex)

export default defineEventHandler((event) => {
  const cookies = useCookies(event)
  return { cookies }
})

Runtime Config

Nuxt는 런타임에 config를 컨트롤 할 수 있는 config API를 제공함.

Exposing runtime config

nuxt.config파일의 runtimeConfig 옵션에 런타임 설정을 할 수 있음

ex) nuxt.config.ts

export default defineNuxtConfig({
  runtimeConfig: {
    // The private keys which are only available within server-side
    apiSecret: '123',
    // Keys within public, will be also exposed to the client-side
    public: {
      apiBase: '/api'
    }
  }
})

Environment Variables

Nuxt CLI 는 built-in 으로 dotenv를 지원함

만약 .env 파일이 프로젝트 root 디렉토리에 존재하면 process.env에 자동으로 load 되고 module과 nuxt.config에서 접근 가능함

ex) .env

NUXT_API_SECRET=api_secret_token
NUXT_PUBLIC_API_BASE=https://nuxtjs.org

ex) nuxt.config.ts

export default defineNuxtConfig({
  runtimeConfig: {
    apiSecret: '',
    public: {
      apiBase: '', // Or a default value
    }
  },
})

Accessing runtime config

  • Vue app

런타임 config에 접근 하기 위해 useRuntimeConfig() 를 호출함

ex)

<template><div><div>Check developer console!</div></div></template><script setup>
const config = useRuntimeConfig()
console.log('Runtime config:', config)
if (process.server) {
  console.log('API secret:', config.apiSecret)
}
</script>

client side에서는 config.pubic만 접근 가능하며 읽기/쓰기가 가능함

server side에서는 모든 key에 접근 가능하지만, 쓰기가 불가능함

useRentimeConfig()는 setup이나 Lifecycle hook에서만 사용가능함

  • Plugins

plugin에서 useRuntimeConfig를 사용하려면 defineNuxtPlugin 안에서 사용함

ex)

export default defineNuxtPlugin((nuxtApp) => {
  const config = useRuntimeConfig()
  console.log('API base URL:', config.public.apiBase)
});
  • Server Routes

ex)

export default async () => {
  const result = await $fetch('<https://my.api.com/test>', {
    headers: {
      Authorization: `Bearer ${useRuntimeConfig().apiSecret}`}
  })
  return result
}

Teleports

Vue3는 <Teleport> 컴포넌트를 제공함. 이는 Vue application 이 아닌 DOM의 어느곳에서든지 렌더링이 됨

Nuxt는 SSR에서 teleport는 body만 가능함, client-side에서 사용은 <ClientOnly> warpper로 감싸야 함

  • body teleport

ex)

<template>
	<button @click="open = true">
    Open Modal
  </button>
	<Teleport to="body">
		<div v-if="open" class="modal">
			<p>Hello from the modal!</p>
			<button @click="open = false">
        Close
      </button>
		</div>
	</Teleport>
</template>
  • client-side teleport

ex)

	<ClientOnly>
		<Teleport to="#some-selector">
				<!-- content -->
	  </Teleport>
	</ClientOnly>
</template>

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 파일에선 위와 같이 사용함

자동 라우팅

pages 디렉토리 안에 있는 vue 파일들은 자동으로 라우팅 됩니다 ⇒ router.js 파일이 필요 없습니다.

동시에 code splitting 도 함께 제공합니다.

네비게이션

페이지간 이동에는 NuxtLink를 사용해야 합니다.

page 디렉토리는 view와 route를 담고 있습니다. Nuxt는 page디렉토리안의 모든 .vue파일을 읽고 그 파일에 해당하는 라우터를 만듭니다.

 

 

Nuxt SSR

Nuxt의 Server Side Rendering steps

1. Browser to Server

브라우저가 최초 요청을 보내면 내부 Node.js 서버에 도달합니다. Nuxt는 lifecycle hooks, asyncData, nuxtServerInit, fetch 등 실행된 함수의 결과들과 함께 HTML을 생성하고 이를 다시 브라우저로 돌려보냅니다.

2. Server to Browser

브라우저가 서버로부터 렌더링된 HTML 페이지를 받으면 vue.js hydration 을 적용하여 page를 상호작용할 수 있는 형태로 변경합니다.

3. Browser to Browser

브라우저 안에서 NuxtLink 태그를 이용하여 client side에서 네비게이션 합니다.

Nuxt SSG

Nuxt의 Static Site Generation steps

1. Browser to CDN

브라우저가 CDN으로 최초의 요청을 보냅니다.

2. CDN to Browser

CDN은 이미 만들어진 HTML 과, 자바스크립트, 정적 assets 파일을 브라우저로 보냅니다.

내용이 보여지고 Vue.js hydration이 일어나 상호작용이 가능한 형태로 변경됩니다.

3. Browser to Browser

클라이언트 사이드에서 네비게이팅이 일어나 더이상 CDN으로의 요청은 가지 않습니다. API Call은 이미 캐싱된 정적 폴더에서 로드됩니다.

현재 상황

현재 vue.js를 이용한 SPA을 만들어 배포하고 있는데, Code Splitting을 적용한 이후로 새로 앱을 배포할 때

ChunkLoadError라는 에러를 만나게 되었다..

배포를 해야 에러를 테스트 할 수 있기 때문에 테스트도 굉장히 힘들었는데

이 에러를 해결한 과정을 정리하여 써보려고 한다.

 

 

Code Splitting이란?

webpack을 이용해 번들링을 하면, 초기 로딩시 번들링을 통해 합쳐진 js파일을 로딩하기 때문에 앱이 점점 커지면서 초기로딩이 길어지는 문제가 생긴다.

이를 해결하기 위한 것이 바로 Code Splitting, Lazy Loading이다.

 

문제점?

사용자가 앱을 사용하는 도중에 웹앱 재배포가 일어날 때, index.html은 새로 배포된 파일이 아닌 이전 파일의 정보를 담고 있기 때문에 배포 이전 파일들을 요청하게 되어 이 파일들을 찾지 못하는 상황이 일어난다. (파일은 새롭게 배포되어있는 상태이기 때문에 이전 파일들은 찾아도 찾을 수 없게 된다)

 

디버깅 테스트 과정

우리 프로젝트는 vue2+ vue router + vue CLI + webpack 4 로 구성되어 있는 상태였다.

테스트 과정에서 새롭게 안 사실은 개발모드로 빌드를 한 파일들은 파일 끝에 해시값이 붙지 않아서 위와 같은 오류가 애초에 발생하지 않는 다는 사실이다. 그래서 개발환경에서 앱을 재배포하는 과정에서는 발견되지 않은 에러가 운영 환경에서는 발견 되었던 것이다.

 

이와 같은 사실은, 개발환경에서 아이폰에서 웹앱 개발시 앱을 껐다가 켜도 계속해서 변경사항이 반영되지 않는 현상도 설명이 가능했다. 개발버전으로 빌드 시 청크파일에 해시값이 애초에 붙지 않기 때문에 이전에 배포된 파일의 이름과 새롭게 빌드되어 배포된 파일의 이름이 같아 새롭게 index.html을 배포해도 브라우저에 캐시된 새로 배포되기 이전 파일을 가져오기 때문이었다. 

 

production 모드로 빌드를 진행하면 각 chunk파일 이름 뒤에 랜덤의 해시 값이 붙게 되는데, 해시값 설정은 webpack의 config 파일에 여러가지 변경이 가능하다.

 

우리는 vue CLI를 이용하고 있었기 때문에 vue.config.js 파일의 웹팩 설정 밑에

 output: {
            filename: `[name].[chunkhash].js`,
            chunkFilename: `[name].[chunkhash].js`,
        },
 
을 추가하여 해시값을 개발모드에서도 붙여주었다.
 
참고로 해시값은 변경된 파일에 한해서만 변경이 되고, 만약 그 파일에 변경사항이 없다면 해시값을 유지함으로써 캐싱된 화면에 대해서 새롭게 로딩을 할 필요성을 줄여준다.
 
테스트는 
 
1.앱을 실행한다.

2. 특정 화면에 해당하는 파일에 변경사항을 추가한다.

3. 앱을 재 배포한다.

4. 변경사항을 추가했던 화면에 접근한다.
 
순으로 진행 되었고, 드디어 개발 모드에서 ChunkError를 만나게 될 수 있었다
 

해결해야 하는 문제점

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는 위에서 설명했던 스크롤 위치이다)

 

 

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

현재 진행하고 있는 프로젝트는 첫화면에 많은 이미지 리소스를 요청한다. (쇼핑몰)

이때 최초에 lazy loading이 적용되어 있지 않아, 사용자가 보고 있지 않은 화면에 대한 리소스도 모두 요청해 가져오고 있었다. 이에 custom directive를 만들어 img태그에 이 directive를 추가하면 lazy loading이 적용될 수 있도록 만들어 누구나 적용할 수 있게 만들어 두었다. 현재 Vue2 를 이용하여 개발중이다.

 

Lazy Loading 이란?

페이지에서 실제로 필요할 때까지 리소스의 로딩을 미루는 것으로 웹 최적화에 많이 쓰인다. 실제로 사용자가 페이지를 보고 있는 타이밍에 이미지를 로딩한다. 또한 이미지 말고 다른 파일 (ex. js파일) 에도 적용 될 수 있다. 현재 이 글에서는 우선적으로 이미지만 다루려고 한다.

 

Custom Directive란?

vue에서는 v-show v-if 같은 기본 디렉티브가 주어져 손쉽게 Dom을 조작할 수 있다. 뿐만 아니라 기본 디렉티브 이외에 사용자가 커스텀하여 디렉티브를 제작할 수도 있는데, element를 매개변수로 받아 dom을 조작하는 커스텀 디렉티브를 만들어 사용 할 수 있다.

 

 

적용법

일단 img태그에 data-src라는 속성을 만들어서 이미지 리소스를 할당합니다. (속성 이름은 마음대로 하셔도 좋습니다)

이후 자바스크립트에서 제공하는 Intersection Observer API르르 이용하여 타겟 element와 document의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하여 타겟 element가 사용자의 viewport에 들어왔을 때, data-src에 할당 했던 이미지 리소스를 src속성에 재 할당하여 이미지를 로드하고, data-src 속성을 제거합니다.

 

Intersection Observer는 최신버전의 크롬과 사파리에서는 작동하지만 IE에서는 작동하지 않고 있음을 주의해야합니다. 저희 서비스는 모바일 웹앱 서비스로 크롬과 사파리로만 서비스 하고 있기 때문에 Intersection Observer를 사용하여 개발하는 것이 적절하다고 판단하여 아래와 같은 코드를 적용시켰습니다. 

 

 

directives: {

        lazyload: {

            inserted(el) {

                function imageLoad(targetElement) {

                    const imgElement = targetElement;

 

                    imgElement.setAttribute("src", imgElement.getAttribute("data-src"));

                    imgElement.onload = function () {

                        imgElement.removeAttribute("data-src");

                    };

                }

 

                function callIntersectionApi() {

                    const options = { root: null, threshold: 0.5, rootMargin: "0px" };

 

                    const lazyLoadCallback = (entries, observer) => {

                        entries.forEach((entry) => {

                            if (entry.isIntersecting) {

                                imageLoad(entry.target);

                                observer.unobserve(entry.target);

                            }

                        });

                    };

                    const lazyLoadingIO = new IntersectionObserver(lazyLoadCallback, options);

                    lazyLoadingIO.observe(el);

                }

                window.IntersectionObserver ? callIntersectionApi() : imageLoad(el);

            },

        },

    },

 

 

Intersection Observer를 생성 할때 options에 들어가는 넣은 요소들은 다음과 같습니다.

1. root : 대상 객체의 가시성을 확인할 때 사용되는 뷰포트 요소입니다. 이는 대상 객체의 조상 요소여야 합니다. 기본값은 브라우저 뷰포트이며, root 값이 null 이거나 지정되지 않을 때 기본값으로 설정됩니다.

 

2. threshold : observer의 콜백이 실행될 대상 요소의 가시성 퍼센티지를 나타내는 단일 숫자 혹은 숫자 배열입니다. 만일 50%만큼 요소가 보여졌을 때를 탐지하고 싶다면, 값을 0.5로 설정하면 됩니다. 기본값은 0이며(이는 요소가 1픽셀이라도 보이자 마자 콜백이 실행됨을 의미합니다). 1.0은 요소의 모든 픽셀이 화면에 노출되기 전에는 콜백을 실행시키지 않음을 의미합니다.

 

3. rootMargin : root 가 가진 여백입니다. 이 속성의 값은 CSS의 margin 속성과 유사합니다. e.g. "10px 20px 30px 40px" (top, right, bottom, left). 이 값은 퍼센티지가 될 수 있습니다. 이것은 root 요소의 각 측면의 bounding box를 수축시키거나 증가시키며, 교차성을 계산하기 전에 적용됩니다. 기본값은 0입니다.

 

 

https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API

 

Intersection Observer API - Web API | MDN

Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다.Intersection Observer API는 타겟 요소와 상위 요소 또는

developer.mozilla.org

여기서 더 많은 예제를 보실수 있습니다 :)

 

SPA앱 프로젝트를 진행하면서 앱의 기능이 많아져 bundle의 크기가 점점 증가해 초기로딩이 오래걸리는 문제가 생겼고, 이를 해결 하기위해 code splitting을 적용했습니다.

 

현재 제가 하고있는 프로젝트는 vue2 + CLI 3 입니다.

 

개봘 환경 조건은 

1. 싱글 파일 컴포넌트 체계(.vue)

2. 웹팩 - 모듈 번들러 (2.x이상)

3.바벨 Syntax-dynamic-import입니다

 

CLI로 생성한 경우 1,2번은 만족하고 있지만 3번은 별도 설치가 필요합니다.

 

1. npm으로 설치해 줍니다.

npm install --save-dev babel-plugin-syntax-dynamic-import

2. .babelrc, 혹은 babel.config 파일에 플러그인을 추가합니다.

{
  "plugins": ["syntax-dynamic-import"]
}
3. CLI3를 사용하고 계신다면 webpack의 prefetch preload 플러그인을 삭제해야합니다. 그렇지 않으면 분리된 chunk파일들을 미리 받아오기 때문입니다.
저는 vue.config.js를 사용중이며
  chainWebpack: (config) => {
        config.plugins.delete("prefetch");
        config.plugins.delete("preload");
    },

를 추가하여 prefetch 와 preload 기능을 껐습니다.

 

4. 라우터 파일에 들어가 코드를 수정합니다.

 

기본으로 import 했던 아래의 방식과는 다르게

import MainMenu from "@/views/main/Main.vue";
동적으로 import 하는 아래의 방식으로 변경합니다!
{
	path: 'url 이름',
	component: () => import('컴포넌트 이름')
}
 
여기서 chunk파일들을 load하는것을 조절하고 싶다면 아래처럼 webpackChunkName에 공통적으로 같은 이름을 부여하면 이 이름을 가진 chunk가 최초로 로드되는 순간 같은 이름을 설정한 chunk들이 함께 로드되게 됩니다.
또한 chunk파일 이름이 여기서 설정한 이름으로 만들어집니다. 

const MainMenu = () => import(/* webpackChunkName: "MainMenu" */ "@/views/main/Main.vue");

 

+ Recent posts