환경 변수 란 ?

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

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

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>

+ Recent posts