Webpack5 설정 무작정 해보며, 자바스크립트의 역사에 대해 알아보기

Design.C·2022년 12월 29일
0

번들러 정복하기

목록 보기
1/1

프론트엔드 개발자라면 언젠가는 꼭 거쳐야 할 번들러에 대한 공부를 시작했다. 가장 널리 알려진 webpack5 를 기반으로 무작정 실습해 봤다.

연말에 우연찮게 재택근무를 하게되어, 퇴근 후 시간이 조금 넉넉해졌기에 조금 여유롭게 공부했고, 이해한 대로 한번 기술해 보고자 한다.

번들링과 모듈은 무엇이고, 왜 해야 하는가?
그럼 웹팩은 무엇인가?
그래서 웹팩은 어떻게 사용하는건가?
웹팩의 대체재는 무엇인가?

번들링과 모듈은 무엇이고, 왜 해야 하는가?

번들링

번들링은 묶음 이라는 뜻이고, 말 그대로 파일을 하나로 묶는 행위라고 할 수 있다.
웹어플리케이션을 만들게 되면, 많은 파일이 생겨나는 것은 불가피하다.
간단하게 말하면,
이 많은 파일들이 서로 충돌하지 않고, 줄일 수 있는 부분은 압축해서 효율화 하기 위해 해야한다.

복잡하게 말하면,
예를 들어, 아래와 같이 html 파일이 있고, 무수히 많은 자바스크립트 파일을 이 html에서 로드해서 사용한다고 가정했을 때,

<!DOCTYPE html>
<html lang="kr">
<head>
    <title>번들링과 모듈은 무엇이고, 왜 해야 하는가?</title>
</head>
<body>
  <script src="/1.js"></script>
  <script src="/2.js"></script>
  <script src="/3.js"></script>
  
  ...
  
  <script src="/10000.js"></script>
</body>
</html>
// 1.js
// '리턴'이라는 문자열을 반환하는 abcdefghijklmnopqrstuvwxyz라는 함수를 선언하였다.
function abcdefghijklmnopqrstuvwxyz(){
	return "리턴"
}
// 2.js
// 1.js에서 선언한 abcdefghijklmnopqrstuvwxyz라는 함수에 undefined를 바로 접근해서 할당했다.
abcdefghijklmnopqrstuvwxyz = undefined

1.js에서 선언된 함수 abcdefghijklmnopqrstuvwxyz는 다른 어느 파일에서나 접근이 가능하게 되고, 2.js에서는 abcdefghijklmnopqrstuvwxyz에 undefined를 할당시켜버리는 등, 예측 불가능한 코드가 되어버린다.

이처럼, 모듈화 하지 않은 전통적 방식으로 자바스크립트파일을 로드한다면 일차적으로는 예측이 불가능하게 된다.

이러한 문제는 즉시 실행 함수 표현(IIFE, Immediately Invoked Function Expression)으로 어느정도는 해결이 가능하다.

// IIFE 선언 즉시 실행되는 함수
(function(){
 console.log("I'm IIFE);
})()

시간이 지난 후, 자바스크립트를 표준화하려는 시도가 있었고, 모듈의 춘추전국시대가 시작된다.
이 중, 대표적인 모듈 두 가지(Commonjs, AMD(Asynchronous Module Definition)가 있다.

모듈화

  • 스코프가 독립적이어야 한다.
  • 모듈을 생성하고 그 모듈을 사용할 수 있어야 한다.

CommonJS

  • 동기적이다.
  • 후에, nodejs에서 채택한 모듈시스템이다.
  • 모듈을 생성하여 외부에서 사용토록 할 때는 module.exports를 사용한다.
// a_module_exports.js
const nameForThisScope = (a) => {
	return a;
}
module.exports = nameForThisScope;
  • 모듈을 단일이 아닌, 여러 개를 생성하여 외부에서 사용토록 할 때는 exports를 사용한다. 만약 둘을 동시에 한 파일에서 사용한다면, module.exports가 우선순위가 더 높기때문에, module.exports로 생성한 모듈이 이 파일이 export한 모듈이 된다.
// a_exports.js
exports.a = () => {
	return 'a';
}  
  • 모듈을 불러올 때는 require 를 사용한다.
  1. module.exports를 통해 내보낸 모듈을 require한 경우,
// a_module_exports.js와 b.js가 같은 디렉토리 내에 같은 계층이 존재한다고 하면
// 이 모듈 스코프에서 사용할 newNameForThisScope라는 변수명에 require를 통해 불러온 'a_module_exports.js'모듈을 가져와서 할당한다.
const newNameForThisScope = require('./a.js')
console.log(newNameForThisScope());
  1. exports를 통해 내보낸 모듈을 require한 경우,
// b.js
// a_exports.js와 b.js가 같은 디렉토리 내에 같은 계층이 존재한다고 하면
// 이 모듈 스코프에서 사용할 newNameForThisScope라는 변수명에 require를 통해 불러온 'a_exports.js'모듈을 가져와서 할당한다.
// 이 때에는 defaultA:{a: () => {return 'a'}} 와 비슷한 형태로, defaultA 모듈의 프로퍼티 형태로 a함수가 존재하게 된다.
const defaultA = require('./a.js')
console.log(defaultA.a());

AMD(Asynchronous Module Definition)

  • 비동기적이다.
  • define 으로 스코프를 관리한다.
/*
*define(id?// 모듈 식별 인자
*       ,dependencies? // 모듈의 dependency를 나타낸 배열
*       ,factory // 모듈의 구현부
*      );
*/
// 첫번째 모듈 식별 인자는 생략
// 두번째 배열은 이 모듈 스코프에 가져올 외부 모듈들이다.
// 세번째로 오는 함수에서는 첫번째 변수(a)가 a.js에서 가져온 모듈이며, 두번째 변수(b)가 b.js에서 가져온 모듈이다.
// 따라서 function aa 안에서 가져온 모듈을 이용하여 로직을 작성하고, 그 결과물을 return하게 되면 외부에서 이 define한 모듈을 사용할 수 있게된다.
define(1,['./a.js','./b.js'], function(a,b){
	function aa(){
    	console.log(a());
    	console.log(b());
    }
  	return aa;
});

Module

es2015(es6)가 되며 본격적으로 모듈이 도입되었다.

  • 모듈 생성 및 내보내기는 export(exports가 아님!) 키워드를 사용함
//a.js
// 이 파일 자체를 a모듈로 내보내기
var a = 'a';
export default a;
//b.js
// 이 파일의 프로퍼티 중 하나로 b 모듈을 내보내기
export var b = 'b';
  • 모듈 가져오기는 import 키워드를 사용함
// default를 통해 export 했을 경우
import a from './a.js';
// export로 해당 모듈을 따로 내보냈을 경우
import {b} from './b.js'; //혹은
// import defaultB from './b.js';
// defaultB.b = b

번들링 수많은 자바스크립트 파일 및 정적 파일로 인해 생기는 부하 이슈를 모듈화, 최적화, 단순 반복 작업에 대한 자동화라는 방법으로 해결하기 위해 필요하다고 할 수 있다.

그럼 웹팩은 무엇인가?

웹팩은 가장 보편적으로 쓰이는 번들러이다. webpack5가 최신버전이다.

작성일(2022.12.29)기준

그래서 웹팩은 어떻게 사용하는건가?

웹팩은 아무런 세팅을 하지 않는다면, default setting으로 자동 설정된다.

아래는 webpack의 설정을 직접 오버라이드 할 수 있는 파일이다.
웹팩의 설정을 건드리는 파일의 이름은 기본적으로 webpack.config.js(json) 이어야 한다.
webpack은 기본적으로 commonjs 문법을 사용한다.

웹팩에는 크게 아래와 같은 개념들이 있다.

Context

Context는 이 webpack.config.js 파일에서의 현재 디렉토리 위치 './'를 정해주는 옵션이다.

const path = require('path');

module.exports = {
	context: path.resolve(__dirname, 'src'), // './'위치를 루트/src 로 정하겠다.
}

Mode

Mode로 번들링 할 환경을 설정한다.
개발 환경이면 'development', 상용 환경이면 'production'이다.
단순히 식별해주는 역할 뿐 아니라,
sourceMap, optimization, devServer 등의 옵션에서 특정 환경에서만 활성화되는 속성들이 존재한다.

// webpack.config.js
module.exports = {
  	...
	mode: 'development', // 혹은 'production'
}

Entry

Entry는 번들링 시의 진입점을 어디로 할 지 설정한다.
단일한 진입점을 만들수도있고, 여러 진입점을 만들 수도 있다.

아래 두 가지 형태로 작성 가능하다.

module.exports = {
  	...
	entry: './index.js', //루트/src/index.js 파일을 진입점으로 삼겠다.
}
module.exports = {
	...
	entry: {
      //루트/src/index.js 파일을 진입점으로 삼고, 번들링된 index.js의 이름을 chicken.js로 하겠다.
      chicken: './index.js', 
    }
}

Output

Output은 번들링 후의 결과물을 어느 위치에 어떤 형태로 생성할 지를 설정한다.

module.exports = {
	...
  	output: {
      // 번들링 시, [name]에는 entry에서 정한 이름(chicken)이 들어감.
      // 따라서, chicken.aaaa.js라는 이름으로 index.js가 번들링된 파일이 저장됨.
  		filename: '[name].aaaa.js', 
      // 번들링된 파일이 생길 디렉토리를 dist라는 이름으로 함
      	path: path.resolve(__dirname. 'dist') 
    }
}
// 결과적으로, 루트/src/index.js 파일은 번들링되어 ==> dist라는 폴더 안에 chicken.aaaa.js라는 파일로 저장됨

Loader

Loader는 js, json 파일 이외에, css, txt, png 등 다른 형식의 파일도 웹팩이 이해할 수 있도록 해주는 옵션이다.
Loader는 module 옵션 안에, rules라는 이름의 배열 안에 각 object 안에 필요한 속성을 기재하여 설정한다.
test 속성에는 정규표현식을 이용한다.

module.exports = {
	...
  	module:{
    	rules: [
          {
        	test: /\.css$/i, // .css로 끝나는 파일에 한 해 이 규칙을 적용하겠다.

            /*
              !!순서가 유의미하다.
              배열의 마지막요소부터 역순으로 로더가 적용되므로, 
              css 파일을 올바르게 읽어오려면 먼저 css 파일을 해석한 후, 
              그 안에있는 스타일을 적용하는 방향으로 진행되어야 하기에, 위와 같은 순서로 작성되었다.
              (css-loader => style-loader 순)
            */
          	use: ['style-loader', 'css-loader'], 
            
          },
          {
            test: /\.txt$/i,
            // webpack4의 raw-loader와 유사한 역할을 수행
            type: 'asset/source',
          },
          {
            test: /\.(jpg|png|jpeg|gif)$/i,
            // webpack4의 file-loader와 유사한 역할을 수행
        	type: 'asset/resource',
            // 위 test에 통과된 형식의 파일들을 번들링 후 dist 폴더 안에 생성할 때의 규칙
        	generator: {
              	// 파일 이름은 IMG라는 디렉토리 안에/해시.본래이미지명.확장자로 생성하겠다.
          		filename: 'IMG/[hash].[name][ext]',
              	// 실제 경로가 아닌, 일종의 prefix 개념으로서, css혹은 html파일 속에서 이 파일들을 불러올때 해당경로 앞에 추가해주는 string이다. 
          		publicPath: '/hamburger/',
              	// 실제로 dist 폴더 안에 해당 디렉토리 안에 만들겠다고 작성하는 속성이다.
          		outputPath: 'cheeseBurger/',
        	},
          },
          {
            test: /\.svg$/i,
            // webpack4의 url-loader와 유사한 역할을 수행
            type: 'asset/inline',
          },
        ]
    }
  
}
// 해시값이 hash111이라고 했을 때, 
// 결과적으로, 어디선가 css 혹은 html파일이 사용중인 닭발.png이 번들링되었을 때, dist/cheeseBurger/hash111.닭발.png라는 파일이 생성된다.
// 또한, css와 html 파일 내에서 이 닭발.png를 읽어올 때에는 url/hamburger/닭발.png 로 읽어올 수 있다.

Optimization

Optimization은 말그대로, 번들링 시, 최적화를 하는 옵션이다.
최적화는 공통으로 사용되는 라이브러리를 따로 분리하여 의존성을 추가하여 중복을 줄이는 등의 전략이 가능하다.

module.exports = {
  	...
  	optimization:{
      // 나누어진 chunk의 id를 설정하는 규칙
      // 'named'면 디버깅을 위한 식별가능한 id
      // 'deterministic'이면 컴파일 시 변경되지 않는 숫자 id
      // 'natural' 사용 순서에 따른 숫자 id
    	chunkIds: 'named',
      // chunk를 나누는 설정
      	splitChunks:{
        	minSize: 3000, // chunk의 최소 사이즈이다. 단위는 byte
          	// async는 동적 모듈에 대해서 최적화를 진행한다.
          	// all는 모든 모듈에 대해서 최적화를 진행한다.
          	// initial는 정적 모듈에 대해 최적화를 진행한다.
          	chunks: 'async', // async, all, initial이 있다.
        }
      
    },
  	output: {
    	...,
      	clean: true, // 매 번들링 시, 이전 outpuf file을 삭제할 지 여부
      	filename: '[name].initial.js', // initial chunk의 이름
      	chunkFilename: '[id].chunk.js', // split된 chunkemfdml 파일명
      	path: path.resolve(__dirname, 'dist'),
    }
}

SourceMap

SourceMap은 개발하는 코드와 번들링된 코드 사이 관계를 나타낸다.
SourceMap이 없다면, 직접 번들링된 자바스크립트 파일을 수정해야 하는데, 이와 같은 어려움을 회피할 수 있게 해주는 데이터이다.

아래 옵션 말고도 더욱 많은 소스맵 옵션들이 있다.

module.exports = {
  // inline옵션은, map파일을 별도로 생성하지 않고, 번들링된 파일 내에, 주석으로 기재한다.
	devtool: 'inline-source-map',
  // eval옵션은, eval 함수를 이용하여, source map을 만들고, 수정된 모듈만 리빌드하여 효율적이다. 그러나, 콘솔 상에서 원본코드의 라인 번호가 아닌 번들링된 파일의 라인의 번호를 알려주어 디버깅 시 번거롭다는 단점이 있다.
	devtool: 'eval-source-map',
  // cheap옵션은, 라인 번호만 매핑 한다. 빠른 빌드가 가능하지만 정확성은 떨어진다.
	devtool: 'cheap-source-map',
}

devServer

개발모드에서 webpack 개발 서버와 관련된 옵션을 설정한다.

module.exports = {
  	...
  	devServer: {
      	// 개발 서버에서 정적파일을 제공하기 위해 사용
      	// 루트/src/public 폴더를 사용하겠다.
		static: {
        	directory: path.join(__dirname, 'public'),
        },
      	// 제공되는 항목에 대한 gzip 압축여부
      	compress: true,
      	// 개발 서버가 실행될 포트 번호
      	port: 8080
    }
}

혹시나 잘못되거나 부족한 정보가 있다면, 수정해야겠다.

웹팩의 대체재는 무엇인가?

  • parcel
  • vite
  • rollup

등의 많은 번들러가 있다. 추후 공부해서 작성해보도록 하겠다.

profile
코더가 아닌 프로그래머를 지향하는 개발자

0개의 댓글