2 웹팩

source: categories/study/vue-principle/vue-principle_2.md

2.1 웹팩 사용하기

  • script 가 많아졌을 때 웹팩 배우는 어려움보다 script 파일 관리하는 어려움이 더 커졌을 때, 그때 웹팩의 필요성이 대두가 된다.
  • 웹팩을 사용하기 위해선 node 설치
  • node 설치하면 npm 설치된다.
    • npm 다른 사람이 만든 코드를 가져다 쓸 수 있게 해놓은 것
    • 요즘은 타인이 만들어놓은 스크립트 코드를 가지고와 퍼즐 맞추듯이 코딩하는 시대
    • 그거를 도와주는 것이 npm
    • vue도 남이 만들어놓은 스크립트 코드임

npm init

  • package.json 생성
    • 내가 쓰는 남의 소스 코드들을 package.json 에 정리
    • 각각의 소스 코드마다 버전이 존재
    • 나중에 실무를 하다보면 남이 만들어놓은 코드가 몇천개까지 될 수 있다.
    • 이렇게 많은 스크립트 코드들 버전관리하기 매우 어려움
    • 그래서 그 버전을 정확하게 기억하기 위해서 package.json 을 만든다.

npm i vue

  • 위 명령어를 통해 vue 설치
    • 이전까지 script 로 직접 파일을 가져왔지만, 이제 더 이상 script 로 불러오지 않을 것이다.
    • 지금부턴 이렇게 npm 으로 패키지 관리를 할 것이다.

npm i webpack webpack-cli -D

  • webpack, webpack-cli 설치 -D (개발할 때만 webpack, webpack-cli를 사용하겠다 라는 뜻)
  • 위에서 설치한 vue 는 dependencies에 기록
    • vue 는 배포할 때(사용자에게)도 필요한 라이브러리
  • webpack, webpack-cli 는 devDependencies에 기록
    • webpack, webpack-cli 는 개발할 때만 필요한 라이브러리이다.

# webpack.config.js 파일 생성

  • 참고로 npm i를 하면 node_modules 폴더와 package-lock.json 파일이 생긴다.
    • node_modules 폴더엔 우리가 설치한 vue, webpack, webpack-cli 뿐만 아니라 해당 라이브러리들이 의존하고 있는 라이브러리들도 모두 설치된다.
    • 그래서 실제 node를 사용하는 프로젝트를 하다보면 node_modules에 설치되는 남이 만든 코드들이 수백개, 수천개가 된다.
    • 이런 것들을 효율적으로 관리하기 위해서 package.json이 존재

webpack.config.js 에 webpack 관련 설정 작성

// webpack 처리를 할 때 아래 객체를 사용한다.
// entry, module, plugins, output 4가지가 주된 설정, 나머지는 부가적인 설정
module.exports = {
  entry: {
    // app <- 하나로 합쳐질 파일의 이름 app.js
    app: './main.js', 
  },
  module: { // webpack의 핵심
    rules: [ // javascript 파일을 합칠 때 어떤 방식으로 합칠건지를 명시
      {},
    ],
  },
  plugins: [],
  output: {
    filename: '[name].js', // 위에 정의한 app 이라는 이름이 왼쪽 [name]에 매칭되어 들어온다.
    path: './dist', // output 경로
  },
}
  • 기본적인 웹팩 구조는 위와 같다.
  • 위와 같은 형태에서 부수적인 설정들이 많아 복잡해지는 것이다.
<!doctype html> 
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
  <div id="root"></div>
  <script src="/dist/app.js"></script>
</body>
</html>
  • 위에처럼 html 파일엔 웹팩 결과물을 연결해주기만 하면 끝이다.

2.2 프로젝트 구조와 웹팩 빌드

main.js

// main.js
import Vue from 'vue'; // ES6 module 문법

new Vue().$mount('#root'); // 이전에 el 속성에 vue가 통제할 태그를 적었었다. 그거와 같은 역할을 한다.

NumberBaseball.vue (확장자는 vue이지만 javascript 파일이라고 생각하면됨)

  • 아래 내용을 보시면 html이라고 생각드실 수도 있음
  • 특수한 문법을 사용하는 자바스크립트 파일이라고 생각하시면됨
  • 쉽게 작성할 수 있도록 문법적으로 도움을 준 것이지 실제론 자바스크립트 파일이라고 생각하면됨

<template>
  <div>
    <h1>{{ result }}</h1>
    <form @submit="onSubmitForm">
      <input type="text" maxlength="4" v-model="value" ref="answer">
      <button type="submit">입력</button>
    </form>
    <div>시도: {{  }}</div>
  </div>
</template>

<script>
  export default {
    name: 'NumberBaseball',
    data() {
      return {
        value: '',
        result: '',
      }
    },
    methods: {
      onSubmitForm(e) {
          e.preventDefault();
      }
    }
  }
</script>

<style>
  
</style>

  • NumberBaseball.vuemain.js를 합쳐서 dist/app.js 파일로 내보내야지 dist/app.js파일을 불러오는 html 파일이 완성된다.
{
  "name": "number-baseball",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "vue": "2.6.10"
  },
  "devDependencies": {
    "webpack": "4.35.2",
    "webpack-cli": "3.3.5"
  }
}

npm run build

# 지금은 build 에러가 날 것이다.
# 이 에러들을 하나하나 해결해나가면 웹팩이 정상적으로 작동할 것이다.
# 에러를 해결해보자.

  1. Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema - configuration.output.path: The provided value "./dist" is not an absolute path!
    • The output directory as absolute path (required).

위 에러를 해결하기 위해 절대 경로를 설정해준다.
절대 경로를 설정할 때 C:W//~~ 이런식으로 설정하면 너무 복잡해진다.
이를 쉽게 설정할 수 있도록 node에서 제공하는 path 라이브러리를 사용하면 된다.

const path = require('path');

// webpack 처리를 할 때 아래 객체를 사용한다.
// entry, module, plugins, output 4가지가 주된 설정, 나머지는 부가적인 설정
module.exports = {
  entry: {
    // app <- 하나로 합쳐질 파일의 이름 app.js
    app: path.join(__dirname, 'main.js'), 
  },
  module: { // webpack의 핵심
    rules: [ // javascript 파일을 합칠 때 어떤 방식으로 합칠건지를 명시
      {},
    ],
  },
  plugins: [],
  output: {
    filename: '[name].js', // 위에 정의한 app 이라는 이름이 왼쪽 [name]에 매칭되어 들어온다.
    // 아래와 같이 path 라이브러리를 사용해서 경로를 명시해줘야 절대 경로로 설정된다.
    path: path.join(__dirname, 'dist'), // output 경로
  },
}

2.3 웹팩 로더 사용하기

main.js

import Vue from 'vue';

import NumberBaseball from './NumberBaseball.vue';

new Vue().$mount('#root');
  • 위와 같이 main.jsNumberBaseball.vue 파일을 import해야 두 파일이 합쳐져서 app.js 파일로 나온다.
  • main.js 파일이 가장 중요한 파일이다.
  1. Module parse failed: Unexpected token
    • You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file See https://webpack~~
  • webpack 입장
    • entry 에 설정되어있는 파일로 부터 시작
    • main.js 맨 위에 Vue 를 불러옴 <- 이거부터 처리
    • 그 다음에 NumberBaseball.vue 불러오라그러네? 이거 불러오자.
      • 불러와서 읽을라고했더니 못읽음. 너 자바스크립트 아니잖아!?
      • 웹팩은 자바스크립트만 합쳐준다고 했잖아? 그런데 NumberBaseball.vue 는 자바스크립트 파일이 아니다.
  • 그래서 이를 해결해주는게 바로 rules

webpack.config.js

const path = require('path');

// webpack 처리를 할 때 아래 객체를 사용한다.
// entry, module, plugins, output 4가지가 주된 설정, 나머지는 부가적인 설정
module.exports = {
  entry: {
    // app <- 하나로 합쳐질 파일의 이름 app.js
    app: path.join(__dirname, 'main.js'), 
  },
  module: { // webpack의 핵심
    rules: [ // javascript 파일을 합칠 때 어떤 방식으로 합칠건지를 명시
      {
        test: /\.vue$/,
        loader: 'vue-loader',
      },
    ],
  },
  plugins: [],
  output: {
    filename: '[name].js', // 위에 정의한 app 이라는 이름이 왼쪽 [name]에 매칭되어 들어온다.
    // 아래와 같이 path 라이브러리를 사용해서 경로를 명시해줘야 절대 경로로 설정된다.
    path: path.join(__dirname, 'dist'), // output 경로
  },
}

npm i vue-loader -D

npm run build

  • 이렇게 하면 .vue 파일은 웹팩이 처리하는 것이 아니라 vue-loader가 처리한다.
  • 웹팩은 기본적으로 자바스크립트 파일만 처리
  1. [vue-loader] vue-template-compiler must be installed as a peer dependency, or a compatible compiler implementation must be passed via options.
  2. vue-loader was used without the corresponding plugin. Make sure to include VueLoaderPlugin in your webpack config.
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const path = require('path');

// webpack 처리를 할 때 아래 객체를 사용한다.
// entry, module, plugins, output 4가지가 주된 설정, 나머지는 부가적인 설정
module.exports = {
  mode: 'development', // 개발중일 땐 development, 배포할 땐 production
  devtool: 'eval', // 개발할 땐 eval, 배포할 땐 hidden-source-map
                   // eval로 하면 웹팩 빌드 속도가 빨라진다.
                   // 이 모드별로 app.js에 나오는 내용이 달라진다.
  resolve: { // 확장자 처리 가능
    extensions: ['.js', '.vue'], // 해당 확장자들은 import 할 때 생략 가능
  },
  entry: {
    // app <- 하나로 합쳐질 파일의 이름 app.js
    app: path.join(__dirname, 'main.js'), 
  },
  module: { // webpack의 핵심
    rules: [ // javascript 파일을 합칠 때 어떤 방식으로 합칠건지를 명시
      {
        test: /\.vue$/,
        loader: 'vue-loader', // use: 'vue-loader'라고 작성해도된다.
      },
    ],
  },
  plugins: [ // module 처리 이후 output 으로 내보내기 전에 한번 더 가공해주는 역할
    new VueLoaderPlugin(),
  ],
  output: {
    filename: '[name].js', // 위에 정의한 app 이라는 이름이 왼쪽 [name]에 매칭되어 들어온다.
    // 아래와 같이 path 라이브러리를 사용해서 경로를 명시해줘야 절대 경로로 설정된다.
    path: path.join(__dirname, 'dist'), // output 경로
  },
}
  • node 환경에서는 require 문법을 사용 (웹팩에선 require)
  • vue 환경에서는 es6 module 문법 사용

npm i vue-template-compiler -D

  • vue, vue-template-compiler는 서로 버전이 일치해야된다.

main.js 위에서 실수한점

import Vue from 'vue';

import NumberBaseball from './NumberBaseball.vue';

// 아래와 같이 NumberBaseball을 넣어줘야한다.
new Vue(NumberBaseball).$mount('#root');

NumberBaseball.html

  • /dist/app.js로 입력하니깐 절대경로로 잡히는 듯, app.js 파일을 불러오지 못함
  • ./dist/app.js 상대경로로 수정해주자.
<!doctype html> 
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
  <div id="root"></div>
  <script src="./dist/app.js"></script>
</body>
</html>

2.4 v-for 반복문 사용하기


<template>
  <div>
    <h1>{{ result }}</h1>
    <form @submit.prevent="onSubmitForm">
      <input type="text" maxlength="4" minlength="4" v-model="value" ref="answer">
      <button type="submit">입력</button>
    </form>
    <div>시도: {{ tries.length }}</div>
    <ul>
      <li v-for="t in tries">
        <div>{{ t.try }}</div>
        <div>{{ t.result }}</div>
      </li>
    </ul>
  </div>
</template>

<script>
  export default {
    name: 'NumberBaseball',
    data() {
      return {
        tries: [],
        value: '',
        result: '',
      }
    },
    methods: {
      onSubmitForm() {
        this.tries.push({
          try: this.value,
          result: '홈런',
        });
        this.value = '';
        this.$refs.answer.focus();
      }
    }
  }
</script>

<style>
  
</style>

  • 코드 수정하고나서 매번 다시 빌드해야되는 것도 귀찮
  • 나중에 빌드도 자동으로 다시 되도록 하는 방법 알려줌
    • 처음부터 안한 이유는 이 불편함 간접체험 할 수 있도록하기 위해..

2.5 숫자야구 완성하기

2.5.1 어떤게 데이터이고 어떤게 그냥 변수일까요?

  • 화면에 보여지는 것들은 데이터
  • 계산식에서 쓰이는 화면과는 관련없는 애들은 그냥 변수로 선언하면됨
  • 더 정확하게하려면 인풋에 중복 숫자까지 입력 방지

<template>
  <div>
    <h1>{{ result }}</h1>
    <form @submit.prevent="onSubmitForm">
      <input type="text" maxlength="4" minlength="4" v-model="value" ref="answer">
      <button type="submit">입력</button>
    </form>
    <div>시도: {{ tries.length }}</div>
    <ul>
      <li v-for="t in tries">
        <div>{{ t.try }}</div>
        <div>{{ t.result }}</div>
      </li>
    </ul>
  </div>
</template>

<script>
  // getNumbers 함수 - methods 로 구현해도 됨
  // 그런데 getNumbers 는 화면이랑 크게 관련이 없음, 그래서 굳이 methods에 같이 엮지 않는다.
  // data, methods에는 현재 컴포넌트, 화면과 밀접한 연관이 있는 애들만 묶어주시면 된다.
  const getNumbers = () => {
    const candidates = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    const array = [];
    for (let i = 0; i < 4; i++) {
      const chosen = candidates.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
      array.push(chosen);
    }
    return array;
  }
  
  export default {
    name: 'NumberBaseball',
    data() {
      return {
        answer: getNumbers(), // ex) [1, 5, 3, 4]
        tries: [], // 시도 내용 및 시도 횟수를 알아내기 위한 배열
        value: '', // 입력
        result: '', // 결과
      }
    },
    methods: {
      onSubmitForm() {
        // this.value는 문자열
        // this.answer은 배열
        if (this.value === this.answer.join('')) {
          this.tries.push({
            try: this.value,
            result: '홈런'
          })
          this.result = '홈런';
          alert('게임을 다시 실행합니다.');
          this.value = '';
          this.answer = getNumbers();
          this.tries = [];
          this.$refs.answer.focus();
        } else { // 정답 틀렸을 때
          if (this.tries.length >= 9) { // 10번째일 때
            this.result = `10번 넘게 돌려서 실패! 답은! ${this.answer.join(',')}였습니다!`;
            alert('게임을 다시 시작합니다.');
            this.value = '';
            this.answer = getNumbers();
            this.tries = [];
            this.$refs.answer.focus();
          }
          let strike = 0;
          let ball = 0;
          const answerArray = this.value.split('').map(v => parseInt(v));
          for (let i = 0; i < 4; i++) {
            if (answerArray[i] === this.answer[i]) { // 숫자 자릿수 모두 정답
              strike++;
            } else if (this.answer.includes(answerArray[i])) { // 숫자만 정답
              ball++;
            }
          }
          this.tries.push({
            try: this.value,
            result: `${strike} 스트라이크, ${ball} 볼입니다.`
          })
          this.value = '';
          this.$refs.answer.focus();
        }
      }
    }
  }
</script>

<style>
  
</style>

2.5.2 import, export VS. require, module.exports

  • 노드의 모듈 시스템
    • require, module.exports
  • 자바스크립트 모듈 시스템
    • import, export