환경 변수 란 ?

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

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>

현재 상황

현재 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를 만나게 될 수 있었다
 

문제 설명

0 또는 양의 정수가 주어졌을 때, 정수를 이어 붙여 만들 수 있는 가장 큰 수를 알아내 주세요.

예를 들어, 주어진 정수가 [6, 10, 2]라면 [6102, 6210, 1062, 1026, 2610, 2106]를 만들 수 있고, 이중 가장 큰 수는 6210입니다.

0 또는 양의 정수가 담긴 배열 numbers가 매개변수로 주어질 때, 순서를 재배치하여 만들 수 있는 가장 큰 수를 문자열로 바꾸어 return 하도록 solution 함수를 작성해주세요.

제한 사항
  • numbers의 길이는 1 이상 100,000 이하입니다.
  • numbers의 원소는 0 이상 1,000 이하입니다.
  • 정답이 너무 클 수 있으니 문자열로 바꾸어 return 합니다.
입출력 예numbersreturn
[6, 10, 2] "6210"
[3, 30, 34, 5, 9] "9534330"

 

풀이

이 문제를 풀이는 2개의 방법이 있다

 

1. 비교하는 문자를 앞뒤로 바꿔서 더 큰수를 만들 수 있는 순서대로 분류 시키는 방법 

ex) 6, 2 ,10에서 62 26를 비교, 210 102 비교, 더 큰 수를 만들 수 있는 순서대로 sorting

 

2. 각각의 요소를 4자리 수로 만들고, 이를 내림차순으로 sorting 

ex) 6,2,10에서 6666, 2222, 1010 을 만들고 이 수의 크기를 비교해 각각의 요소를 내림차순으로 sorting

 

여기서 주의할 점은 모든 요소가 0일때 0을 요소갯수만큼 붙인 문자열이 아닌 "0"으로 리턴해야 한다는 점이다.

 

나는 1번 풀이법으로 풀어보았다.

function solution(numbers) {

    
    let newNumbers = numbers.map((v) => {
        return v+""
    }).sort().reverse().sort((a,b) => {
        let tempA = a + b
        let tempB = b + a
        return parseInt(tempB) - parseInt(tempA)
    })
  
    
    
    return newNumbers.join("")[0] == "0" ? "0" : newNumbers.join("");
    
}

 

Set 이란?

es6 에서 추가된 자료형으로

1. 배열의 중복을 제거 하고 싶을 때

2. 자료전체를 순회할 필요성이 있을 경우

3. 값의 유무를 판단 할 때

 

유용하게 쓸 수 있습니다.

 

1 번의 경우 Set의 중복된 값을 허용하지 않는다는 속성을 이용한 것이고

2,3 번의 경우 Set은 순회를 더 빨리 돈다는 점에서 유용하게 쓰입니다.

 

특히 저는 코딩테스트에서 중복제거를 위해 자주 사용하곤 합니다.

 

Set의 사용

set은 아래와 같이 생성 할 수 있으며, 생성자 안에는 iterable 한 객체들이 들어갈 수 있습니다. ex) Array

var mySet = new Set();
mySet2 = new Set([1, 2, 3, 4]);

set은 array의 push 처럼 add를 이용하여 값을 추가 할 수 있고, 중복을 허용하지 않습니다.

mySet.add(1); // Set { 1 }
mySet.add(5); // Set { 1, 5 }
mySet.add(5); // Set { 1, 5 }

set은 has 메서드를 이용하여 해당 값이 set에 포함되어 있는지의 여부를 확인할 수 있습니다. 

mySet.has(1); // true
mySet.has(3); // false, 3은 set에 추가되지 않았음
mySet.has(5);              // true

set의 size 메서드는 set의 크기를 반환하며, delete메서드를 이용하여 해당 요소를 제거할 수 있습니다.

mySet.size; // 5

mySet.delete(5); // set에서 5를 제거함

array를 set의 생성자 안에 넣어 set으로 변경할 수도 있지만, set을 array로 변경할 수도 있습니다.

// set을 Array로 변환하기 위해 전개 연산자 사용함.
console.log([...mySet]); // myArray와 정확히 같은 배열을 보여줌

이는 set은 iterable하기 때문에 spread연산자를 사용할 수 있어 가능합니다.

 

 

이 외로 WeakSet이라는 자료형도 존재합니다. 이는 set과 비슷하나 iterable하지 않고, 참조카운트를 증가시키기 않는 자료형입니다. iterable하지 않기때문에 안에 속성들을 순회하거나 탐색할 수 없습니다. 

 

WeakSet은 활용도가 미미하기 때문에 따로 다루지는 않습니다.

WeakSet의 활용도가 미미하기 때문인지 혹자들은 WeakMap과 통일성을 위해 만들어졌다고도 합니다..

 

문제

문제 설명

rows x columns 크기인 행렬이 있습니다. 행렬에는 1부터 rows x columns까지의 숫자가 한 줄씩 순서대로 적혀있습니다. 이 행렬에서 직사각형 모양의 범위를 여러 번 선택해, 테두리 부분에 있는 숫자들을 시계방향으로 회전시키려 합니다. 각 회전은 (x1, y1, x2, y2)인 정수 4개로 표현하며, 그 의미는 다음과 같습니다.

  • x1 행 y1 열부터 x2 행 y2 열까지의 영역에 해당하는 직사각형에서 테두리에 있는 숫자들을 한 칸씩 시계방향으로 회전합니다.

다음은 6 x 6 크기 행렬의 예시입니다.

이 행렬에 (2, 2, 5, 4) 회전을 적용하면, 아래 그림과 같이 2행 2열부터 5행 4열까지 영역의 테두리가 시계방향으로 회전합니다. 이때, 중앙의 15와 21이 있는 영역은 회전하지 않는 것을 주의하세요.

행렬의 세로 길이(행 개수) rows, 가로 길이(열 개수) columns, 그리고 회전들의 목록 queries가 주어질 때, 각 회전들을 배열에 적용한 뒤, 그 회전에 의해 위치가 바뀐 숫자들 중 가장 작은 숫자들을 순서대로 배열에 담아 return 하도록 solution 함수를 완성해주세요.


제한사항
  • rows는 2 이상 100 이하인 자연수입니다.
  • columns는 2 이상 100 이하인 자연수입니다.
  • 처음에 행렬에는 가로 방향으로 숫자가 1부터 하나씩 증가하면서 적혀있습니다.
    • 즉, 아무 회전도 하지 않았을 때, i 행 j 열에 있는 숫자는 ((i-1) x columns + j)입니다.
  • queries의 행의 개수(회전의 개수)는 1 이상 10,000 이하입니다.
  • queries의 각 행은 4개의 정수 [x1, y1, x2, y2]입니다.
    • x1 행 y1 열부터 x2 행 y2 열까지 영역의 테두리를 시계방향으로 회전한다는 뜻입니다.
    • 1 ≤ x1 < x2 ≤ rows, 1 ≤ y1 < y2 ≤ columns입니다.
    • 모든 회전은 순서대로 이루어집니다.
    • 예를 들어, 두 번째 회전에 대한 답은 첫 번째 회전을 실행한 다음, 그 상태에서 두 번째 회전을 실행했을 때 이동한 숫자 중 최솟값을 구하면 됩니다.

입출력 예시rowscolumnsqueriesresult
6 6 [[2,2,5,4],[3,3,6,6],[5,1,6,3]] [8, 10, 25]
3 3 [[1,1,2,2],[1,2,2,3],[2,1,3,2],[2,2,3,3]] [1, 1, 5, 3]
100 97 [[1,1,100,97]] [1]

입출력 예 설명

입출력 예 #1

  • 회전을 수행하는 과정을 그림으로 표현하면 다음과 같습니다.

입출력 예 #2

  • 회전을 수행하는 과정을 그림으로 표현하면 다음과 같습니다.

입출력 예 #3

  • 이 예시에서는 행렬의 테두리에 위치한 모든 칸들이 움직입니다. 따라서, 행렬의 테두리에 있는 수 중 가장 작은 숫자인 1이 바로 답이 됩니다.

 

 

풀이

function solution(rows, columns, queries) {
    var answer = [];
    let graph = [];
    let temp = [];
    for(let i = 0; i < rows * columns; i++){
        temp.push(i+1)
        if(temp.length == columns){
            graph.push(temp)
            temp = []; //초기화
        }
    }
    
   
    queries.forEach((item) =>{
        let [x1,y1,x2,y2] = item.map((value) => { return value - 1})
        
        let newGraph = [] //빈배열
        
        //1. 일단 1차원으로 해당 숫자들을 펴보자
        for(let i = y1; i < y2; i++){  
            newGraph.push(graph[x1][i])
        }
        for(let i = x1; i < x2; i++){
            newGraph.push(graph[i][y2])
        }
        
        for(let i = y2;  i > y1; i--){
            newGraph.push(graph[x2][i])
        }
        for(let i = x2; i > x1; i--){
            newGraph.push(graph[i][y1])
        }
        
        //2. 한칸씩 이동시키려면 맨뒤에 숫자를 맨 앞으로 보내면 된다.
        newGraph.unshift(newGraph.pop())
        
        //3. 그중에서 가장 작은 값을 배열에 넣어보자
        let min = Math.min(...newGraph)
        answer.push(min)
        //3. 그 배열들을 다시 graph에 넣어주자
        for(let i = y1; i < y2; i++){
            graph[x1][i] = newGraph.shift()
        }
        
        for(let i = x1; i < x2; i++){
            graph[i][y2] = newGraph.shift()
        }
        
        for(let i = y2; i > y1; i--){
            graph[x2][i] = newGraph.shift()
        }
        
        for(let i = x2; i > x1; i--){
            graph[i][y1] = newGraph.shift()
        }
        
        
    }) 
   
    
    return answer
}

 

 

2차원 배열을 직접 하나씩 옮기는 방식으로 풀려니 도저히 안풀렸는데,

1차원 배열로 맨 뒤 값을 맨 앞으로 옮겨주는 것 만으로 시계방향으로 이동이 가능하다는걸 알고나서

그 점이 완전 유레카였다는 것을.. 깨달음

 

잊지말자 **

 

그리고 array.map, array.filter 등 메서드 사용법 헷갈리지않기.

 

setTimeout 이란?

일정 시간이 지나고 지정한 코드를 실행시키는 메서드로 보통 아래와 같은 형태로 자주 사용한다.

setTimeout(() => {console.log("첫 번째 메시지")}, 5000);
setTimeout(() => {console.log("두 번째 메시지")}, 3000);
setTimeout(() => {console.log("세 번째 메시지")}, 1000);

위에서 , 뒤에 있는 숫자가 지연 시간을 나타내는데, 단위는 밀리세컨드이다. (1000 = 1초)

 

정확히 지정한 시간 이후 코드가 실행될까?

그런데 실제로 사용해보면 정확하게 지정한 시간이 지났을때 코드가 실행되지 않고 지연되는 경우가 발생한다. 글쓴이의 경우 스크롤을 이동시킬 때 setTimeout에 딜레이를 0으로 주고 그 안에서 scrollTo메서드를 사용할 때와 setTimeout으로 감싸지 않고 scrollTo를 사용할 때 결과에 차이가 발생하여(setTimeout으로 감싼 경우에만 스크롤이 이동하였다) setTimeout에대해 더 찾아보게 되었고, 그 결과 다음과 같은 문서를 보게 되었다. 실제로 찾아보면 scroll이동 이벤트는 많은 사람들이 setTimeout과 함께 사용하는 것을 추천하고 있다.

 

https://developer.mozilla.org/ko/docs/Web/API/setTimeout#%EB%94%9C%EB%A0%88%EC%9D%B4%EA%B0%80_%EC%A7%80%EC%A0%95%ED%95%9C_%EA%B0%92%EB%B3%B4%EB%8B%A4_%EB%8D%94_%EA%B8%B4_%EC%9D%B4%EC%9C%A0

 

setTimeout() - Web API | MDN

전역 setTimeout() 메서드는 만료된 후 함수나 지정한 코드 조각을 실행하는 타이머를 설정합니다.

developer.mozilla.org

 

위 문서에 나온 내용을 요약해보면

1. 중첩 타임아웃 사용시

브라우저는 setTimeout 호출이 5번 이상 호출된 경우 4ms의 최소 타임아웃을 강제함

2. 비활성탭의 타임아웃

백그라운드 탭으로 인한 부하(와 그로 인한 배터리 사용량)를 경감하기 위해, 브라우저는 비활성 탭에서의 최소 딜레이에 최소 값을 강제함

3. 페이지, 운영체제, 브라우저가 다른 작업으로 인해 바쁠 경우

페이지, 운영체제, 브라우저가 다른 작업으로 인해 바쁠 경우 타임아웃이 예상보다 늦게 실행될 수 있습니다.

 

나의 경우 3번에 해당하는 경우라고 추정하였다. (서버에서 데이터를 불러오고, 이를 이용하여 렌더링을 마치고 나서 스크롤이 생긴 이후 스크롤을 이동시켜야 하기 때문에)

 

이를 더 자세히 이해하기 위해서는 자바스크립트 런타임에 대한 이해가 필요한데, 이는 다음장에서 설명하도록 하겠다.

해결해야 하는 문제점

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에서 사용하는 방식으로 변경하니 이슈가 해결되었다. 그런데 정확한 원인은 아직 제대로 파악하지 못하였다..

 

 

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