Front-End/vue

[nuxt] nuxt3 + vue3 개념 및 용법 정리

찐코딩 2023. 5. 10. 10:51

🔎nuxt 폴더 구조

.
├── assets/
├── components/
├── layouts/
├── middleware/
├── pages/
├── plugins/
├── static/
├── store/
├── nuxt.config.js
└── package.json
  • assets
    이미지, 스타일시트 및 기타 자산을 저장하는 디렉토리. 이 자산은 빌드 후에 /_nuxt/ 디렉토리에 복사됨.
  • components
    Vue.js 컴포넌트를 저장하는 디렉토리.
  • layouts
    애플리케이션의 레이아웃을 저장하는 디렉토리. 레이아웃은 페이지 컴포넌트를 렌더링할 때 사용됨.
  • middleware
    미들웨어 함수를 저장하는 디렉토리. 미들웨어는 페이지 렌더링 전에 실행됨.
  • pages
    애플리케이션의 페이지 컴포넌트를 저장하는 디렉토리. 파일 시스템 기반 라우팅을 사용하여 URL 경로와 매핑됨.
  • plugins
    Vue.js 플러그인을 저장하는 디렉토리. 애플리케이션이 시작될 때 자동으로 로드됨.
  • static
    정적 파일을 저장하는 디렉토리. 이 파일은 빌드 후에 /_nuxt/ 디렉토리에 복사됨.
  • store
    Vuex 스토어 모듈을 저장하는 디렉토리. 상태 관리를 위해 사용됨.
  • nuxt.config.js
    Nuxt.js 구성 파일. 빌드, 렌더링 및 애플리케이션 구성을 위한 다양한 옵션을 제공함.
  • package.json
    NPM 패키지 구성 파일. 애플리케이션의 종속성 및 스크립트를 정의함.

🎉필요한 경우 이 구조를 조정하고 사용자 지정 폴더를 추가하여 애플리케이션을 더욱 유연하게 만들 수 있음

 


🔎nuxt 프로젝트의 app.vue

app.vue 파일은 Nuxt.js에서 기본적으로 제공하는 파일 중 하나로, 애플리케이션의 최상위 컴포넌트.

이 파일은 pages 디렉토리에 있는 모든 페이지 컴포넌트의 공통 레이아웃을 정의하고, 라우터 및 미들웨어 등의 설정을 포함함.

(아래 예제코드에는 미들웨어 설정이 없음)

<!-- 디렉토리가 없는 경우 -->
<template>
  <h1>Hello World!</h1>
</template>

<!-- 디렉토리가 있는 경우 -->
<!-- NuxtPage는 pages 폴더의 index.vue 파일을 가리킨다. -->
<template>
  <div>
    <NuxtLayout>
      <NuxtPage/>
    </NuxtLayout>
  </div>
</template>

응용

<script lang="ts" setup>
import { AppConfigInput } from '@nuxt/schema'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { AppSetup } from './utils/app'
import { ITheme } from './utils/theme'

AppSetup()
const theme = useState<ITheme>('theme.current')
const locale = useState<string>('locale.setting')
const app = useAppConfig() as AppConfigInput

// pinia 라이브러리 사용
const pinia = createPinia()
// piniaPluginPersistedstate 플러그인 적용
// pinia의 상태를 브라우저의 로컬 스토리지에 저장할 수 있음
pinia.use(piniaPluginPersistedstate)

// useHead 함수를 사용하여 HTML 헤더 내부의 정보를 설정하고 있음.
useHead({
  title: app.name,
  titleTemplate: 'example',
  meta: [
    { name: 'viewport', content: 'width=device-width, initial-scale=1' },
    {
      hid: 'description',
      name: 'description',
      content: 'example',
    },
  ],
  link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
})
</script>

<template>
  <Html :class="`${theme === 'dark' ? 'dark' : ''}`" :lang="locale">
    <!--  :class 속성을 통해 현재 테마 모드에 따라 body 태그의 클래스 이름이 결정되고, :lang 속성을 통해 현재 언어 설정에 따라 HTML 태그의 lang 속성이 결정됨 -->
    <Body
      class="antialiased duration-300 transition-colors text-gray-800 dark:text-gray-200 bg-white dark:bg-gray-800"
    >
      <NuxtLayout>
        <NuxtLoadingIndicator :height="5" :duration="3000" :throttle="400" />
        <!--NuxtPage는 기본적으로 pages 폴더 내부의 index.vue 파일을 참조.-->
        <NuxtPage />
      </NuxtLayout>
    </Body>
  </Html>
</template>

https://kyounghwan01.github.io/blog/Vue/vue3/composition-api/#setup

 

vue3 composition api 사용법, vue, computed, reactive, ref, watch, watchEffect, props, vuex, composable

vue3 composition api 사용법, vue, computed, reactive, ref, watch, watchEffect, props, vuex, composable

kyounghwan01.github.io

🔎 script setup 구문이란?
<script setup> 구문은 Vue 3에서 도입된 새로운 구문 중 하나

🎉 <script setup> 구문을 사용 시 이점
코드의 간결성
이전에는 Vue 컴포넌트를 작성할 때 data, computed, methods 등의 속성을 일일이 정의해야 했음
그러나 <script setup> 구문에서는 이런 작업을 대부분 생략할 수 있음 => SFC 방식에 아주 적합!

타입스크립트와 같은 타입 체킹 도구를 사용할 때 유용
컴포넌트의 타입을 정의하는 작업이 단순해짐
최신 자바스크립트 기능을 사용할 수 있음
예를 들어, <script setup> 구문에서는 import 문을 사용해 모듈을 불러올 수 있음
더보기

composition api 방식

<script setup>
import { ref } from 'vue'

const count = ref(0)

onMounted(() => {console.log(count)})
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>

options api 방식(~vue 2버전)

<template>
  <button @click="increment">{{ count }}</button>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  mounted() {
    console.log(this.count); // 0
  },
  methods: {
    increment() {
      this.count++;
    },
  },
};
</script>

📌app.vue 파일의 역할

  • pages 디렉토리에 있는 모든 페이지 컴포넌트의 공통 레이아웃을 정의
  • 전역적으로 사용되는 CSS 파일이나 플러그인 등을 설정
  • 라우터 설정 및 미들웨어 등의 설정을 포함
  • 애플리케이션 전역에서 사용되는 상태 관리 라이브러리를 설정
  • app.vue 파일은 다음과 같은 속성과 라이프사이클 훅을 가짐.

🔎components

📌 컴포넌트 사용 이유

기본 HTML 엘리먼트를 확장하여 재사용 가능한 코드를 캡슐화하는 데 도움이 됨

 

📌 컴포넌트의 등록

SpanBtn.vue

<!--기본 용법-->
<template>
  <span class="text-xs bg-blue-400 px-3 py-1 rounded-full text-white">컴포넌트 예시</span>
</template>

📌 컴포넌트의 사용

example.vue

<template>
  <div>
    <SpanBtn />
  </div>
</template>

상단에서 해당 컴포넌트를 import 하지 않아도 알아서 불러온다.

컴포넌트를 불러온 모습

 

📌 부모 컴포넌트에서 자식 컴포넌트로 값 넘기기(defindProps)

SpanBtn.vue(자식 컴포넌트)

<script lang="ts" setup>
// const props는 생략해도 된다.
//  const props = defineProps({
//   text: {
//     type: String,
//     default: '',
//   },
// })
defineProps({
  text: {
    type: String,
    default: '',
  },
})
</script>
<template>
  <span class="text-xs bg-blue-400 px-3 py-1 rounded-full text-white">{{
    text
  }}</span>
</template>

 

type: props의 타입 정의
default: 기본값 → 설정하게 되면 required가 true가 됨
required: 필수 유무(boolean)

example.vue(부모 컴포넌트)

<template>
  <div>
    <SpanBtn :text="'props 예제'" />
    <SpanBtn :text="'속성 변경도 가능'" class="bg-red-500" />
  </div>
</template>

컴포넌트를 불러온 모습

props로 function도 넘길 수 있다!

 

 

📌 자식 컴포넌트에서 부모컴포넌트로 값 넘기기(emit)

더보기

자식 컴포넌트

<script setup lang="ts">
import { defineEmits } from 'vue'

const props = defineProps({
  label: {
    type: String,
    default: '',
  },
  inputType: {
    type: String,
    default: 'text',
  },
  placeholder: {
    type: String,
    default: '',
  },
  required: {
    type: Boolean,
    required: false,
    default: true,
  },
  maxLength: {
    type: Number,
    default: 30,
  },
  initValue: {
    type: String,
    required: false,
    default: '',
  },
  constraint: {
    type: Function,
    required: false,
  },
  constraintText: {
    type: String,
    required: false,
    default: '',
  },
  className: {
    type: String,
    required: false,
    default: '',
  },
})

// emit 정의
const emits = defineEmits(['update-value'])

// emit 함수
const updateInputValue = (value: string | null) => {
  value = value?.trim() ?? ''
  if (props.constraint) {
    const element = document.querySelector('.' + props.className) as HTMLElement
    if (value && !props.constraint(value)) {
      element.style.visibility = 'visible'
      element.style.opacity = '0.8'
    } else {
      element.style.visibility = 'hidden'
      element.style.opacity = '0'
    }
  }
  emits('update-value', value)
}
</script>
<template>
  <div class="flex flex-row w-full justify-between items-center">
    <div>
      <span class="text-sm font-bold">{{ label }}</span>
      <span v-if="!required" class="text-xs">(선택)</span>
    </div>
    <!-- input text-->
    <div
      v-if="inputType === 'text' && initValue === ''"
      class="w-2/3 relative inline-block"
    >
      <input
        type="text"
        :placeholder="placeholder"
        class="w-full text-sm rounded-lg border border-gray-200 bg-white dark:bg-gray-700 py-1.5 px-3"
        :maxlength="maxLength"
        @input="(event) => updateInputValue((event.target as HTMLInputElement).value)"
      />
      <span
        v-if="constraintText !== ''"
        class="constraint-text text-xs bg-rose-400 px-3 py-1.5"
        :class="className"
        >{{ constraintText }}</span
      >
    </div>
  </div>
</template>

 

<script lang="ts" setup>
definePageMeta({
  layout: 'dashboard',
})

useHead(() => ({
  title: '토큰 발행',
  meta: [
    {
      name: '토큰 발행',
      content: '토큰 발행',
    },
  ],
}))

const coinName = ref('')
// 영문 대소문자, 숫자만 허용
function regexEngNum(value: string) {
  const regex = /^[a-zA-Z0-9\s]+$/
  return regex.test(value)
}
</script>

<template>
  <PageWrapper>
    <PageHeader :text="['토큰 발행']">
      <PageTitle :text="'토큰 발행'" />
      <p class="mt-2 text-sm font-normal">신규 토큰을 발행할 수 있습니다.</p>
    </PageHeader>
    <PageBody>
      <PageSection>
        <PageLabelInput
          :label="'토큰 이름'"
          :placeholder="'토큰 이름을 설정하세요(영문 대소문자, 숫자, 공백만 허용)'"
          :constraint="regexEngNum"
          :constraint-text="'영문 대소문자와 숫자만 입력해주세요'"
          :class-name="'coinName'"
          :max-length="20"
          @update-value="(value: string) => (coinName = value)"
        />
      </PageSection>
    </PageBody>
  </PageWrapper>
</template>