본문으로 건너뛰기
SoulLog

자바스크립트로 서버와 클라이언트 구축하기 — Part 2. Nuxt.js

15분 읽기

1. Nuxt.js 시작하기

Nuxt.js로 프로젝트를 생성하기 위해서는 기본적으로 vue-cli가 설치되어 있어야 합니다.

npx create-nuxt-app <project-name>
npm init nuxt-app <project-name>

프로젝트 생성 시 아래와 같은 옵션들이 나옵니다.

create-nuxt-app v4.0.0
  Generating Nuxt.js project in test
? Project name: (test)
 
? Programming language:
 JavaScript
  TypeScript
 
? Package manager:
 Yarn
  Npm
 
? UI framework:
  None
  Ant Design Vue
 Bootstrap Vue
  (... 기타 생략)
 
? Nuxt.js modules:
❯◯ Axios - Promise based HTTP client
 Progressive Web App (PWA)
 Content - Git-based headless CMS
 
? Rendering mode:
 Universal (SSR / SSG)
  Single Page App
 
? Deployment target:
 Server (Node.js hosting)
  Static (Static/Jamstack hosting)
 
? Version control system:
 Git
  None

프로젝트 구조

Nuxt.js 버전 2.15.7부터 프로젝트 폴더 구조가 바뀌었습니다. assets, layouts, middleware, plugins가 프로젝트 초기 세팅 시 디렉터리가 생성되지 않습니다.

nuxt.config.js

Nuxt.js의 전역 설정 정보가 들어있습니다.

package.json

프로젝트와 관련된 모듈 및 스크립트 정보를 담고 있습니다. 주요 property를 요약하면 다음과 같습니다.

필드설명
name214자 이하, 대문자 사용 불가
versionMAJOR.MINOR.PATCH 형식 (ex. 1.11.1)
descriptionNPM 레지스트리 검색 결과에 표시되는 설명
keywordsNPM 레지스트리 인덱싱 키워드
scriptsnpm run <scriptName>으로 실행하는 명령어
dependencies애플리케이션 동작과 연관된 라이브러리
devDependencies개발 시 필요하지만 배포 시 포함되지 않는 라이브러리
engines작동하는 node/npm 버전 지정
browserslist브라우저 지원 정보
privatenpm 게시 여부 (true일 경우 비공개)

버전 표기 방식의 의미:

  • version: 정확히 일치
  • ~version: 입력한 값에 따라 허용 범위가 달라짐 (~1.2.3은 1.2.3 이상, 1.3 미만)
  • ^version: 가장 마지막 0이 아닌 요소를 수정하지 않는 범위 내에서 변경 허용 (^1.2.3은 1.2.3 이상 2.0.0 미만)

devDependenciesdependencies를 구분해서 잘 설치해주면 빌드 시간이 줄어들고 배포 시 불필요한 라이브러리를 포함하지 않게 되는 이점이 있습니다.

assets

Stylus, SASS, 이미지나 폰트처럼 컴파일되지 않은 파일이 포함된 디렉터리입니다.

<img src="../assets/your_image.png" />
<img src="~/assets/your_image.png" />

components

기존 Vue.js에서 활용했던 것과 동일하게 컴포넌트들을 모아두는 디렉터리입니다. LazyLoading을 동적으로 가져오려면 템플릿에 Lazy 접두사를 추가하면 됩니다.

layouts

레이아웃과 긴밀한 연관성이 있는 컴포넌트가 모여있는 디렉터리입니다. Nuxt에서는 에러 페이지를 layouts에서 관리합니다.

middleware

페이지나 레이아웃을 렌더링하기 전에 실행해야 하는 파일이 정의되는 폴더입니다.

pages

화면단에 대한 파일을 포함하는 폴더입니다. Nuxt에서는 화면으로 보여주려면 반드시 해당 폴더 안에 넣어야 합니다.

plugins

Vue 관련 플러그인이 들어가는 폴더입니다.

static

서버에 직접 매핑되어 변경되지 않을 가능성이 높은 파일을 포함하는 폴더입니다. robots.txt, reset.css, 고정 버전으로 활용 중인 JS 플러그인 등을 관리합니다.

store

Vuex Store를 활용하는 디렉터리입니다.


2. Pages

Nuxt.js는 pages의 디렉터리와 package.json만으로도 웹 애플리케이션을 만들 수 있습니다. Vue.js에서는 vue-router를 통해 설정했다면, Nuxt.js에서는 디렉터리 구조만으로도 URL이 자동으로 생성됩니다.

asyncData

비동기적으로 생성되는 데이터에 대해 asyncData를 사용합니다. asyncData로 생성된 데이터는 data에 생성된 데이터와 머지됩니다.

export default {
  name: 'PlayersPage',
  async asyncData() {
    const payload = {
      nickname: '코인',
      wordType: 'full',
      limit: 30,
      apikey: NEOPLE_API_KEY,
    }
    const playersData = await playerApi.getPlayers({ params: payload })
    const characterData = await playerApi.getCharacters()
    return { players: playersData.data.rows, characters: characterData.data.rows }
  },
  data() {
    return {
      nickname: '코인'
    }
  }
}

asyncData 실행 결과 — data와 머지된 것을 확인할 수 있습니다

asyncData에서는 thisdatacomputed, props, methods에 있는 값을 가져올 수 없습니다 (단, Vuex의 store는 활용 가능).

  • 기존 Vue에서 API를 불러올 때 일시적으로 데이터가 빌 경우 생기던 UX/UI 이슈가 발생하지 않습니다.
  • 첫 번째 parameter로 context를 받아 페이지 URL의 params나 query 값을 가져올 수 있습니다.
  • pages 디렉토리 내 파일에서만 사용 가능하고, componentslayouts에서는 사용할 수 없습니다.

this를 활용할 수 없어 초기 값을 data에서도 활용하려면 다음과 같은 방식을 사용할 수 있습니다.

const getDefaultData = () => {
  return {
    nickname: '코인',
    wordType: 'full',
    limit: 30,
  }
}
 
export default {
  async asyncData() {
    const payload = {
      ...getDefaultData(),
      apikey: NEOPLE_API_KEY,
    }
    // ...
  },
  data() {
    return {
      ...getDefaultData(),
    }
  }
}

fetch

asyncData와 달리 어떠한 컴포넌트에서든 사용 가능합니다.

  • 로딩 state나 error 상태값을 제공합니다.
  • 데이터에 대한 갱신 처리도 원활하게 할 수 있습니다.
export default {
  name: 'CharacterPlayers',
  props: {
    selected: {
      type: String,
      default: String,
    }
  },
  data() {
    return {
      players: [],
    }
  },
  async fetch() {
    this.players = await fetch(
      `https://api.neople.co.kr/cy/ranking/characters/${this.$route.params.id}/${this.selected}?apikey=${NEOPLE_API_KEY}`
    ).then(res => res.json()).then(data => data.rows)
  },
  watch: {
    'selected': '$fetch',
  },
}

mixin 내부에 fetchasyncData가 정의되고, 컴포넌트나 페이지에도 정의될 경우, mixin 함수가 호출되지 않고 덮어씌워집니다. (즉, 컴포넌트나 페이지에서 정의된 내용을 불러옵니다)

head

vue-meta를 활용하여 Nuxt에서 자동으로 변경해주기 때문에 페이지별로 메타태그 관리가 가능합니다. CSR보다 SSR을 관리하기도 수월한 이유 중 하나입니다.

layout

  • 페이지에서 사용하려는 레이아웃을 지정할 수 있습니다.
  • 따로 설정하지 않을 경우 layouts/default.vue를 기준으로 레이아웃을 잡습니다.
  • 레이아웃을 지정할 때는 컴포넌트 name이 아닌 vue 파일명으로 지정해야 합니다.

middleware

  • Vue.js에서 router에서 권한에 따라 페이지 리다이렉트 처리했던 역할을 대신합니다.
  • 레이아웃이 렌더링되기 전에 처리가 가능합니다.
  • 특정 페이지에서만 진행될 경우에도 middleware 값을 설정할 수 있습니다.
  • middleware의 값은 다수일 경우 Array로, 한 가지일 경우 String으로 넣을 수 있습니다.

3. 설정 파일 (nuxt.config.js)

설정설명
cache컴포넌트 캐시의 허용 유무
headhtml head 메타 태그 관련 내용
alias경로 관련 커스텀 설정
cssCSS 관련 초기 설정값
plugins플러그인 관련 설정
modulesUI framework, Axios 등 모듈 설정
axiosaxios baseURL 등 설정
build빌드 관련 설정
server포트번호 등 서버 환경 설정
loading페이지 이동 시 상단 프로그레스 바 설정
generateHTML 파일 변환 시 고려되는 설정
routerVue Router 기본 구성옵션 덮어씌우기
dev개발 환경 여부 파악

css 설정 예시

export default {
  css: [
    'bulma',
    '@/assets/css/main.css',
    '@/assets/css/main.scss'
  ]
}

동일한 이름이 있을 경우 기본 순서에 따라 하나의 파일만 로드됩니다.

기본 순서: ['css', 'pcss', 'postcss', 'styl', 'stylus', 'scss', 'sass', 'less']

axios 설정: baseURL 변경 시 .nuxt/axios.js와 연동

axios baseURL 변경 시 .nuxt/axios.js 파일 내에서도 함께 변화되는 것을 확인할 수 있습니다

server 설정 예시

server: {
  port: 8080
},

4. 라우트

Nuxt.js에서는 기본적으로 pages의 디렉터리 구조에 따라 라우팅할 수 있습니다. 대표적으로 정적 라우트동적 라우트가 있습니다.

정적 라우트

pages 내 디렉토리 명을 토대로 경로가 생성됩니다. 에러페이지, 로그인, 상품 리스트처럼 특정 param에 따라 변경되지 않는 페이지에 해당합니다.

동적 라우트

파일명 작성 시 앞에 언더바를 붙여야 합니다 (ex. _id.vue). 상품 상세페이지나 유저 정보페이지처럼 각 페이지별로 유니크한 정보를 확인해야 하는 페이지에서 활용합니다.

validate 메서드를 활용하면 param 값에 대한 유효성 체크가 가능합니다.

페이지 이동

Nuxt에서는 페이지 이동 시 <nuxt-link> 컴포넌트를 사용합니다 (<router-link>와 동일).


5. 레이아웃

기존에는 컴포넌트에 slot을 활용하는 경우가 많았는데, Nuxt에서는 레이아웃 폴더에서 별도로 관리합니다.

이 방식의 장점은 기존에는 mounted 되는 파일에 온갖 조건이 붙어서 작업되는 경우들이 생겼는데, 그렇게 처리하지 않게 되어 코드가 훨씬 깔끔해집니다.

  • layouts/default.vue: 기본 레이아웃
  • layouts/error.vue: 에러 레이아웃 (props를 통해 error 인자를 받아올 수 있음)

6. 컴포넌트

반복되는 요소를 분리하여 모듈로 만든 것을 컴포넌트라고 합니다.

  • import ... from을 이용하여 가져온 뒤 components 속성에서 컴포넌트를 선언합니다.
  • 컴포넌트끼리 데이터를 주고받을 때는 props 속성을 활용합니다.
  • props로 넘기는 것은 데이터만이 아닌, methods에 정의된 함수도 넘길 수 있습니다.

props

productOptions: {
  type: Array,
  default: Array,
},
속성설명
type데이터의 타입 정의. Array로 선언하면 여러 타입 허용
required필수값 유무
default넘겨진 값이 없을 경우 지정되는 기본값
validator넘어온 값에 대한 검증 코드
size: {
  type: [String, Number],
  default: '16',
  validator: function (value) {
    return !isNaN(Number(value))
  }
},

props의 한계

props를 직접 수정하려 하면 다음과 같은 경고가 발생합니다.

props를 직접 건드리지 말라는 Vue 경고문

양방향 바인딩을 원한다면 두 가지 방법을 사용할 수 있습니다.

방법 1: 부모의 method를 자식에게도 전달

// 부모 컴포넌트
<PropsSample :message="n" :increment="increment" />
// 자식 컴포넌트
props: {
  message: {
    type: [String, Number],
    default: null,
  },
  increment: {
    type: Function,
    default: Function,
  }
},

방법 2: .sync 수식어와 $emit을 활용

// 부모 컴포넌트
<PropsSample :message.sync="n" />
// 자식 컴포넌트 — $emit으로 부모의 데이터를 변경
methods: {
  increment() {
    const count = this.message + 1
    this.$emit('update:message', count)
  }
}

props의 가장 큰 문제점은 컴포넌트 계층이 깊어질수록 데이터를 전달하기가 복잡해진다는 것입니다. 이것이 Vuex와 Redux가 나온 이유입니다.


7. Vuex store를 이용한 데이터 관리

Vuex를 사용할 경우, 중앙에서 데이터를 효율적으로 관리할 수 있게 됩니다.

  • 로그인한 유저의 정보 및 토큰
  • 로그인한 유저의 권한
  • 메뉴 depth 관련 정보
속성Vue 컴포넌트 대응설명
statedata상태 데이터
getterscomputedstate를 기반으로 커스텀된 값 또는 변동 감지
mutationsstate 값에 대한 처리
actionsmethodsmutation을 실행하기 위한 메서드

computeddataprops의 변동을 감지하지만, gettersstate의 값을 기준으로만 감지한다는 차이가 있습니다.