목표

  • 투두리스트 상세페이지 만들기
  • useRoute()로 파라미터 가져오기
  • useFetch()로 서버 데이터 가져오기

 


결과 화면

 


버전

node v.20.8.1

nuxt v.3.8.1

vue v.3.3.8

 

1. 상세페이지 만들기

  • pages/[id].vue
  1. pages 디렉터리 내에 [id].vue 파일을 만든다. 별도의 설정 없이 자동으로 동적 라우팅이 생성된다.

투두리스트 목록 페이지 = index.vue

투두리스트 상세 페이지 = [id].vue

 

Nuxt3의 핵심 기능 중 하나는 파일 시스템 라우터이다.

pages/ 디렉터리 내의 모든 Vue 파일은 URL을 생성한다.

 

https://nuxt.com/docs/getting-started/routing

라우팅에 대한 자세한 설명은 공식문서를 참고한다.

 

주의

Vue나 Nuxt2에서는 동적 라우팅 페이지가 _id.vue(언더바 _) 였다. Nuxt3부터는 [id].vue(대괄호 []) 이다.

 

2. 파라미터 가져오기

  • pages/[id].vue
  1. route 변수를 만든다.
  2. ${route.params.id}로 url의 파라미터 값을 가져온다.
  3. useFetch()로 해당 파라미터에 대한 서버 데이터를 가져온다.

 

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

useRoute()에 대한 자세한 설명은 공식 문서를 확인한다.

<script setup lang="ts">
const route = useRoute()
console.log(route)

const { data } = await useFetch(`http://localhost:3001/todo/${route.params.id}`)
console.log(data)
</script>

 

3. 마크업

  • pages/[id].vue
  1. 테일윈드를 사용해 CSS를 작성했다.
  2. { data }의 id와 todo 값을 화면에 보여준다.
<script setup lang="ts">
const route = useRoute()

const { data } = await useFetch(`http://localhost:3001/todo/${route.params.id}`)
</script>

<template>
  <section>
    <h1 class="mt-6 mb-12 md:mt-10 text-5xl font-bold text-center">To Do Detail</h1>

    <div class="p-4 md:p-6 md:pb-12 border rounded">
      <p class="block mb-6 text-xl font-bold text-right">No.{{ data.id }}</p>
      <p class="flex gap-2 text-base md:text-lg">{{ data.todo }}</p>
    </div>

    <NuxtLink to="/"  class="block w-full md:w-1/3 m-auto mt-12 px-3 py-4 bg-teal-950 hover:bg-teal-500 transition rounded text-center">
      Go to To Do List
    </NuxtLink >
  </section>
</template>

목표

  • useFetch()로 서버 데이터 가져오기
  • watch()를 사용해 서버 데이터 변경 감지하기
  • $fetch 사용해 HTTP 메서드 사용하기 (GET, POST, DELETE)
  • 서버 데이터를 등록순/최신순 정렬하기

 


결과 화면

 


버전

node v.20.8.1

nuxt v.3.8.1

vue v.3.3.8

 

1. Nest.js + PSQL (백앤드 + 데이터베이스)

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

Nest.js와 Psql을 사용해서 백앤드와 데이터베이스를 만들었다.

 

직접 백앤드와 데이터베이스를 만들어도 되고,

JSONPlaceholder 사이트를 통해 가짜 데이터를 받아와도 된다.

 

2. toDo 가져오기

  • pages/index.vue

Nuxt3에서는 axios 대신 useFetch()를 사용해 데이터 패칭을 한다.

useFetch()는 데이터를 비동기적으로 불러올 수 있고, 기존 axios 코드보다 로직을 간소화할 수 있다.

 

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

useFetch()에 대한 자세한 설명은 위의 링크를 참고한다.

 

  1. Nuxt3는 타입스크립트 기반이기 때문에 useFetch()로 받아오는 데이터의 타입을 정의한다. (IToDo[])
  2. 서버에서 받은 데이터를 가공하기 위해 toDoData 변수를 만든다. useFetch()로 받아오는 데이터의 타입은 기본적으로 객체(object)이다. 이것을 배열로 만들어 정렬(sort 메서드)을 하고 싶어 toDoData 변수를 만들었다.
  3. toDo를 추가하고 삭제할 때마다 실시간으로 렌더링 하기 위해 watch 함수를 사용한다.
<script setup lang="ts">
interface IToDo {
  id: number
  todo: string
  created_at: Date
}

// axios.get 기능 (toDo 가져오기)
const { data, refresh } = await useFetch<IToDo[]>('http://localhost:3001/todo')
const toDoData = ref(data.value ? [...data.value] : [])

watch(() => data.value, () => {
  toDoData.value = isActive.value ? [...data.value] : [...data.value].reverse()
})

...
</script>

 

3. toDo 추가하기

  • pages/index.vue
  1. newToDo 변수를 만들어 빈 값이면 alert() 창이 나오게 하고, 값이 있으면 POST 메서드를 실행한다.
  2. POST가 완료되면 newToDo를 빈 값으로 만들고, 실시간 값 변화를 감지하기 위해 refresh()를 실행한다.
<script setup lang="ts">
const newToDo = ref('')

// axios.post 기능 (toDo 추가하기)
async function addToDo() {
  if(newToDo.value === '') {
    alert('할 일을 입력해 주세요.')
  } else {
    await $fetch('http://localhost:3001/todo', {
      method: 'POST',
      body: { todo: newToDo.value }
    })

    newToDo.value = ''
    await refresh()
  }
}

...
</script>

 

4. toDo 삭제하기

  • pages/index.vue
  1. 리스트 1개만 삭제하고 싶으면 해당하는 id 값을 매개변수로 받아 DELETE 메서드를 실행한다.
  2. 리스트 전체를 삭제하고 싶으면 api 전체 리스트에서 DELETE 메서드를 실행한다.
  3. DELETE가 완료되면 실시간 값 변화를 감지하기 위해 refresh()를 실행한다.
<script setup lang="ts">
// axios.delete 기능 (toDo 삭제하기)
async function deleteToDo(id: number) {
  await $fetch(`http://localhost:3001/todo/${id}`, {
    method: 'DELETE'
  })

  await refresh()
}

// axios.delete 기능 (toDo 전체 삭제하기)
async function clearToDo() {
  if(window.confirm('리스트를 모두 삭제하시겠습니까?')) {
    await $fetch(`http://localhost:3001/todo`, {
      method: 'DELETE'
    })

    await refresh()
  }
}

...
</script>

 

5. toDo 정렬하기

  • pages/index.vue
  1. useFetch()로 받은 데이터를 배열로 만들어 sort() 메서드를 사용해 등록순/최신순으로 정렬한다.
<script setup lang="ts">
const isActive = ref(true)

// 등록순, 최신순 정렬하기
function sortToDo(compareFn: (a: IToDo, b: IToDo) => number) {
  toDoData.value = [...data.value].sort(compareFn)
  isActive.value = !isActive.value
}

...
</script>

<template>
  <section class="w-full max-w-screen-lg min-h-screen m-auto">
    ...

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

 

6. 마크업

  • pages/index.vue
  1. 테일윈드를 사용해 CSS를 작성했다.
  2. 각각의 버튼에 맞는 @click 이벤트를 작성한다.
<template>
  <section class="w-full max-w-screen-lg min-h-screen m-auto">
    <h1 class="mt-6 mb-12 md:mt-10 text-5xl font-bold text-center">To Do List</h1>

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

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

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

      <ul>
        <li
            v-for="(data, key) in toDoData"
            :key="key"
            class="flex justify-between items-center gap-3 mb-6 px-4 py-3 md:px-5 border rounded">
          <p class="flex gap-2 text-base md:text-lg">
            <span>{{ key + 1 }}.</span> {{ data.todo }}
          </p>

          <button
            class="min-w-fit px-5 py-2 bg-teal-600 hover:bg-teal-500 transition rounded"
            @click="deleteToDo(data.id)">
            삭제
          </button>
        </li>
      </ul>

      <button
        class="block w-full md:w-1/3 m-auto mt-12 px-3 py-4 bg-teal-950 hover:bg-teal-500 transition rounded"
        @click="clearToDo()">
        전체 삭제하기
      </button>
    </div>
  </section>
</template>

 


상세 코드는 깃허브 페이지를 참고한다.

 

https://github.com/heejae0811/nuxt3-todo/tree/axios

 

GitHub - heejae0811/nuxt3-todo: main: Pinia To Do List / axios: Nest.js + PSQL To Do List (useFetch)

main: Pinia To Do List / axios: Nest.js + PSQL To Do List (useFetch) - GitHub - heejae0811/nuxt3-todo: main: Pinia To Do List / axios: Nest.js + PSQL To Do List (useFetch)

github.com

 

https://github.com/heejae0811/todo-backend

 

GitHub - heejae0811/todo-backend: Nest.js + PSQL ToDo 백앤드

Nest.js + PSQL ToDo 백앤드. Contribute to heejae0811/todo-backend development by creating an account on GitHub.

github.com

결과 화면

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

+ Recent posts