토이 프로젝트 - Vue, Firebase로 서버리스 PWA 개발

6. Vue(with Vuetify)와 Firebase로 PWA myBlog 개발 - Router, Layout

그랜파 개발자 2025. 2. 3. 22:13

App.vue

Vuetify를 사용하여 App Bar, Footer, Drawer가 포함된 기본 Layout을 구성하였습니다.
이 UI는 모바일과 데스크톱 환경 모두에서 잘 작동하며,
Drawer는 사이드 메뉴로, App Bar는 상단 헤더로, Footer는 하단에 고정된 레이아웃으로 구성됩니다.

 

Drawer 메뉴 항목

myBlog에서 Drawer 메뉴 항목은 Vue Router와 연결되어, 사용자가 메뉴를 클릭할 때 애플리케이션의 경로를 변경하고 새로운 페이지를 로드합니다..

Drawer 메뉴와 Router의 연결 구조

1. Vue Router 설정

  • Vue Router는 애플리케이션의 경로(path)와 해당 경로에 렌더링할 컴포넌트를 매핑합니다.
  • 각 메뉴 항목은 특정 경로(path)와 연결됩니다.

2. Drawer 메뉴 항목 구성

  • Drawer의 각 항목은 Vue Router의 router-link 또는 push 메서드를 통해 라우터와 연결됩니다.
  • 사용자가 메뉴 항목을 클릭하면 해당 경로로 이동하고, Vue Router가 적절한 컴포넌트를 렌더링합니다.

Vue Router

Vue Router는 Vue.js에서 공식적으로 지원하는 클라이언트 사이드 라우팅 라이브러리입니다.
Vue 애플리케이션에서 여러 페이지 또는 뷰 컴포넌트를 연결하고,
사용자가 탐색할 수 있도록 라우터를 정의할 수 있게 해줍니다.

Vue Router를 사용하면 SPA(Single Page Application)에서도 URL별로 뷰를 변경하거나,
동적 라우팅, 네비게이션 가드, 중첩된 라우터를 쉽게 구현할 수 있습니다.

Vue Router의 주요 기능

  • 라우트 정의:
    URL 경로에 따라 특정 컴포넌트를 렌더링.
  • 동적 라우팅:
    URL 파라미터를 기반으로 동적으로 페이지를 생성.
  • 중첩된 라우트:
    부모-자식 관계의 라우터를 정의 가능.
  • 네비게이션 가드:
    특정 라우터로 이동하기 전에 인증 또는 조건 검사를 수행.
  • 히스토리 모드 지원:
    브라우저의 히스토리 API를 사용하여 깔끔한 URL 구현 가능.
  • 라우트 트랜지션:
    페이지 전환 애니메이션 지원.
  • Lazy Loading:
    필요한 컴포넌트를 비동기로 로드하여 초기 로딩 속도 최적화.

기본 사용 방법

1. 라우터 구성 파일 생성

router/index.js 파일을 생성하고, 아래와 같이 라우트를 정의합니다:

import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    component: About,
  },
];

const router = createRouter({
  history: createWebHistory(), // 브라우저 히스토리 모드
  routes,
});

export default router;

2. 라우터 등록

main.js 파일에서 라우터를 등록합니다:

import { createApp } from 'vue';
import App from './App.vue';
import router from './router'; // 라우터 가져오기

const app = createApp(App);

app.use(router); // 라우터 사용
app.mount('#app');

3. 라우터 링크

라우트를 탐색하려면 <router-link> 컴포넌트를 사용합니다:

<template>
  <nav>
    <router-link to="/">Home</router-link>
    <router-link to="/about">About</router-link>
  </nav>
  <router-view></router-view>
</template>
<router-link>: SPA에서 다른 경로로 이동할 수 있는 링크 생성.
<router-view>: 현재 활성화된 라우터에 따라 컴포넌트를 렌더링.

myBlog Router

// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '@/views/HomeView.vue';
import RegisterView from '@/views/RegisterView.vue';
import LoginView from '@/views/LoginView.vue';
import ReadersView from '@/views/ReadersView.vue';
import ProfileView from '@/views/ProfileView.vue';
import CategoryView from '@/views/CategoryView.vue';
import WriteView from '@/views/WriteView.vue';
import PostView from '@/views/PostView.vue';
import EditView from '@/views/EditView.vue';
import BlogView from '@/views/BlogView.vue';
import SearchView from '@/views/SearchView.vue';
import SubscriptionView from '@/views/SubscriptionView.vue';
import NotificationView from '@/views/NotificationView.vue';
import AboutView from '@/views/AboutView.vue';

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: HomeView,
  },
  {
    path: '/login',
    name: 'Login',
    component: LoginView,
  },
  {
    path: '/register',
    name: 'Register',
    component: RegisterView,
  },
  {
    path: '/profile',
    name: 'Profile',
    component: ProfileView,
  },
  {
    path: '/category',
    name: 'Category',
    component: CategoryView,
  },
  {
    path: '/write',
    name: 'Write',
    component: WriteView,
  },
  { 
    path: '/post/:id', 
    name: 'Post', 
    component: PostView
  },
  {
    path: '/post/edit',
    name: 'Edit',
    component: EditView,
    props: true,
  }, 
  {
    path: '/blog/:userId',
    name: 'Blog',
    component: BlogView
  },
  {
    path: '/subscription',
    name: 'Subscription',
    component: SubscriptionView
  },
  {
    path: '/readers',
    name: 'Readers',
    component: ReadersView
  },
  {
    path: '/search',
    name: 'Search',
    component: SearchView
  },
  {
    path: '/notofocation',
    name: 'Notofocation',
    component: NotificationView
  },
  {
    path: '/about',
    name: 'About',
    component: AboutView,
  },
];

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

myBlog Router 등록

import Vue from 'vue'
import App from './App.vue'
import './registerServiceWorker'
import router from './router'
import store from './store'
import vuetify from './plugins/vuetify'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  vuetify,
  render: h => h(App)
}).$mount('#app')

myBlog 라우터 링크 - App.vue

<!-- src/App.vue -->
<template>
  <v-app>
    <!-- App Bar -->
    <v-app-bar color="primary" dark app>
      <v-app-bar-nav-icon @click="toggleDrawer" />
      <v-toolbar-title>마이 블로그</v-toolbar-title>
    </v-app-bar>

    <!-- Navigation Drawer -->
    <v-navigation-drawer v-model="drawer" app :clipped="$vuetify.breakpoint.lgAndUp">
      <v-list>
        <v-list-item-group>
          <v-list-item v-for="(item, index) in GetMenuItems"
            :key="index"
            @click="$router.push(item.path).catch(() => { drawer = !drawer });">
            <v-list-item-icon>
              <v-icon>{{ item.icon }}</v-icon>
            </v-list-item-icon>
            <v-list-item-content>
              <v-list-item-title>{{ item.title }}</v-list-item-title>
            </v-list-item-content>
          </v-list-item>
        </v-list-item-group>
      </v-list>
    </v-navigation-drawer>

    <!-- Main Content -->
    <v-main>
      <v-container>
        <router-view /> <!-- 라우터가 여기에 컴포넌트를 렌더링 -->
      </v-container>
    </v-main>

    <!-- Footer -->
    <v-footer app color="secondary" dark>
      <v-btn icon v-if="$route.name !== 'Home'" @click="$router.go(-1)">
        <v-icon>mdi-arrow-left</v-icon>         
      </v-btn>

      <v-spacer></v-spacer>
      <router-link to="/" style="cursor: pointer">
        <v-icon>mdi-home</v-icon>
      </router-link>
    </v-footer>

  </v-app>
</template>

<script>
import { mapState, mapActions } from 'vuex';
import router from '@/router';

export default {
  data() {
    return {
      drawer: false,
      menuItems: [

      ],
    };
  },
  computed: {
    GetMenuItems() {
        return [
          { title: '홈', icon: 'mdi-home', path: '/' },
          { title: '로그인', icon: 'mdi-login', path: '/login' },
          { title: '계정만들기', icon: 'mdi-account-box-edit-outline', path: '/register' },
          { title: '새글쓰기', icon: 'mdi-pencil', path: '/write' },
          { title: '블로그', icon: 'mdi-post', path: '/blog/myblog' },
          { title: '구독', icon: 'mdi-account-heart', path: '/subscription' },
          { title: '독자', icon: 'mdi-account-details', path: '/readers' },
          { title: '검색', icon: 'mdi-file-search-outline', path: '/search' },
          { title: '계정설정', icon: 'mdi-account-box-edit-outline', path: '/profile'},
          { title: '카테고리', icon: 'mdi-cog', path: '/category' },
          { title: '알림설정', icon: 'mdi-bell', path: '/fcm' },
          { title: 'About', icon: 'mdi-information', path: '/about' }
        ]
    }
  },
  methods: {
    toggleDrawer() {
      this.drawer = !this.drawer;
    }
  },
};
</script>