개발하는 애플리케이션의 크기가 커지면 언젠간 파일을 여러 개로 분리해야 하는 시점이 온다. 이때 분리된 파일 각각을 '모듈(module)'이라고 부르는데, 모듈은 대개 클래스 하나 혹은 특정한 목적을 가진 복수의 함수로 구성된 라이브러리 하나로 구성된다.
모듈은 단지 파일 하나. 스크립트 하나는 모듈 하나.
이런 모듈이란 개념은 왜 필요한걸까? 🤔
우선 간단한 코드 예시를 보자.
index.html
<html>
<head>
<script src="./hello.js"></script>
<script src="./world.js"></script>
</head>
<body>
<div id="root"></div>
<script>
document.querySelector('#root').innerHTML = word;
</script>
</body>
</html>
hello.js
var word = 'Hello';
world.js
var word = 'World';
위와 같은 경우 화면에는 어떤 글자가 나타날까?
World 가 나타난다.
hello.js와 world.js가 서로 같은 이름의 변수를 사용하면서 덮어쓰면서 뒤에 있는 World가 출력된 것이다.
현재 예시에선 2개지만 우리의 애플리케이션이 수백개의 js파일들을 로드한다면 이런 사태들이 더욱 더 심하게 발생될 것이다.
이런 문제를 극복하기 위해서 등장한 개념이 모듈이다.
이 예시에 모듈을 적용시켜보자.
index.html
(모듈은 특수한 키워드나 기능과 함께 사용되므로 < script type="module"> 같은 속성을 설정해 해당 스크립트가 모듈이란 걸 브라우저가 알 수 있게 해줘야 한다.)
<html>
<head>
/*<script src="./hello.js"></script>
<script src="./world.js"></script>*/
</head>
<body>
<div id="root"></div>
<script type="module">
import hello_word from "./hello.js";
import world_word from "./world.js";
document.querySelector('#root').innerHTML = hello_word + '' + world_word;
</script>
</body>
</html>
hello.js
var word = 'Hello';
export default word;
world.js
var word = 'World';
export default word;
이 경우엔 어떤 값이 나타날까?
Hello World 가 나타난다.
hello.js와 world.js가 모듈이다.
hello.js와 world.js 안에 있는 word라는 변수는 해당 파일 안에서만 유효한 값이 되는 것이다.
이것이 바로 모듈이라는 개념이다.
🖐 여기서 문제가 몇가지 있다. 🖐
😱 우선 import, export는 비교적 최신 기능이라 구형 브라우저에서 동작하지 않는다.
🤧 또한 이 예시에선 js파일 2개지만 만약 수많은 개수의 파일 (js뿐만 아니라 css, image 파일들 포함)들을 다운로드 받는다면, 네트워크 커넥션이 엄청나게 많아질 것이다. 이로 인해 로딩 속도가 느려지고 사용자 입장에선 경험이 떨어지게 된다. -> 즉 번들링이 필요하다
이러한 문제점들을 겪으며 웹에서도
1. 모듈의 개념을 사용하고(파일 단위의 자바스크립트 모듈 관리)
2. 여러 개의 파일들을 하나로 묶어서 제공하여 웹 애플리케이션의 빠른 로딩과 높은 성능을 제공하고자
만들게 된 도구가 번들러(Bundler) 이다.
그리고 이 번들러 중 대표 주자가 웹팩인 것이다.
아래 그림과 같이 웹 애플리케이션을 구성하는 몇십, 몇백개의 자원들을 하나의 파일로 병합 및 압축 해주는 동작을 모듈 번들링이라고 한다.
input -> process -> output
빌드, 번들링, 변환 이 세 단어 모두 같은 의미이다.
이제 웹팩을 도입해보도록 하자.
npm init
로 package.json 파일을 생성시킨다.
그리고
npm install -D webpack webpack-cli
이렇게 2가지 패키지를 설치한다.
그리고 index.html을 아래와 같이 수정해보자.
index.html
<html>
<head>
/*<script src="./hello.js"></script>
<script src="./world.js"></script>*/
</head>
<body>
<div id="root"></div>
/*<script type="module">
import hello_word from "./hello.js";
import world_word from "./world.js";
document.querySelector('#root').innerHTML = hello_word + '' + world_word;
</script>*/
<script src="./public/index_bundle.js"></script>
</body>
</html>
index.js 파일을 hello.js, world.js와 같은 레벨에 생성한다.
import hello_word from './hello.js';
import world_word from './world.js';
document.querySelector('#root').innerHTML = hello_word + ' ' + world_word;
그리고 루트 경로와 동일한 레벨에 webpack.config.js 파일을 만들어보자.
참고로 'webpack.config.js'는 웹팩에게 어떤 일들을 시킬지 정의하는,
즉 configuration 설정 파일의 웹팩에서의 약속된 이름이다.
(https://webpack.js.org/concepts/configuration/)
webpack.config.js
const path = require('path');
module.exports = {
mode: "development",
entry: "./source/index.js",
output: {
path: path.resolve(__dirname, "public"),
filename: "index_bundle.js"
}
}
이제 이 파일에 정의한 내용들을 보도록하자.
우선 mode 속성이 정의되어있다.
이는 웹팩 버전 4부터 추가된 것인데, 웹팩의 실행 모드를 설정할 수 있다.
웹팩의 실행 모드에는 3가지가 있다.
각 실행 모드에 따라 웹팩의 결과물 모습이 달라진다. 개발 모드인 경우에는 개발자들이 좀 더 보기 편하게 웹팩 로그나 결과물이 보여지고, 배포 모드인 경우에는 성능 최적화를 위해 기본적인 파일 압축 등의 빌드 과정이 추가된다.
entry 속성은 웹팩에서 웹 자원을 변환하기 위해 필요한 최초 진입점이자 자바스크립트 파일 경로이다. 즉 웹팩이 빌드를 수행할 대상이 명시되어 있다.
따라서 entry 속성에 지정된 파일에는 웹 애플리케이션의 전반적인 구조와 내용이 담겨져 있어야 한다. 웹팩이 해당 파일을 가지고 웹 애플리케이션에서 사용되는 모듈들의 연관 관계를 이해하고 분석하기 때문에 애플리케이션을 동작시킬 수 있는 내용들이 담겨져 있어야 한다.🌟
위의 예시에선 entry 속성을 방금 생성한 index.js로 설정했는데,
index.js에서 hello.js와 world.js 모두 불리고 있다.
또다른 예를 들어보자. 아래는 SPA라고 가정하고 작성된 코드이다.
// 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();
사용자의 로그인 화면, 로그인 후 진입하는 메인 화면, 그리고 게시글을 작성하는 화면 등 웹 서비스에 필요한 화면들이 모두 index.js 파일에서 불려져 사용되고 있기 때문에 웹팩을 실행하면 해당 파일들의 내용까지 해석하여 파일을 빌드해줄 것이다.
위와 같이 모듈 간의 의존 관계가 생기는 구조를 디펜던시 그래프(Dependency Graph)라고 한다.
output 속성은 웹팩을 돌리고 난 결과물의 파일 경로를 의미한다.
entry 속성과는 다르게 객체 형태로 옵션들을 추가해야 한다.
최소한 filename은 지정해줘야 하며 일반적으로 아래와 같이 path 속성을 함께 정의한다.
webpack.config.js
const path = require('path');
module.exports = {
mode: "development",
entry: "./source/index.js",
output: {
path: path.resolve(__dirname, "public"),
filename: "index_bundle.js"
}
}
여기서 filename 속성은 웹팩으로 빌드한 파일의 이름을 의미하고, path 속성은 해당 파일의 경로를 의미한다.
__dirname
는 node.js의, 현재 파일 위치의 경로를 알려주는 약속된 변수이며, path.resolve()
는 인자로 넘어온 경로들을 조합하여 유효한 파일 경로를 만들어주는 Node.js API 이다.
즉 이 API가 해주는 일은 output: './public/index_bundle.js'
이다.
webpack.config.js안에 있는 속성들을 살펴보았으니 이제 이 config 파일을 바탕으로 빌드를 시켜보자.
빌드는
npx webpack --config webpack.config.js
명령어로 수행할 수 있다.
(참고: webpack.config.js는 약속된 이름이므로 그냥 npx webpack
만 쳐도 같은 명령어로 인식된다.)
그리고 configuration 파일 없이도 빌드할 수 있는 명령어 방식이 있는데,명령어에서 옵션을 사용하는 방법이다.
npx webpack --entry ./source/index.js --output-path ./public/index_bundle.js
--entry와 --output으로 옵션을 지정하였고 이는
npx webpack --config webpack.config.js
와 같은 작업을 수행한다.
해당 명령어 수행 후, public 폴더 안에 index bundle.js 파일이 생성된 것을 확인할 수 있고 실행을 시켜보면 기존과 동일하게 화면에는 Hello World 가 나타나는 것을 확인할 수 있다.
그리고 기존과 달라진 점은 네트워크 탭에서 확인할 수 있다.
기존엔 hello.js, world.js 이렇게 2개를 받았다면, 이제는 index_bundle.js 이 파일 하나만 다운로드 받는다.
한번의 커넥션만이 필요한 것이다.
또한 동시에 import, export 같은 구형브라우저에선 동작하지 않는 코드들을 index_bundle.js에서 알아서 오래된 브라우저에서도 동작할 수 있는 코드로 변환해준다.
이 2가지가 웹팩을 사용했을 때 얻을 수 있는 중요한 장점들이다.
(https://webpack.js.org/concepts/)
웹팩에는 아래와 같이 총 4가지의 속성이 있다.
웹팩에서 웹 자원을 변환하기 위해 필요한 최초 진입점이자 자바스크립트 파일 경로.
즉 웹팩이 빌드를 수행할 대상.
웹팩을 돌리고 난 결과물의 파일 경로
🖐 만약 여러가지 형태의 output을 만드려면 어떻게 해야할까?
우선 아래와 같이 entry 속성 또한 이름을 지어 여러개를 선언한다.
그리고 filename은 [name]_bundle.js로 선언한 것을 볼 수 있는데,
[name]은 entry 속성을 포함하는 옵션이다.
따라서 entry가 어떤거냐에 따라
"./source/index.js"를 빌드했다면 결과물이 index_bundle.js로
"./source/about.js"를 빌드했다면 결과물이 about_bundle.js로 나올 것이다.
로더(Loader)는 웹팩이 웹 애플리케이션을 해석할 때 자바스크립트 파일이 아닌 웹 자원(HTML, CSS, Images, 폰트 등)들을 변환할 수 있도록 도와주는 속성.
로더를 사용하지 않으면 css파일 같은 asset들은 별도로 다운로드된다.
css파일까지 js안에 넣으면 얼마나 좋을까?라는 생각을 웹팩의 로더가 실현시켜준다.
웹팩의 로더는 js파일이 아닌 것들(css, image, font 등..)도 번들링해준다.
css로 예시를 들어보자.
style.css
body {
background-color: blue;
}
index.js
import hello_word from './hello.js';
import world_word from './world.js';
import css from './style.css';
document.querySelector('#root').innerHTML = hello_word + ' ' + world_word;
console.log(css);
이렇게하고 빌드를 해보자.
그렇다면 'You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file' 라는 에러가 뜰 것이다.
위 에러 메시지의 의미는 index.js 파일에서 임포트한 style.css 파일을 모듈로써 해석하기 위해 적절한 로더를 추가해달라는 것이다.
따라서 이에 필요한 로더들을 추가해보자.
npm install -D style-loader css-loader
위 명령어 실행 후, webpack.config.js를 아래와 같이 구성하자.
webpack.config.js
const path = require('path');
module.exports = {
mode: "development",
entry: "./source/index.js",
output: {
path: path.resolve(__dirname, "public"),
filename: "index_bundle.js"
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
}
module 속성을 추가하였는데 이는 css형식의 파일들에 대해 style-loader와 css-loader 로더를 적용시키겠다는 뜻이다.
참고로 use에 선언된 배열의 뒤쪽에 있는 로더가 먼저 실행된다.(체이닝 개념)
개발자도구 element 탭을 보면 head의 style태그로 css 내용이 있는 것을 볼 수 있다.
이런 로더를 통해 image와 font 등 다른 asset들도 로드할수있다.
웹팩에는 2가지 확장 기능이 있다: 로더, 플러그인
로더는 모듈을 최종적 아웃풋으로 만들어가는 과정에서 사용되고(모듈로 연결되어있는 여러 파일에 대해 각각 실행),
플러그인은 최종적 결과물을 변형하는데 쓰인다(번들 결과물에 대해서 딱 한번 실행 -> 후처리).
(따라서 플러그인 좀더 복합적이고 자유롭게 쓰일 수 있다.)
플러그인에는 다양한 종류의 플러그인들이 있지만 그 중 HtmlWebpackPlugin을 예시로 들어보겠다.
HtmlWebpackPlugin은 웹팩으로 빌드한 결과물로 HTML 파일을 생성해주는 플러그인이다.
html파일을 자동으로 생성하고싶다, html 파일을 템플릿으로 하여 결과물을 만들고싶다 할 때 사용할 수 있다.
우선 HtmlWebpackPlugin을 설치해보자.
npm install -D html-webpack-plugin
그리고 index.html, about.html, webpack.config.js를 아래와 같이 구성하자.
source/index.html
<html>
<body>
<h1>Hello Index</h1>
<div id="root"></div>
<a href="./about.html">about</a>
</body>
</html>
source/about.html
<html>
<body>
<h1>Hello About</h1>
<div id="root"></div>
<a href="./index.html">index</a>
</body>
</html>
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); // loader와 달리 가져와서 직접 실행시켜야함.
module.exports = {
mode: "development",
entry: {
index: "./source/index.js",
about: "./source/about.js"
},
output: {
path: path.resolve(__dirname, "public"),
filename: "[name]_bundle.js"
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: "./source/index.html", // 어떤걸 템플릿으로 할 것인가
filename: './index.html', // 결과를 어떤 이름으로 저장할 것인가
chunks: ['index']
}),
new HtmlWebpackPlugin({
template: "./source/about.html", // 어떤걸 템플릿으로 할 것인가
filename: './about.html', // 결과를 어떤 이름으로 저장할 것인가
chunks: ['about']
})
]
}
이를 실행하면 public 폴더 안은 아래와 같이 구성되게 된다.
빌드 결과로 기존과 같이 index_bundle.js와 about_bundle.js가 생기고 HtmlWebpackPlugin으로 인해 동시에 index.html과 about.html 또한 생겨났다.
public/index.html
<html>
<body>
<h1>Hello Index</h1>
<div id="root"></div>
<a href="./about.html">about</a>
<script src="index_bundle.js"></script></body>
</html>
public/about.html
<html>
<body>
<h1>Hello About</h1>
<div id="root"></div>
<a href="./index.html">index</a>
<script src="about_bundle.js"></script></body>
</html>
이렇게 HtmlWebpackPlugin으로 템플릿을 이용하고, 웹팩으로 빌드한 결과물로 HTML 파일을 생성하는 과정을 살펴보았다.
dev server
code splitting
큰 코드를 쪼갬
lazy loading
쪼개진 것들을 필요할때마다 로딩하는 것(동적 로딩)