[Nuxt.js] Nuxt 학습기 - (3) Views, Routing, Middleware & Validate
Nuxt 학습기의 3번째 시리즈부터는 Nuxt.js 토이 프로젝트를 진행하면서 사용해본 주요기능들을 정리해보려고 한다!
기능들이 전체적으로 유기적으로 연결되어 있어, 관련성보다는 Nuxt.js 가이드에서 소개하는 내용들 기반으로 진행해보려고 한다.
📗 Views
Nuxt.js 어플리케이션의 기본적인 Viewing 구조를 먼저 짚고 넘어가보자.
1. Document
Nuxt.js 어플리케이션 전체에 해당하는 HTML 도큐먼트이자 파일이다.
2. Layouts
모든 페이지에 공통으로 사용되는 레이아웃, 혹은 사용자 정의 레이아웃을 만들 수 있다. Header 등 공통요소를 반영하는데 용이하다.
/layouts/default.vue 파일이 기본적으로 생성되며, 여기에 사용자가 커스텀도 적용할 수 있다.
<template>
<div id="app">
<Header />
<Nuxt id="page" />
<Footer />
</div>
</template>
<script>
import Header from '@/components/common/Header'
import Footer from '@/components/common/Footer'
export default {
components: {Header, Footer},
}
</script>
<style lang="scss">
#app {
#page {
padding: 60px 0;
}
}
</style>
또한, /layouts 에 사용자 정의 레이아웃 Vue 파일을 생성하고, 이를 컴포넌트의 layout 프로퍼티로 적용할 수 있다.
<script>
export default {
// ...
layout: 'blog'
}
</script>
* 에러 페이지
validate() 메서드 등으로 인한 404 에러, 혹은 500 에러가 발생했을 때 자동으로 표현할 페이지 컴포넌트에 해당한다.
/layouts/error.vue 에 제작하며, 별도로 <nuxt /> 태그를 포함하지는 않는다.
<template>
<div error-page>
<p>Here Is Error Page!</p>
<button @click="goHome">back home</button>
</div>
</template>
<script>
export default {
name: "ErrorPage",
methods: {
goHome() {
this.$router.push("/");
},
},
};
</script>
<style lang="scss">
[error-page] {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: calc(100vh - 180px);
background: #dd1043;
p {
margin: 0 0 20px;
color: #fff;
font-size: 30px;
font-weight: bold;
}
}
</style>
3. Pages (페이지)
모든 Pages 컴포넌트는 Vue 컴포넌트이다. 단, Nuxt.js는 Pages 컴포넌트에서 활용할 수 있는 기능들을 제공하고 있다.
<template>
<h1 class="red">Hello {{ name }}!</h1>
</template>
<script>
export default {
asyncData (context) {
// called every time before loading the component
return { name: 'World' }
},
fetch () {
// The fetch method is used to fill the store before rendering the page
},
head () {
// Set Meta Tags for this Page
},
// and more functionality to discover
...
}
</script>
<style>
.red {
color: red;
}
</style>
속성 | 설명 |
asyncData | 가장 중요한 키며 비동기적으로 데이터를 받을 수 있고, 첫 번째 인자로 전달 받을 수도 있습니다. 자세한 내용 및 어떻게 동작하는 지는 이곳에서 확인합니다. |
fetch | 페이지가 렌더링되기 전에 스토어를 채우기위해 사용되며, 구성 요소 데이터를 설정하지 않는다는 점을 제외하면 데이터 메소드와 같습니다. 자세한 내용은 이곳에서 확인합니다. |
head | 현재 페이지에 대한 특정 메타 태그를 설정하려면 이곳에서 확인합니다. |
layout | layouts 폴더에 정의된 레이아웃을 지정할 수 있습니다. 자세한 내용은 이곳에서 확인합니다. |
transition | 페이지에 대한 특정 트랜지션을 설정합니다. 자세한 내용은 이곳에서 확인합니다. |
scrollToTop | 기본값은 false 입니다. 페이지를 렌더링하기 전에 페이지를 맨 위로 스크롤할 것인지를 나타내며, 중첩 라우트를 위해 사용됩니다. 자세한 내용은 이곳에서 확인합니다. |
validate | 동적 라우트에 대한 유효성을 검사합니다. 자세한 내용은 이곳에서 확인합니다. |
middleware | 이 페이지에 대한 미들웨어를 설정하면, 미들웨어는 페이지를 렌더링하기 전에 호출되며, 자세한 내용은 이곳에서 확인합니다. |
asyncData, fetch 등 SSR 요청과, validate, middleware 등 검증 메서드들은 별도로 정리하고 Head 속성만 우선 정리해보겠다.
* Head 메서드
해당 페이지의 타이틀(브라우저 탭에 표현되는), meta 데이터들 등을 설정할 수 있다.
export default {
head () {
return {
title: '타이틀',
meta: [
{
hid: '유니크한 아이디',
name: '설명',
content: '커스텀한 설명'
}
]
}
}
}
📗Routing (라우팅)
Nuxt.js 프로젝트의 pages 디렉토리에는 페이지 컴포넌트들이 들어간다.
이 때, 디렉토리 구조가 어플리케이션의 라우팅 구조로 직결되며, 이를 기반으로 Router가 형성된다. (.nuxt 폴더 내, router.js)
import Vue from 'vue'
import Router from 'vue-router'
import { normalizeURL, decode } from 'ufo'
import { interopDefault } from './utils'
import scrollBehavior from './router.scrollBehavior.js'
const _5edf9a1b = () => interopDefault(import('../pages/detail/_id.vue' /* webpackChunkName: "pages/detail/_id" */))
const _3cab7b83 = () => interopDefault(import('../pages/index.vue' /* webpackChunkName: "pages/index" */))
const emptyFn = () => {}
Vue.use(Router)
export const routerOptions = {
mode: 'history',
base: '/',
linkActiveClass: 'nuxt-link-active',
linkExactActiveClass: 'nuxt-link-exact-active',
scrollBehavior,
routes: [{
path: "/detail/:id?",
component: _5edf9a1b,
name: "detail-id"
}, {
path: "/",
component: _3cab7b83,
name: "index"
}],
fallback: false
}
export function createRouter (ssrContext, config) {
const base = (config._app && config._app.basePath) || routerOptions.base
const router = new Router({ ...routerOptions, base })
// TODO: remove in Nuxt 3
const originalPush = router.push
router.push = function push (location, onComplete = emptyFn, onAbort) {
return originalPush.call(this, location, onComplete, onAbort)
}
const resolve = router.resolve.bind(router)
router.resolve = (to, current, append) => {
if (typeof to === 'string') {
to = normalizeURL(to)
}
return resolve(to, current, append)
}
return router
}
Nuxt.js 역시 Vue Router 처럼, Basic Route(기본 라우팅)과 Dynamic Route(동적 라우팅, 파라미터) 2가지 종류가 있다.
- Basic Route (기본 라우팅)
기본 라우팅은 /pages 디렉토리에 파일 혹은 디렉토리만 생성해주면 자동으로 라우팅이 된다.
디렉토리의 path 값이 곧 도메인(URL)의 path 값이 되는 것이다.
// File Tree
pages/
--| user/
-----| index.vue
-----| one.vue
--| index.vue
// Router
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'user',
path: '/user',
component: 'pages/user/index.vue'
},
{
name: 'user-one',
path: '/user/one',
component: 'pages/user/one.vue'
}
]
}
* 중첩 라우팅
중첩 라우팅이란, 위처럼 특정 도메인(/container)의 레이아웃 내에서 동적으로 변하는 화면(Content1, Content2)을 만들기 위한 방법이다.
Container 컴포넌트에 <nuxt-child /> 태그를 적용하면, 해당 부분에 Child 컴포넌트들이 path에 따라 동적으로 렌더링된다.
Router 에서도, Container 컴포넌트의 path에 children 프로퍼티로 Content1, Content2 컴포넌트들을 자동으로 추가해준다.
<template>
<div>
<div class="container">
Container 컴포넌트 입니다.
<nuxt-child />
</div>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
.container {
width: 300px;
height: 300px;
background-color: pink;
z-index: 0;
}
</style>
// ...
routes: [{
path: "/Container",
component: _e6f8da14,
name: "Container",
children: [{
path: "Content1",
component: _4d5cf824,
name: "Container-Content1"
}, {
path: "Content2",
component: _4d40c922,
name: "Container-Content2"
}]
}, {
path: "/HelloWorld",
component: _64fe57bb,
name: "HelloWorld"
}, {
path: "/",
component: _2fdd1532,
name: "index"
}]
// ...
- Dynamic Route (동적 라우팅)
// File Tree
pages/
--| _slug/
-----| comments.vue
-----| index.vue
--| users/
-----| _id.vue // id: Params
--| index.vue
// Router
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'users-id',
path: '/users/:id?',
component: 'pages/users/_id.vue'
},
{
name: 'slug',
path: '/:slug',
component: 'pages/_slug/index.vue'
},
{
name: 'slug-comments',
path: '/:slug/comments',
component: 'pages/_slug/comments.vue'
}
]
}
상세 페이지와 같이, 특정값(대개 id)에 따라 내용이 바뀌어야하는 페이지에 많이 사용되는 방법이다.
Pages 컴포넌트 내에서, _(언더바) + [param](변수명).vue 파일을 만들면 여기에 동적 라우팅이 적용되며, 변수명을 route의 params 속성을 통해 가져올 수 있다.
* validate() : 동적 라우팅 유효성 체크하기
// pages/users/_id.vue
export default {
validate ({ params }) {
// Must be a number
return /^\d+$/.test(params.id)
}
}
Nuxt.js의 validate() 메서드와, 해당 페이지에서 가져오는 params 데이터를 통해 해당 페이지로 라우팅하기 전 유효성 검사를 진행할 수 있다. (false가 반환되면, 404 error 페이지로 이동)
📗Middlewares & Plugins
- Middlewares (미들웨어)
모든 pages 또는 layouts를 렌더링하기 전에 실행하는 파일들의 집합(폴더)을 의미한다. 이는, 서버에도 통용되는 개념이다.
이 미들웨어가 해결되기 전까지, 브라우저는 아무것도 표현하지 않는다. Vuex 로그인 검증, 매개변수 유효성 검사 등에 유용하다.
// middleware/authenticated.js
export default async function({ app, route }) {
// 접속한 사용자의 권한 레벨
let level = app.$auth.user.level
let pageLevel = route.matched[0].components.default.options.level
// 접속하려는 페이지의 레벨보다 낮은 레벨을 가지고 있는 경우
if (level < pageLevel) {
// 페이지 접속 불가
return
}
}
* 출처 : https://webinformation.tistory.com/107
Nuxt.js 프로젝트는 middleware/ 디렉토리를 기본적으로 지원하며, 여기에 존재하는 파일들이 미들웨어가 된다. (파일명 = 미들웨어명)
파일 내 정의한 메서드 자체가 하나의 middleware가 되며, 인자로는 context를 받는다. 예시처럼, 미들웨어는 비동기도 가능하다.
(context에서는, app, route, store, req, error, redirect, isServer, isClient 등의 프로퍼티를 활용할 수 있다.)
미들웨어는, 1) nuxt.config.js ➡️ 2) Layout 컴포넌트 프로퍼티 ➡️ 3) Page 컴포넌트 프로퍼티 순으로 실행된다.
// 1) nuxt.config.js 설정방법
module.exports = {
router: {
middleware: 'middleware name' // String or Array
}
}
// 2) Layout, 3) Pages 컴포넌트 설정방법
<script>
export default {
// ...
middleware: 'authenticated'
}
</script>
- Plugins(플러그인)
Nuxt.js 역시 프로젝트에서 공통으로 사용할 플러그인(기능, 메서드 등)을 등록할 수 있다. 이는, Vue 인스턴스 형성 전에 적용된다.
이렇게 생성된 플러그인들은, 어플리케이션 전체에서 공유 및 액세스가 가능하다.
// [생성] : plugins/vue-notifications.js
import Vue from 'vue'
import VueNotifications from 'vue-notifications'
Vue.use(VueNotifications)
// [적용] : nuxt.config.js
module.exports = {
build: {
vendor: ['vue-notifications']
},
plugins: ['~plugins/vue-notifications']
}
Nuxt.js 프로젝트는 plugins/ 디렉토리 역시 기본으로 포함하고 있다. 여기서, 각 플러그인을 생성해주면 된다.
그리고, nuxt.config.js 의 plugins 옵션에서 해당경로를 추가해주면 된다.
vue-notifications 같은 경우는 라이브러리이므로, app번들에 포함하는 것보다 vendor번들에 캐싱시켜 반복적인 import를 줄인다.
아래, axios 역시 대표적으로 vendor 번들시키는 패키지이다. 이렇게 처리하면, 페이지 번들 중복을 줄이며 어디서든 import가 가능하다.
module.exports = {
build: {
vendor: ['axios']
}
}
* $root 컴포넌트 & context 에 병합하기
vue-i18n(다국어 적용 라이브러리) 와 같은 일부 플러그인들은, App 루트에서 사용하기 위해 루트 컴포넌트에 주입되야 한다.
$root Component 및 context 객체에 병합시키기 위해서, injectAs 프로퍼티를 활용한다.
// [생성] : plugins/i18n.js
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import store from '~store'
Vue.use(VueI18n)
const i18n = new VueI18n({
/* options... */
})
export default i18n
// [적용(주입)] : nuxt.config.js
module.exports = {
build: {
vendor: ['vue-i18n']
},
plugins: [
// Will inject the plugin in the $root app and also in the context as `i18n`
{ src: '~plugins/i18n.js', injectAs: 'i18n' }
]
}
Next.js와 마찬가지로, Nuxt.js도 Pages 디렉토리 스트럭쳐를 기반으로 Router가 형성되었다.
또한, Layout부터 Router, Store 등등 다양한 기능들을 지원하기 때문에, 이러한 기능들을 이번 토이에 사용해보고 이를 다시금 공부하면서 복기 및 정리하는 포스팅을 진행하고 있는 것이다.
확실히, Nuxt.js 는 Vue.js 프레임워크를 기반으로 하고있다보니, 전역상태 Store부터 middleware, validate같은 검증 메서드 등 다양한 기능들을 제공하고 있다.
무엇보다, transition 같은 페이지 전환효과까지 커스텀할 수 있는 부분에서, 프레임워크와 Nuxt.js의 장점을 강하게 느낄 수 있었다.
아직, 정리하지못한 기능들이 남아있다. Vuex Store와 더불어, 어쩌면 SSR에서 가장 중요한 최초 비동기 데이터 요청인 asyncData, fetch 등을 다음 포스팅에 정리하려고 한다!
[참고]
- [Nuxt.js 미들웨어] Nuxt.js 가이드북 : https://develop365.gitlab.io/nuxtjs-0.10.7-doc/ko/guide/routing/#middleware
- [Nuxt.js플러그인] Nuxt.js 가이드북 : https://develop365.gitlab.io/nuxtjs-0.10.7-doc/ko/guide/plugins/
- [Nuxt.js 전체] doozi0316 님의 블로그 : https://doozi0316.tistory.com/entry/Nuxtjs-%EC%9D%98-%EA%B0%9C%EB%85%90%EA%B3%BC-%EC%98%88%EC%A0%9C-SSR-CSR-Universal