결과 화면

https://heejae0811.github.io/vue-components/

 

Vue/Nuxt Components

뷰/넉스트 컴포넌트입니다.

heejae0811.github.io

 


https://v2.nuxt.com/deployments/github-pages#github-actions

 

GitHub Pages

How to deploy Nuxt app on GitHub Pages?

v2.nuxt.com

Nuxt 깃허브 배포에 대한 자세한 설명은 위의 공식문서를 참고한다.

 

1. nuxt.config.js

정적 사이트로 배포해야 하기 때문에 target: 'static'을 추가하고,

router에는 깃 레포지토리 이름을 작성한다.

export default {
  target: 'static',
  router: {
    base: '/깃 레포지토리 이름/'
  },
  
  ...
}

 

2. push-dir 설치하기

터미널에서 아래 명령어를 실행한다.

설치된 버전은 package.json 파일에서 확인할 수 있다.

yarn add --dev push-dir

 

3. package.json

scripts 부분에 "deploy" : "~~" 를 추가한다.

{
  ...
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate",
    "deploy": "push-dir --dir=dist --branch=gh-pages --cleanup"
  },
  ...
}

 

4. 배포하기

터미널에서 yarn generate 명령어를 실행하면 dist 폴더가 생기는 것을 볼 수 있다.

yarn deploy 명령어를 실행하면 깃허브에 배포된다.

yarn generate
yarn deploy

 

5. 깃허브 Settings

위의 과정을 수행했음에도 깃허브 페이지가 배포가 되지 않는다면 깃 레포지토리 Settings을 확인해 본다.

깃 레포지토리 - Settings - Pages에서 Branch가 gh-pages로 되어있는지 확인한다.

결과 화면

https://heejae0811.github.io/nuxt3-todo/

 

오늘의 할 일

오늘의 할 일은 무엇인가요?

heejae0811.github.io

 


버전

node v.20.8.1

nuxt v.3.8.1

vue v.3.3.8

 

1. nuxt.config.ts

head 태그 아래에 추가하고 싶은 메타태그를 작성한다. (title, meta, link가 HTML 태그로 변환된다.)

name, content, property 등 작성한 값들이 메타태그 속성으로 들어가기 때문에 다른 메타태그를 추가하고 싶다면 속성에 맞는 값들을 입력한다.

 

메타태그 이미지와 파비콘 이미지 경로는 절대 경로를 사용한다.

깃허브 페이지를 통해 빌드된 파일을 배포하고 있기 때문에 메타태그 이미지는 assets 폴더가 아닌 public 폴더에 넣는다.

export default defineNuxtConfig({
  app: {
    head: {
      title: 'ToDo List',
      meta: [
        { name: 'description', content: '오늘의 할 일은 무엇인가요?' },
        { property: 'og:title', content: '오늘의 할 일' },
        { property: 'og:description', content: '오늘의 할 일은 무엇인가요?' },
        { property: 'og:image', content: 'https://heejae0811.github.io/nuxt3-todo/meta.jpg' }
      ],
      link: [
        { rel: 'icon', type: 'image/x-icon', href: 'https://heejae0811.github.io/nuxt3-todo/favicon.ico' }
      ]
    },
  },
  
  ...
})

 

2. 페이지 별 메타태그 설정하기

nuxt.config.ts에 정의한 메타태그는 모든 페이지에서 공통으로 적용된다.

 

각 페이지 별로 메타태그를 다르게 설정하고 싶으면 아래 링크를 참고한다.

https://jae-study.tistory.com/129

 

[Nuxt3] #16 Nuxt 3 SEO (메타태그 설정하기)

SEO & metas with Nuxt 3 — Course part 16 https://www.youtube.com/watch?v=PpyXtoM5HWQ&list=PL8HkCX2C5h0XT3xWYn71TlsAAo0kizmVc&index=16 유튜브 강의를 참고하고 있습니다. 공통 메타태그 설정하기 nuxt.config.ts 모든 페이지에서

jae-study.tistory.com

결과 화면

https://heejae0811.github.io/nuxt3-todo/

 

오늘의 할 일

오늘의 할 일은 무엇인가요?

heejae0811.github.io

 


버전

node v.20.8.1

nuxt v.3.8.1

vue v.3.3.8

 

1. gh-pages 설치하기

터미널에서 아래 명령어를 실행한다.

설치된 버전은 package.json 파일에서 확인할 수 있다.

npm install gh-pages

또는

yarn add gh-pages

 

2. package.json

package.json 파일의 scripts 부분에 "deploy": "~~"를 추가한다.

-f 옵션은 gh-pages 브랜치 깃 히스토리를 삭제한다. 깃 히스토리를 남기고 싶다면 해당 옵션을 제외한다.

"scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare",
    "deploy": "gh-pages --dotfiles -d .output/public -f"
},

 

3. nuxt.config.ts

nuxt.config.ts 파일의 app 부분에 baseUrl과 buildAssetsDir를 추가한다.

이때 baseUrl은 깃허브 레포지토리 이름으로 한다.

export default defineNuxtConfig({
  app: {
    baseURL: '/깃 레포지토리 이름/',
    buildAssetsDir: 'assets'
  },
  
  ...
})

 

4.  깃허브 페이지 배포하기

터미널에서 npm run generate 명령어를 실행하면 .output 폴더와 dist 폴더가 생기는 것을 볼 수 있다.

npm run deploy 명령어를 실행하면 깃허브에 배포된다.

npm run generate
npm run deploy

 

5. 깃허브 Settings

위의 과정을 수행했음에도 깃허브 페이지가 배포가 되지 않는다면 깃 레포지토리 Settings을 확인해 본다.

깃 레포지토리 - Settings - Pages에서 Branch가 gh-pages로 되어있는지 확인한다.

목표

  • sort 메서드를 이용해 toDoList 정렬하기
  • v-bind를 이용해 class 바인딩하기

 


결과 화면

 


버전

node v.20.8.1

nuxt v.3.8.1

vue v.3.3.8

 

0. 정렬 함수 하나로 만들기 (23.12.27 추가)

  • pages/index.vue

중복된 코드가 많고, 토글 형태로 isActive 변수를 만들어 등록순을 클릭하고 또 등록순을 클릭하면 최신순으로 변경되는 오류가 발생해 코드를 수정했다.

 

  1. sortToDo() 함수를 만든다.
  2. compareFn 매개변수를 만들어 @click 이벤트에서 받아온다. Nuxt3은 타입스크립트 기반이기 때문에 타입을 작성한다.
  3. data.value는 axios에서 받아오는 toDoData이다. 객체 형태이기 때문에 전개연산자를 사용했지만, 배열이라면 바로 sort 메서드를 사용한다.
  4. @click 이벤트에서 isActive를 true/false로 바꿔 class를 바인딩한다.
<script setup lang="ts">
...

interface IToDo {
  id: number
  todo: string
  created_at: Date
}

function sortToDo(compareFn: (a: IToDo, b: IToDo) => number) {
  toDoData.value = [...data.value].sort(compareFn)
}
</script>

<template>
    <section>
    	...
    
        <div v-if="toDoData.length > 0">
            <div class="flex gap-2 mb-6">
                <button
                    :class="{ 'underline underline-offset-4': isActive }"
                    @click="() => {
                    isActive = true
                    sortToDo((a, b) => a.id - b.id)}">
                    등록순
                </button>
                <i>|</i>
                <button
                    :class="{ 'underline underline-offset-4': !isActive }"
                    @click="() => {
                    isActive = false
                    sortToDo((a, b) => b.id - a.id)}">
                    최신순
                </button>
            </div>
        </div>
        
        ...
    </section>
</template>

 

1. 등록순/최신순 정렬 함수 만들기

  • pages/index.vue
  1. sort 메서드로 toDoList의 id를 비교해 등록순/최신순 정렬 함수를 만든다.
  2. <button> 태그에 @click 이벤트를 걸어준다.
<script setup lang="ts">
...

function sortRegistered() {
  toDo.toDoList.sort((a, b) => a.id - b.id)
}

function sortLatest() {
  toDo.toDoList.sort((a, b) => b.id - a.id)
}
</script>

<template>
  <section class="w-full max-w-screen-lg m-auto">
    ...
  
    <div v-if="toDo.toDoList.length > 0">
      <div class="flex gap-2">
        <button @click="sortRegistered()">등록순</button>
        <i>|</i>
        <button @click="sortLatest()">최신순</button>
      </div>
      
      ...
    </div>
  </section>
</template>

 

2. v-bind로 class 바인딩하기

  • pages/index.vue
  1. isActive 변수를 Boolean으로 만든다. (등록순/최신순 버튼에 on/off 효과를 주기 위해서)
  2. sort 함수가 동작할 때마다 isActive 변수가 true/false 될 수 있게 만든다.
  3. v-bind( : )를 이용해 클래스를 바인딩한다. (1개의 클래스만 바인딩하며 ''이 없어도 되지만, 여러 개 클래스를 바인딩할 경우 '' 안에 작성한다.)
<script setup lang="ts">
...

let isActive = true

function sortRegistered() {
  toDo.toDoList.sort((a, b) => a.id - b.id)
  isActive = !isActive
}

function sortLatest() {
  toDo.toDoList.sort((a, b) => b.id - a.id)
  isActive = !isActive
}
</script>

<template>
  <section class="w-full max-w-screen-lg m-auto">
    ...
    
    <div v-if="toDo.toDoList.length > 0">
      <div class="flex gap-2">
        <button
          :class="{ 'underline underline-offset-4' : isActive }"
          @click="sortRegistered()">등록순</button>
        <i>|</i>
        <button
          :class="{ 'underline underline-offset-4' : !isActive }"
          @click="sortLatest()">최신순</button>
      </div>
      
      ...
    </div>
  </section>
</template>

 

목표

  • Pinia 설치하기
  • Pinia store에서 데이터 불러오기
  • toDo 추가 및 삭제하기

 


결과 화면

 


버전

node v.20.8.1

nuxt v.3.8.1

vue v.3.3.8

 

1. Pinia 설치하기

https://jae-study.tistory.com/123

 

[Nuxt3] #10 Nuxt 3 State Management (일반 변수, useState, pinia 상태 관리 비교)

State management with Nuxt 3 — Course part 10 https://www.youtube.com/watch?v=IkpoAKS1s-k&list=PL8HkCX2C5h0XT3xWYn71TlsAAo0kizmVc&index=10 유튜브 강의를 참고하고 있습니다. State Management란 상태 관리(State Management)에서 상태(Sta

jae-study.tistory.com

Pinia 및 Nuxt3 state에 대한 자세한 설명은 위의 링크를 참고한다.

 

2. json 데이터 만들기

  • server/api/toDoList.json

api가 없기 때문에 json 파일로 빈 배열을 만든다.

[]

 

3. Pinia store 만들기

  • stores/toDoStore.ts
  1. pinia를 import 한다.
  2. 만든 toDoList.json 파일을 import 한다.
  3. useToDoStore 변수를 만들고, state를 정의한다. (초기값은 toDoList.json 파일의 빈 배열)
  4. Nuxt3은 타입스크립트 기반이기 때문에 interface을 이용해 state의 타입을 정의한다. (배열 안에 객체가 있는 형태)
import { defineStore } from 'pinia'
import toDoList from '../server/api/toDoList.json'

interface ItoDo {
  id: number,
  content: string
}

export const useToDoStore = defineStore('toDo', {
  state: () => ({
    toDoList: toDoList as ItoDo[]
  }),

  actions: {}
})

 

4. actions 정의하기

4-1. toDoList 추가하기 (addToDo())

  1. 입력한 toDo(content)를 배열에 push한다. 
  2. 이때 id는 고유한 값을 나타내기 위해 현재 시간(Date.now())으로 한다.

 

4-2. toDoList 삭제하기 (deleteToDo())

  1. findIndex 메서드와 id를 이용해 삭제할 toDo의 index를 찾는다.
  2. splice 메서드를 이용해 배열에서 해당 index의 toDo를 자른다.

 

4-3. toDoList 전체 삭제하기 (clearToDo())

  1. toDoList를 빈 배열로 만든다.
  2. 전체 삭제이기 때문에 window.confirm 창을 통해 알럿 메세지를 보여준다.
...

export const useToDoStore = defineStore('toDo', {
  state: () => ({
    toDoList: toDoList as ItoDo[]
  }),

  actions: {
    addToDo(content: string) {
      this.toDoList.push({ id: Date.now(), content: content })
    },

    deleteToDo(id: number) {
      const index = this.toDoList.findIndex(list => list.id === id)

      if(index > -1) {
        this.toDoList.splice(index, 1)
      }
    },

    clearToDo() {
      if(window.confirm('리스트를 모두 삭제하시겠습니까?')) {
        this.toDoList = []
      }
    }
  }
})

 

5. 페이지에서 Pinia store 불러오기

  • pages/index.vue
  1. toDoStore를 import 한다.
  2. newToDo 변수를 만들고, v-model를 이용해 input에 입력한 내용이 양방향 바인딩 될 수 있게 한다.
  3. Pinia store에 만든 actions을 클릭 이벤트로 만들어 toDo를 추가, 삭제, 전체 삭제한다. (toDo.addToDo(), toDo.deleteToDo(), toDo.clearToDo())
<script setup lang="ts">
import { useToDoStore } from '@/stores/toDoStore'

const toDo = useToDoStore()
const newToDo = ref('')

function addToDo() {
  if(newToDo.value) {
    toDo.addToDo(newToDo.value)
    newToDo.value = ''
  }
}
</script>

<template>
  <section class="w-full max-w-screen-lg m-auto">
    <h1 class="mb-10 text-5xl font-bold text-center">ToDo List</h1>

    <form
        class="mb-5"
        @submit.prevent="addToDo()">
      <label for="toDo" class="block mb-3 text-xl font-bold">New ToDo</label>

      <div class="flex justify-between gap-5">
        <input
            v-model="newToDo"
            type="text"
            id="toDo"
            class="w-3/4 px-3 py-4 rounded text-black"
            placeholder="할 일을 입력해 주세요."/>
        <button class="w-1/4 bg-slate-500 rounded">추가하기</button>
      </div>
    </form>

    <div v-if="toDo.toDoList.length > 0">
      <div class="flex gap-2">
        <button>등록순</button>
        <i>|</i>
        <button>최신순</button>
      </div>

      <ul>
        <li
            v-for="(list, key) in toDo.toDoList"
            :key="key"
            class="flex justify-between items-center gap-5 mt-5 px-5 py-2 border rounded">
          <p class="flex gap-5 text-lg">
            <span>{{ key + 1 }}.</span> {{ list.content }}
          </p>
          <button
              @click="toDo.deleteToDo(list.id)"
              class="px-5 py-2 bg-slate-500 rounded">
            삭제
          </button>
        </li>
      </ul>

      <button
          @click="toDo.clearTodo()"
          class="block w-1/3 mt-10 m-auto px-3 py-4 bg-slate-400 rounded">
        전체 삭제하기
      </button>
    </div>
  </section>
</template>

 

toDoStore를 콘솔로 확인하면 store에 만든 state와 action 등 모든 정보를 확인할 수 있다.

목표

  • Nuxt3 설치 및 테일윈드 CSS 설치
  • 투두 리스트 마크업
  • 투두 리스트 테일윈드 css 적용

 


결과 화면

 


버전

node v.20.8.1

nuxt v.3.8.1

vue v.3.3.8

 

1. Nuxt3 설치 및 테일윈드 CSS 설치하기

https://jae-study.tistory.com/114

 

[Nuxt3] #1 Nuxt 3 설치 및 테일윈드 CSS 적용하기

Create an app with Nuxt 3 — Course part 1 https://www.youtube.com/watch?v=hj3NNlTqIJg&list=PL8HkCX2C5h0XT3xWYn71TlsAAo0kizmVc&index=1 유튜브 강의를 참고하고 있습니다. Nuxt는 Vue.js를 사용하여 안전하고 성능이 뛰어난 풀 스

jae-study.tistory.com

Nuxt3 설치 및 테일윈드 CSS 설치에 대한 설명은 위의 링크를 참고한다.

 

 

2. 테일윈드 CSS를 이용해서 마크업 하기

  • pages/index.vue
  1. 새로운 toDo를 입력할 <form> 태그를 만들고, 입력한 toDo들이 나타날 수 있게 <ul> 태그로 목록을 만든다.
  2. 테일윈드를 사용해 CSS를 적용한다.
  3. toDoList 변수와 v-for를 이용해 toDoList를 보여준다.
<script setup lang="ts">
const toDoList = [
  {
    id: 1,
    content: '첫번째 할 일'
  },
  {
    id: 2,
    content: '두번째 할 일'
  },
  {
    id: 3,
    content: '세번째 할 일'
  }
]
</script>

<template>
  <section class="w-full max-w-screen-lg m-auto">
    <h1 class="mb-10 text-5xl font-bold text-center">ToDo List</h1>

    <form class="mb-5">
      <label for="toDo" class="block mb-3 text-xl font-bold">New ToDo</label>

      <div class="flex justify-between gap-5">
        <input
            type="text"
            id="toDo"
            class="w-3/4 px-3 py-4 rounded text-black"
            placeholder="할 일을 입력해 주세요."/>
        <button class="w-1/4 bg-slate-500 rounded">추가하기</button>
      </div>
    </form>

    <div>
      <div class="flex gap-2">
        <button>등록순</button>
        <i>|</i>
        <button>최신순</button>
      </div>

      <ul>
        <li
            v-for="(list, key) in toDoList"
            :key="key"
            class="flex justify-between items-center gap-5 mt-5 px-5 py-2 border rounded">
          <p class="flex gap-5 text-lg">
            <span>{{ key + 1 }}.</span> {{ list.content }}
          </p>
          <button class="px-5 py-2 bg-slate-500 rounded">
            삭제
          </button>
        </li>
      </ul>

      <button class="block w-1/3 mt-10 m-auto px-3 py-4 bg-slate-400 rounded">
        전체 삭제하기
      </button>
    </div>
  </section>
</template>

SEO & metas with Nuxt 3 — Course part 16

https://www.youtube.com/watch?v=PpyXtoM5HWQ&list=PL8HkCX2C5h0XT3xWYn71TlsAAo0kizmVc&index=16

 

유튜브 강의를 참고하고 있습니다.

 


공통 메타태그 설정하기

  • nuxt.config.ts

모든 페이지에서 동일한 title과 description이 적용된다.

export default defineNuxtConfig({
  app: {
    head: {
      title: 'Nuxt course on Youtube',
      meta: [
        { name: 'description', content: 'This is a repository for a course about Nuxt 3 for youtube.' }
      ]
    }
  },
  ...
})

 

페이지 별 메타태그 설정하기

1. useHead()

useHead() 컴포져블을 사용하면 unhead에서 제공하는 프로그래밍 방식 및 반응형 방식으로 <head> 태그를 관리할 수 있다.

'~~' 따음표를 이용한 텍스트 또는 변수로 SEO 설정이 가능하다.

 

  • pages/index.vue
<script setup lang="ts">
const route = useRoute()

useHead({
  title: `타이틀: ${route.name}`,
  meta: [
    { name: 'description', content: 'This is an index page.' }
  ],
  bodyAttrs: {
    class: 'test'
  },
  script: [ { innerHTML: 'console.log(\'Hello world\')' } ]
})
</script>

<template>
  <h1>인덱스 페이지</h1>
</template>

 

2. useSeoMeta()

useSeoMeta() 컴포져블을 사용하면 타입스크립트가 완전히 지원하는 SEO 메타 태그 객체를 정의할 수 있다.

'~~' 따음표를 이용한 텍스트 또는 변수로 SEO 설정이 가능하다.

 

  • pages/auth/index.vue
<script setup lang="ts">
const route = useRoute()

useSeoMeta({
  title: route.name,
  ogTitle: 'Login Page',
  description: 'This is a login page.',
  ogDescription: 'This is a login page.',
  ogImage: 'https://example.com/image.png',
  twitterCard: 'summary_large_image',
})
</script>

<template>
  <div>로그인 페이지</div>
</template>

 

3. <Head> 태그

Nuxt는 <Head>, <Title>, <Style>, <Meta>, <Link> 등의 태그를 제공하기 때문에 HTML 영역에 직접적으로 메타태그를 설정할 수 있다.

 

  • pages/auth/index.vue
<script setup lang="ts">
const description = ref('This is dynamic description.')
</script>

<template>
  <Head>
    <Title>Login Page</Title>
    <Meta name="description" :content="description"/>
    <Style type="text/css" children="body { background-color: gray !important; }" />
  </Head>

  <div>{{ description }}</div>
</template>

useAsyncData with Nuxt 3 — Course part 15

https://www.youtube.com/watch?v=3xeAElHzMe4&list=PL8HkCX2C5h0XT3xWYn71TlsAAo0kizmVc&index=15

 

유튜브 강의를 참고하고 있습니다.

 


1. useAsyncData

useAsyncData()는 비동기 로직의 resolve 동작을 반환한다.

useFetch('url')과 useAsyncData('url', () => $fetch('url')) 와 거의 동일하게 동작한다.

 

  • server/api/products.ts

async, await를 사용해 비동기 함수를 만든다.

함수가 동작하면 1초 뒤에 productCount가 1씩 증가한다.

let productCount = 0

export default async() => {
  await new Promise((resolve) => {
    setTimeout(() => {
      resolve(productCount++)
    }, 1000)
  })

  return { productCount }
}

 

  • pages/index.vue

useAsyncData()를 사용해 data를 가지고 온다.

클릭 이벤트를 만들어 버튼을 클릭하면 data가 새로고침 된다.

<script setup lang="ts">
const { data: productCount, pending } = await useAsyncData('count', () => $fetch('/api/products'))

const refresh = () => refreshNuxtData('count')

</script>

<template>
  <div v-if="pending">
    <p>pending</p>
    {{ productCount }}
  </div>

  <div v-else>
    <p>resolve</p>
    {{ productCount }}
  </div>

  <button @click="refresh">Refresh</button>
</template>

 

2. useLazyAsyncData

  • pages/index.vue

useLazyAsyncData()를 사용하는 것만 제외하면 나머지는 동일한 코드이다.

얼핏 보면 차이점이 없는 것처럼 보이지만, 페이지 처음 진입 시 useLazyAsyncData()를 사용하면 v-if="pending" 부분이 나타나는 것을 볼 수 있다.

pending 상태의 화면 로직을 보여주고 싶을 때 사용한다.

<script setup lang="ts">
const { data: productCount, pending } = await useLazyAsyncData('count', () => $fetch('/api/products'))

const refresh = () => refreshNuxtData('count')

</script>

<template>
  <div v-if="pending">
    <p>pending</p>
    {{ productCount }}
  </div>

  <div v-else>
    <p>resolve</p>
    {{ productCount }}
  </div>

  <button @click="refresh">Refresh</button>
</template>

 


$fetch

$fetch는 Nuxt3에서 제공하는 전역 메서드이고, HTTP request를 만들 수 있다.

$fetch는 서버와 클라이언트 환경에서의 중복 호출 문제가 있기 때문에 클라이언트 사이드에서만 사용해야 한다.

서버 사이트 렌더링을 위해서는 useFetch나 useAsyncData를 사용해야 한다.

 

useFetch() vs useAsyncData()

2가지 메서드는 동일한 기능을 하는 것처럼 보이지만, 사용법에서 몇 가지 차이가 있다. 

그중 가능 큰 차이는 실행 context의 차이이다.

 

useAsyncData()는 page 값이 바뀌면 url의 params도 변경되지만, useFetch()는 page 값이 바뀌어도 params가 바뀌지 않는다.

 

확실히 이해한 부분이 아니기 때문에 실제로 사용해 보면서 공부가 더 필요하다..

 

 

 

참고자료

https://nuxt.com/docs/api/composables/use-fetch

https://nuxt.com/docs/api/composables/use-async-data

https://jongmin4943.tistory.com/entry/Nuxt3-fetch-useAsyncData-useFetch-%EC%9D%98-%EC%B0%A8%EC%9D%B4

useFetch, useLazyfetch with Nuxt 3 — Course part 14

https://www.youtube.com/watch?v=rU92oLYjTGY&list=PL8HkCX2C5h0XT3xWYn71TlsAAo0kizmVc&index=14

 

유튜브 강의를 참고하고 있습니다.

 


Data Fetching

Nuxt는 서버 또는 브라우저에서 데이터를 가져와 처리하는 컴포저블을 제공한다.

  • useFetch()
  • useAsyncData
  • $fetch

 


1. useFetch()

useFetch()는 컴포넌트 설정 기능 중에서 데이터 패칭을 수행하는 가장 간단한 방법이다.

 

  • server/api/products.json

data를 json 파일로 만든다.

[
  {
    "name": "책상",
    "price": "$50.99",
    "description": "책상입니다."
  },
  {
    "name": "의자",
    "price": "$30.99",
    "description": "의자입니다."
  },
  {
    "name": "컴퓨터",
    "price": "$999.99",
    "description": "컴퓨터입니다."
  }
]

 

  • server/api/products.ts

json 파일을 import 해 return 한다.

import data from './products.json'

export default defineEventHandler((event) => {
  return {
    data
  }
})

 

  • pages/index.vue

useFetch()를 사용해 data를 가지고 온다.

<script setup lang="ts">
const { data } = await useFetch('/api/products')

console.log(data)
</script>

<template>
  {{ data }}
</template>

 

{ data: products } 라고 변수 이름을 설정할 수도 있다.

결과는 동일하다.

<script setup lang="ts">
const { data: products } = await useFetch('/api/products')

console.log(toRaw(products.value))
</script>

<template>
  {{ products }}
</template>

 

2. useLazyFetch()

useLazyFetch()는 데이터 패칭을 하기 전(EventHandler가 resolve 되기 전)의 과정을 제공한다.

 

  • server/api/products.ts

asycn()과 setTimeout()을 사용해 data를 2초 뒤에 return 한다.

import data from './products.json'

export default defineEventHandler(async()=> {
  return new Promise<any>((resolve) => {
    setTimeout(() => {
      resolve(data)
    }, 2000)
  })
})

 

  • pages/index.vue

useLazyFetch()를 사용해 data를 가지고 온다.

이때 pending(보류: 초기 상태로 이행되기 전이나 거절되기 전의 상태)을 사용해 data를 가지고 오기 전에 화면 로직을 보여준다.

data가 화면에 보이기 전에 Loading 텍스트가 나오고, console.log도 그전에 실행되기 때문에 null 값이 찍힌다.

<script setup lang="ts">
const { data: products, pending } = await useLazyFetch('/api/products')

console.log(toRaw(products.value))
</script>

<template>
  {{ pending ? 'Loading' : products }}
</template>

 

  • nuxt.config.ts

만약 useLazyFetch()와 pending을 사용했는데도 새로고침 즉시 data가 나온다면 SSR 설정이 false인지 확인한다.

SSR은 서버에서 렌더링이 완료된 다음에 브라우저에 보여지기 때문에 useLazyFetch()가 동작하지 않는다.

export default defineNuxtConfig({
  devtools: { enabled: true },
  modules: [],
  ...
  ssr: false
})

Rendering modes with Nuxt 3 — Course part 13

https://www.youtube.com/watch?v=TmgIylXsLuE&list=PL8HkCX2C5h0XT3xWYn71TlsAAo0kizmVc&index=13

 

유튜브 강의를 참고하고 있습니다.

 


Nuxt 모드

Nuxt 프로젝트를 설치할 때 2가지의 렌더링 모드를 선택할 수 있다.

  1. Universal (SSR/SSG)
  2. Single Page App

 

Universal 모드를 설명하기 전에 웹 페이지 렌더링 방식에 대해 설명한다.

 


1. Client Side Rendering (CSR, 클라이언트 사이드 렌더링)

클라이언트(브라우저)에서 웹 페이지를 렌더링 한다.

사용가가 웹 페이지를 방문하면(서버 요청을 받으면) 서버는 클라이언트에게 HTML과 JS를 보내고, 클라이언트가 그것을 렌더링 한다.

 

ex) React, Vue

 

CSR 장점

  • 페이지 이동시 전체 HTML을 로드할 필요가 없기 때문에 SSR에 비해 속도가 빠르다.
  • 필요한 부분만 변경되기 때문에 새로고침이 발생하지 않아 사용성이 뛰어나다.
  • API 호출이 필요 없기 때문에 지연 로딩 모듈이 필요하지 않다.

 

CSR 단점

  • 초기 페이지 로드 시간이 SSR에 비해 느리다.
  • SEO(Search Engine Optimization, 검색 엔진 최적화)가 어렵다.

 

2. Server Side Rendering (SSR, 서버 사이드 렌더링)

서버에서 웹 페이지를 렌더링 한다.

사용자가 웹 페이지를 방문하면 서버는 리소스를 확인해 렌더링이 끝난 상태로 클라이언트에게 전송한다.

 

ex) Next, Nuxt

 

SSR 장점

  • 초기 페이지 로드 시간이 CSR에 비해 빠르다.
  • SEO가 유리하다.

 

SSR 단점

  • 페이지 이동시 서버에서 새로운 html을 불러와야 하기 때문에 CSR보다 속도가 느리고, 새로고침이 발생해 사용성이 떨어진다.
  • 렌더링을 서버에서 하기 때문에 서버에 부담이 크다.

 

3. Static Site Generation (SSG)

클라이언트에서 필요한 페이지들을 사전에 미리 준비하고, 서버 요청을 받으면 이미 완성된 파일을 브라우저에 렌더링 한다.

SSR과 비슷하지만 서버에서 요청을 할 때 즉시 만드는지 미리 만들어 놓는지의 차이가 있다.

 

SSG 장점

  • 캐싱이 가능하다.
  • SEO가 유리하다.

 

SSG 단점

  • 모든 URL에 대해 개별 HTML 파일을 생성해야 한다. URL을 예측할 수 없다면 적용하기 어렵다.

 

검색 엔진 노출보다 서버의 부담이나 데이터 보호가 중요하다 -> CSR
검색 엔진이 중요하고, 업데이트가 자주 일어난다 -> SSR
검색 엔진이 중요하고, 업데이트가 거의 없다. -> SSG

 

만약 검색 엔진과 새로고침 없는 인터렉션이 모두 중요하다면 CSR + SST인 Universal 렌더링을 고려해야 한다.

 


1. Single Page Application (SPA, 단일 페이지 애플리케이션)

SPA는 말 그대로 하나의 페이지를 사용하는 애플리케이션이다.

서버에서 새로운 페이지를 요청하는 것이 아니라 하나의 페이지에서 내용만 동적으로 변경한다. (CSR 방식으로 렌더링 한다.)

 

SPA 장점

  • 페이지 이동 시 변경된 부분만 갱신되기 때문에 앱과 같은 자연스러운 사용자 경험을 제공한다.
  • 서버가 해야 하는 화면 구성을 클라이언트가 수행하기 때문에 서버 부담이 경감된다.
  • 모듈화 또는 컴포넌트 개발이 용이해 효율성이 증가한다.
  • 프론트앤드와 백앤드 구분이 확실하다.

 

SPA 단점

  • 처음 사이트 접속 시 모든 리소스를 한 번에 받기 때문에 초기 로딩 속도가 느리다.
  • 비즈니스 로직(화면이 변하는 모습)이 사용자에게 노출될 수 있다.
  • SEO가 어렵다.

 

2. Multiple Page Application (MPA, 다중 페이지 애플리케이션)

MPA는 말 그대로 여러 개의 싱글 페이지를 사용하는 애플리케이션이고, 전통적인 웹 방식이라고도 한다.

새로운 페이지를 요청할 때마다 서버에서 리소스가 다운되며 전체 페이지를 다시 렌더링(새로고침)해 내용을 변경한다. (SSR 방식으로 렌더링한다.)

 

MPA 장점

  • 처음 사이트 접속 시 초기 로딩 속도가 빠르다.
  • SEO가 유리하다.

 

MPA 단점

  • 페이지 이동 시 새로고침이 발생해 깜빡임이 발생한다.
  • 서버 요청이 있을 때마다 서버에서 렌더링을 하기 때문에 서버 부담이 증가한다.

 

CSR과 SPA, SSR과 MAP는 개념이 비슷해서 같은 것이라고 착각할 수 있지만,
렌더링을 어떻게 하는지와 페이지를 몇 개 사용하는지의 차이이기 때문에 단어 사용에 주의해야 한다.

 


Universal Mode (유니버셜 모드)

Nuxt는 CSR과 SSR의 장점을 모두 합친 Universal Mode 렌더링을 선택했다.

쉽게 말해 Nuxt 페이지를 처음 접속하면 서버에서 렌더링 하고, 이후 페이지 간 이동은 클라이언트에서 렌더링 한다.

이를 위해서 Nuxt는 클라이언트 사이드 하이드레이션(Client Side Hydration)과 코드 분할(Code Splitting), 프리패칭(Prefetching)을 활용하고 있다.

 

  • 클라이언트 사이드 하이드레이션

서버로부터 받은 정적 HTML을 사용자와 상호작용할 수 있는 다이나믹 DOM으로 바꾸는 방법이다.

 

  • 코드 분할

코드 전체를 로드하지 않고 분할해서 필요에 맞는 번들로 나눠 가져오는 것을 의미한다.

필요한 번들만 가져오기 때문에 로딩 속도가 빠르다.

 

  • 프리패칭

Nuxt는 화면에 보이는 <NuxtLink> 컴포넌트에 한해서 해당 페이지들을 렌더링 하는데 필요한 파일들을 미리 서버에 요청한다.

따라서 사용자가 클릭하기도 전에 이미 CSR에 준비가 되어있고, 사용자가 패칭을 요구하기 전에 미리 패칭 되어 있는 것을 프리패칭이라고 한다.

 

 

  • nuxt.config.ts

클라이언트 측 전용 렌더링을 활성화할 수 있다.

export default defineNuxtConfig({
  ssr: false
})

 

경로 규칙을 사용해 경로별로 다른 캐싱 규칙을 허용하고 서버가 지정된 URL의 요청을 응답하는 방법을 결정한다.

export default defineNuxtConfig({
  routeRules: {
    // Homepage pre-rendered at build time
    '/': { prerender: true },
    
    // Product page generated on-demand, revalidates in background
    '/products/**': { swr: 3600 },
    
    // Blog post generated on-demand once until next deploy
    '/blog/**': { isr: true },
    
    // Admin dashboard renders only on client-side
    '/admin/**': { ssr: false },
    
    // Add cors headers on API routes
    '/api/**': { cors: true },
    
    // Redirects legacy urls
    '/old-page': { redirect: '/new-page' }
  }
})

 

 

 

참고자료

https://nuxt.com/docs/guide/concepts/rendering
https://joshua1988.github.io/vue-camp/nuxt/universal-mode.html

https://hanamon.kr/spa-mpa-ssr-csr-%EC%9E%A5%EB%8B%A8%EC%A0%90-%EB%9C%BB%EC%A0%95%EB%A6%AC/

https://velog.io/@ka0son/%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%82%BC%ED%98%95%EC%A0%9C-CSR-SSR-SSG-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0

 

+ Recent posts