[바닐라 자바스크립트로 SPA 구현] 2. 웹팩 세팅
바닐라 자바스크립트로 SPA를 구현하는 프로젝트 진행 중에, 멘토님의 제안으로 처음으로 웹팩을 사용해보게 되었습니다. 이번 포스팅에서는 프로젝트를 진행하면서 웹팩에 대해 공부하고 적용한 내용에 대해서 정리해봅니다.
웹팩의 필요성
1. 모듈관리
자바스크립트의 함수나 변수는 기본적으로 전역공간에 노출되어서, 서로 다른 파일에서 동일한 이름의 식별자를 사용하려고 하면 충돌하기 마련입니다. 과거에 자바스크립트가 단순한 용도로만 사용되던 경우에는 큰 문제가 아니었지만, 자바스크립트의 발전과 함께 그 용도가 점점 다양해지고, 점점 더 커다란 프로젝트를 진행하게 되면서, 자바스크립트의 유효범위 문제를 해결하기 위한 방법들이 등장했습니다.
대표적으로 node.js 환경에서 서버 개발을 하는데 사용하기 위한 모듈 시스템인 CommonJS와, ES2015의 표준 모듈 시스템인 ESModule이 있습니다. 혹은 아래와 같이 즉시실행함수(IIFE)를 통해 스코프를 만들어 네임스페이스를 사용하는 방법도 있습니다.
var math = math || {} // math 네임스페이스
;(function () {
function sum(a, b) {
return a + b
}
math.sum = sum // 네임스페이스에 추가
})()
웹팩도 이와 같은 모듈화 도구 중의 하나입니다. 다만 웹팩은 자바스크립트 파일뿐만 아니라 모든 파일에 대해 모듈 개념을 사용합니다.
2. 서버 요청량을 줄임
모듈화를 통해서 코드를 예상 가능하게 작성하는 것이 쉬워지고 디버깅과 테스트가 편리해지는 개발상의 장점이 있지만, 모듈화를 통해서 발생하는 단점도 있습니다. 모듈화에 의해서 코드가 파일 여러 개로 나뉘면, 서버에 파일을 요청해야하는 횟수도 증가하게 된다는 문제가 바로 그것입니다.
이때 웹팩과 같은 번들러는 서로 의존성이 있는 모듈을 하나의 번들로 묶어주어 네트워크 요청을 줄일 수 있게 도와줍니다. 웹팩은 디펜던시 그래프를 통해 엔트리 포인트에서 시작하여 애플리케이션에서 필요한 모든 모듈을 빌드한 다음, 하나 이상의 번들로 묶어줍니다.
또한, 이 과정에서 HTML/CSS/JS 등의 파일이나 이미지를 압축해주기도해서, 서버에 요청하는 양을 줄일 수 있게 만듭니다.
3. 애플리케이션의 초기 로딩속도를 높임
앞에서 웹팩을 통해 파일을 압축하고 병합하여 서버로 요청하는 파일 숫자를 줄일 수 있다고 했는데, 웹팩이 없을 때는 Grunt나 Gulp같은 웹 테스크 매니저를 통해 이러한 일을 진행할 수 있었습니다.
웹팩은 이와 더불어 코드 스플리팅을 통해 초기 로딩속도를 높이고 나머지 자원은 필요해지는 시점에 로딩하는 레이지 로딩을 구현하였습니다.
프로젝트에 적용한 웹팩 설정
프로젝트에 웹팩을 사용하기 위해 설정한 webpack.config.js 파일은 아래와 같습니다.
어떠한 설정을 추가했는지 보면서 웹팩의 기본 4가지 속성에 대해서 알아보겠습니다.
import CopyPlugin from "copy-webpack-plugin";
import HtmlWebpackPlugin from "html-webpack-plugin";
export default {
mode: "development",
entry: "./src/index.js",
output: {
filename: "bundle.js",
clean: true,
},
devServer: {
hot: true,
historyApiFallback: true,
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
filename: "./index.html",
}),
new CopyPlugin({
patterns: [{ from: "./src/templates/*" }],
}),
],
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
},
};
1. entry
엔트리 포인트는 웹팩에서 자원을 변환하기 위한 최초의 진입점이자 자바스크립트 파일 경로입니다.
엔트리 포인트를 index.js로 지정하면, 해당 파일에서 사용하는 모든 파일의 내용을 재귀적으로 불러와서 파일을 빌드해줍니다.
SPA의 경우 자연스럽게 하나의 파일이 엔트리포인트가 되고, 엔트리 포인트를 여러개 지정해 멀티 페이지 애플리케이션을 만들 수도 있습니다.
2. output
웹팩으로 빌드한 결과물의 파일 경로를 지정할 수 있습니다.
저는 filename외에 별도 설정은 진행하지 않아서 기본값인 ./dist/ 아래로 bundle.js가 생성됩니다. path 옵션을 통해 별도 경로를 지정해줄수도 있습니다.
clean 옵션은 true로 설정하여 이전 빌드 파일은 삭제되게 하였습니다.
filename에도 여러가지 옵션을 추가할 수 있습니다.
- [name].bundle.js : 결과 파일 이름에 entry에서 사용한 이름을 사용하기
- [id].bundle.js : 웹팩 내부적으로 사용하는 모듈 ID를 사용하기
- [name].[hash].bundle.js : 매 빌드마다 고유 해시값을 붙이는 옵션
- [chunkhas].bundle.js : 웹팩의 각 모듈 내용을 기준으로 생성된 해시값을 붙이는 옵션
3. Loader
로더는 웹팩이 웹 애플리케이션을 해석할때 자바스크립트 파일이 아닌 웹 자원(HTML, CSS, Images, 폰트 등)을 변환할 수 있도록 도와주는 속성으로, module이라는 이름을 사용합니다.
저는 일단 css를 사용하기 위해서 css-loader와 style-loader를 추가해주었습니다.
css-loader는 자바스크립트에서 모듈로 css 파일을 불러올 수 있게 하는 로더입니다. 로더를 통해 css를 모듈화 하고나면 아래와 같이 js 파일에서 css를 import해 사용할 수 있습니다.

import한 css 파일은 bundle.js 결과물을 열어보면 css 파일이 들어가 있는 것을 확인할 수 있습니다.

이렇게 css를 모듈화 한 뒤에 실제로 style 태그를 통해 스타일을 적용하는 일은 style-loader가 해줍니다. css를 모듈로 import 하더라도 style 태그로 적용하지 않으면 실제 스타일이 적용되지 않습니다.
이때 주의할 점은, 로더는 배열의 오른쪽부터 작동하기 때문에, 번들된 css파일을 style-loader가 사용하기 위해서 꼭 왼쪽부터 style-loader, css-loader 순으로 세팅해 주어야 한다는 점입니다.
이밖의 로더나 플러그인은 프로젝트를 진행하면서 그때 그때 추가해줄 계획입니다.
4. Plugin
플러그인은 웹팩의 기본동작에 추가적인 기능을 제공해줍니다.
로더가 파일을 해석하고 변환하는 과정에 관여한다면, 플러그인은 번들된 결과물의 형태를 변환하는 역할을 합니다.
제가 사용한 플러그인은 HtmlWebPackPlugin과 CopyPlugin입니다.
먼저 HtmlWebPackPlugin은 HTML결과 파일 후처리 플러그인으로, 웹팩으로 빌드한 html파일을 결과물 디렉토리에 옮겨준 뒤 자동으로 자바스크립트를 로딩하는 코드를 주입해주고, 빌드타임에 환경정보를 부여해주는 등의 역할을 합니다.
CopyPlugin은 정적 파일을 자동으로 원하는 경로로 복사해주는 플러그인입니다. 저는 templates 디렉토리에 있는 html 파일들을 복사했는데, 이 파일들은 지난번 포스팅에서 언급한대로, 자바스크립트가 html을 문자열로 가지고 있는 것이 불편해서 SPA임에도 불구하고, 여러 html을 만든뒤 자바스크립트에서 index.html에 모든 templates 파일들을 포함할 수 있게 해주었습니다. 처음 웹팩 세팅시에는 html 파일을 모두 HtmlWepPackPlugin을 통해 가져오려고 했었는데, html 파일 하나씩 가져오려고 하니 불편해서 다른 방법이 없나 찾아보니 단순 html 로딩을 위한 정적파일 이므로 CopyPlugin으로 복사만 하면 사용할 수 있다는 것을 알게되었습니다.
주제인 웹팩에서 벗어난 내용이지만 조금 더 설명하자면, 해당 기능을 구현하기 위해 링크를 참고하였습니다.
위 내용을 참고해 제가 구현한 include.js 코드는 아래와 같습니다.
export const includeHTML = () => {
const allElements = document.getElementsByTagName("*");
const promises = Array.from(allElements).map(async (target) => {
let includePath = target.dataset.includePath;
if (includePath) {
const response = await fetch(includePath);
const html = await response.text();
target.outerHTML = html;
} else {
return Promise.resolve();
}
});
return Promise.all(promises);
};
원본 자료에서는 XMLHttpRequest를 이용해 데이터를 불러왔는데, 저는 fetch를 이용해 불러오도록 변경하였습니다.
그리고 해당 함수가 비동기로 데이터를 불러오기 때문에 프로젝트에서 사용하는 모든 자바스크립트 내용은 해당 데이터가 모든 html파일을 로딩한 뒤에 사용하게 해야 자바스크립트 코드에서 문제가 발생하지 않았습니다. 따라서 index.js에서는 includeHTML 함수를 아래와 같이 사용하였습니다.
includeHTML().then(() => {
const parent = document.querySelector('[data-component="header"]');
new Header(parent);
router.init();
});
이 코드를 작성하면서 비동기 작업을 위한 코드 작성을 연습할 수 있었습니다. 처음에는 아래와 같이 콜백을 이용해 처리해주었었는데, 고민 끝에 Promise와 async/await를 사용해 위의 코드와 같이 조금 더 깔끔하게 작성할 수 있었습니다. 매번 책으로만 봐서 내용은 알고 있었어도 막상 관련 코드를 작성해본 적이 별로 없어서 리팩토링이 마냥 쉽지는 않았고 덕분에 좋은 경험을 할 수 있었습니다.
export const includeHTML = (cb) => {
let allElements = document.getElementsByTagName("*");
Array.from(allElements).forEach((target) => {
let includePath = target.dataset.includePath;
if (includePath) {
fetch(includePath)
.then((response) => {
return response.text();
})
.then((html) => {
target.outerHTML = html;
cb();
});
}
});
};
includeHTML(() => {
registry.add("header", Header);
render(state);
});
개발 편의성을 위한 Webpack Dev Server
"scripts": {
"build": "webpack",
"start": "webpack serve"
}
웹팩 데브 서버는 개발중에 사용할 수 있는 편리한 도구입니다. 매번 웹팩 명령어를 실행해 빌드하지 않아도, 데브서버를 이용하면 빌드 대상 파일이 변경되었을때 코드를 저장하는 순간 새로 빌드 후 브라우저를 자동으로 새로고침해줍니다. 이 때 데브서버를 통해 빌드된 결과물은 파일로 저장되지 않고 메모리에만 저장되어 빠르게 사용할 수 있습니다.
또한 데브서버를 통해서 CORS 오류를 해결하기 위한 프록시 설정을 할 수도 있습니다. 클라이언트에서 서버로 요청을 보낼 때, 도메인이나 포트 번호가 다르면 CORS 오류가 발생하기 때문에 프록시서버로 요청을 보내고 프록시 서버가 실서버로 요청을 보내도록하면, 서버는 같은 도메인에서 온 요청으로 인식하여 CORS 에러가 발생하지 않습니다. 이번 프로젝트에서는 서버 요청을 보낼 일이 없어서 별도로 설정하지는 않았습니다.
module.exports = {
devServer: {
proxy: {
"/api": "http://localhost:8081",
},
},
};
저는 웹팩 데브서버와 함께 사용하는 설정으로 HMR(Hot Module Reload)와 HistoryApiFallback을 주었습니다.
HMR은 브라우저를 새로 고치지 않아도 데브서버에서 빌드한 결과물이 애플리케이션에 실시간 반영될 수 있게 하는 설정입니다. 데브서버는 기본적으로 자동으로 새로고침을 해서 변경사항을 반영해주는데, SPA의 경우 새로고침하면 상태값이 초기화되기 때문에 자동 새로고침이 곤란한 상황을 만들 수 있습니다. 따라서 데브서버의 HMR 기능을 이용해 변경된 모듈만 바꿔주면 편리하게 SPA 개발을 할 수 있습니다.
HistoryApiFallback은 추후 별도로 포스팅할 라우팅 기능을 위해서 추가하였습니다. HistoryAPI를 사용해 라우팅을 구현하면 window.pushState로 경로를 변경하고 알맞은 컴포넌트를 보여줄 수는 있지만, SPA의 특성상 루트(/) 가 아닌 다른 경로로 직접 URL을 수정해서 이동했을 때 404 에러가 발생할 수 밖에 없습니다. 이 경우 historyApiFallback를 설정하면, / 가 아닌 다른 경로로 이동했을 때도 모두 index.html을 보여주어 이러한 문제를 해결할 수 있습니다. 따라서 저는 해당 속성을 true로 준 뒤, 애플리케이션 내부에서 location.pathname을 확인해 알맞은 컴포넌트를 보여주도록 구현하였습니다.
항상 CRA를 통해 프로젝트를 세팅하던 탓에 웹팩에 대한 막연한 두려움이 있었는데, 이번 기회에 조금이나마 웹팩과 번들러에 대해서 이해할 수 있게 된 것 같습니다.