[SSR] Nuxt.js 시작하기

SeoYng·2022년 3월 12일
1
post-thumbnail

🤔 Nuxt ?

Nuxt는 Vue.js로 빠르게 웹을 제작할 수 있게 도와주는 프레임워크
=> 프레임워크의 프레임워크!

❗️ 👍 웹 애플리케이션을 제작할 때 필요한 뷰엑스, 라우터, Axios와 같은 라이브러리들을 미리 구성하여
싱글 페이지 애플리케이션, 서버 사이드 렌더링, 정적 웹 사이트를 쉽게 제작

서버 사이드 렌더링(Server Side Rendering)이란
웹 페이지의 내용을 서버에서 모두 작성해서 클라이언트(브라우저)로 보낸 뒤 화면에 그리는 방식

❗️ 👍 보통 클라이언트 사이드 렌더링으로 SPA 제작시
초기 구동 속도가 느리고 검색엔진 최적화(SEO)가 어려운 단점이 있는데
이를 보완 가능!
또, 링크 공유시 정보를 나타내는 OG태그, 오픈 그래프 정보를 다양하게 나타낼수 없음

싱글 페이지 애플리케이션(Single Page Application)은
서버에서 브라우저로 빈 HTML을 던져주고, hashChangeEvent와 같이 url이 변경되었을 경우
내부에서 JS가 동작하여 DOM을 그리는 형태

📚 참고

Nuxt.js 공식 문서 사이트
캡틴판교님 블로그 - Nuxt 부분

⭐️ Nuxt의 장점

  • 검색 엔진 최적화
  • 초기 프로젝트 설정 비용 감소와 생산성 향상
  • 라우터, 스토어 등의 라이브러리 설치 및 설정 파일 필요 X
  • 파일 기반의 라우팅 방식. 설정 파일 자동 생성
  • 페이지 로딩 속도와 사용자 경험 향상
  • 브라우저가 하는 일을 서버에 나눠준다
  • 코드 스플리팅이 기본으로 설정

개인적으로 생각하는 큰 장점은 정말 편하고 쉽다는 것..!!
기존의 vue를 잘 아는 사람들은 감탄하며 재밌게 개발할 수 있을 것 같다 !

⭐️ Nuxt의 특징

  • 서버 사이드 렌더링
  • 규격화된 폴더 구조(layout, store, middleware, plugins 등)
  • 비동기 데이터 요청 속성
  • ES6/ES6+ 변환
  • 웹팩을 비롯한 기타 설정

⭐️ 폴더구조 / 라우팅

❗️ pages 폴더에 만약 main.vue 파일을 만든다면
빌드 후 router 파일에 /main 경로가 자동으로 생성되고 해당 컴포넌트가 뜨게된다

어머머..

위와 같이 🗂 pages 폴더에 파일들을 생성한다면 router.js에 자동으로 추가되며
해당 url 진입시 해당 컴포넌트가 나타난다.
url구조에 따라 폴더, index, _{id} 등의 vue 파일들을 활용하여 라우팅을 설정할 수 있다.

✨ vue router와 비교했을때

📃 default.vue

<Nuxt /> 
<!-- Nuxt 태그 => <router-view></ router-view> --->
<NuxtLink to="main"></NuxtLink>
<nuxt-link class="logo" to="/">공통 헤더</nuxt-link>
<!-- NuxtLink 태그 => <router-link to="main"></ router-view> --->

🗂 layouts > 🗂 pages > 🗂 components

  • 에러페이지 (error.vue) 파일은 layout폴더에 존재

⭐️ ❗️ 데이터 호출방식 - asyncData

넉스트는 서버 사이드 렌더링 프레임워크이기 때문에
뷰 싱글 페이지 애플리케이션과 REST API를 호출하는 방식을 다르게 접근해야한다.

기존 CSR에는 created 등에서 컴포넌트가 생성될때 데이터를 fetch하여 저장하는 방식으로
이러한 방식은 로딩바 등장 / 화면 새로고침시 화면 깜빡임 등으로 이어진다. (일단 빈 데이터로 시작하기 때문)

vue 속성중 asyncData를 이용하여 페이지가 뜨기전에 데이터를 set해준다. (페이지 진입 전 호출)

주의사항 ❗️

  • 🗂 pages 폴더안에 있는 컴포넌트에만 유효한 데이터
  • asyncData 안에서 컴포넌트를 참조하기 위해 this 사용 불가 (컴포넌트가 생성되기 전에 호출되기 때문에 엑세스 불가)

asyncData 메소드는 첫 인자로 context를 받아온다

const {
    app,
    store,
    route,
    params,
    query,
    env,
    isDev,
    isHMR,
    redirect,
    error,
   $config
  } = context

asyncData의 반환 값은 data와 합쳐지기 때문에 이후에 this.데이터이름으로 접근가능하다.

export default {

  async asyncData({ params }) { // context 객체에서 params 사용가능
    try {
      const { data } = await fetchProducts(params.id)
      const items = data || []
      return { items } // this.items에 저장됨
    } catch (e) {
      // 에러 페이지로 이동가능
      error({ statusCode: 503, message: 'API 요청이 실패했습니다 다시 시도해 주세요' })
    }
  },
  
  computed: {
    proceedItems() { // this.items로 접근하여 가공하여 사용가능
      return this.items.map((item) => ({
        ...item,
        imageUrl: `${item.imageUrl}?random=${Math.random()}`,
      }))
    },
  },
  
}

⭐️ 데이터 호출방식 - fetch

asyncData와 다르게 🗂 components 폴더에 있는 컴포넌트에서도 사용 가능하다
this 접근이 가능하다

this가 접근은 가능하지만 fetch 특성상 두가지 상황에서 다르게 나타남

  • 해당 URL바로 접속시 호출된 상태에서 페이지 표시됨

  • 브라우저에서 URL 주소를 변경해서 페이지를 이동할 때 - 컴포넌트가 생성되고나서 호출 실행

⭐️ vuex적용 - store

자동으로 생성된 🗂 store 폴더는 vuex와 같은 역할을 하며 index.js 파일은 rootState가 된다
원래 new Vue()로 객체 생성시 use vuex 등으로 설정을 해줘야하지만 이 역시 자동으로 미리 셋팅 되어있어 편리하다!
만약 index.js 를 생성하고 다른 이름의 자바스크립트 파일을 생성하면 뷰엑스의 모듈이 됨

🗂 store > 📃 index.js

import { fetchCartItems } from '~/api'

export const Types = {
  getters: {
    PROCEED_CARTS: 'proceedCarts',
  },
  mutations: {
    ADD_ITEM_TO_CART: 'addItemToCart',
    SET_CARTS: 'setCarts',
  },
  actions: {
    FETCH_CARTS: 'fetchCarts',
  },
}

export const state = () => ({
  carts: [],
})

export const getters = {
  [Types.getters.PROCEED_CARTS](state) {
    return state.carts.map((item) => ({
      ...item,
      imageUrl: `${item.imageUrl}?random=${Math.random()}`,
    }))
  },
}

export const mutations = {
  [Types.mutations.ADD_ITEM_TO_CART](state, item = {}) {
    state.carts = [...state.carts, item]
  },
  [Types.mutations.SET_CARTS](state, carts) {
    state.carts = [...carts] || []
  },
}

export const actions = {
  async [Types.actions.FETCH_CARTS](context) {
    const { data } = await fetchCartItems() || {}
    const items = data || []
    context.commit(Types.mutations.SET_CARTS, items)
  },
}

📃 cart.vue

import { mapGetters, mapMutations } from 'vuex'
import { createCartItem } from '~/api'
export default {
  async asyncData({ store }) {
    // store에서 fetch수행
    await store.dispatch('fetchCarts')
  },

  computed: {
    // store에서 가공한 mapGetters 사용 가능
    ...mapGetters(['proceedCarts']),
    // 컴포넌트에서 가공하여 사용
    carts() {
      return this.$store.state.carts
    },
  },

  methods: {
	// ...mapMutations(['addItemToCart']), - mapMutations로도 사용 가능
    async addToCart() {
      await createCartItem(this.product)
      // store commit
      this.$store.commit('addItemToCart', this.product)
      // this.addItemToCart(this.product)
      this.$router.push('/cart')
    },
  },
}

⭐️ ❗️ NuxtServerInit

Store > actions 안 nuxtServerInit 함수는 넉스트의 universal 모드에서 사용할 수 있는 액션 함수
스토어에 데이터가 담겨진 형태로 시작
=> 서버 사이드 렌더링 시점에 실행되기 때문에 스토어에 미리 데이터를 설정해 놓거나 서버에서만 접근할 수 있는 데이터를 다룰 때 유용
ex) 사용자 정보 등

actions: {
  // 두 번째 파라미터 nuxtContext는 asyncData 메서드의 context 파라미터와 동일
  nuxtServerInit(storeContext, nuxtContext) {
    storeContext.commit('뮤테이션 함수명');
    if (process.server) {
      const { req, res, beforeNuxtRender } = nuxtContext;
    }
  }
}

🗂 store > 📃 index.js

export const actions = {
  async nuxtServerInit(storeContext, nuxtContext) {
    const { data } = await fetchCartItems() || {}
    onst items = data || []
    storeContext.commit(Types.mutations.SET_CARTS, data)
  },

  // async [Types.actions.FETCH_CARTS](context) {
  //   const { data } = await fetchCartItems() || {}
  //   const items = data || []
  //   context.commit(Types.mutations.SET_CARTS, items)
  // },
}

📃 cart.vue

export default { // nuxtServerInit으로 변경
  // async asyncData({ store }) {
  //   await store.dispatch('fetchCarts')
  // },
}

⭐️ Nuxt 라이프 사이클


⭐️ 배포방식

SSR(Server Side Rendering)
배포하려는 서버에 Node.js 서버를 실행할 수 있는 형태로 배포
🗂 .nuxt 폴더

// nuxt.config.js
export default {
  target: 'server'
}
 
$ npm run build
$ npm run start

SSG(Static Site Generation)
페이지 URL 요청이 들어올 때마다 서버에서 그려서 브라우저에 보내주는 SSR 모드와 다르게
웹 서비스를 구성하는 모든 페이지를 미리 그려야 하기 때문에 스태틱 서버에 배포하는 형태
🗂 dist 폴더

// nuxt.config.js
export default {
  target: 'static'
}
 
$ npm run generate

⭐️ 페이지별 메타 태그 설정

export default {
  
  async asyncData({ params }) {
    const response = await fetchProductById(params.id)
    return { product: response.data }
  },
	
  head() {
    return {
      title: `상품 상세 페이지 - ${this.product.name}`,
      meta: [
        {
          hid: 'description', // 다른 곳과 동일하게 설정해야 덮어써짐
          name: 'description',
          content: `이 페이지는 ${this.product.name} 상품입니다`,
        },
      ],
    }
  },
  /*
	head: {
      title: '페이지 타이틀',
      meta: [
        {
          hid: 'description', // 다른 곳과 동일하게 설정해야 덮어써짐
          name: 'description',
          content: '페이지 설명 내용',
        },
      ],
  	}, 
  */
}

⭐️ OG태그

특정 페이지의 링크를 SNS 상에서 공유할 때
해당 페이지의 정보가 잘 드러날 수 있도록 지원해 주는 메타 태그

open graph protocol 공식문서
카카오 OG태그 캐시 비우는 사이트

export default {
  head() {
    return {
      title: `상품 상세 페이지 - ${this.product.name}`,
      meta: [
        {
          hid: 'og:title', // og:을 붙여준다
          property: 'og:title',
          content: `상품 상세 페이지 - ${this.product.name}`,
        },
        {
          hid: 'og:description',
          property: 'og:description',
          content: `이 페이지는 ${this.product.name} 상품입니다`,
        },
        {
          hid: 'og:image',
          property: 'og:image',
          content: this.product.imageUrl,
        },
      ],
    }
  },
  /*
  head: {
    title: '상품 상세 페이지',
    meta: [
      {
        hid: 'og:title', // og:을 붙여준다
        property: 'og:title',
        content: '상품 상세 페이지'
      },
      {
        hid: 'og:description',
        property: 'og:description',
        content: '상품의 상세 정보를 확인해보세요'
      },
      {
        hid: 'og:image',
        property: 'og:image',
        content: 'http://placeimg.com/640/480/fashion'
      },
    ]
  }
  */
}
profile
Junior Web FE Developer

0개의 댓글