Warning

이 블로그글은 Nuxt2 환경으로 쓴 글입니다.
개인적으로 복습을 하기 위해 쓴 글이니 Nuxt3와 해깔려하지마시길 🥲

Nuxt의 폴더 구조

넉스트의 프로젝트 생성 며령어를 실행하고 난 뒤 생성된 넉스트의 기본 폴더 구조를 살펴보겠다.

다음은 넉스트의 기본 폴더 구조이다.

  • .nuxt: 넉스트 빌드 결과물 폴더
  • assets: 스타일 시트, 이미지, 폰트 등 웹 리소스 폴더
  • components: 뷰 컴포넌트 폴더
  • layouts: 레이아웃 컴포넌트 폴더
  • middleware: 미들웨어(페이지를 화면에 표시하기 전에 실행할 수 있는 함수) 파일 폴더
  • pages: 특정 URL에 접근했을 때 표시될 페이지 컴포넌트 폴더
  • plugins: 뷰 플러그인 폴더
  • static: 빌드 했을 때 서버의 루트에 존재해야 하는 파일들의 폴더. 파비콘이나 robots.txt 등의 파일이 위치한다.
  • store: 뷰엑스 폴더

Nuxt의 페이지 라우팅

뷰 싱글 페이지 애플리케이션에서는 라우터 설정을 아래와 같이 일일이 해줘야 하는데 반해 넉스트에서는 폴더와 파일 기반으로 라우터를 자동으로 생성해 준다.

// 뷰 싱글 페이지 애플리케이션의 라우터 설정 파일
new VueRouter({
  routes: [
    {
      path: 'URL 주소',
      component: URL 주소에 접근했을 때 표시할 페이지 컴포넌트
    }
  ]
})

페이지 기반 라우터 자동 생성

넉스트에서 어떻게 파일과 폴더 기반으로 라우팅 할 수 있는지 살펴보겠다.
넉스트 프로젝트를 생성하면 pages 폴더가 존재하는데 그 아래에 다음과 같이 파일을 생성한다.

위와 같이 파일을 생성하면 URL을 각각 /, /login, /main으로 접근했을 때 index.vue, login.vue, main.vue 컴포넌트가 화면에 표시된다.
이때 레이아웃 컴포넌트에 </Nuxt>라는 태그가 있어야 한다.

<!-- layouts/default.vue -->
<template>
  <div>
    <Nuxt />
  </div>
</template>

여기서 일일이 URL을 입력하여 들어가기엔 번거로우므로 아래와 같이 페이지 이동 링크인 <NuxtLink to="/url주소">를 추가한다.

<!-- layouts/default.vue -->
<template>
  <div>
    <h1>
      <NuxtLink to="/">시작</NuxtLink>
      <NuxtLink to="/login">로그인</NuxtLink>
      <NuxtLink to="/main">메인</NuxtLink>
    </h1>
  </div>
</template>

이제 각 링크를 클릭하면 아래와 같이 동작한다.

뷰 라우터 비교

위에서 살펴본 넉스트의 라우팅 방식을 뷰 라우터 방식과 비교하면 아래와 같다.

Nuxt.jsVueRouter
<Nuxt><router-view>
<NuxtLink to="/"><router-link to="/">

이처럼 넉스트는 라우터 설정ㅈ 파일을 일일이 생성 및 설저아지 않아도 되어 편리하다.

<router-view>, <Nuxt> 모두 뷰 컴포넌트이다.
따라서 <RouterView><router-view>와 같다.
<Nuxt>도 그럼 동일한 케밥 네이밍으로 변환하면 <nuxt>로도 쓸 수 있다.

Nuxt의 레이아웃 컴포넌트

레이아웃 컴포넌트란 페이지의 레이아웃을 잡아주는 역할을 하는 컴포넌트를 의미한다.

레이아웃 컴포넌트 소개

nuxt 프로젝트를 생성하고 나서 아래와 같이 페이지 컴포넌트를 추가하고 나면 layouts/default.vue 컴포넌트가 페이지의 레이아웃을 구성해 준다.

URL에 /login/main을 각각 접근하면 main.vue 컴포넌트에 정의된 내용이 화면에 표시될 것이다.
이 때 코드가 아래와 같다고 가정하겠다.

<!-- login.vue -->
<template>
  <div>
    <h1>login 페이지</h1>
    <form></form>
  </div>
</template>
<!-- main.vue -->
<template>
  <div>
    <h1>main 페이지</h1>
    <main></main>
  </div>
</template>

이제 각 URL로 접근하면 기본적으로 화면에 표시되는 컴포넌트는 layouts/defalut.vue 컴포넌트이다.

<!-- '/login'에 접근하는 경우 -->
<template>
  <div>
    <!-- 로그인 컴포넌트 내용 -->
  </div>
</template>
<!-- '/main'에 접근하는 경우 -->
<template>
  <div>
    <!-- 메인 컴포넌트 내용 -->
  </div>
</template>

이 때 페이지마다 반복되는 UI 영역인 헤더를 레이아웃 컴포넌트에 아래와 같이 정의할 수 있다.

<!-- default.vue -->
<template>
  <div>
    <h1>{{ $route.name }} 페이지</h1>
  </div>
</template>

이제 각 페이지 컴포넌트(page 폴더에 있는 컴포넌트)에는 헤더 내용을 작성하지 않고 바로 해당 페이지의 UI 요소에 집중할 수 있다.

<!-- login.vue -->
<template>
  <form></form>
</template>
<!-- main.vue -->
<template>
  <main></main>
</template>

커스텀 레이아웃

앞에서 다룬 기본 레이아웃 defalut.vue 이외에도 다른 레이아웃을 정의하고 사용할 수 있다.
먼저 레이아웃 폴더에 새로운 뷰 파일을 하나 생성한다.

<!-- layouts/blog.vue -->
<template>
  <div>
    <nav>네비게이션 영역</nav>
    <Nuxt />
  </div>
</template>

이제 위 레이아웃을 사용할 페이지 컴포넌트에서 아래와 같은 속성을 추가한다.

// pages/main.vue
export default {
  layout: 'blog'
}

Nuxt의 비동기 데이터 호출 방법

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

싱글 페이지 애플리케이션과 다른 점

클라이언트 사이드 렌더링인 뷰 싱글 페이지 애플리케이션에서의 데이터 호출 방식은 아래오 같다.

<!-- UserProfile.vue -->
<template>
  <div>
    <p>{{ user }}</p>
  </div>
</template>

<script>
import axios from 'axios';
  
export default {
  data() {
    return {
  	  user: {},
    }
  },
  methods: {
    async fetchUser() {
  	  const response = await axios.get('/users/1');
  	  this.user = response.data;
    }
  },
  created() {
    this.fetchUser();
  }
}
</script>

created() 라이프 사이클 훅을 이용해서 컴포넌트가 생성되자마자 서버에 데이터를 요청해 받아온 값을 화면에 표시하는 코드이다.
이때 서버에 데이터를 요청하는 시점은 브라우저에서 Vue.js 코드가 화면의 DOM을 구성하고 스크립트를 실행하는 시점이다.
클라이언트 사이드 렌더링과 서버 사이드 렌더링 차이점의 그림에서도 볼 수 있지만 클라이언트 사이드 렌더링은 빈 화면을 브라우저가 받아 화면에 뿌릴 요소와 데이터를 모두 브라우저에서 구성하기 때문에 위와 같은 코드가 가능하다.

하지만, 넉스트는 서버에서 페이지의 내용을 모두 그려서 브라우저로 가져가야 한다.

넉스트의 REST API 호출 방식

앞에서 살펴본 것처럼 싱글 페이지 애플리케이션에서 페이지에 데이터를 표시하기 위해서는 뷰 라이프 사이클 훅을 사용했다.
넉스트에서는 아래의 2가지 인스턴스 옵션 속성이 별도로 제공된다.

  • asyncData
  • fetch

asyncData

asyncData는 페이지 컴포넌트(pages 폴더 아래에 위치하는 컴포넌트)에만 제공되는 속성이다. asyncData로 아래와 같이 서버 데이터를 호출할 수 있다.

<!-- pages/user.vue -->
<template>
  <div>
    <p>{{ user }}</p>
  </div>
</template>

<script>
import axios from 'axios';
  
export default {
  // params의 id가 1이라고 가정
  async asyncData({ params, $http }) {
    const response = await axios.get(`/users/${params.id}`);
    const user = response.data;
    return { user }
  }
}
</script>

위 코드는 URL /user로 접근할 때 user.vue 컴포넌트를 화면에 그리기 전에 데이터를 요청하는 코드이다.
데이터를 다 받아와야지만 데이터를 들고 <template></template> 영역의 코드를 화면에 표시한다.
마치 싱글 페이지 애플리케이션의 뷰 라우터에서 네비게이션 가드에서 데이터를 호출하고 받아왔을 때 페이지를 진입하는 것과 같다.

asyncData의 파라미터

asyncData 속성의 파라미터는 context 속성이다.
컨텍스트 속성은 넉스트 프레임워크 전반에 걸쳐 공용으로 사용되는 속성으로써 플러그인, 미들웨어 등의 속성에서도 접근할 수 있다.
컨텍스트에는 스토어, 라우터 관련 정보뿐만 아니라 서버 사이드에서 요청, 응답 관련된 속성도 접근할 수 있다.

function (context) { // asyncData, plugins, middleware, ...
  // Always available
  const {
    app,
    store,
    route,
    params,
    query,
    env,
    isDev,
    isHMR,
    redirect,
    error,
    $config
  } = context
  
  // Only available on the Server-side
  if (process.server) {
    const { req, res, beforeNuxtRender } = context
  }
  
  // Only available on the Client-side
  if (process.client) {
    const { from, nuxtState } = context
  }
}

asyncData의 에러 헨들링

asyncData 속성에서 API 호출 에러가 발생했을 때는 아래와 같이 에러 페이지로 이동시킬 수 있다.

export default {
  async asyncData({ params, $http, error }) {
    try {
      const response = await axios.get(`/user/${params.id}`);
      const user = response.data;
      return { user }
    } catch(e) {
      error({ statusCode: 503, message: 'API 요청이 실패했습니다 다시 시도해 주세요' })'
    }
  }
}

fetch

fetch는 페이지 컴포넌트 뿐만 아니라 일반 뷰 컴포넌트에서도 사용할 수 있는 데이터 호출 속성이다.
다음 2가지 상황에서 호출된다.

  • 서버 사이드 렌더링을 위해 서버에서 화면을 구성할 때 컴포넌트가 생성되고 나서 실행됨
  • 브라우저에서 URL 주소를 변경해서 페이지를 이동할 때
<!-- components/UserProfile.vue -->
<template>
  <div>{{ user }}</div>
</template>

<script>
import axios from 'axios';
  
export default {
  data() {
    return {
  	  user: {},
    }
  },
  async fetch() {
    const res = await axios.get('https://jsonplaceholder.typicode.com/user/1');
    this.user = res.data;
  }
}
</script>

만약 위 컴포넌트가 아래와 같이 main.vue 컴포넌트에 등록되어 있고 URL 주소가 /에서 /main으로 변경되면 컴포넌트가 화면에 부착되고 나서(mounted) fetch 안의 데이터 호출 로직이 실행된다.

<!-- pages/main.vue -->
<template>
  <div>
    <h1>메인 페이지</h1>
    <UserProfile></userProfile>
  </div>
</template>

<script>
import UserProfile from '@/components/UserProfile.vue';
  
export default {
  components: {
    UserProfile,
  }
}
</script>

위의 동작에서 볼 수 있듯이 최초에 /로 접근하고 /main으로 이동했기 때문에 컴포넌트가 화면에 먼저 뿌려지고 나서 fetch 호출이 실행된다.
따라서, /main으로 이동하고 나면 데이터를 받아오는 동안 user 속성의 기본 값인 {}가 먼저 화면에 보이고 잠시 후에 받아온 데이터가 화면에 그려지는 걸 볼 수 있다.

여기서 만약 최초로 웹 서비스를 /main으로 접근하게 되면 서버에서 호출을 구성할 때 호출된다.
따라서, 화면에 데이터가 호출된 상태로 페이지가 표시된다.

fetch 특징

fetchasyncData와 다르게 아래와 같은 속성들을 제공한다.

  • $fetchState: 데이터 호출 상태를 나타내는 속성이며 인스턴스로 접근할 수 있다.
    호출 상태에 따라 pending, error, timestamp를 제공한다.
  • $fetch: fetch로직을 다시 실행시킬 수 있는 함수이다.
  • fetchOnServer: 서버 사이드 렌더링 시에 서버에서 fetch를 실행할지 말지 결정하는 속성이다.
    기본값은 true 이다.
<template>
  <div>
    <article>
      <p v-if="$fetchState.pending">사용자 API 호출 중</p>
      <p v-else-if="$fetchState.error">에러가 발생했습니다</p>
      <div v-else>{{ user }}</div>
    </article>
    <button @click="fetchUser">다시 호출하기</button>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      user: {},
    }
  },
  methods: {
    fetchUser() {
      // fetch 속성의 로직을 실행합니다.
      this.$fetch();
    }
  },
  async fetch() {
    const res = await axios.get('https://jsonplaceholder.typicode.com/users/1');
    this.user = res.data;
  },
  // 아래 속성을 'false'로 바꾸면 서버에서 화면을 구성할 때 `fetch` 속성의 로직이 실행되지 않습니다.
  fetchOnServer: false
}
</script>

Vuex in Nuxt

넉스트에서 뷰엑스(Vuex)를 사용하려면 아래와 같이 프로젝트 폴더의 루트 레벨에 store 폴더를 생성하고 그 밑에 js 파일을 생성한다.

이렇게 하면 Vuex 라이브러리가 임포트 되면서 뷰엑스를 사용할 수 있다.

만약 플젝트 폴더에 store 폴더가 없다면 뷰엑스 라이브러리는 자동으로 비활성화된다.
여기서 비활성화란 빌드 파일에 포함되지 않는 것을 의미한다.

뷰엑스 시작하기

뷰엑스는 Vue.js의 상태 관리 라이브러리이자 패턴을 의미한다.

넉스트에서 뷰엑스를 시작하기 위해서는 store/index.js 파일을 생성하고 아래와 같은 내용을 입력해야 한다.

// store/index.js
export const state = () => ({
  user: {}
})

export const mutations = {
  setUser(state, user) {
    state.user = user;
  }
}

export const actions = {
  async fetchUser(context) {
    const response = await axios.get('users/1');
    context.commit('setUser', response.data);
  }
}

기존 싱글 페이지 애플리케이션과는 다르게 위와 같이 기본 코드만 작성해 주면 넉스트 라이브러리 내부적으로 뷰엑스 라이브러리 임포트와 설정 작업을 진행해 주기 때문에 뷰엑스 라이브러리 다룰 때 작성하던 아래 설정 코드가 필요하지 않는다.

// 아래 설정 코드는 Nuxt 라이브러리 내부적으로 생성하고 설정해 줌
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

new Vuex.Store({
  // ...
});

앞에서 안내한 store/index.js 파일의 내용은 뷰 컴포넌트 내부에서 기존에 뷰엑스를 접근하던 방식과 동일하게 접근할 수 있다.

<template>
  <div>
    <p>{{ $store.state.user }}</p>
    <button @click="displayUser"></button>
  </div>
</template>

<script>
export default {
  methods: {
  	displayUser() {
  	  const user = { name: '넉스트' };
  	  this.$store.commit('setUser', user);
  	}
  }
}
</script>

뷰엑스의 모듈화

store 폴더 밑에 생성한 index.js는 뷰엑스의 모듈화 관점에서 루트 모듈이 된다.
만약 index.js 를 생성하고 다른 이름의 자바스크립트 파일을 생성하면 뷰엑스의 모듈이 된다.
products.js 파일을 예로 들어보겠다.

// store/products.js
export const state = () => ({
  items: []
})

export const mutations = {
  addItems(state, item) {
    state.items.push(item);
  }
}

위 코드는 마치 싱글 페이지 애플리케이션의 아래 코드와 같이 동작한다.

// Vue CLI로 생성한 프로젝트에서 Vuex를 사용하는 경우
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

new Vuex.Store({
  state: () => ({
    user: {}
  }),
  modules: {
    products: {
      namespaced: true,
      state: () => ({
        items: []
      }),
      mutations: {
        addItems(state, item) {
          state.items.push(item);
        }
      }
    }
  }
})

nuxtServerInit

nuxtServerInit 속성은 넉스트의 universal 모드에서 사용할 수 있는 액션 함수이다.

// store/index.js
actions: {
  nuxtServerInit(context, { req }) {
    if (req.session.user) {
      context.commit('user', req.session.user)
    }
  }
}

위 함수는 서버 사이드 렌더링 시점에 실행되기 때문에 스토어에 미리 데이터를 설정해 놓거나 서버에서만 접근할 수 있는 데이터를 다룰 때 유용하다.
만약, 서버에서 세션을 관리하는 구조이고 세션에 연결된 사용자 정볼르 스토어에 저장 해야 할 때 위와 같은 방식으로 사용자 정보를 스토어에 미리 담아놓을 수 있다.

nuxtServerInit 액션 함수의 첫 번째 파라미터는 스토어의 컨텍스트 정보를 접근할 수 있는 객체이다.
두 번째 파라미터는 넉스트 컨텍스트 정보가 담긴 객체이다.

// store/index.js
actions: {
  nuxtServerInit(storeContext, nuxtContext) {
    storeContext.commit('뮤테이션 함수명');
    if (process.server) {
      const { req, res, beforeNuxtRouter } = nuxtContext;
    }
  }
}

위 두 번재 파라미터 nuxtContextasyncData 메서드의 context 파라미터와 같다.

Middleware

Nuxt에서 미들웨어(Middleware)는 페이지나 레이아웃이 렌더링 되기 전에 호출되는 커스텀 훅(Hook)이다.
미들웨어의 대표적인 특징은 아래와 같다.

  • Vue 인스턴스가 생성되기 전에 호출되는 asyncData 보다 더 일찍 호출된다.
  • asyncData와 마찬가지로 context를 인자로 받기 때문에 store, route, params, query, redirect 등에 접근할 수 있다.
  • 위의 두 특징들을 활용하여 네비게이션 가드 형태로 미들웨어 제작이 가능하다.

서버 미들웨어와 구분하기 위해서 해당 페이지에 다루는 미들웨어를 라우트(Route) 미들웨어라고 부른다.

종류

미들웨어는 특정 페이지나 레이아웃에 종속되는지에 따라서 익명 미들웨어와 네임드 미들웨어로 나뉜다.

익명 미들웨어(Anonymous Middleware)

익명 미들웨어는 특정 페이지나 레이아웃에 종속되는 미들웨어를 뜻한다.
페이지나 레이아웃에서 직접 middleware 훅을 통해 명시하기 때문에 다른 페이지나 레이아웃에서 공유할 수 없다.

// pages/secret.vue 또는 layouts/authenticated.vue
export defalut {
  middleware({ store, redirect }) {
    // 만약 유저가 인증 받지 못한 경우 로그인 페이지로 이동
    if (!store.state.authenticated) {
      return redirect("/login");
    }
  }
}

네임드 미들웨어(Named Middleware)

네임드 미들웨어는 여러 페이지나 레이아웃에서 공유될 수 있도록 middleware 디렉토리에서 파일로 관리되는 미들웨어들을 뜻한다.
이러한 미들웨어들은 Nuxt 환경 파일을 통해 전역적으로 적용하거나 반대로 각각의 페이지나 레이아웃을 통해 선별적으로 적용 가능하다.

middleware 디렉토리에서 관리되는 파일의 이름이 미들웨어의 이름이 되기 때문에 네임드 미들웨어라고 부른다.

아래 코드는 전역 설정과 지역 설정의 예시 코드에서 참조할 네임드 미들웨어의 예시이다.

// middleware/state.js
import http from 'http';

export default function({ route }) {
  return http.post('http://my-stats-api.com', {
    url: route.fullPath,
  });
}

전역 설정

router.middleware의 값에 적용하고 싶은 미들웨어의 이름을 문자열 또는 문자열의 배열 형태로 지정하면 모든 라우팅에 대해서 해당 미들웨어가 작동한다.

// nuxt.config.js
export default {
  router: {
    // 모든 라우팅에 대해서 stats 미들웨어가 적용된다.
    middleware: 'stats',
  }
}

지역 설정

미들웨어를 적용하고 싶은 페이지나 레이아웃에 해당 미들웨어를 명시한다.

// pages/index.vue 또는 layouts/default.vue
export default {
  // 전역 설정처럼 middleware: 'stats'도 가능하다.
  middleware: ['stats'],
};

활용법 예시

아래의 예시들은 네비게이션 가드처럼 사용할 수 있는 미들웨어 활용법이다.

인증

인증을 받지 못한 경우 로그인 페이지로 리다이렉트 시킬 수 있다.

// middleware/auth.vue
export default function({ store, redirect }) {
  if (!store.state.auth) {
    return redirect('/login');
  }
}

로케일

언어별 컨텐츠를 지원할 경우 찾고자 하는 언어가 없을 때 404 페이지로 폴백(Fallback)이 일어날 수 있다.
로케일 미들웨어를 활용하면 404 폴백을 막고 기본 언어 컨텐츠로 리다이렉트 시킬 수 있다.

// 지원되는 로케일 항목
const SUPPORTED_LOCALES = ['ko-kr', 'en-us', 'en-de'];

export default function({ route, redirect }) {
  const { language, pageName, country } = route.params;
  const locale = `${language}-${country}`
  
  if (SUPPORTED_LOCALES.includes(locale)) return;
  
  // 현재 로케일이 지원되는 로케일이 아닐 경우 en-us의 콘텐츠로 리다이렉트
  const redirectRoute = `en/us/${pageName}`;
  redirect(redirectRoute);
}
profile
Always happy coding 😊

0개의 댓글