WEBPACK

모듈과 번들러를 아시나요?

모듈이란 프로그램을 구성하는 구성요소로 관련된 데이터와 함수를 묶은 가장 작은 단위를 말합니다. 함수의 기능별로 모듈을 만들면 유지보수가 쉬워지고 더 좋은 프로그래밍 개발이 가능합니다.

번들링이란 모듈의 의존성 관계를 파악하여, 그룹화시켜주는 작업으로서 모듈들을 묶어 용량도 줄이고 더 사용하기 쉽게 만들어주는 작업을 뜻합니다.

이렇게 모듈들을 번들링해주는 모듈번들러중 가장 많이 사용하는 웹팩에 대해 알아보겠습니다.

등장배경

웹팩이 등장하게된 배경은 다음과 같습니다.

다음과 같은 2개의 js파일이 있다고 가정합니다.

// app.js
var num = 10
function getNum() {
  console.log(num)
}

// main.js
var num = 20
function getNum() {
  console.log(num)
}

이 2개의 js파일을 하나의 html파일에서 가져와 사용한다면 어떻게 될까요?

<!-- index.html -->
<html>
  <head>
    <!-- ... -->
  </head>
  <body>
    <!-- ... -->
    <script src="./app.js"></script>
    <script src="./main.js"></script>
    <script>
      getNum()
    </script>
  </body>
</html>

app.js 에서 선언한 num 변수를 main.js 에서 다시 선언앴기 때문에 정답은 20입니다.

사실 파일이 많아지면 많이질수록 변수의 이름을 모두 기억하지 못하기 때문에 중복선언에 대한 위험은 언제나 존재합니다.

그래서 파일 단위로 변수를 관리하고 싶은 욕구, 자바스크립트 모듈화에 대한 욕구를 예전까진 AMD, Common.js와 같은 라이브러리로 풀어왔습니다.

그 외에도, 코드를 수정하고 새로고침을 눌러야하는 문제라던가 js외에 css,html파일 또는 이미지 등을 압축해야 하는 문제 그리고 빠른 로딩속도등 클라이언트 개발시 다양한 문제점들이 있었습니다.

웹팩은 이런 문제들을 다음과 같은 방식으로 해결합니다.

WEBPACK 이전까지의 여정

위 예시처럼 파일간 전역변수의 문제를 해결하기 위해 번들러 이전에도 많은 시도가 있었습니다.

즉시실행함수

첫번째로는 즉시실행함수 (IIFE) 가 있습니다.

var increase = (function() {
  // 카운트 상태를 유지하기 위한 자유 변수
  var counter = 0
  // 클로저를 반환
  return function() {
    return ++counter
  }
})()

increase() // 1
increase() // 2

위와 같이 counter 지역변수를 선언한 후 해당 변수를 증가시키는 함수를 리턴하는 즉시실행함수를 increase라는 변수에 저장합니다.

그러면 해당 즉시실행함수는 클로저를 반환하기 때문에 렉시컬 환경에 포함된 counter라는 변수를 기억하고 있게 됩니다.

즉 이렇게 선언된 counter라는 변수는 외부에서 접근할수 없으며, 보다 안정적인 모듈화가 가능해졌습니다.

CommonJS, AMD

두번째로 등장한 CommonJS 와 AMD 는 자바스크립트를 브라우저외에서도 사용하려는 즉, 범용적인 언어로 사용하고자 하는 시도로 인해 다른 라이브러리나 모듈을 적용시킬수 있는 호환성, 표준이 필요해져서 개발되었습니다.

CommonJS

  • 모든 파일이 로컬에 존재하여 바로 불러있음을 전제로 함
  • 동기적
  • 대표적으로 Node.js에서 사용
  • exports : 모듈을 생성
  • require() 함수 : 모듈 import
// package/lib is a dependency we require
var lib = require('package/lib')

// behavior for our module
function foo() {
  lib.log('hello world!')
}

// export (expose) foo to other modules as foobar
exports.foobar = foo

AMD (Asynchronous Module Definition)

  • Dynamic Loading, 혹은 Lazy Loading 지원
  • 특이하게 define함수가 파일스코프의 역할을 함
  • 브라우저/서버사이드 에서 동일한 코드로 동작

    • id : id는 모듈을 식별하는데 사용하는 인수로, 선택적으로 사용하며 id가 없으면 로더가 요청하는 script 태그의 src 값을 기본 id로 설정함
    • dependencies : 정의하려는 모듈의 의존성 배열로, 먼저 로드되어야 하는 모듈
    • factory : 모듈이나 객체를 인스턴스화하는 실제 구현을 담당
// package/lib is a dependency we require
// define(id?, dependencies?, factory);
define('alpha', ['package/lib'], function(lib) {
  // behavior for our module
  function foo() {
    lib.log('hello world!')
  }

  // export (expose) foo to other modules as foobar
  return {
    foobar: foo,
  }
})

Module (ES2015)

CommonJS 와 AMD로 어느정도 해결을 했지만, 여전히 복잡하고 사용에 어려움을 느끼던 와중 ES2015 ECMAscript에서 신기능인 모듈을 도입했습니다.

// hello.js
var word = 'Hello'
export default word

// world.js
var word = 'World'
export default word

이제 각각의 변수는 해당 파일안에서만 유효하게 되었으며, 이를 사용하는 방법도 너무나 간단해졌습니다.

<html>
  <head></head>
  <body>
    <div id="root"></div>
    <script type="module">
      import hello from './source/hello.js'
      import world from './source/world.js'
      document.querySelector('#root').innerHTML = hello + '' + world
    </script>
  </body>
</html>

하지만 여전히 하나의 html에서 사용하려면 규모가 커질수록 수많은 import때문에 서버접속비용이 증가하게 됩니다.

그렇다면 WEBPACK은?

ex_screenshot

웹팩과 같은 모듈 번들러는 하나의 시작점 (ex. react app의 index.js)로부터 의존성을 가지는 모듈을 모두 추적해 dependency graph를 만들고, 하나의 결과물을 만들어 내는 방식으로 문제를 해결합니다.

특히 js뿐이 아닌 hmtl, css, image같은 정적파일 또한 모두 번들링을 통해 더 효과적인 방법으로 제공합니다.

이러한것들을 가능하게 하는 웹팩의 중요한 개념인 entry, output, loader, plugins에 대해 알아보겠습니다.

entry

웹팩이 실행되기전 webpack.config.js을 참조하여 실행됩니다.

const path = require('path')

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js',
  },
  output: {
    filename: '[name].js',
    path: path.resolve('./dist'),
  },
}

위에서 보이는 entry가 다른 모듈을 사용하고있는 최상위 자바스크립트 파일에 위치를 알려주는 속성입니다.

따라서 웹팩은 entry 속성에 명시된 파일을 기준으로 종속성 그래프를 만들어 하나의 번들 파일을 만들어냅니다.

일반적으로 싱글 엔트리 포인트를 갖지만, 목적에 따라 멀티 엔트리 포인트를 갖기도 합니다.

module.exports = {
  entry: ' ./src/index.js',
}

/* Entry 포인트는 여러개일 수 있다. */
module.exports = {
  entry: {
    login: './src/LoginView.js',
    main: './src/MainView.js',
  },
}

entry 속성에 지정된 index파일은 다음과 같을수 있습니다.

// index.js
import LoginView from './LoginView.js'
import HomeView from './HomeView.js'
import PostView from './PostView.js'

function initApp() {
  LoginView.init()
  HomeView.init()
  PostView.init()
}

initApp()

output

output은 생성한 번들을 내보낼 위치와 번들 파일의 이름을 지정하는 속성입니다. default 값으로는 ./dist/main.js를 사용합니다.

entry와 비교하여 주의할점이 있는데 entry의 경로는 프로젝트 디렉터리 내부이기 때문에 상대 경로이지만, output 의 경로는 프로젝트 디렉터리 내부라는 보장이 없으므로 절대 경로로 설정해야 합니다.

const path = require('path')

module.exports = {
  ...
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js',
  },
}

loader

웹팩은 자바스크립트 뿐만 아니라, loader를 이용하여 css, image, 웹폰트, JSX, VUE 등 다양한 종류의 파일을 함께 번들링할 수 있습니다.

만약 아래와 같은 파일을 웹팩을 통해 번들링하려고 하면 에러가 발생합니다.

// app.js
import './common.css'

console.log('css loaded')

에러의 내용은 app.js 파일에서 임포트한 common.css 파일을 해석하기 위해 적절한 로더를 추가해달라는 것입니다.

module.exports = {
  entry: './index.js',
  output: {
    filename: 'main.js',
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['css-loader'],
      },
    ],
  },
}

위의 module 쪽 코드를 보면 rules 배열에 객체 한 쌍을 추가했습니다. 그리고 그 객체에는 2개의 속성이 들어가 있는데 각각 아래와 같은 역할을 합니다.

  • test : 로더를 적용할 파일 유형 (일반적으로 정규 표현식 사용)
  • use : 해당 파일에 적용할 로더의 이름

추가적으로 css로더 외에도 많은 로더가 있으므로, 해당 로더들을 rules 배열에 추가해주면 됩니다.

추가한 로더는 오른쪽에서 왼쪽 순으로 적용됩니다.

plugin

플러그인(plugin)은 웹팩의 기본적인 동작에 추가적인 기능을 제공하는 속성입니다. 로더랑 비교하면 로더는 파일을 해석하고 변환하는 과정에 관여하는 반면, 플러그인은 해당 결과물의 형태를 바꾸는 역할을 한다고 보면 됩니다.

웹팩 변환 과정 전반에 대한 제어권을 갖고 있습니다.

// webpack.config.js
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  plugins: [new HtmlWebpackPlugin(), new webpack.ProgressPlugin()],
}

단, 플러그인의 배열에는 생성자 함수로 생성한 객체 인스턴스만 추가될 수 있습니다.

mode

추가적으로 웹팩4 부터 해당개념이 추가되었는데, mode에는 development, production, none 세가지 옵션이 있습니다.

default는 production입니다.

정리

ES2015에 나온 모듈을 더 유용하게 사용하도록 도와주는 번들러인 웹팩은, 엔트리포인트를 시작으로 연결되어 었는 모든 모듈을 하나로 합쳐서 하나의 파일을 임포트함으로서 네트워크 비용도 줄이고, js 뿐 아니라 css, image, html등을 까지도 모듈로 제공해주기 때문에 일괄적으로 개발할수 있습니다.

또한 Dynamic Loading & Lazy Loading 지원을 통해 사용하지 않는 코드를 줄여 메모리를 더 잘 활용할수 있게 해주며 유저에게 더 빠른 웹 사용속도를 제공해줍니다.


Written by@JeongYeonJae
이것저것 쓰는 개발블로그

ResumeGitHub