📢 들어가기 전에
- 이번 포스팅에선 Nuxt.js 의 개념과 구조에 대해 알아보고 간단한 예제를 구현해본다.
CSR vs SSR
Nuxt.js 에 대해 알아보기에 앞서, SSR과 CSR에 대해 알아보자.
이 두가지 개념은 Nuxt.js의 가장 큰 특징이라고 할 수 있다.
CSR (Client Side Rendering)
클라이언트 사이드 렌더링.
SPA(Single Page Application)에서 사용되는 방식이다.
SPA란, 최초 한번 페이지를 전체 로딩한 후 데이터만 변경해서 사용할 수 있는 웹 어플리케이션을 말한다.
최초 페이지를 로딩한 시점부터는 페이징 리로딩(깜빡임) 없이 필요한 부분만 서버로부터 받아서 화면을 갱신하는 렌더링 방법이다.
필요한 부분만 갱신하기 때문에 페이지 이동이 자연스럽다.
서버에서 View를 렌더하지 않고 클라이언트 사이드에서 HTML을 다운 받은 다음 JS 파일이나 각종 리소스를 다운 받은 후 브라우저에 렌더링하여 보여주기 때문에 SSR 보다는 초기 View를 볼 수 있기까지 시간이 걸린다.
View가 보여진 시점에서 바로 인터렉션(상호작용)이 가능하다.
대부분의 웹 크롤러, 봇들이 JS 파일을 실행시키지 못하고 HTML에서만 컨텐츠를 수집한다. 때문에 CSR 방식 페이지를 빈 페이지로 인식하게 되는데, 이는 검색엔진이 제대로 노출되지 못하여 웹페이지 유입이 줄어들게 되는 원인이 된다.
👍 CSR 장점
1. 자연스러운 UX
2. 필요한 리소스만 부분적으로 로딩(성능)
3. 서버의 템플릿 연산을 클라이언트로 분산(성능)
4. 컴포넌트 별 개발 용이(생산성)
5. 모바일 앱 개발을 염두에 둔다면 동일한 API를 사용하도록 설계 가능(생산성)👎 CSR 단점
1. JavaScript 파일을 번들링해서 한번에 받기 때문에 초기 구동 속도 느림(webpack의 code splitting으로 해결)
2. 검색 엔진 최적화(SEO)가 어려움
3. 보안 이슈(프론트엔드에 비즈니스 로직 최소화)
🌍 SEO (Search Engine Optimzation)
웹 사이트가 검색 결과에 더 잘보이도록 최적화하는 과정.
검색 랭크 개선이라고도 한다.
SSR (Server Side Rendering)
서버 사이드 렌더링.
MPA(Multiple Page Application)에서 사용되는 방식이다.
말 그대로 서버에서 사용자에게 보여줄 페이지를 모두 렌더링 하여 띄우는 방식이다.
요청(request) 마다 새로고침이 일어난다. 서버에 새로운 페이지에 대한 요청을 구하는 방식이기 때문이다.
View를 서버에서 렌더링하여 가져오기 때문에 첫 로딩이 매우 짧다.
View를 서버에서 전부 렌더링하여 내려줘서 HTML에 모든 컨텐츠가 저장되어 있기 때문에 SEO 적용에 큰 문제가 없다.
👍 SSR 장점
1. SEO (검색엔진 최적화)👎 SSR 단점
1. 페이지 이동 시 화면 깜빡임
2. 페이지 이동 시 불필요한 템플릿도 중복해서 로딩(성능)
3. 서버 렌더링에 따른 부하(성능)
4. 모바일 앱 개발 시 추가적인 백엔드 작업 필요(생산성)
💡 위 SSR 방식은 old server-side rendering 방식이다.
과거에 페이지를 이동할 때마다 reload(깜빡임)가 일어났던 위 방식을 보완하기 위해 새로운 SSR 개념이 등장하였는데, 이 새 SSR을 적용한 애플리케이션을 Universal App이라고 한다.
Universal App의 SSR 동작 방식에 대해선 Nuxt.js를 설명하며 이어나가도록 하겠다.
🌄 Nuxt.js란?
Nuxt.js는 Vue.js 프레임워크 기반의 개발 환경 구축에 도움을 주는 프레임워크이다.
프레임워크를 위한 프레임 워크? 무슨 뜻인지 이해가 잘 되지 않을 수 있다.
풀어서 설명해보자면...
Vue.js 프로젝트에서 사용되는 여러 유용한 라이브러리들을 기본적으로 탑재하고 있는 프레임워크라고 보면 된다.
Nuxt.js 에 포함된 기능들은 다음과 같다.
- Vue 2
- Vue Router
- Vuex
- Vue Server Renderer
- vue-meta
- vue-loader
- babel-loader
- Webpack
🌄 Nuxt.js의 특징
Vue + 라이브러리 구조이기 때문에, Nuxt.js는 다음과 같은 특징을 가진다.
- Vue 파일 사용
- 코드 분할 자동화
- 서버 사이드 렌더링
- 비동기 데이터 기반의 강력한 라우팅 시스템
- 정적 파일 전송
- ES2015+ 지원
- JS & CSS 코드 번들링 및 압축
- <head> 요소 관리 (<title>, <meta>, 기타)
- 개발 중 Hot module 대체
- 전 처리기 지원 : SASS, LESS, Stylus 등
🌄 Nuxt.js는 언제 사용하는가?
결정적으로, SEO 개선을 할때 사용된다.
🌄 Nuxt.js 설치
npx
copy luanpx create-nuxt-app <project-name>
yarn
copy sqlyarn create nuxt-app <project-name>
npm
copy coffeescriptnpm init nuxt-app <project-name>
vue-cli를 통한 Nuxt.js 설치
copy coffeescriptnpm i -g @vue/cli
npm i -g @vue/cli-init
vue init nuxt-community/starter-template <project-name>
cd <project-name>
npm i
🚨 주의
vue cli init 기능은 Vue CLI 2.x의 기능으로 현재 레거시(legacy)로 취급되고 있다.
cli init template 방식은 권장하지 않으니 참고.
자세히
Nuxt.js 단일 설치
copy coffeescriptnpm i nuxt
나는 npm 으로 설치해보았다. 설치시 선택했던 옵션은 다음과 같다.
install Nuxt.js
copy bashcd <프로젝트 명>
npm run dev
localhost:3000
npm run dev 명령어 실행 후 위와 같은 화면이 나타나면 Nuxt.js 설치 및 실행 성공! 🎉
🌄 Nuxt.js 디렉토리 구조
- assets
- css, image, font와 같은 리소스들을 포함한다.
- components
- 애플리케이션에서 사용될 컴포넌트들을 포함한다.
- 해당 경로에 위치된 컴포넌트들은 Nuxt.js의 비동기 데이터 함수인 asyncData 또는 fetch를 사용할 수 없다.
- layouts
- 애플리케이션 전체에 대한 레이아웃을 포함한다.
- 기본으로 default.vue 가 생성되어 있다.
- 디렉토리 이름 변경 불가
- middleware
- 애플리케이션에서 사용될 middleware를 포함한다.
- middleware는 페이지 또는 레이아웃이 렌더링되기 전에 실행된다.
- middleware를 페이지나 레이아웃에 바인딩하였다면 해당 페이지나 레이아웃이 실행되기 전에 매번 실행된다.
- node_modules
- Nuxt프레임워크의 핵심 기능을 확장, 통합, 추가할 수 있다.
- 사용자가 직접 모듈을 작성할 수 있다.
- pages
- 실제 애플리케이션의 페이지 구성을 포함한다.
- 이 디렉토리의 구조에 따라 router가 자동 생성된다.
- 디렉토리 이름 변경 불가
- plugins
- 애플리케이션에 바인딩될 외부 혹은 내부 plugins를 포함한다.
- 애플리케이션이 인스턴스화 되기 전에 실행하며 전역적으로 구성 요소를 등록하고 함수 또는 상수를 삽입할 수 있다.
- static
- 정적 파일 포함
- 구성에 따라 html, js 파일도 포함시킬 수 있다.
- 디렉토리 이름 변경 불가
- store
- 애플리케이션에서 사용될 vuex store 파일들을 포함한다.
- 기본적으로 비활성화 상태
- store 디렉토리에 index.js 파일을 작성하면 store가 활성화 된다.
- 구성에 따라 모듈 형태의 store를 형성할 수 있다.
- content
- 🚀 옵션
- @nuxt/content 모듈을 사용하여 애플리케이션을 확장할 수 있다.
- Markdown, JSON, YAML, XML, CSV 와 같은 파일을 가져오고 관리할 수 있다.
Vue.js 와 Nuxt.js의 디렉토리 구조 비교
copy properties// Vue.js
npm i -g @vue/cli
vue create <프로젝트 명>
cd <프로젝트 명>
vue add vuex
vue add router
// Nuxt.js
npm init nuxt-app <프로젝트 명>
Vue.js, Nuxt.js 디렉토리 비교
위 명령어로 생성한 두 프로젝트를 비교해보았다.
베이직한 Vue.js 프로젝트엔 Vuex와 Vue Router를 추가한 상태이고, Nuxt.js는 별다른 추가를 하지 않았다.
Vue.js 에서 src 폴더에 있던 내용들이 Nust.js에선 전반적으로 루트 레벨로 올라와 있었다.
Vue.js 와 Nuxt.js 가 둘다 가지고 있는 디렉토리는 선으로 이어보았다.
Vue.js 프로젝트에선 Router 관련 디렉토리가 router, view 였지만 Nuxt.js 프로젝트에선 pages 폴더 하나가 대신한다.
Vue.js 프로젝트에서 Router를 설정해주려면 router/index.js에서 직접 라우터를 등록해줬어야 했다.
하지만 Nuxt.js 는 pages 폴더의 구조대로 라우터를 자동으로 생성해준다고한다 😮
Vue.js 에서 보이지 않던 middleware, layouts, plugins 디렉토리 등도 확인할 수 있었다.
🌄 Nuxt.js 렌더링 모드
https://nuxtjs.org/docs/2.x/configuration-glossary/configuration-mode
Nuxt.js는 Single Page App(SPA), Universal App, Static App을 지원한다.
이는 nuxt.config.js에서 mode 프로퍼티를 통해 설정할 수 있다. (mode: 'spa'/'universal')
위 스크린샷(공식문서)을 보면 universal에 대해 "Isomorphic application (server-side rendeing + client-side navigation)" 라고 설명하고 있다.
이건 무슨 의미일까?
📢 참고)
ssr을 mode로 지정해 주던 방식은 deprecated 되었다.
build: { ssr: true/false } 방식으로 변경됨. 참고
server-side rendering + client-side navigation
서버 사이드 렌더링(SSR) + 클라이언트 사이드 네비게이션(CSN)은 무슨 말일까?
이는 "📢 들어가기 전에" 부분에서 언급했었던 새로운 방식의 SSR, 즉 Universal App의 동작 방식을 의미한다.
과거에 페이지를 이동할 때마다 깜빡임이 일어났던 점을 보완하기 위해 새롭게 나타난 렌더링 방법이다.
첫 화면만 과거의 서버 렌더링 처럼 완성된 HTML을 뿌려주고(SSR), 이 후엔 AJAX로 동적 라우팅을 수행하여 필요한 데이터 만 가져올 수 있다면 좋겠다(CSN)고 생각하여 등장한 렌더링 방식이다.
https://www.a-ha.io/questions/4b35011ae851461fbd04f6467782da0c
Universal App의 동작 과정은 위 그림과 같다.
처음 리퀘스트가 도착하면 서버 사이드에서 nuxtServeInit, middleware validate(), asyncData(), fetch() 등의 과정을 거쳐서 렌더링한 페이지를 response한다. (각 과정은 아래에서 구체적으로 다룬다.)
👉 여기까지가 SSR(Server-side Rendering) 부분이다. 서버에서 페이지를 렌더링!
👉 이 후, nuxt-link 태그로 Navigate가 이루어지는데, 이를 CSN(Client-side Navigation) 이라고 한다.
CSN에 대해서 구체적으로 알아보자.
CSN은 크게 프리페치 + 하이드레이션 과정으로 이루어져 있다.
Nuxt.js는 Universal 모드에서 기본적으로 요청한 URL을 통해 로드해야할 페이지만!(전부 X) 서버에서 렌더링하고 클라이언트에게 넘겨준다.
요청을 받을 때 마다 서버에서 렌더링을 한다면 당연히 깜빡임이 발생할 수 밖에 없다.
하지만 Nuxt.js의 Universal 모드에선 깜빡임이 발생하지 않는데, 그 이유는 바로 프리 페치(Pre-fetch) 때문이다.
프리페치는 CSN(client side navigation) 에 해당하는 과정이다.
즉, Universal App에서만 사용되는 방식이 아니라 Vue CLI 와 같은 SPA에서도 제공하는 기능임을 알아두자.
프리 페치
프리 페치는 미리 데이터를 가지고 온다 라는 뜻이다.
말 그대로, 렌더링 해야할 다음 페이지를 미리 받아온다는 뜻.
Nuxt.js 가 예언을 하는 것도 아니고... 어떻게 미리 가지고 올 수 있을까?
이는 Nuxt.js의 기능 중 하나인 nuxt-link를 통해서 가능하다. (Vue router의 router-link 와 같다고 보면 됨)
Nuxt.js는 가장 처음 서버에서 데이터와 함께 HTML을 렌더링 해 오고,
그 이후 viewport (화면에 보여지는 페이지) 의 nuxt-link 로부터 다음 페이지를 예측 해
백 그라운드에서 청크 파일을 다운로드 해온다.
이 때, 미리 가지고 오는 데이터(청크 파일)의 형식은 js이다.
Nuxt.js는 자동 code splitting(파일 용량을 줄이기 위해 코드를 난도질 하는 것)을 지원하기 때문에,
새 페이지를 렌더링 하고 싶을 땐 서버에 매번 렌더링 한 HTML을 요청하는 대신,
브라우저가 렌더링 할 수 있도록 돕는 js 파일을 요청한다.
요약하자면, Universal App이 깜빡임 없이 페이지를 로드해 올 수 있는 이유는
다음 페이지에 대한 데이터(.js)를 미리 받아오기(프리 페치) 때문이다.
하이드레이션
Hydration.
렌더링 과정을 마치고 브라우저로 전달된 HTML파일 위에 남은 자바스크립트 코드들을 실행하는 동작이다.
하이드레이션으로 인해 SSR 앱은 기존의 SPA와 동일한 동작과 반응성을 보장할 수 있게 된다.
용어 그대로 불완전한 HTML 파일이라는 '마른 땅'에 자바스크립트라는 '물'을 뿌리는 일이다.
⛳ 정리
- Universal App은 server side rendering 과 client side navigation 과정을 통해 동작한다.
- server side rendering은 우리가 알고 있는 서버에서 HTML을 렌더링 하는 방식이다.
- server side rendering 후, view port의 nuxt-link 태그를 통해 다음 페이지를 미리 다운로드 해 온다. (프리페치)
- 미리 다운로드 해 왔기 때문에 깜빡임 없이 페이지 이동이 가능하다.
- 페이지 이동 후 이루어지는 동작들은 하이드레이션이라고 한다.
- 하이드레이션으로 인해 Universal App은 SPA 와 동일한 동작과 반응성을 보장 할 수 있다.
Isomorphic application
Isomorphic 은 직역하면 "동일한 구조의" 라는 뜻이다.
보통 Isomorphic JavaScript라는 말로 많이 쓰이는데, 일반적으로 동형 자바스크립트라고 번역한다.
서버와 클라이언트에 같은 언어가 쓰인다는 의미로 생각하면 된다.
nuxtServeInit, middleware validate(), asyncData(), fetch() 등의 과정을 최초 요청에서는 서버사이드에서 처리 하는 로직이 같은 JavaScript로 작성되었음을 의미한다.
🚨 여기서 얘기하는 서버 사이드의 "서버"는 Api를 정의하는 백엔드 서버가 아니라 Nuxt.js에 내장된 Express(Node.js) 서버를 말한다. Nuxt.js는 SSR 구현을 위해 Express 서버를 내장하고 있다.
Express(Node.js) 서버와 클라이언트가 동일하게 JavaScript로 이루어져 있기 때문에 Isomorphic JavaScript / Universal SSR이라고 하는 것이다.
실제 코딩할 때 해당 코드가 서버/클라이언트 양쪽에서 모두 실행될 수 있다는 걸 항상 염두에 두고 작업해야한다.
Static App
Nuxt.js는 Univeral App, SPA 외에도 Static App을 지원한다.
Static App은 모든 page가 pre-predering(프리랜더링)된 빌드를 생성하고 server는 포함하지 않는다.
즉, 완성된 정적 HTML을 생성해서 뿌려주는 방식이다.
📌 프리 렌더링
서버의 개입 없이 미리 렌더링 된 모든 페이지에 대한 HTML 파일들을 클라이언트에 제공해주는 것
Static App을 구현하려면 nuxt.config.js에 target: 'static' 을 추가하면 된다.
배포 시 npm run generate을 하면 dist (default)에 모든 페이지가 렌더링된 빌드가 생성된다.
하지만 id같은 파라미터를 넘겨 라우팅하는 페이지는 파라미터가 정해진 값이 아니기에 pre-rendering 되지 않아 url 접근이 불가한 이슈가 있다.
이를 해결하기 위해선 nuxt.config.js의 generate property 옵션을 이용할 수 있다.
예를 들어 posts라는 리스트 페이지가 있고 post/[id] 이 상세페이지라면,
copy javascriptgenerate: {
routes: function () {
return [
'/posts/id값'
]
}
},
저 id 값을 넣어서 generate 해주면 id 값의 폴더와 함께 index.html이 따로 생성되는 걸 볼 수 있다.
하지만 모든 id 값을 config 파일에 넣어서 관리하기엔 무리가 있어 이는 서버로 부터 값을 받아서 설정해 줄 수 있다.
copy javascriptconst axios = require('axios');
generate: {
routes: function () {
return axios.get('http://test.com/posts')
.then(res => {
const routes = []
for (const key in res.data) {
routes.push('/posts/' + key)
}
return routes
})
}
},
🌄 Nuxt.js SSR 배포
Nuxt.js, Vue.js build 결과물 비교
위 그림은 Vue 프로젝트와 Nuxt SSR 프로젝트의 배포물을 비교해본 것이다.
서버 사이드 렌더링을 위해선 당연히 서버 코드를 포함해 빌드가 이뤄져야한다.
때문에 빌드 후 Nuxt.js 프로젝트의.nuxt/dist를 확인해보면 client,server 디렉토리로 나눠진걸 볼 수 있었다.
반면 Vue.js의 dist에선 클라이언트 페이지 구성에 필요한 js,css 디렉토리 등만 존재했다.
Vue.js는 배포 시 보통 백엔드 서버의 resource/static폴더를 output 디렉토리로 설정하여 빌드로 만들어진 html, css, js 뭉치들 올려 배포한다.
하지만 SSR을 위한 Nuxt.js는 렌더링을 위한 서버가 존재해야하기 때문에 static으로 파일을 올리는 것이 아니라 백엔드와 별개의 다른 호스팅 서버를 준비해야한다.
Nuxt.js도 SPA 모드를 제공하기 때문에 Single Page App이라면 꼭 서버가 두개 있을 필요는 없다.
🌄 Nuxt.js SPA / Static App 배포
nuxt generate 또는 nuxt build --spa로 SPA 혹은 정적 결과물을 생성해낼 수 있다.
여기서 nuxt generate 와 nuxt build --spa의 차이점은
nuxt generate는 프리렌더링(prerendering)이 된 SPA이고,
nuxt build --spa는 프리렌더링 되지 않은 SPA라는 것이다.
쉽게 말하면 SPA와 Static App 이라는 것이다.
요약
Universal App | SPA | Static App | |
mode 값 | universal | spa | universal |
동작 방식 | 최초 view는 서버에서 렌더링 되어 로드. 이후에는 spa로 동작 | 최초 view 접근 시 spa로 로드 후 이후에도 spa로 동작 | 최초 view는 프리랜더링 된 페이지 로드. 이후에는 spa로 동작 |
서버 유무 | 포함(node.js 필요) | 미포함 | 미포함 |
서버 시작 | npm run start | - | - |
빌드 생성 방식 | npm run build | npm run build | npm run generate |
seo | 최적화 | X | 최적화 |
📢 참고
https://developers.google.com/web/updates/2019/02/rendering-on-the-web?hl=ko
🌄 Universal App인지 SPA인지 확인하는 법
브라우저에서 이 애플리케이션이 SPA인지 Universal App 인지 확인하는 법은 간단하다.
브라우저에서 오른쪽 마우스 클릭 후,
페이지 소스보기를 누르면 코드를 확인할 수 있다.
브라우저에서 보였던 어떤 데이터가 해당 페이지 소스에 존재한다면 SSR 방식이고,
그렇지 않다면 SPA 방식이다.
🌄 Nuxt.js Routing
copy javascriptimport Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
/src/router/index.js
위 코드는 Vue.js 의 라우터 설정 파일이다.
Vue.js 에선 /src/views 디렉토리에 컴포넌트를 생성하고 이 라우터 설정 파일(/src/router/index.js)에 해당 컴포넌트에 대한 라우터 설정을 일일이 입력해줘야했다.
하지만 Nuxt.js에선 그럴 필요가 없다!
Nuxt.js Router에 대한 간단한 예제를 살펴보자.
라우터는 크게 두가지 종류가 있다. "파라미터를 받는 Dynamic Route"와 "파라미터를 받지 않는 Basic Route" 이다.
Basic Route
Nuxt.js는 /pages 폴더에 파일만 생성해주면 자동으로 라우팅이 된다.
/pages 에 HelloWorld.vue 페이지를 생성해보겠다.
copy xml<template>
<div>
Hello World!!
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
/pages/HelloWorld.vue
localhost:3000/helloworld
페이지 생성 후 localhost:3000/helloworld에 접속해보면 HelloWorld.vue 페이지가 출력되는걸 확인할 수 있다.
어떻게 가능한 일일까?
페이지를 생성한 즉시 .nuxt/router.js 에선 자동 설정이 이뤄지기 때문이다.
copy javascriptimport Vue from 'vue'
import Router from 'vue-router'
import { normalizeURL, decode } from 'ufo'
import { interopDefault } from './utils'
import scrollBehavior from './router.scrollBehavior.js'
const _64fe57bb = () => interopDefault(import('..\\pages\\HelloWorld.vue' /* webpackChunkName: "pages/HelloWorld" */))
const _2fdd1532 = () => 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: "/HelloWorld",
component: _64fe57bb,
name: "HelloWorld"
}, {
path: "/",
component: _2fdd1532,
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/router.js
이 파일을 따로 건들지도 않았는데, HelloWorld 라우터가 설정되었다.
Basic Route - 중첩 라우팅
중첩 라우팅
중첩 라우팅이란 위 그림처럼 중첩된 컴포넌트에 대한 라우터 설정을 말한다.
경로를 보고 어떤 컴포넌트들이 중첩되어있는지 판단할 수 있다.
위 그림과 같은 예제를 구현해보겠다.
/pages 구조
copy xml<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>
/pages/Container.vue
copy xml<template>
<div>
<div class="content1">
Content1 컴포넌트 입니다.
</div>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
.content1 {
background-color: lightblue;
margin: 30px;
width: 200px;
height: 200px;
}
</style>
/pages/Container/Content1.vue
copy xml<template>
<div>
<div class="content2">
Content2 컴포넌트 입니다.
</div>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
.content2 {
background-color: lightgoldenrodyellow;
margin: 30px;
width: 200px;
height: 200px;
}
</style>
/pages/Container/Content2.vue
localhost:3000/container
localhost:3000/container/content1
localhost:3000/container/content1
중첩 라우팅 역시 자동으로 설정 됐다.
상위 컴포넌트와 이름이 같은 디렉토리를 생성한 뒤
해당 디렉토리 내에 하위 컴포넌트를 생성했다.
상위 컴포넌트 내에 하위 컴포넌트가 들어갈 자리에 <nuxt-child /> 정의해주면 끝.
Nuxt.js가 자동으로 라우팅을 완료한다.
copy properties// ...
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"
}]
// ...
/.nuxt/router.js
라우팅 설정파일을 보면 상위 라우터 Container에 children 속성이 추가되고 하위 라우터들이 정의된걸 확인할 수 있다.
Dynamic Route
언더바 + 파일이름 형태로 파일을 생성하면 해당 파일 이름의 파라미터를 받는다.
dynamic route directory
_product_id.vue 라는 파일을 만들고 아래와 같이 입력해줬다.
copy xml<template>
<div>
<div>Editing Product {{ $route.params.product_id }}</div>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
product_id라는 route의 파라미터를 받아와 화면에 뿌려주는 코드이다.
자동 컴파일 후 .nuxt/router.js를 확인해보면 아래와 같은 코드가 추가되어 있을 것이다.
copy css{
path: "/products/edit/:product_id?",
component: _1cad02a9,
name: "products-edit-product_id"
}
product_id를 파라미터로 받아온다는 설정이 추가되었다.
localhost:3000/products/edit/1 결과화면
localhost:3000/products/edit/1 로 접근하면, 경로의 params를 받아 화면에 뿌려주는 걸 확인할 수 있다.
파라미터의 유효성 검사도 가능하다.
copy kotlinvalidate({ params, query, store }) {
return true // if the params are valid
return false // will stop Nuxt.js to render the route and display the error page
}
Nuxt.js에서 제공하는 validate() 메소드를 이용하면 파라미터의 유효성 검사가 가능하다.
validate()는 새 라우터로 네비게이팅(navigating)되기 전에 call 된다.
Nuxt context 객체를 argument로 갖는다. 자세히
copy xml<template>
<div>
<div>Editing Product {{ $route.params.product_id }}</div>
</div>
</template>
<script>
export default {
validate({ params }) {
// must be a number
return /^\d+$/.test(params.product_id)
}
}
</script>
<style scoped>
</style>
http://localhost:3000/products/edit/test 결과화면
product_id를 숫자만 받도록 유효성 검사를 하고, http://localhost:3000/products/edit/test로 접근하면
위와 같이 404에러를 뱉는다.
🌄 Nuxt.js Store
Nuxt는 pages 디렉토리와 유사한 구조로 store를 구축할 수 있다.
Store 는 크게 Classic 과 Module 모드를 제공한다.
(Store 기능이 필요 없다면 store 폴더를 지우면 됨)
Classic
클래식 모드는 store 디렉토리에 index.js를 필요로 한다. (Vuex 설정 파일)
이 index.js엔 Vuex 인스턴스를 리턴하는 export 함수를 구현하면 된다.
이를 통해 일반 Vue 프로젝트에서 Vuex를 사용하는 것처럼 원하는대로 스토어를 만들 수 있다.
copy coffeescriptimport Vuex from 'vuex'
const createStore = () => {
return new Vuex.Store({
state: ...,
mutations: ...,
actions: ...
})
}
export default createStore
Module
모듈 모드는 또한 store 디렉토리에 index.js 따위를 필요로 한다. (꼭 index.js 일 필요 없음. 원하는 모듈의 네임 스페이스로 지정)
그러나 모듈 모드에선 이 파일에서 루트 state/mutations/actions 만 export 시키면된다.
copy coffeescriptexport const state = () => ({})
예를 들어, store 디렉토리 내에 product.js 를 생성하고 아래와 같이 구현하면,
product라는 네임스페이스가 생성되어 모듈화를 할 수 있다.
copy javascriptexport const state = () => ({
_id: 0,
title: 'Unknown',
price: 0
})
export const actions = {
load ({ commit }) {
setTimeout(
commit,
1000,
'update',
{ _id: 1, title: 'Product', price: 99.99 }
)
}
}
export const mutations = {
update (state, product) {
Object.assign(state, product)
}
}
/store/product.js
copy xml<template>
<div>
<h1>View Product {{ product._id }}</h1>
<p>{{ product.title }}</p>
<p>Price: {{ product.price }}</p>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
created () {
this.$store.dispatch('product/load')
},
computed: {
...mapState(['product'])
}
}
</script>
/pages/product/view.vue
product 모듈의 id, title, price를 화면에 뿌리는 코드이다.
약 1초 뒤에 load 액션을 거치며 state가 갱신되는 것을 확인할 수 있다.
localhost:3000/product/view
🚨 주의
위 예제에서 load라는 가짜 API를 구현해 사용했다.
여기서 문제점은, 1초뒤 state가 갱신되기 전까지 0, Unknown 따위의 초기화 데이터가 보인다는 것이다.
아마 실 API에서도 response가 완료되기 전까지 초기값이 보이게 될 것이다.
우린 이 문제를 해결하기 위해 Nuxt에서 제공하는 fetch를 사용할 수 있다.
자세한건 아래에서 설명!
🌄 Nuxt.js Layouts
Nuxt.js는 navbar, footer, header 따위의 레이아웃 기능을 제공한다.
일반적인 Vue 프로젝트의 App.vue 와 비슷한 기능이다.
copy xml<template>
<div>
<h1>Admin Layout</h1>
<nuxt />
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
/layouts/admin-layout.vue
copy xml<template>
<div>
admin page
</div>
</template>
<script>
export default {
layout: 'admin-layout'
}
</script>
<style scoped>
</style>
/pages/admin.vue
layouts 디렉토리에 admin-layout.vue라는 레이아웃 파일을 만들고,
pages 디렉토리에 admin.vue라는 페이지를 만들어 주었다.
admin.vue에 layout: 'admin-layout' 이라고 정의해주는 것 만으로 해당 파일이 admin-layout.vue의 <nuxt /> 요소에 위치할 수 있다.
localhost:3000/admin
중요한 것은, 레이아웃 파일에 반드시 <nuxt /> 요소가 포함되어야 한다는 것이다.
🌄 Nuxt.js Middleware
middleware는 pages 또는 layouts를 렌더링하기 전에 실행할 수 있는 기능이다.
미들웨어가 해결되기 전까지 사용자에게 아무것도 표시하지 않는다.
이는 Vuex 저장소에서 유효한 로그인을 확인하거나, 일부 매개 변수의 유효성을 검사할 때 사용할 수 있다. (validate() 메소드 대신)
예제 - CodeSandBox
🌄 Nuxt.js Plugins
plugins 디렉토리를 사용하면 어플리케이션이 생성되기 전에 Vue 플러그인을 등록할 수 있다.
이를 통해 Vue 인스턴스의 앱 전체에서 공유하고 모든 구성 요소에서 액세스할 수 있다.
예를 들어 vue-notifications 플러그인을 주입한다고 해보자.
- npm i vue-notifications (여기까진 Vue와 동일)
- plugins 디렉토리에 vue-notifications.js 파일을 파고 아래와 같이 입력.
- import Vue from 'vue' import VueNotifications from 'vue-notifications' Vue.use(VueNotifications)
- nuxt.config.js 에 plugins: ['~/plugins/vue-notifications'] 입력
끝이다. Vue 프로젝트의 플러그인 주입법과 유사하다.
Nuxt.js 비동기 데이터 가져오기
Nuxt.js는 컴포넌트의 mounted 후크에서 데이터를 가져오는 것과 같이,
클라이언트 쪽에서 데이터를 로드하기 위한 기존 Vue 패턴을 지원한다.
하지만 universal 앱을 구현하고 있다면, 서버 측 렌더링 중에 데이터를 렌더링 할 수 있도록 Nuxt.js 관련 후크(fetch, asyncData)를 써야한다.
asyncData
- 컴포넌트 데이터를 세팅하기 전에 비동기 처리를 할 수 있도록 한다.
- 컴포넌트를 로드하기 전에 호출된다.
- pages 컴포넌ㅌ트에서만 사용 가능하다.
- Context 객체를 첫번째 인수로 받으며, 이를 사용해 일부 데이터를 가져와 컴포넌트 데이터로 반환할 수 있다.
- 반환 값은 컴포넌트의 data와 병합된다.
- 컴포넌트를 초기화하기 전에 실행되기 때문에 메서드 내부에서 this를 통해 컴포넌트 인스턴스에 접근할 수 없다.
copy csexport default {
async asyncData({ params }) {
const { data } = await axios.get(`https://my-api/posts/${params.id}`);
return { title: data.title };
},
};
fetch
- 페이지가 렌더링 되기 전에 데이터를 Store에 넣기 위해 사용된다.
- 모든 컴포넌트에서 사용 가능하다.
- 컴포넌트를 로드하기 전에 호출된다.
- Context 객체를 첫번째 인수로 받으며 그 데이터를 스토어에 넣을 수 있다.
- return 값은 Promise이다.
- Promise를 반환하면 Nuxt는 렌더링 전에 Promise가 끝날때까지 기다린다.
위 쪽 Store Module 예제에서 이어 설명하겠다.
<주의> 부분에서 설명했지만, 어떤 Api를 통해 데이터를 가져올 때, response가 오기 전까지
초기값이 그대로 노출된다는 문제가 존재했었다.
이 문제는 위 fetchd의 특징 중 6번 "Promise를 반환하면 Nuxt는 렌더링 전에 Promise가 끝날때까지 기다린다."를 통해 해결할 수 있다.
/store/product.js의 load 액션 메소드가 Promise를 반환하도록 수정하고, (async/await 를 써도 상관 없음)
/pages/products/view.vue 파일을 fetch로 수정해보았다.
copy javascriptexport const state = () => ({
_id: 0,
title: 'Unknown',
price: 0
})
export const actions = {
load ({ commit }) {
return new Promise(resolve => {
setTimeout(() => {
commit('update', { _id: 1, title: 'Product', price: 99.99 })
resolve()
}, 1000)
})
}
}
export const mutations = {
update (state, product) {
Object.assign(state, product)
}
}
/store/product.js
copy xml<template>
<div>
<h1>View Product {{ product._id }}</h1>
<p>{{ product.title }}</p>
<p>Price: {{ product.price }}</p>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
fetch() {
this.$store.dispatch('product/load')
},
computed: {
...mapState(['product'])
}
}
</script>
<style scoped>
</style>
/pages/product/view.vue
localhost:3000/product/view 결과 화면
우린 product/load 가 실행되기 전까지 렌더링이 되지 않다가, response가 오면 화면에 product/load의 결과 값이 뜰 것이라고 예상했다.
하지만 결과적으로, product/load 액션 메소드가 실행되지 않았다. 왜일까?
이유는 fetch, asyncData 같은 Supercharged 메소드는 Vue 컴포넌트가 생성되기 전에 실행되므로 컴포넌트 this 를 가리키지 않는다. 때문에 위 예제의 this.$store는 undefined 상태 인것이다. 컴포넌트가 생성된 이후엔 this 사용이 가능하다.
(fetch와 asyncData의 실행 타이밍은 아래 lifeCycle 부분에서 한번 더 다루겠다.)
그렇다면 fetch나 asyncData를 사용할 때 어떻게 Store에 접근할 수 있을까?
바로 Context 를 사용하면 된다.
🌄 Nuxt.js Context
Nuxt는 모든 메소드에 Context라는 매우 유용한 객체를 포함하는 인수를 제공한다.
여기엔 앱 전체에서 참조해야하는 모든 것이 있다. 즉, Vue가 컴포넌트에 대한 참조를 먼저 생성할 때까지 기다릴 필요가 없다.
(Context가 무엇을 가지고 있는지는 공식문서를 참조하자.)
위 fetch 예제에서 컨텍스트를 구조화하고 여기서 Store를 추출해오자.
copy coffeescriptexport default {
fetch ({ store }) {
return store.dispatch('product/load')
},
computed: {...}
}
/pages/product/view.vue
localhost:3000/product/view 실행 결과
초기값이 뜨지 않고, response가 오고나서야 렌더링이 되는 모습을 확인할 수 있었다.
🌄 Nuxt.js 메타 태그
Vue.js에선 SEO 를 위한 메타 태그를 달기 위해 vue-meta 등 외부 라이브러리를 이용해야했지만,
Nust.js에선 별도의 라이브러리 추가 없이 메타 데이터를 설정할 수 있다. 기본으로 vue-meta 라이브러리가 탑재되어 있기 때문이다.
전역 설정
copy lessexport default {
head: {
title: 'my website title',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
hid: 'description',
name: 'description',
content: 'my website description'
}
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
}
}
nuxt.config.js
Nuxt.js 설정 파일(nuxt.config.js)에 meta 데이터를 설정하면 어플리케이션의 모든 기본 태그를 정의할 수 있다.
SEO 목적으로 기본 제목 및 설명 태그를 추가하거나 뷰포트를 설정하거나 파비콘을 추가하는데 매우 유용하다.
위 코드와 같이 설정하면 모든 페이지에 동일한 제목과 설명이 표시되게 된다.
지역 설정
컴포넌트 파일의 <script> 태그 내부에 있는 메소드를 사용하여 페이지별 메타 데이터를 추가할 수도 있다.
copy xml<script>
export default {
head: {
title: 'Home page',
meta: [
{
hid: 'description',
name: 'description',
content: 'Home page description'
}
],
}
}
</script>
// 메소드로도 표현 가능
<template>
<h1>{{ title }}</h1>
</template>
<script>
export default {
data() {
return {
title: 'Home page'
}
},
head() {
return {
title: this.title,
meta: [
{
hid: 'description',
name: 'description',
content: 'Home page description'
}
]
}
}
}
</script>
🌄 Nuxt.js LifeCycle
https://ko.nuxtjs.org/docs/2.x/concepts/nuxt-lifecycle/#server
뭔가 굉장히 복잡해보이는 이미지이지만, 위 내용을 모두 읽고 왔다면 이해하기 수월할 것이다.
먼저 가운데 그어진 선을 살펴보자.
가운데 선을 기준으로, 위쪽은 Vue 컴포넌트가 생기기 전, 아래는 그 후가 되겠다.
좀 더 쉽게 얘기하자면, 위는 Server Side, 아래는 Client Side이다.
Vue Component가 생기기 전, Server Side에서 페이지가 렌더링 되기 전 해야할 일을 수행한다.
당연한 말이지만 서버 사이드에선 Vue Component가 아직 생성되기 전이기 때문에 this를 사용할 수 없다.
앞서 설명했던 middleware, validate(), asyncData() 를 수행한다.
💡 참고
Nuxt.js 2.12 전엔 fetch()를 asyncData() 와 같이 컴포넌트가 생성되기 전에 실행을 해줬어야했지만,
2.12 이후엔 created() 이후, 즉, Vue 컴포넌트가 생성되고 난 후에 사용할 수 있게 되었다.
정확히는 브라우저에 DOM이 렌더 되기 전에 실행된다.(beforeMount 전)
이로써 fetch()에서 this 의 사용이 가능해진 것이다.
즉, fetch의 특징이었던 "데이터를 Store에 넣기 위해 사용" 은 선택사항이 되었다.
Vuex 스토어 작업을 전달하거나 페이지 구성 요소에서 변형을 커밋하지 않고도 구성 요소의 로컬 데이터를 설정할 수 있기 때문이다.
컴포넌트가 생성된 후 데이터를 가져올 수 있기 때문에 아래와 같이 활용 가능하다.
copy css<button @click="$fetch">Refresh</button>
copy javascriptexport default {
methods: {
refresh() {
this.$fetch()
}
}
}
👉 [Nuxt.js 2.12 fetch() 자세히 알아보기 1]
👉 [Nuxt.js 2.12 fetch() 자세히 알아보기 2]
아무튼 요약하자면 Nuxt.js의 대부분의 기능이 Vue 컴포넌트가 생성되기 전에 이루어지고,
그 이후는 Vue.js와 동일하다는 것이다. (fetch 제외)
이번 포스팅에선 Nuxt.js에 대해 알아 보았다.
웹의 가장 기본적인 "렌더링" 이 생각보다 다양한 종류를 가지고 있다는 걸 알 수 있었다.
피드백 댓글은 언제나 환영입니다. 👼
'IT창업' 카테고리의 다른 글
◀정부 지원금을 활용하여 안전하게 기업 경영하기 - 44화 : 절대 하지 말아야 할 사업비 횡령 범죄 행위-연구수당 회수를 통한 자금 횡령 (1) | 2024.04.04 |
---|
댓글