웹을 만들고자 새 프로젝트를 시작하기 위해서는, 항상 라이브러리를 받아와 개발환경을 구축하는 일부터 시작해야 한다. 처음에는 CRA(create-react-app)으로 개발환경을 구축했지만 이제는 CRA 없이 개발환경을 구축하고 싶어서 이 시리즈를 작성하게 되었다.
가장 큰 이유는 1년 가까이 CRA로 웹 프로젝트를 해왔는데 CRA가 포함하는 라이브러리 중에 한 번도 사용한 적이 없는 testing-library나 web-vitials와 같은 라이브러리가 있었기 때문이다. 또한 babel이나 webpack, typescript같은 라이브러리를 직접 설치하며 react도 그냥 하나의 라이브러리라는 것을 느꼈기 때문도 있고, 직접 개발환경을 구축해보는 것이 분명히 나에게 도움이 될 것이라고 생각했기 때문도 있다.
첫 번째는 package.json 파일을 생성하는 것 부터다.
나는 cli에 yarn init -y
명령어를 사용하여 package.json를 생성했다. npm init -y
을 사용해도 무관하다.
yarn init
만 입력하게 되면 사용자에게 package.json파일의 name이나 version, license등을 묻는데, -y
명령을 붙이면 yarn의 package.json 기본값에 따라 파일이 생성된다. 자세한 내용은 yarn cli | init docs에서 확인할 수 있다.
yarn init -y
로 생성된 파일은 아래와 같다.
//package.json
{
"name": "test",
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
}
이제 필요한 라이브러리를 붙여나가면 된다.
나는 브라우저가 ES6를 지원하든 지원하지 않든 ES6를 사용하고 싶으니 babel 라이브러리를 설치할 생각이다. 라이브러리를 설치하기 위해서는 yarn add [설치할 라이브러리]
명령어를 입력해야 한다.
우선 설치해야 할 라이브러리는 @babel/core, @babel/cli이다. @babel/core는 babel이 트랜스파일을 할 수 있게 해주는 심장같은 역할을 하고, @babel/cli는 CLI(Command Line Interface, git bash나 cmd 같은 거)에서 babel을 실행할 수 있게 해준다.
나는 yarn add -D @babel/core @babel/cli
명령어를 사용했다.
-D
명령어는 이 라이브러리들을 개발단계에서만 사용하겠다는 뜻이다. -D
명령어를 쓰지 않고 라이브러리를 설치하면 package.json의 dependencies 객체에 라이브러리의 이름이 추가된다.
이 패키지가 배포되어서 설치됐을 때 dependencies 객체에 포함된 라이브러리들이 같이 설치된다. -D
명령어를 사용하고 라이브러리를 설치하면 devDependencies 객체에 라이브러리가 추가되고, 이 라이브러리들은 배포된 패키지에 포함되지 않는다.
babel은 상위 ES 문법이 사용된 js파일을 하위 ES 문법으로 바뀐 js파일로 변환시켜주는 라이브러리이기 때문에 개발단계에서만 사용한다. 그래서 -D
명령어를 사용해서 설치한 것이다.
// pacakage.json
{
"name": "test",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10"
}
}
이렇게 devDependencies에 설치한 라이브러리가 추가되었다. 하지만 이것만으로 babel을 사용할 수는 없고 plugin이 필요하다.
babel의 plugin은 실제로 코드를 변환시키는 기능을 담당한다.
예를 들어 내가 ES6의 화살표 함수 문법을 사용했다면, 이를 변환시키기 위해 @babel/plugin-transform-arrow-functions 라이브러리의 설치가 필요하고, 블럭 스코프를 사용했다면 @babel/plugin-transform-block-scoping 라이브러리의 설치가 필요하다.
그런데 babel plugin Docs를 보면 plugin이 워낙 세세하게 나뉘어져 있어 설치하는 것이 번거로울 뿐만 아니라 내가 필요한 plugin을 하나하나 찾는 것 또한 힘든 일이다.
그래서 preset이 등장하게 된다.
preset은 목적에 따라 plugin들을 모아놓은 라이브러리다. preset도 여러가지가 있는데, 그 중에서도 @babel/preset-env 라이브러리는 targets 옵션을 이용해서 어떤 브라우저에도 유연하게 대응할 수 있기 때문에 가장 자주 쓰인다.
그럼 @babel/preset-env을 설치해서 사용해보겠다.
yarn add -D @babel/preset-env
로 설치한다. 그 다음 babel.config.js 파일을 만들어서 내가 preset을 사용하겠다는 것을 선언해야한다.
// babel.config.js
const presets = [
[
"@babel/preset-env",
{
targets: {
chrome: "87",
},
useBuiltIns: "usage",
corejs: "3.6.4",
},
],
];
module.exports = { presets };
@babel/preset-env의 옵션 중 useBuiltIns는 polyfill을 사용할 것인지 아닌지를 선택할 수 있는 옵션이다. 옛날에는 @babel/polyfill을 따로 설치해야 했으나, babel 7.4v부터 이런 편리한 옵션을 지원하기 시작했다.
useBuiltIns 옵션은 "usage" | "entry" | false 중 하나를 선택할 수 있고 기본값은 false다. "usage"는 내 코드를 분석해서 필요한 polyfill plugin만을 적용시킨다. "entry"는 targets 브라우저에 필요한 polyfill plugin을 모두 적용시킨다. false는 polyfill을 사용하지 않는다.
corejs 옵션은 useBuiltIns 옵션이 활성화되어 있을 때 적용가능하며 2, 3 혹은 { version: 2 | 3, proposals: boolean } 중 하나를 선택할 수 있고 기본값은 2다. polyfill을 사용할 때 삽입되는 corejs의 버전을 선택하는 옵션인데, 2는 업데이트가 중단되어서 3을 사용하는 것이 좋다.
@babel/preset-env의 모든 옵션들과 자세한 정보는 @babel/preset-env Docs에서 확인할 수 있다.
이렇게 완성된 preset 옵션 배열을 module.exports 해주면 preset 적용이 완료된다.
JS도 CSS처럼 크로스 브라우징을 신경쓰며 코드를 짜야한다면 얼마나 끔찍한 일이겠는가? 그런 부분을 babel 라이브러리 하나로 해결할 수 있으니 정말 고마운 일이다.
그리고 여기 프론트 개발에서 빠질 수 없는 라이브러리가 하나 더 있으니 바로 webpack이다.
webpack은 entry파일을 지정해서 entry파일이 의존성을 띄고 있는 모듈들을 모두 하나로 묶어내는 라이브러리다.
백문이불여일견이니 직접 사용해본다. yarn add -D webpack webpack-cli
명령어로 webpack과 webpack-cli를 설치한다.
이제 번들링할 js파일을 만든다.
// src/main.js
const pow = require("./lib");
const a = 3;
const powedA = pow(a);
console.log(powedA);
// src/lib.js
const pow = function (a) {
const result = a * a;
return result;
};
module.exports = pow;
그리고 webpack.config.js 파일을 만들어서 옵션을 설정한다.
// webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/js/main.js",
output: {
path: path.resolve(__dirname, "dist/js"),
filename: "bundle.js",
},
devtool: "inline-source-map",
mode: "development",
};
entry 옵션은 번들링의 시작점이 될 파일을 설정하는 옵션이다. 배열이나 객체의 형태로 여러 개의 entry를 지정할 수도 있다.
output 옵션은 번들링이 완료된 파일에 대한 옵션이다. output.path는 번들링된 파일이 저장될 위치이고, output.filename은 번들링된 파일의 이름이다.
devtool 옵션에는 여러 종류의 source-map 중 하나를 입력할 수 있다. source-map은 번들링 된 코드의 원본 위치를 알려주는 역할을 한다.
mode옵션은 "production" | "development" 중 하나를 선택할 수 있다. 목적에 맞게 코드를 최적화시켜주는 역할을 한다.
이제 npx webpack
명령어로 webpack을 실행하자. 그럼 dist/js/bundle.js 이 생성된다.
bundle.js가 정말 생각한대로 작동하는지 확인하기 위해 index.html 을 만들어서 bundle.js를 script로 불러온다.
// index.html
<!DOCTYPE html>
<html>
<body>
<script src="./dist/js/bundle.js"></script>
</body>
</html>
그리고 index.html을 실행시켜 보면...?
powedA가 잘 출력되는 것을 확인할 수 있다. source-map 덕분에 이 console.log가 main.js:6에서 발생했다는 것도 알 수 있다. 아! 너무 아름답다.
이것이 webpack의 기본적인 기능이다. 이제 webpack의 "정말 대단한 기능"들에 대해 알아보자.
webpack은 entry 파일이 의존성을 띄고 있는 모든 것을 모듈로 받아들여 묶어낸다. 그렇다면 내가 React를 쓰고 있고, app 컴포넌트가 css나 scss를 import한다면? 이미지 파일을 import한다면? 혹은 TypeScript를 사용해서 js가 아닌 ts나 tsx를 사용한다면?
궁금해졌으니 css 파일을 entry로 지정해서 webpack을 사용해보겠다.
/* src/css/main.css */
@import url("./background.css");
@import url("./font.css");
/* src/css/background.css */
body {
background-color: thistle;
}
/* src/css/font.css */
h1 {
color: red;
}
아래는 webpack.config.js 설정이다.
// webpack.config.js
const path = require("path");
module.exports = {
entry: ["./src/js/main.js", "./src/css/main.css"],
output: {
path: path.resolve(__dirname, "dist/js"),
filename: "bundle.js",
},
devtool: "source-map",
mode: "development",
};
이 상태에서 webpack을 작동시키면 어떻게 될까? 나는 main.css도 bundle.js에 포함될 것이라고 기대한다.
하지만 webpack은 오류를 반환한다. 그리고 이 유형의 파일을 처리하려면 loader가 필요할 것이며, 현재 이 파일에 대한 loader가 설정되어있지 않다는 조언도 해준다.
그럼 css에는 어떤 loader가 필요할까? css를 처리하기 위해서는 css-loader와 style-loader가 필요하다. yarn add -D css-loader style-loader
로 설치한 뒤 적용해보겠다.
// webpack.config.js
const path = require("path");
module.exports = {
entry: ["./src/js/main.js", "./src/css/main.css"],
output: {
path: path.resolve(__dirname, "dist/js"),
filename: "bundle.js",
},
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
},
devtool: "source-map",
mode: "development",
};
loader를 적용하려면 module 객체안에 rules 배열을 두어야한다. rules 배열 안에는 객체들이 들어있고, 객체는 필수적으로 test와 use를 가져야 한다. test는 loader를 적용시킬 파일의 정규표현식이고, use는 test 확장자명을 가진 파일에 적용시킬 loader다.
use는 배열을 사용해서 하나 이상의 loader를 적용시킬 수 있고, 배열의 오른쪽에 있는 loader부터 적용된다.
그러니 이 옵션은 .css로 끝나는 파일에 1. css-loader 2. style-loader 순서로 loader를 적용시키겠다는 뜻이다. css-loader는 css 스타일시트를 JS로 변환시켜주고, style-loader는 JS로 변환된 css 스타일시트를 DOM에 동적으로 추가시켜준다.
설정도 끝났으니 다시 webpack을 실행시킨다.
아까는 오류를 출력했지만 이번엔 잘 작동했다. 이제 bundle.css를 index.html에 적용시켜서 실행한다.
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<script src="./dist/js/bundle.js"></script>
</head>
<body>
<h1>webpack 너무 굉장해</h1>
</body>
</html>
놀랍게도! main.css가 적용되어 있다.
또한 style-loader가 main.js에서 css를 분리하여 style으로 적용시킨 것도 확인할 수 있다.
이제 webpack으로 묶어낼 때 js에 babel을 적용시키기 위해 babel-loader를 사용하겠다.
babel-loader를 사용하기위해서는 babel-loader, @babel/core, 적용시킬 preset이 필요하다. @babel/core와 @babel/preset-env는 이미 설치했으니 yarn add -D babel-loader
명령어를 사용하고 webpack.config.js에서 babel-loader를 적용시킨다.
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: ["./src/js/main.js", "./src/css/main.css"],
output: {
path: path.resolve(__dirname, "dist/js"),
filename: "bundle.js",
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: "3.6.4",
targets: {
chrome: "87",
},
},
],
],
},
},
],
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
},
devtool: "source-map",
mode: "development",
};
자, rules 배열에 새 객체가 생겼다. 나는 처음에 이 부분이 복잡하고 어렵게 느껴졌으나, 직접 config을 설정하며 이해하려 하니 어려울 것은 하나도 없었다. 천천히 살펴보자.
먼저 test는 .js 로 끝나는 파일들을 대상으로 하고 있다.
그리고 use인데, css-loader를 사용할 때와는 다른 형태다. use 배열안에 객체가 들어있다. 이것은 babel-loader의 option을 설정하기 위해서다. 이 객체를 살펴보자.
loader에는 적용할 loader인 "babel-loader"가 들어있다.
options는 babel-loader의 옵션을 담는 객체다. presets 배열에 사용할 preset들을 입력한다. 나는 @babel/preset-env를 사용한다.
이외에도 plugin을 설정할 수도 있고, preset-env의 polyfill 기능도 설정할 수도 있다.
exclude는 말 그대로 webpack의 번들에서 제외할 폴더나 파일이다. node_modules 폴더에는 적게는 수 십개, 많게는 수 백개의 라이브러리들이 들어있다. 이 코드들에 babel-loader를 적용시키는 데에 얼마나 많은 시간이 걸리겠는가? 또한 라이브러리의 코드를 변환시켰다가 호환에 문제라도 생긴다면? 그래서 보통 node_modules는 .js 확장자에 적용시킬 loader에서 제외시킨다.
이것으로 babel-loader가 적용되었다.
"왜 이런 형태인가?"
처음엔 이런 생각을 했었다. "왜 loader와 options를 포함한 객체가 들어있으며, options에는 presets 배열이 들어있지? 왜 계속 뭔가 추가되는 것이지? 뭐지?"
찾아낸 답은 매우 단순했다. 만든사람이 그렇게 하기로 정해서다. 그리고 정해놓은 규칙은 전부 그 명세에 적혀있다. 그럼 그냥 명세를 읽어보고 적힌대로 따라하고 응용하면 된다.
loader만 있으면 정말이지 무서울 게 없다. 어떤 확장자를 가졌건 전부 webpack으로 묶어낼 수 있다. webpack에서 어떤 로더를 사용할 수 있는지, 어떻게 사용해야하는지 알고 싶다면 webpack loaders Docs에서 찾아볼 수 있다.
plugin은 webpack의 편의성을 높여준다. webpack Docs에 따르면 webpack을 더 flexible하게 만들어 준다고 한다. 그 중에서 편리하다고 생각되는 것 몇 개를 적용시키겠다.
HtmlWebpackPlugin은 webpack을 실행한 뒤 번들링 된 js파일을 포함한 html을 자동으로 생성해준다.
이전에 나는 bundle.js 파일을 webpack으로 생성한 뒤, index.html 파일을 만들어서 bundle.js를 포함시켰다. 이 과정을 자동으로 해주는 것이다.
yarn add -D htm-webpack-plugin
명령어로 설치한 뒤 적용시킨다.
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: ["./src/js/main.js", "./src/css/main.css"],
output: {
path: path.resolve(__dirname, "dist/js"),
filename: "bundle.js",
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: "3.6.4",
targets: {
chrome: "87",
},
},
],
},
},
],
exclude: "node_modules",
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
},
plugins: [new HtmlWebpackPlugin()],
devtool: "source-map",
mode: "development",
};
webpack 명령어를 사용하기 전 전체 디렉토리의 모습이다. 이제 webpack을 실행해보면...?
output 경로 안에 index.htm 이 자동으로 생성된다. 그리고 index.html 은 output file인 bundle.js 를 포함하고 있다.
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Webpack App</title>
<meta name="viewport" content="width=device-width, initial-scale=1"></head>
<body>
<script src="bundle.js"></script></body>
</html>
HtmlWebpackPlugin Docs에서 옵션에 대해 알아볼 수 있다. filename, title 지정이나 minifying 같은 설정들이 있다.
webpack을 사용하다 보면 이전에 번들링했던 결과물이 계속 남아있어 불편한 때가 있다. CleanWebpacPlugin은 새로 번들링했을 때 이전에 번들링한 결과물을 없애준다.
webpack으로 실습을 하면서 계속 반복중인 것이 있다.
이 과정을 계속 반복중이다. 물론 VS Code를 사용한다면 Live Server를 사용해서 어느정도 반복을 제거할 수 있지만, webpack 자체에서 실시간으로 코드의 변경을 반영해주는 WebpackDevServer라는 기능을 제공하고 있다.
이 기능을 사용하려면 yarn add -D webpack-dev-server
명령어로 webpack-dev-server 를 설치해야한다. 그 후 npx webpack serve
명령어로 webpack을 실행하면 번들된 js 파일이 자동으로 index.html에 삽입되어 localhost에서 실행된다.
WebpackDevServer Docs에서 자세한 설명을 볼 수 있다.
이제 babel과 webpack에 대해서 어느정도 이해했다. 처음 webpack이나 babel을 접했을 때는 아무리봐도 이해가 잘 안 되어 막막했으나, 직접 해보니 이해가 된다.
다음 포스트에선 TypeScript와 React를 설치해서 적용할 계획이다.
웹팩 너무 어려웠는데 꼼꼼한 설명 정말 감사합니다!! 한줄 한줄 읽어보면서 치고있는데 정말 귀중한 포스팅이네요. 너무 감사드려요!!