모듈 시스템의 필요성

자바스크립트 코드가 많아지면서 하나의 파일로 관리하는데 한계가 있습니다. 그렇다고 여러 개의 파일로 브라우저에서 로드하는 것은 그만큼 네트워크에 비용을 지불해야하는 단점이 있습니다. 뿐만 아니라 여러 개의 자바스크립트 파일을 하나의 html문서에서 로드하여도 하나의 전역을 바라보기 때문에 전역 스코프가 오염된다는 단점도 존재합니다..

이로 인해 모듈을 사용하게 되었으며 모듈은 "자체적인 모듈 스코프"를 갖고있습니다. 자바스크립트의 문법으로 모듈을 지원하기 시작한 것은 ES6부터 입니다. importexport 키워드를 사용하여 모듈 시스템을 제공하고 있습니다.

Module System

자바스크립트의 대표적인 모듈 시스템으로는 ESM(ES6 Module System), CommonJS, AMD이 존재합니다.

  • ESM은 클라이언트 사이드(브라우저)에서 사용하는 모듈 시스템입니다. ES6부터 자바스크립트의 문법으로 지원하고 있습니다.

  • CommonJS는 클라이언트 사이드 뿐만 아니라 서버 사이드, 즉 모든 환경에서 모듈 시스템을 사용하는 것을 목표로 합니다. 대표적으로 서버 사이드 환경인 Node.js는 CommonJS를 표준으로 채택하여 사용되고 있습니다.

  • AMD는 비동기로 로드되는 환경에서 모듈 시스템을 사용하기 위해서 사용합니다.

CommonJS를 클라이언트 사이드에서 사용할 수 있는 방법이 있고, ESM 또한 Node.js로 사용할 수 있는 방법이 있습니다. 즉, 환경 종속성이 정확하게 구분되어 사용되지 않습니다.

ESM(ES6 Module System)

각 모듈은 "독자적인 모듈 스코프"를 갖게 됩니다. 즉, 모듈 내에 var 키워드로 선언한 전역 변수들은 더이상 전역 변수가 아니며 전역 객체의 프로퍼티도 아니게 됩니다. 모듈의 자체적인 스코프에 등록됩니다.

일반적인 자바스크립트 파일은 script 태그로 분리해서 로드해도 전역을 공유하는 문제가 있지만 모듈의 경우 "독자적인 모듈 스코프"를 갖게 됩니다.

모듈 내에 선언한 식별자는 일반적으로 해당 모듈 내부에서만 참조할 수 있으며 외부로 공개하여 다른 모듈에서 재사용하기 위해서는 export 키워드를 사용하며, 다른 모듈이 export한 대상을 가져오기 위해서는 import 키워드를 사용합니다.

// index.html
// app.js 파일을 모듈로서 로드
// app.js 파일은 독자적인 모듈 스코프를 갖고 있다
<script type="module" src="./app.js"><script>

script 태그로 자바스크립트 파일을 로드할 때 type="text/javascript" 대신에 type="module"을 사용하면 로드된 app.js 파일은 모듈로서 동작하게 됩니다. 즉, 모듈을 사용할 수 있게 됩니다. 이때 모듈은 기본적으로 "strict mode"가 적용됩니다.

그리고 이전처럼 모든 파일을 script 태그로 로드하는 대신, 만약 app.js에서 math.js를 import 했다면 html 문서에는 app.js 파일만을 로드하도록 작성합니다. math.js 파일은 app.js파일에 의해 로드되는 "의존성"을 갖게 됩니다. 따라서 app.js파일만을 script 태그로 로드하면 자동적으로 math.js 파일도 로드됩니다.

export

export 키워드는 "선언문 앞"에 사용할 수 있습니다. 또한 "식별자 앞"에서도 사용이 가능합니다.

// 변수 선언문 앞에 export 키워드 사용
export const name = 'user name';

// 함수 선언문 앞에 export 키워드 사용
export function sum(a, b) { return a + b; };

// 클래스 선언문 앞에 export 키워드 사용
export class MyClass {,,,};

// 식별자 name을 export
export name;

export할 대상(식별자)을 "하나의 객체"로 구성하여 한 번에 export할 수도 있습니다.

그리고 as 키워드를 사용하여 다른 이름으로 export할 수도 있습니다.

// 객체 리터럴 앞에 export 키워드 사용
export { abs, max, min };

// 선언된 식별자를 as를 사용하여 다른 이름으로 export
export { name as userName };

export default

모듈 내 export할 대상이 하나 뿐이라면 default 키워드를 사용할 수 있습니다. 즉, 모듈 내 export defualt는 한 번만 사용할 수 있습니다.

default 키워드를 사용하는 경우 기본적으로 이름 없이 하나의 값을 export합니다.

default 키워드를 사용하는 경우 var, let, const 키워드 앞에 사용할 수 없습니다.

// 함수 선언문 앞에 export default
export default function sum(a, b) { return a + b; };

// 식별자 앞에 export default
export default name;

// let, const, var 키워드 앞에서는 export default가 불가능, 에러 발생
export default const name = 'user name';
export defualt let name = 'user name';
export default var anem = 'user name';

import 키워드

import 키워드로 다른 모듈에서 공개한 식별자를 자신의 스코프 내부로 로드할 수 있습니다. 다른 모듈이 export한 식별자 이름을 { } 내부에 작성하여 import해야 하며, from 뒤에 모듈의 경로를 작성합니다. 모듈인 경우 파일 확장자를 생략할 수 없습니다. 반드시 "export한 식별자 이름으로 import" 해야 합니다.

// math.js
export function sum(a, b) { return a + b; };

export {abs, max, min};



// main.js
import { sum } from './math.js';

import { abs, max, min } './math.js';

이때 math.js 파일은 import 문에 의해 로드되는 "의존성"을 갖게 됩니다. 따라서 math.js는 script 태그로 로드하지 않아도 main.js가 로드되면 자동적으로 math.js가 로드됩니다.


다른 모듈이 export한 식별자 이름을 일일이 지정하지 않고, 하나의 이름으로 한 번에 import할 수도 있습니다.

import * as 객체이름 from '경로' 형식을 사용합니다. 이때 import되는 식별자는 as 뒤에 작성한 이름의 객체에 프로퍼티로 할당됩니다.

// math.js
export function sum(a, b) {,,,};
export { abs, max, min };



// app.js
// math.js가 export한 모든 식별자를 math 객체의 프로퍼티로 할당하여 사용
import * as math from './math.js';

console.log(math); // { sum: f, abs: f, max: f, min: f } 

math.sum(1, 2); // 3

default 키워드와 함께 export한 모듈은 { } 없이 임의의 이름으로 import합니다.

// math.js
export default function sum(a, b) {,,,};



// app.js
import customSum from './math.js';

CommonJS

CommonJS는 자바스크립트를 사용하는 모든 환경에서 모듈을 사용하는 것을 목표로 합니다. module.exports혹은 exports 키워드로 모듈을 만들고, require() 함수에 인수로 사용할 모듈을 포함하는 자바스크립트 파일의 경로를 인수로 전달하여 모듈을 불러 들이는 방식입니다.

대표적으로 서버 사이드 플랫폼인 Node.js에서 이를 사용합니다.

module

module이라는 키워드는 하나의 모듈에 대한 정보를 갖고 있는 객체입니다.

기본적으로 module.exports = 값; 또는 module.exports.식별자 = 값;형식으로 사용합니다.

// math.js
// 1. module.exports = value; 방식

module.exports = 10;

첫 번째 방식으로 사용한 경우 module 객체는 다음과 같은 구조를 갖습니다.

Module {
    ,,,
    exports: 10,  // -> "module.exports = value" 방식
    ,,,
}

// math.js
// 2. module.exports.key = value; 방식

module.exports.number = 10;

두 번째 방식으로 사용한 경우 module 객체는 다음과 같은 구조를 갖습니다.

Module {
    ,,,
    exports: {
        number: 10  // -> "module.exports.key = value" 방식
    },
    ,,,
}

즉, export할 것이 여러 개라면 module.exports.key = value방식을 사용하고, export할 것이 한 개라면 module.exports = value방식을 사용합니다.

exports

exports 라는 키워드로도 module.exports 와 같은 동작을 수행할 수 있습니다.

exports.number = 10;

module.exports === exports // -> true
exprots // -> { number: 10 }

exports 객체는 module.exports 객체를 바라봅니다. 즉, 서로 동일한 객체를 바라보고 있습니다.
실제로 exports 객체는 require되기 전 module.exports의 값을 할당받습니다.

그러므로 exports 객체에 다른 값을 할당하는 방식을 사용해서는 안됩니다. 값을 할당한다면 module.exprots 객체를 할당받지 못하게 됩니다.

exports = value; // -> X

exports.key = value // -> O

require

require로 다른 모듈 파일을 불러올 수 있습니다. require로 모듈을 불러오는 방식은 아래와 같습니다.

const 식별자 = require('모듈 경로');

이때 모듈 경로를 작성할 때 상대경로에 존재하며 파일 확장자가 .js, .json, .node인 파일들의 확장자 생략이 가능합니다.

// 상대 경로에 존재하는 파일이 js, json, node인 경우 파일 확장자 생략 가능

const value = require('./파일경로');

또한 모듈 경로에 절대 경로나 상대 경로에 대한 표시(/,./,../)를 작성하지 않은 경우 자동적으로 /node_module을 앞에 붙여 탐색합니다. 즉, node_module 폴더 내 모듈 파일을 탐색합니다.

// 경로에 대한 표시가 붙지 않은 경우 node_modules 폴더 내 모듈 탐색
// path 파일을 node_modules 폴더 내 탐색하여 가져온다

const path = require('path');

Webpack

현재 모든 브라우저에서 모듈 시스템을 사용가능한 것은 아닙니다. 모든 브라우저에서 모듈 시스템을 사용하기 위해서 Webpack을 사용합니다. Webpack은 각 파일들을 모듈로서 인식하고 여러 개의 파일(모듈)을 하나로 합쳐주는 "번들러"입니다.

번들 과정에서 하나의 시작점(entry)으로부터 의존적인 모듈을 전부 찾아 "하나의 결과물"을 만들어냅니다. 이때 의존성은 importrequire을 통해 의존 관계가 형성되고, 의존 관계를 따라 모든 파일을 하나의 파일로 합치게 됩니다.

모듈간 의존 관계

모듈로 개발하게 되면 모듈간 의존 관계가 형성됩니다. 하나의 자바스크립트 모듈에서 다른 모듈을 import하는 방식으로 형성됩니다. 위에서 살펴본 예제에서 app.js에서 math.js를 import하게 되면 app.js가 math.js를 가져오기 때문에 의존 관계가 형성됩니다.

이처럼 자바스크립트가 로딩하는 모듈이 많이질 수록 모듈간의 의존성은 증가합니다. 의존성 그래프의 시작점을 웹팩에서는 "entry"(엔트리 포인트)라고 합니다.

웹팩 설치하기

여기서 웹팩은 의존 관계로 연결된 여러 개의 모듈 파일을 하나로 합쳐주는 역할을 합니다.

여러 개의 파일을 하나로 합치는 과정을 "번들링(bundling)"이라고 하고, 이렇게 하나로 합쳐진 파일을 "번들(bundle)"이라고 합니다. 즉, 웹팩이 번들을 만드는 "번들러(bundler)"역할을 하게됩니다.


번들링 작업을 하는 "웹팩 패키지(webpack)"와 웹팩을 터미널 명령어로 사용할 수 있는 "웹팩 cli(webpack-cli)"를 설치하는 방법은 터미널에 아래 명령어를 입력합니다.

npm install -D webpack webpack-cli

npm으로 설치했으므로 package.json 파일의 devDependencies에 설치한 패키지의 정보가 입력된 것을 확인할 수 있습니다. 설치할 때 -D 옵션을 사용했기 때문에 devDependencies에 존재하게 됩니다. devDependencies는 개발용 패키지입니다. 그렇지 않은 패키지들은 dependencies에 존재합니다.

webpack과 webpack-cli를 설치하면 node_modules 폴더의 .bin 폴더 안에 webpack과 webpack-cli가 설치된 것을 확인할 수 있습니다.

웹팩 설정하기

웹팩을 실행(번들링)할 때 사용되는 필수적인 옵션 세 가지가 존재합니다.

  1. --mode : mode는 development, production, none이라는 세 가지 값을 지정할 수 있습니다. 이것은 개발 환경인지 운영 환경인지에 따라 development 혹은 production으로 결정됩니다. 개발용 정보를 추가하려면 development를 입력하고, 운영을 위해 배포하기 위한 최적화 설정을 하려면 production을 입력합니다.

  2. --entry : 모듈의 시작 지점을 entry 혹은 entry point라고 합니다. 해당 옵션을 통해 시작 지점을 설정할 수 있습니다.

  3. --output-path, -o : entry를 시작으로 의존되어 있는 모든 모듈을 웹팩이 하나의 결과로 저장할 경로를 설정하는 옵션입니다.

이 세가지 옵션을 사용하여 모듈을 번들링해보겠습니다.

node_module/.bin/webpack --entry <시작파일> --mode <번들링모드> -o <번들파일경로>

위 명령어는 "개발 환경"(development)으로 번들링을 하며 "entry는 app.js"이고 "번들링 결과물인 번들 파일의 경로는 dist폴더"에 main.js로 설정하는 명령어입니다.

그리고 html 문서에 script 태그의 src 어트리뷰트 값으로 ./dist/main.js를 지정하고 모듈로 인식하도록 작성한 type 어트리뷰트를 삭제합니다.

웹팩의 역할은 이렇게 여러 개의 모듈을 하나의 파일로 만들어주는 역할을 합니다.

웹팩 설정 파일로 웹팩 설정하기

매번 번들링을 위해 명령어를 입력할 때마다 터미널에 긴 옵션을 작성하기 보다는 "웹팩 설정 파일"을 만들어 웹팩 설정을 해보겠습니다.

--config 옵션을 보면 웹팩 설정 파일을 지정할 수 있으며 이때 기본값은 webpack.config.js 또는 webpackfile.js를 사용합니다.

아래 예시에서는 "webpack.config.js" 파일명을 사용하여 웹팩 설정 파일을 만들겠습니다.

먼저 프로젝트 상단에 webpack.config.js라는 파일을 생성합니다.

module.exports 객체의 프로퍼티로 이전에 작성한 세 가지 옵션을 작성합니다.

  • mode 프로퍼티의 값으로는 "development"를 할당합니다.

  • entry 프로퍼티의 값은 객체입니다. 이 객체에 프로퍼티 키가 main인 프로퍼티를 생성하고, 값으로는 모듈이 시작되는 지점, 즉 entry point인 app.js의 경로를 작성합니다.

  • output 프로퍼티의 값은 객체입니다. 이 객체에는 pathfilename 프로퍼티가 존재합니다.

    • path 프로퍼티의 값으로는 번들링의 결과물인 번들의 디렉토리명을 작성하는데 "절대 경로"를 사용해야 합니다. 절대 경로를 가져오기 위해서 node의 path 모듈을 가져와서 사용했습니다.
    • filename 프로퍼티에는 번들링된 파일명을 작성하는데 [name]부분은 entry 객체의 키인 main으로 치환됩니다. 이러한 방식으로 파일명을 작성한 이유는 entry가 여러 개 존재할 수 있기 때문에 entry가 여러 개라면 그에 맞게 output 또한 여러 개가 되어야 하므로 output 이름을 동적으로 만들기 위함입니다.
entry: {  // -> entry가 여러 개인 경우
    main: '경로1',  // -> main.js 번들 파일 생성
    main2: '경로2',   // -> main2.js 번들 파일 생성
    main3: '경로3'   // -> main3. js 번들 파일 생성
},
output: {
    path: '절대경로',
    
    // name은 entry 객체의 프로퍼티 키로 치환되어 번들 파일 생성
    filename: '[name].js'
}

위 예제와 같이 entry가 세 개인 경우, output의 파일명은 각각 main.js, main2.js, main3.js로 동적으로 만들어집니다.

웹팩 번들링 실행 명령어를 script 추가

npm은 프로젝트를 관리하는 도구로서 "스크립트를 자동화"해주는 기능을 갖고 있습니다. 이 기능을 이용하여 웹팩을 통해 코드를 번들링하는 과정을 npm scripts에 등록하여 사용해보겠습니다.

package.json 파일의 scripts에 build 프로퍼티의 값으로 "webpack"을 작성합니다. npm scripts를 등록할 때 node_modules/.bin을 다 작성하지 않고 webpack만 작성해도 npm은 현재 프로젝트의 node_modules에서 자동으로 webpack 명령어를 찾아 실행합니다.
즉, node_modules/.bin/webpackwebpack은 동일한 명령어 입니다.

webpack 명령어는 기본 config 파일인 webpack.config.js 파일을 읽어서 웹팩 번들링 작업을 하게 됩니다. 이를 실행하기 위해서는 터미널에 npm run build 명령어를 입력합니다.

로더

로더 역할

웹팩은 모든 파일을 모듈로 인식합니다. 자바스크립트 파일 뿐만 아니라 CSS 파일, 이미지 파일, 폰트 파일 등 모든 파일을 모듈로 처리합니다. 그렇기 때문에 ES6의 import라는 키워드를 사용하여 자바스크립트 코드 안으로 가져올 수 있습니다.

이것이 가능한 이유는 웹팩에 "로더"가 있기 때문입니다. 웹팩에서 로더가 하는 역할"모든 파일을 자바스크립트의 모듈처럼 만들어주는 역할"을 합니다. 예를 들어, CSS 파일을 자바스크립트에서 직접 로드해서 사용할 수 있게 해주고, 이미지 파일의 경우에는 data URL 형식의 문자열로 변환한 뒤에 자바스크립트에서 이미지 파일을 직접 사용할 수 있도록 해줍니다.

로더 동작

로더의 동작을 이해하기 위해서 직접 로더를 생성하여 로더가 어떻게 동작하는지 알아보겠습니다.

먼저 프로젝트 상단에 로더 모듈을 위한 my-wepack-loader.js 파일을 생성하고 아래와 같이 작성합니다.

module.exports로 노드 모듈을 생성하고 이 모듈에 "함수"를 할당합니다. 이렇게 로더는 "함수 형태"입니다. 로더가 읽은 파일의 내용이 문자열로 content라는 매개변수로 전달됩니다. 로더가 동작하는지 확인하기 위해서 console.log로 로그를 찍도록 정의하였습니다.

로더를 사용하기 위해서 webpack.config.js 파일에 아래와 같이 module 객체의 rules 배열에 추가할 수 있습니다. 즉, 로더는 "module.rules 배열의 요소로 로더를 추가"하는데, testuse 프로퍼티로 구성된 객체로 추가합니다.

  • test 프로퍼티에는 처리해야할 "파일명" 또는 "파일의 패턴(정규표현식)"을 작성해줍니다. 해당 예제에서는 .js로 끝나는 모든 파일명을 지정했습니다.

  • use 프로퍼티에는 배열을 작성하는데 배열의 요소로 "사용할 로더의 절대경로"를 작성합니다. 즉, 모든 자바스크립트 파일에 대해서 my-webpack-loader라는 로더가 실행되도록 설정하였습니다.


이후 npm run build 명령어를 터미널에 입력하면 터미널에 'myWebpackLoader가 동작함'이라는 문자열이 표시되는 것을 확인할 수 있습니다. 즉, 로더가 정상적으로 실행되었다는 것을 확인할 수 있습니다.


이번에는 로더로 자바스크립트 코드에서 console.log()를 alert()으로 변경하는 동작으로 수정해보겠습니다.


정리하자면 웹팩의 로더는 "특정 파일을 어떤 처리"를 하기 위해서 사용합니다. 처리할 파일명이나 파일의 패턴을 test에 명시하고, 일치하는 파일을 use에 명시한 로더 함수에게 전달하여 실행되도록 합니다. 처리할 파일이 여러 개라면 로더 함수도 그에 맞게 여러 번 실행될 것입니다.

자주 사용하는 로더

1. css-loader

웹팩은 모든 것을 모듈로서 인식하기 때문에 자바스크립트 뿐만 아니라 CSS도 import 키워드로 불러올 수 있습니다. CSS 파일을 자바스크립트로 불러와 사용하려면 "CSS를 모듈로 변환"하는 작업이 필요합니다. 실제로 CSS 파일의 스타일 코드를 자바스크립트 문자열로 변환하여 삽입해줍니다.

만약 자바스크립트 파일에서 어떠한 처리도 하지 않은 상태에서 CSS 파일을 불러온다면 다음과 같이 웹팩이 CSS 파일을 해석하지 못하여 에러가 발생하게 됩니다.

ES6의 import 키워드로 CSS 파일을 가져오려면 app.css 파일이 모듈이 되어야 하는데 웹팩의 css-loader"CSS파일을 모듈로 변환"해주는 역할을 합니다. 즉, 웹팩이 "CSS 파일을 모듈로서 가져오게 CSS 파일을 처리"해주는 것이 css-loader입니다.

css-loader를 설치하기 위해서 터미널 npm install css-loader를 입력합니다.

그리고 웹팩 설정 파일인 webpack.config.js 파일에 css-loader를 추가해줍니다. module 객체의 rules 배열에 test와 use 프로퍼티로 구성된 객체를 작성합니다. 이때 use에 로더의 절대 경로를 작성하는 대신로더 이름을 문자열로 전달해도 같은 동작을 하게됩니다.

이렇게 작성하면 웹팩은 entry point인 app.js부터 시작해서 연결된 모든 모듈들을 검색합니다. 그리고 test에 설정한 정규표현식 패턴과 일치하는 CSS 파일을 만나게 되면 css-loader 함수가 해당 CSS 파일을 모듈로 변환하는 처리가 동작합니다.

npm run build 명령어로 번들링을 진행하게 되면 dist 폴더에 번들인 main.js에 "자바스크립트 문자열 형태로 css 코드가 추가"된 것을 확인할 수 있습니다.


하지만 브라우저에 렌더링된 결과를 보면 CSS가 적용되어있지 않습니다.

html 문서는 DOM이라는 자료구조로 변환되어 브라우저가 해석하여 렌더링을 하는 것처럼 CSS 코드도 CSSOM으로 변환되어야 브라우저가 해석하여 렌더링을 할 수 있습니다. 이는 html 문서에서 link 태그로 CSS 파일을 직접 불러오거나, html 요소중 style 태그를 사용하여 인라인으로 스타일을 작성해야 합니다.

현재는 이러한 처리가 되지 않고 자바스크립트 파일에만 CSS 코드가 존재합니다. 즉, 우리는 이 CSS 코드를 "html 문서에 삽입해주는 과정이 추가적으로 필요합니다.

2. style-loader

이를 위해 나온 것이 style-loader입니다. style-loader는 "자바스크립트 문자열로 변경된 CSS 코드를 html 문서에 style 태그의 content로 삽입하는 역할"을 합니다.

즉, CSS를 모듈로 사용하고 웹팩을 통해 번들링하기 위해서는 css-loader를 통해 "CSS 파일을 모듈로 변환"해야하고, style-loader를 통해 "html 문서에 CSS 코드를 삽입하는 과정"이 필요합니다.


style-loader를 다운받기 위해서 터미널에 npm install style-loader 명령어를 작성합니다.

이후에 웹팩 설정 파일인 webpack.config.js 파일에서 이전에 작성한 CSS 파일을 처리하는 로더부분의 use에 style-loader를 추가해줍니다.

이제 하나의 CSS 파일마다 두 개의 로더가 실행되는데 이때 로더가 실행되는 순서"use에 작성한 마지막 로더부터 역순으로 실행"됩니다. 즉, 하나의 CSS 파일에 대해서 css-loader가 먼저 실행되고 그 다음에 style-loader가 실행됩니다.

그리고 npm run build 명령어로 번들링을 하면 브라우저에 CSS가 적용된 것을 확인할 수 있습니다.

개발자 도구에서 확인한 결과 index.html 문서의 head 부분에 style 요소로 CSS 코드가 삽입된 것 또한 확인할 수 있습니다.

2. asset/resource

에셋 모듈은 로더를 추가로 구성하지 않아도 에셋 파일(이미지, 폰트, 아이콘 등)을 사용할 수 있도록 해주는 모듈입니다.

위에서 설명한 file-loader와 url-loader는 webpack 5 이전에 사용하던 로더이며, webpack 5부터는 Asset Module을 사용합니다.

  1. asset/resource

asset/resource 는 파일을 모듈 형태로 사용할 수 있게 도와주며, 웹팩 아웃풋 경로에 파일을 옮겨주는 역할을 합니다. 이전에는 file-loader를 사용하여 처리할 수 있었습니다.

webpack.config.js 파일에 아래와 같이 asset module을 추가해줍니다. file-loader와 다른점으로는 로더의 경로를 작성하는 use 대신에 type에 'asset/resource'를 작성합니다.

이는 모든 .jpg 파일을 output 경로에 복사하여 옮기고, 복사된 파일의 경로를 번들 파일에 삽입합니다. 파일을 복사할 때 파일 이름은 기본적으로 해시코드.jpg이름으로 복사되고, 번들 파일에서 .jpg 파일을 모듈로 import하는 구문의 경로가 자동적으로 ouptut 경로가 선두에 prefix로 삽입"되어 './dist/해시코드.jpg' 문자열로 변환되어 번들에 삽입됩니다.

// app.js

import image from './image/main.jpg';
// /dist/main.js -> 번들파일

// 경로가 자동적으로 아웃풋 경로에 복사된 파일의 경로로 삽입된다
import image from '/dist/151cfcfa1bd74779aadb.jpg'

파일을 output으로 복사할 때 asset/resouce는 기본적으로 "[hash][ext][query] 파일명"을 사용합니다. 이는 webpack.config.js 파일의 output에 assetModuleFilename으로 설정하여 템플릿을 수정할 수 있습니다.

다음과 같이 output에 assetModuleFilename에 '[name][hash][ext][query]'를 작성 되면, output에 복사되는 파일의 이름은 파일이름해시코드.파일확장자?쿼리스트링으로 복사되고, 모듈로 사용하는 부분에서도 './dist/파일이름해시코드.파일확장자?쿼리스트링' 문자열로 변환되어 로더에 삽입됩니다.

asset/module로 처리된 파일은 기본적으로 output에 복사됩니다. 하지만 output내 다른 특정 디렉토리에 파일을 복사하고싶은 경우 output.assetModuleFilename에 파일 이름 앞에 특정 경로를 작성합니다. 즉, '특정경로/파일이름' 을 작성하여 output 경로 내 특정 폴더에 파일을 복사할 수 있습니다.


assetModuleFilename에 'static/[hash][ext][query]'를 작성하면 output 경로의 static 폴더에 [hash][ext][query]형식의 파일명으로 파일이 복사됩니다. 그리고 파일을 사용하는 곳에서는 'dist/static/[hash][ext][query]' 문자열로 변환되어 로더에 삽입됩니다.

assetModuleFilename과 같은 역할로 rules 배열의 generator.filename에도 '특정경로/파일이름'을 작성하여 output 경로 내 특정 폴더에 파일을 복사할 수 있습니다. assetModuleFilename과 차이점은 generator.filename은 asset/resouce와 asset 모듈에서만 사용할 수 있습니다.


위 그림에서 generator.filename에 'static/[hash][ext][query]'는 파일을 복사할 때 ./dist/static 경로에 복사하며, 파일이름은 assetModuleFileaname에 작성한 [hash][ext][query]와 동일하게 작성합니다.


generator.filename에 'static/[hash][ext][query]'를 작성함으로서 output 경로내 static 폴더에 파일이 [hash][ext][qeury] 형식의 이름으로 복사됩니다.

참고로 output에서 assetModuleFilename과 module의 rules에서 rules.generator.filename을 동시에 사용하면 "rules.generator.filename이 우선시"됩니다.

3. asset/inline

asset/inline 은 파일을 data URL로 변환하여 번들 파일에 삽입합니다. 이전에는 url-loader를 사용하여 처리할 수 있었습니다.

아는 모든 .svg 파일을 data URL 문자열로 번들에 삽입합니다. 이는 url-loader처럼 따로 output에 파일을 복사하지 않고, 파일 자체를 data URL 형식의 문자열로 변환하여 번들에 삽입합니다.

4. asset

asset은 파일의 "크기"에 따라 asset/resource혹은 asset/inline중 선택되어 로더가 실행됩니다.

기존처럼 test에 로더가 실행할 파일명을 패턴으로 작성하고, type 부분에 asset을 입력합니다.

parser.datUrlCondition.maxSize에 파일의 크기를 작성하고, 해당 크기 미만인 경우 asset/inline로더가 실행되고, 해당 크기 이상인 경우 asset/resource로더를 실행합니다.


정리하자면 다음과 같습니다.

  • css-loaderCSS 파일을 자바스크립트 모듈처럼 사용할 수 있도록 CSS 파일을 처리하는 로더입니다.

  • style-loader는 자바스크립트 문자열로 처리된 CSS 코드를 html 문서에 주입시켜 브라우저에 스타일이 적용되도록 처리해주는 로더입니다.

  • file-loader와 url-loader는 webpack 5이전에 사용하던 로더입니다. webpack 5 이후부터는 asset module을 사용하여 별도의 로더를 설치하지 않고 모든 파일들을 사용할 수 있게 해주는 모듈을 사용합니다. file-loader 대신에 asset/resource를 사용하고, url-loader 대신 asset/inline을 사용합니다.

  • asset지정한 파일 크기에 따라 asset/resource또는 asset/inline 로더가 선택적으로 실행됩니다.

플러그인

플러그인의 역할

로더가 파일 단위로 처리했더라면 플러그인은 번들링된 결과물 하나인 번들을 처리합니다. 자바스크립트 코드를 "난독화"한다거나, "특정 텍스트를 추출"하는 용도로 플러그인이 사용됩니다.

플러그인 동작

플러그인의 동작 원리를 이해하기 위해서 플러그인을 직접 만들어보겠습니다. 웹팩 문서의 Writing a Plugin을 보면 플러그인은 로더처럼 함수로 정의하는 것과는 다르게 "클래스"로 정의합니다.

프로젝트 상단에 플러그인을 위한 my-webpack-plugin.js 파일을 생성하고 아래와 같은 샘플 코드를 작성했습니다.

apply라는 메서드는 웹팩이 compiler 매개변수에 객체를 전달하고, 전달된 객체안에 있는 tap 메서드를 호출합니다. 이때 첫 번째 인수로 문자열을 전달하고, 두 번째 인수로는 콜백함수를 전달합니다. 이 콜백함수는 플러그인의 실행이 완료되었을 때 호출되는 콜백함수입니다.

이후에 webpack.config.js 파일에서 플러그인을 다음과 같이 설정합니다.

로더를 module이라는 객체에 넣었다면 플러그인은 plugins라는 배열에 추가합니다. 이때 작성한 플러그인 클래스를 가져온 다음 plungins 배열에 new 키워드를 이용해서 MyWebpackPlugin 인스턴스를 생성합니다.

이후 npm run build 명령어를 입력하면 apply 메서드의 콜백 함수에 작성한 console.log('MyPlugin: done'); 이 출력되는 것을 확인할 수 있습니다.

로더는 여러 개 파일에 대해서 실행되었다면, "플러그인은 하나의 결과물인 번들 파일에 대해서 딱 한 번 실행"됩니다.


그렇다면 플러그인이 어떻게 번들링된 결과물인 번들에 접근하는지에 알아보겠습니다.

이전에 만든 커스텀 플러그인인 MywebpackPlugin을 다음과 같이 수정했습니다. compiler객체에서 tapAsync 메서드를 호출하는데 첫 번째 매개변수에서는 'My Plugin'이라는 문자열을 전달하고, 두 번째 매개변수에는 콜백함수를 전달합니다. 이 콜백함수가 실행될 때 번들된 결과물에 접근하게 됩니다.

콜백함수는 compilation과 callback 두 가지 인수를 전달받는데, compilation 객체를 통해 웹팩이 번들링한 결과에 접근할 수 있습니다.

compilation.assets['main.js']를 통해서 번들 파일인 main.js에 접근하고, source() 메서드를 호출하여 main.js파일의 내용을 가져와서 source 변수에 할당합니다. 그리고 source 변수를 console.log로 호출합니다. 즉, source 변수에는 번들링된 결과인 main.js의 소스코드가 할당되어 있습니다.

npm run build 명령어를 실행하면 apply 메서드의 콜백 함수가 실행되어console.log(source);를 통해 main.js 소스코드가 출력되는 것을 확인할 수 있습니다.

즉, compilation 객체를 통해서 웹팩이 "번들링한 결과에 접근"할 수 있다는 사실을 알 수 있습니다.



이번에는 source 함수를 재정의 하였습니다. 재정의된 source 함수는 banner 변수에 할당된 문자열 다음에 줄바꿈을 한 뒤 원본 소스코드 문자열로 합친 문자열을 반환합니다. 이 반환된 문자열이 최종 번들 파일이 됩니다.

웹팩의 로더들은 모듈로 연결된 각 파일들을 처리합니다. 그리고 하나의 파일로 만들어주는데 그 직전에 플러그인이 개입하여 번들의 후처리를 해주는 역할을 합니다.

자주 사용하는 플러그인

플러그인을 직접 작성하여 사용하는 일은 거의 없습니다. 웹팩에서 제공하는 플러그인을 사용하거나 써드파티 라이브러리를 찾아 사용하는데 자주 사용하는 플러그인에 대해서 알아보겠습니다.

BannerPlugin

앞에서 작성한 MyWebpackPlugin과 비슷한 것이 Banner Plugin입니다. 결과물(번들)에 "빌드 정보"나 "커밋 버전"같은 것을 추가할 수 있습니다. Banner Plugin은 웹팩이 기본적으로 제공하는 플러그인입니다.

먼저 webpack.config.js 파일에서 webpack 노드 모듈을 require('webpack');로 가져와서 webpack이라는 변수에 할당합니다.

그리고 plugins 배열에 new 연산자와 함께 webpack.BannerPlugin을 호출합니다. 이때 인수로 객체를 전달하는데 예시로 다음과 같은 객체를 전달했습니다.

그리고 npm run build 명령어를 입력하고, dist 폴더에 번들링된 번들인 main.js 상단에 banner에 작성한 문자열이 상단에 추가된 것을 확인할 수 있습니다.

혹은 빌드된 시간을 번들 파일의 최상단에 추가하는 후처리도 가능합니다. 아래와 같이 banner를 작성합니다.

이후 npm run build 명령어로 빌드하면 아래와 같이 번들링된 번들 파일인 main.js 상단에는 빌드된 시간이 문자열로 삽입되는 후처리가 동작합니다.

즉, webpack.BannerPlugin의 인스턴스를 생성하기 위해 new 연산자와 함께 호출할 때 인수로 객체를 전달하는데 객체의 banner 프로퍼티에 작성한 문자열이 번들의 최상단에 삽입되는 문자열이 됩니다.

DefinePlugin

프론트엔드 소스코드는 개발환경과 운영환경을 나누어 운영합니다. 만약 환경에 따라 API 서버 주소가 다를 수 있습니다.
같은 소스 코드를 두 환경에 배포하기 위해서는 이러한 API 주소처럼 환경 의존적인 정보를 소스가 아닌 다른 곳에서 관리하는 것이 좋습니다. 배포할 때마다 코드를 수정하는 것은 곤란하기 때문입니다.

웹팩은 이러한 "환경 정보(환경 변수)를 어플리케이션에게 제공"하기 위해서 Define Plugin을 사용합니다.

Define Plugin도 웹팩의 기본 플러그인입니다.

Define Plugin을 사용하기 위해서 webpack.config.js 파일의 plugins 배열에 webpack.DefinePlugin을 new 연산자와 함께 호출합니다. 이때 인수로 빈 객체를 전달해도 기본적으로 어플리케이션에 주입해주는 환경 변수가 있는데 바로 "노드의 환경 변수"입니다.

노드에서 제공하는 process.evn.NODE_ENV 변수에 웹팩 설정의 mode에 설정한 값이 들어값니다. 현재 mode는 "development"를 설정했으므로 process.env.NODE_ENV 변수에는 "development"가 할당되어 있습니다.

어플리케이션에서 코드에서 process.evn.NODE_ENV 변수에 접근하면 "development" 값을 얻을 수 있습니다.

위 결과처럼 process.env.NODE_ENV 변수를 콘솔창에 출력하면 development가 출력되는 것을 확인할 수 있습니다.


노드 환경 변수 이외에 환경 변수를 어플리케이션에 주입해주기 위해서는 webpack.DefinePlugin을 new 연산자와 호출할 때 인수로 전달하는 객체로 주입할 수 있습니다.

아래 예제는 어플리케이션에서 TWO라는 전역 변수로서 접근할 수 있으며, 1+1이라는 코드가 들어가있습니다. 코드이기 때문에 TWO는 2라는 값을 갖고 있습니다.

어플리케이션에서 TWO를 참조하면 2라는 값이 출력됩니다.

코드가 아닌 문자열 값을 전달하고 싶은 경우 JSON.stringify() 메서드를 사용합니다.

어플리케이션에서 TWO에 접근하면 2가 아닌, '1+1' 문자열을 출력합니다.

HtmlTemplatePlugin

HtmlTemplatePlugin은 웹팩의 기본 플러그인이 아닌 써드 파티 패키지입니다. HtmlTemplatePlugin을 통해 HTML 파일도 빌드 과정에 포함시킬 수 있습니다. 즉, HtmlTemplatePlugin은 HTML 파일을 후처리하는데 사용합니다. 빌드 타임의 값을 넣거나 코드를 압축할 수 있습니다.

먼저 패키지를 설치하기 위해 터미널에 npm install html-webpack-plugin을 입력합니다.

그리고 index.html 파일을 src 폴더로 옮겨 소스로 관리하고, index.html 문서에서 script 태그를 제거합니다.

webpack.config.js 파일에서는 설치한 html-webpack-plugin 가져오고, plugins 배열에 추가합니다.
그리고 HtmlWebpackPlugin을 new 연산자와 함께 호출할 때 인수로 객체를 전달하는데 객체의 template 프로퍼티는 index.html 문서의 경로를 작성합니다. 현재 index.html 문서는 src 폴더에 존재하므로 './src/index.html'을 작성합니다.

그리고 객체의 templateParameters를 통해 html에 EJS를 사용하여 html 문서에 자바스크립트 내용을 삽입할 수 있습니다.

html 문서에서 EJS를 통해 env라는 변수를 작성하면 웹팩이 빌드할 때 노드 환경 변수인 process.env.NODE_ENV의 값이 'development'인 경우 (개발용)이라는 문자열로 삽입되고, 'production'인 경우 빈문자열로 포함됩니다.

또한 html을 압축하고 주석을 제거하는 기능도 있습니다. 옵션으로 minify를 추가하고 객체의 공백을 제거하려면 collapseWhitespace에 true를 지정하고, removeComments에 true를 지정하면 주석을 제거합니다.

이후에 npm run build 명령어를 입력하여 빌드하면, dist 폴더에 index.html 문서가 복사되어 존재하고 있으며, index.html 문서에 script 태그가 추가된 것을 확인할 수 있습니다.

이렇게 HtmlTemplatePlugin을 사용하면 빌드 과정에 HTML 문서를 포함하므로 좀 더 의존적이지 않은 코드로 html을 만들 수 있습니다.

CleanWebpackPlugin

CleanWebpackPlugin은 웹팩의 기본 플러그인이 아닌 써드 파티 패키지입니다. CleanWebpackPlugin은 빌드 이전 "결과물을 제거"하는 플러그인입니다. 빌드 결과물은 output 경로에 모이는데 과거 파일이 남아있을 수 있습니다. 이전 빌드 내용이 덮여씌여지면 상관없지만 그렇지 않으면 output 폴더에 여전히 남아있게 됩니다.

즉, CleanWebpackPlugin은 빌드를 할 때마다 이전 빌드된 결과를 지우고 매번 새롭게 빌드된 결과를 저장합니다.

CleanWebpackPlugin을 설치하기 위해서 터미널에 npm install clean-webapck-plugin을 입력합니다.

그리고 webpack.config.js 파일에서 require 함수로 clean-webpack-plugin을 가져와서 CleanWebpackPlugin 변수에 할당합니다. 주의할 점으로 clean-webpack-plugin은 default로 export되어있지 않기 때문에 { CleanWebpackPlugin }으로 가져옵니다.

그리고 plugins 배열에 CleanWebpackPlugin을 new 연산자와 함께 호출하여 인스턴스를 요솔 추가해줍니다.

이후에 npm run build 명령어를 입력하여 빌드할 때마다 이전 dist 폴더가 삭제되고, 새롭게 dist 폴더를 생성합니다.

MiniCssExtractPlugin

CSS 파일이 많아지면 하나의 자바스크립트 결과물로 만드는 것이 부담일 수 있습니다. 이는 브라우저에서 큰 파일 하나를 로딩하는 것이 로딩 성능에 영향을 줄 수 있기 때문입니다.

개발 환경에서는 CSS를 하나의 모듈로 처리해도 상관없지만, 프로덕션 환경에서는 분리하는 것이 효과적이다. MiniCssExtractPlugin은 CSS를 별로 파일로 뽑아내는 플러그인이다.

번들 결과에서 CSS 코드만 추출하여 별도의 CSS 파일로 만들어 역할에 따라 파일을 분리하는 것이 좋습니다. 즉, 최종 결과물로 자바스크립트 파일 하나와 CSS 피일 하나로 만드는 것입니다. 이렇게 각각 용량이 분할된 파일 두 개를 다운받는 것이 페이지 로딩하는 성능이 더 좋습니다.

MiniCssExtractPlugin을 설치하기 위해서 터미널에 npm install mini-css-extract-plugin을 입력해줍니다.

webpack.config.js 파일에서 require 함수로 설치한 mini-css-extract-plugin을 가져옵니다. 그리고 plugins 배열에 MiniCssExtractPlugin을 new 연산자와 함께 호출하는데 인수로 전달하는 객체의 filename 프로퍼티에는 추출될 CSS 파일 이름을 설정하고, 추출된 CSS 파일은 output 경로에 설정한 파일 이름으로 생성될 것입니다.

MiniCssExtractPlugin은 자바스크립트에서 CSS 코드를 추출하는 것으로 개발 환경에서는 굳이 하지 않아도 되는 작업입니다. 개발 환경에서는 하나의 자바스크립트 파일로 빌드하는 것이 더 빠르게 빌드될 것입니다.
그러므로 MiniCssExtractPlugin은 운영 환경일 때만 동작하도록 하기 위해서 환경 변수 process.env.NODE_ENV를 사용하여 값이 "production"인 경우에만 동작하도록 작성합니다.

그리고 다른 플러그인과는 다르게 로더 설정도 다시 해주어야 합니다. CSS 파일을 위한 로더 부분에서 MiniCssExtractPlugin을 사용하기 위해서는 style-loader 대신에 MiniCssExtractPlugin이 제공하는 자체적인 로더인 MiniCssExtractPlugin.loader를 사용해야 합니다.
이때도 운영 환경일 때만 MiniCssExtractPlugin 로더가 동작하도록 환경 변수가 "production"인지 검사합니다. 만약 "production"이라면 MiniCssExtract.loader를 사용하고, 아니라면 style-loader를 사용하도록 작성합니다.

이후게 npm run build 명령어를 입력하면 output인 dist 폴더에 main.css가 생성된 것을 확인할 수 있습니다.

그리고 index.html에 CSS 파일을 로드하는 link 태그가 삽입된 것을 확인할 수 있습니다.


자주사용하는 플러그인 5가지를 정리하면 다음과 같습니다.
  • Banner Plugin: 번들링된 결과 상단에 빌드 정보를 추가하는 용도

  • Define Plugin: 빌드 타임에 결정되는 환경 변수를 어플리케이션에 주입하는 용도

  • HtmlTemplatePlugin: HTMl 파일을 빌드 과정에 추가해주는 용도

  • CleanWebpackPlugin: 빌드할 때마다 output 경로에 존재하는 파일을 삭제하고 새로 생성하는 역할

  • MiniCssExtractPlugin: 번들된 자바스크립트 파일에서 CSS 코드만 추출하여 별도의 CSS 파일을 생성하는 역할

profile
Frontend Dev

0개의 댓글