Vue 3 ๊ธฐ๋ฐ ํฌ๋ก์ค ํ๋ซํผ ํ๋ ์์ํฌ: ์ ๊ท ์ค์น ๋ฐ ๊ธฐ์กด Vue 3 ์ฐ๋, ๋ ์ด์์, UI ์ปดํฌ๋ํธ, ํผ ๊ฒ์ฆ, ํ๋ฌ๊ทธ์ธ, ๋ฉํฐ ํ๋ซํผ ๋น๋ ํต ๋ ํผ๋ฐ์ค
## 1. ์ ๊ท Quasar ํ๋ก์ ํธ ์์ฑ (Quasar CLI)
```bash
# 1. ๊ณต์ ์ถ์ฒ ๋ฐฉ์์ผ๋ก ์ Quasar ํ๋ก์ ํธ ์์ฑ (Vite ๊ธฐ๋ฐ ๊ถ์ฅ)
npm create quasar@latest
# ๋๋ pnpm create quasar / yarn create quasar
# ํ๋กฌํํธ ์ค์ ๊ถ์ฅ ์ต์
:
# ? What would you like to build? โฆ App with Quasar CLI, Vite (๊ถ์ฅ)
# ? Project folder: โฆ my-quasar-app
# ? Pick Quasar version: โฆ Quasar v2 (Vue 3)
# ? Pick script type: โฆ Typescript (๊ถ์ฅ)
# ? Pick Quasar App CLI: โฆ Quasar CLI with Vite
# ? Pick Component style: โฆ Composition API with <script setup> (๊ถ์ฅ)
# ? Pick features: โฆ ESLint, Pinia, Axios
# 2. ํ๋ก์ ํธ ํด๋ ์ด๋ ๋ฐ ๊ฐ๋ฐ ์๋ฒ ์คํ
cd my-quasar-app
quasar dev # ๋๋ npm run dev (๊ธฐ๋ณธ ํฌํธ http://localhost:9000)
# 3. ์ด์ ํ๊ฒฝ ํ๋ก๋์
๋น๋ (SPA ๊ธฐ๋ณธ)
quasar build # dist/spa ํด๋์ ์ต์ข
๋ฒ๋ค ์์ฑ
```
```text
[Quasar CLI ํ๋ก์ ํธ ๊ตฌ์กฐ]
my-quasar-app/
โโโ quasar.config.js โ ํ๋ฌ๊ทธ์ธ(Notify, Dialog), ์์ด์ฝ, ๋น๋ ํ๊ฒ ์ค์ ํ์ผ
โโโ src/boot/ โ ์ฑ ์์ ์ ์๋ ์คํ ์ด๊ธฐํ ์คํฌ๋ฆฝํธ (axios, i18n ๋ฑ)
โโโ src/layouts/ โ MainLayout.vue (ํค๋, ๋ค๋น๊ฒ์ด์
๋๋ก์ด, ํธํฐ)
โโโ src/pages/ โ IndexPage.vue (๊ฐ ํ๋ฉด ํ์ด์ง ์ปดํฌ๋ํธ)
โโโ src/App.vue โ ์ต์์ ๋ฃจํธ ์ปดํฌ๋ํธ (<router-view />)
```
## 2. ๊ธฐ์กด Vue 3 ํ๋ก์ ํธ์ Quasar ์ถ๊ฐ (@quasar/vite-plugin)
```bash
# 1. ๊ธฐ์กด Vite + Vue 3 ํ๋ก์ ํธ์ Quasar, ์์ด์ฝ, ํ๋ฌ๊ทธ์ธ, Sass ์ค์น
npm i quasar @quasar/extras
npm i -D @quasar/vite-plugin sass@^1.32.0
```
```typescript
// 2. vite.config.ts ์ค์ (Vite ํ๋ฌ๊ทธ์ธ ๋ฑ๋ก)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
export default defineConfig({
plugins: [
vue({
template: { transformAssetUrls }
}),
quasar({
sassVariables: 'src/quasar-variables.sass' // (์ ํ: ์ปค์คํ
ํ
๋ง ๋ณ์)
})
]
})
```
```typescript
// 3. src/main.ts ์ค์ (Quasar ์ธ์คํด์ค ๋ฑ๋ก ๋ฐ CSS import)
import { createApp } from 'vue'
import { Quasar, Notify, Dialog, Loading } from 'quasar'
// Quasar ํต์ฌ CSS ๋ฐ ๋จธํฐ๋ฆฌ์ผ ์์ด์ฝ ํฐํธ ๋ถ๋ฌ์ค๊ธฐ
import '@quasar/extras/material-icons/material-icons.css'
import 'quasar/src/css/index.sass'
import App from './App.vue'
const app = createApp(App)
app.use(Quasar, {
plugins: {
Notify,
Dialog,
Loading
}
})
app.mount('#app')
```
```bash
# ๐ก (์ฐธ๊ณ ) ๋ง์ฝ Vue CLI ๊ธฐ๋ฐ ํ๋ก์ ํธ๋ผ๋ฉด ๊ณต์ ๋ช
๋ น์ด๋ก 1์ค ์ถ๊ฐ ๊ฐ๋ฅ:
vue add quasar
```
## 3. ๋ ์ด์์ ์์คํ
(q-layout & view ์์ฑ)
```vue
<!-- src/layouts/MainLayout.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const leftDrawerOpen = ref(false)
const toggleLeftDrawer = () => {
leftDrawerOpen.value = !leftDrawerOpen.value
}
</script>
<template>
<!-- ๐ก view="lHh Lpr lFf": Header, Drawer, Footer์ ๋ฐฐ์น ๊ตฌ์กฐ๋ฅผ 9์๋ฆฌ๋ก ์ ์ด -->
<q-layout view="lHh Lpr lFf">
<!-- ์๋จ ํค๋ -->
<q-header elevated class="bg-primary text-white">
<q-toolbar>
<q-btn flat dense round icon="menu" @click="toggleLeftDrawer" />
<q-toolbar-title>๋ด ํ์ด์ฌ ์ฑ</q-toolbar-title>
<q-btn flat round icon="account_circle" />
</q-toolbar>
</q-header>
<!-- ์ข์ธก ์ฌ์ด๋๋ฐ ์๋ (Drawer) -->
<q-drawer v-model="leftDrawerOpen" show-if-above bordered>
<q-list>
<q-item-label header>ํ์ ๋ฉ๋ด</q-item-label>
<q-item clickable v-ripple to="/">
<q-item-section avatar><q-icon name="home" /></q-item-section>
<q-item-section>ํ</q-item-section>
</q-item>
</q-list>
</q-drawer>
<!-- ํ์ด์ง ์ปจํ
์ด๋: ๋ผ์ฐํธ ํ์ด์ง ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ง๋๋ ๊ณณ -->
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
```
## 4. ๋ฒํผ, ์์ด์ฝ & ๋ฑ์ง (q-btn, q-icon, q-badge)
```vue
<script setup lang="ts">
import { ref } from 'vue'
const isLoading = ref(false)
</script>
<template>
<!-- ๋ค์ํ ๋ฒํผ ์คํ์ผ -->
<q-btn color="primary" label="๊ธฐ๋ณธ ๋ฒํผ" />
<q-btn outline color="secondary" label="์ธ๊ณฝ์ ๋ฒํผ" />
<q-btn flat color="accent" label="ํ๋ซ ๋ฒํผ" />
<q-btn round color="negative" icon="delete" />
<!-- ๋ก๋ฉ ์ํ ์ง์ ๋ฒํผ -->
<q-btn
:loading="isLoading"
color="primary"
label="์ ์ฅํ๊ธฐ"
@click="isLoading = true"
/>
<!-- ์์ด์ฝ ๋ฐ ํ๋กํ
๋ฑ์ง -->
<q-btn dense round flat icon="notifications">
<q-badge color="red" floating>4</q-badge>
</q-btn>
</template>
```
## 5. ํผ ์
๋ ฅ & ์ ํจ์ฑ ๊ฒ์ฌ (q-input, q-select, q-toggle)
```vue
<script setup lang="ts">
import { ref } from 'vue'
const email = ref('')
const role = ref('USER')
const isAgreed = ref(false)
// ๐ก rules ์์ฑ์ ์ ํจ์ฑ ๊ฒ์ฌ ํจ์ ๋ฐฐ์ด์ ๋ฃ์ด ์๋ ์๋ฌ ํ์
const emailRules = [
(val: string) => !!val || '์ด๋ฉ์ผ์ ์
๋ ฅํ์ธ์.',
(val: string) => /.+@.+\..+/.test(val) || '์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ด ์๋๋๋ค.'
]
</script>
<template>
<q-form @submit="onSubmit" class="q-gutter-md">
<!-- ํ
์คํธ ์ธํ (outlined, clearable, rules ์ง์) -->
<q-input
v-model="email"
label="์ด๋ฉ์ผ ์ฃผ์"
outlined
dense
clearable
:rules="emailRules"
/>
<!-- ์
๋ ํธ ๋ฐ์ค -->
<q-select
v-model="role"
:options="['ADMIN', 'USER', 'GUEST']"
label="๊ถํ ์ ํ"
outlined
dense
/>
<!-- ํ ๊ธ ์ค์์น & ์ฒดํฌ๋ฐ์ค -->
<q-toggle v-model="isAgreed" label="์๋ฆผ ์์ ๋์" />
<q-btn type="submit" color="primary" label="ํ์๊ฐ์
" />
</q-form>
</template>
```
## 6. ๋ฐ์ดํฐ ํ
์ด๋ธ (q-table)
```vue
<script setup lang="ts">
import { ref } from 'vue'
const filter = ref('')
const columns = [
{ name: 'id', label: 'ID', field: 'id', sortable: true, align: 'left' },
{ name: 'name', label: '์ด๋ฆ', field: 'name', sortable: true, align: 'left' },
{ name: 'role', label: '์ญํ ', field: 'role', align: 'center' }
]
const rows = [
{ id: 1, name: 'ํ๊ธธ๋', role: 'ADMIN' },
{ id: 2, name: '์ด์์ ', role: 'USER' }
]
</script>
<template>
<!-- ๊ฒ์, ์ ๋ ฌ, ํ์ด์ง๋ค์ด์
๊ธฐ๋ณธ ๋ด์ฅ ํ
์ด๋ธ -->
<q-table
title="ํ์ ๋ชฉ๋ก"
:rows="rows"
:columns="columns"
row-key="id"
:filter="filter"
>
<!-- ์๋จ ๊ฒ์์ฐฝ ์ฌ๋กฏ ์ปค์คํฐ๋ง์ด์ง -->
<template v-slot:top-right>
<q-input borderless dense debounce="300" v-model="filter" placeholder="๊ฒ์...">
<template v-slot:append><q-icon name="search" /></template>
</q-input>
</template>
<!-- ํน์ ์
์ปค์คํ
๋ ๋๋ง (๋ฑ์ง ํ์) -->
<template v-slot:body-cell-role="props">
<q-td :props="props">
<q-badge :color="props.value === 'ADMIN' ? 'red' : 'blue'">
{{ props.value }}
</q-badge>
</q-td>
</template>
</q-table>
</template>
```
## 7. ํ์
, ์๋ฆผ & ๋ก๋ฉ (useQuasar ํ๋ฌ๊ทธ์ธ)
```vue
<script setup lang="ts">
import { useQuasar } from 'quasar'
const $q = useQuasar()
// 1. ํ ์คํธ ์๋ฆผ (Notify)
const showToast = () => {
$q.notify({
type: 'positive', // positive, negative, warning, info
message: '์ฑ๊ณต์ ์ผ๋ก ์ ์ฅ๋์์ต๋๋ค!',
position: 'top',
timeout: 2000
})
}
// 2. ํ์ธ ๋ชจ๋ฌ ๋ํ์์ (Dialog)
const confirmDelete = () => {
$q.dialog({
title: '์ญ์ ํ์ธ',
message: '์ ๋ง๋ก ์ด ํญ๋ชฉ์ ์ญ์ ํ์๊ฒ ์ต๋๊น?',
cancel: true,
persistent: true
}).onOk(() => {
showToast()
})
}
// 3. ์ ์ญ ๋ก๋ฉ ์คํผ๋ (Loading)
const loadData = () => {
$q.loading.show({ message: '๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...' })
setTimeout(() => $q.loading.hide(), 1500)
}
</script>
```
## 8. ๋ชจ๋ฌ ๋ค์ด์ผ๋ก๊ทธ & ๋๋กญ๋ค์ด ๋ฉ๋ด (q-dialog & q-menu)
```vue
<script setup lang="ts">
import { ref } from 'vue'
const isDialogOpen = ref(false)
</script>
<template>
<!-- 1. ์ปค์คํ
์นด๋ ๋ชจ๋ฌ ๋ค์ด์ผ๋ก๊ทธ -->
<q-btn label="๋ชจ๋ฌ ์ด๊ธฐ" color="primary" @click="isDialogOpen = true" />
<q-dialog v-model="isDialogOpen">
<q-card style="min-width: 350px">
<q-card-section><div class="text-h6">ํ๋กํ ์ค์ </div></q-card-section>
<q-card-section><q-input label="๋๋ค์" outlined dense /></q-card-section>
<q-card-actions align="right">
<q-btn flat label="์ทจ์" v-close-popup />
<q-btn color="primary" label="ํ์ธ" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
<!-- 2. ๋๋กญ๋ค์ด ํ์
๋ฉ๋ด (q-menu) -->
<q-btn color="secondary" label="๋๋ณด๊ธฐ">
<q-menu>
<q-list style="min-width: 100px">
<q-item clickable v-close-popup><q-item-section>์์ ํ๊ธฐ</q-item-section></q-item>
<q-item clickable v-close-popup class="text-negative"><q-item-section>์ญ์ </q-item-section></q-item>
</q-list>
</q-menu>
</q-btn>
</template>
```
## 9. ๋คํฌ ๋ชจ๋ & ๋ฐ์ํ ์ ํธ๋ฆฌํฐ (Dark & Screen)
```vue
<script setup lang="ts">
import { useQuasar } from 'quasar'
const $q = useQuasar()
// 1. ๋คํฌ ๋ชจ๋ ํ ๊ธ
const toggleDark = () => {
$q.dark.toggle() // ๋๋ $q.dark.set(true | false | 'auto')
}
</script>
<template>
<!-- ๋คํฌ ๋ชจ๋ ๋ฒํผ -->
<q-btn
flat
round
:icon="$q.dark.isActive ? 'dark_mode' : 'light_mode'"
@click="toggleDark"
/>
<!-- 2. ๋ฐ์ํ Flex ๊ทธ๋ฆฌ๋ ๋ ์ด์์ (Bootstrap ์คํ์ผ) -->
<div class="row q-col-gutter-md">
<!-- ๋ชจ๋ฐ์ผ: 12์นธ ์ ์ฒด(1์ด), ํ๋ธ๋ฆฟ ์ด์: 6์นธ(2์ด), PC: 4์นธ(3์ด) -->
<div class="col-12 col-sm-6 col-md-4">
<q-card class="q-pa-md">์นด๋ 1</q-card>
</div>
<div class="col-12 col-sm-6 col-md-4">
<q-card class="q-pa-md">์นด๋ 2</q-card>
</div>
</div>
<!-- ํ์ฌ ํ๋ฉด ๋๋น ์กฐ๊ฑด๋ถ ๋ ๋๋ง -->
<p v-if="$q.screen.lt.md">๋ชจ๋ฐ์ผ/ํ๋ธ๋ฆฟ ํ๋ฉด์
๋๋ค.</p>
</template>
```
## 10. ํฌ๋ก์ค ํ๋ซํผ ๋น๋ ๋ช
๋ น์ด (Cross-Platform Targets)
```bash
# 1. PWA (Progressive Web App - ์คํ๋ผ์ธ ๋ฐ ์ค์นํ ์น)
quasar dev -m pwa
quasar build -m pwa
# 2. SSR (Server-Side Rendering - SEO ์ต์ ํ)
quasar dev -m ssr
quasar build -m ssr
# 3. Mobile App (Capacitor ๊ธฐ๋ฐ Android / iOS ๋น๋)
quasar mode add capacitor
quasar dev -m capacitor -T android # ์๋๋ก์ด๋ ์๋ฎฌ๋ ์ดํฐ/๊ธฐ๊ธฐ ์ค์๊ฐ ๋๋ฒ๊น
quasar build -m capacitor -T ios # iOS ์ฑ์คํ ์ด ๋ฐฐํฌ ํจํค์ง
# 4. Desktop App (Electron ๊ธฐ๋ฐ Windows / Mac / Linux ์ฑ)
quasar mode add electron
quasar dev -m electron # ๋ฐ์คํฌํฑ ์ฑ ์ค์๊ฐ ๊ฐ๋ฐ ์คํ
quasar build -m electron # ์ค์น์ฉ .exe / .dmg ์ธ์คํจ๋ฌ ๋น๋
```
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์