여러 파일을 하나의 파일로 합쳐서 묶는다. 이게 핵심이다.
이미지 출처 : https://webpack.js.org/
예를들어 JS
파일은 ESM방식의 모듈 파일이 산재되어있다. 브라우저가 자바스크립트를 실행하려면, 각 기능에 대한 스크립트를 전부 받아와야한다. 이는 HTTP요청으로 이루어지므로, 네트워크 병목 현상이 발생할 수도 있다.
이를 방지하기위하여 하나의 파일로 묶는 작업이 번들링이다.
웹팩은 JS모듈 번들러다. 그런데 JS뿐만이 아니라 다양한 에셋(html, jpg, css...등)도 같이 번들링 해준다.
또한 트리 쉐이킹이라는 기능을 통해 사용하지 않는 코드를 제거해 용량최적화를 해준다.
아무튼 다양한 기능이 있는데, 먼저 번들링에 집중해보자.
명령어npm i -D webpack webpack-cli
로 설치해서 개발환경에서만 실행할수 있게 하자.
설치하면 webpack
명령어를 이용해서 바로 번들링할 수 있다.
하지만 기본 세팅이 아래와 같으므로 폴더위치를 변경해야한다면 설정을 해주어야함.
webpack.config.js
파일을 빌드 할 루트폴더에 만든다.
const path = require("path");
module.exports = {
entry: "/public/js/main.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "public/dist"),
},
};
entry
: html파일에서 로드하는 메인 자바스크립트 파일의 진입점. 이 파일로부터 각 모듈의 의존성을 더하여 하나의 번들로 만든다.output
: 번들링 파일 출력시 기타 설정값들 적용. 파일이름, 경로, 번들링 옵션 등 활용 가능하다.이후 index.html
에서 스크립트 로드 위치를 바꿈.
<body id="root">
<script src="/public/dist/bundle.js"></script>
</body>
정상적으로 실행되나 싶었으나 특정 부분에서 오류가 발생한다.
변수들이 난독화되어있어 알아보기 살짝 어려웠는데, 다행히도 useSelector()
에서 문제가 발생한 걸 알 수 있었다.
번들링에는 문제가 없다는걸 알아서 다행인데...일단 마지막 목차에서 살펴보자.
웹팩은 번들링하는데 사용된다. 그런데 index.html
파일이 번들링된 소스를 참조하고 있다면 개발시에도 계속 빌드해줘야 할까?
만약 프로젝트의 규모가 크다면 시간 손해가 막심할 것이다.
그렇기에 webpack-dev-server
가 필요하다!
해당 패키지는 이름답게 개발환경을 위한 서버다.
디스크에 저장되지 않는 메모리 컴파일을 사용하기 때문에 기존보다 컴파일 속도가 훨씬 빠르다.
npm i -D webpack-dev-server
명령어로 개발서버 패키지를 설치한다.
이후 npm i -D css-loader style-loader html-webpack-pulgin
명령어로 설정에 필요한 세가지 패키지도 설치해준다.
webpack.config.js
에서 설정해준다.
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
entry: "/public/js/main.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
publicPath: "/dist/",
},
devServer: {
static: {
directory: path.resolve(__dirname, "dist"),
},
port: 9000,
hot: true,
historyApiFallback: true,
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "public/index.html"),
}),
],
module: {
rules: [
//...
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
},
};
차근차근 설명해보자. 이전에 설명했던 옵션들은 생략하겠음.
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
publicPath: "/dist/",
},
publicPath
: 브라우저가 참조할 번들링된 결과 파일들의 url주소를 지정한다.index.html
이 번들링 된 경우 아래와 같은 스크립트 소스를 가리킨다.<script defer="defer" src="bundle.js"></script>
bundle.js
는 html
과 같은 파일 내부에 있으므로 파일 디렉토리상문제가 없지만, 브라우저의 요청에서는 조금 달라진다.
historyApi로 인하여 브라우저 url이 달라진 채 새로고침을 하게되면
http://example.com/bundle.js
로 가야 할 요청이 http://example.com/something/bundle.js
등으로 가게되고, 이는 원치 않는 404에러를 발생시키게된다.
따라서 브라우저가 번들링된 결과파일들의 url주소를 잘 참조할 수 있도록 해당 번들링 파일이 서버 디렉토리상 어디에 존재하는지 절대경로로 적어주어야 한다.
- root/
- dist/
- bundle.js
- index.html
- public/
- js/
- main.js(번들링될 entry지점 스크립트)
- index.html(뼈대가 되는 파일)
위와같은 구조이므로 /dist/
를 적어주었다.
devServer: {
static: {
directory: path.resolve(__dirname, "dist"),
},
port: 9000,
hot: true,
historyApiFallback: true,
},
static
: 개발환경에서 사용 할 정적파일의 주소를 가리킨다. 기본적으로 웹팩이 산출해 낸 번들링 폴더로 되어있다.port
: 개발서버용 포트hot
: 핫 리로드 온오프historyApiFallback
: historyApi를 사용한 SPA웹에서 발생하는 404에러시 index.html
를 반환. plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "public/index.html"),
}),
],
html-webpack-puglin
: 번들링된 스크립트를 부착한 html파일을 만들어준다. 기존 템플릿을 지정할 수도 있다.module: {
rules: [
//...
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
},
webpack은 js와 json만 인식한다. 따라서 css파일 또한 번들링 하려면, js로 변경해주어야한다.
기존 css파일을 html의 <link>
태그에서 제거해주고, 자바스크립트 진입파일 내부에서 css를 임포트 해준다.
//main.js
import Style from './style.css';
이후 위 2가지 모듈style-loader, css-loader
을 이용하여 css를 js로 바꾼다.
배열 내 모듈 순서도 중요하다. 우측부터 실행된다고 하니 참고하자.
이렇게 모든 준비가 끝났으면 package.json
파일에 가서 webpack dev server를 작동시킬 명령어를 스크립트화 해준다.
"webpack-dev-server": "webpack serve --mode development --open"
이제 npm run webpack-dev-server
명령어를 입력하면 개발서버가 켜진다.
이제 번들링된 파일이 클라이언트에게 서빙되어야 하므로 기존 /public/index.html
를 서빙하던 로직을 /dist/index.html
로 살짝 바꿔주었다.
node로 접속한 웹서버와 webpack-dev-server 둘다 잘 작동하는 모습을 볼 수 있다.
node 웹서버 포트 3000
webpack-dev-server 포트 9000
useSelector()
를 사용하는 부분에서 문제가 발생했으니, 해당 코드의 구현체를 따라가보자.
참고로 해당 메서드는 React-redux메서드가 아니라 내가 만든 요상한 메서드다...ㅎㅎ
const useSelector = (selector) => {
const selectedState = selector(getState());
return selectedState;
};
단순하게 selector을 이용해 현재 상태를 반환함. getState()
로 반환되는 상태를 어떻게 구현했나 살펴봄.
const state = {
[reducer.name]: observable(reducer(undefined, undefined)),
};
무언가 잘못된게 바로 와닿는다. 특히 reducer.name
...함수의 이름을 키로 넣은 뒤 observable()
로 구독하고있다.
아마 그때의 사고방식은 아래와 같았을 것 같다.
createStore()
메서드는 리듀서를 받아와서 상태를 저장한다.useSelector()
메서드에서는 useSelector((state) => state.reducername.some...)
이렇게 접근하는 것 처럼 보였으므로, 함수의 이름을 키로하면 되겠구나!결국 리듀서 또한 함수니, 웹팩이 코드를 압축하는 과정에서 리듀서의 함수명이 바뀌고, 바뀐 리듀서의 함수명과 다른 documentsReducer
로 상태를 접근하려하니 에러가 발생했던 것이다.
또한 redux에서는 리듀서가 여러개 존재할 시, 리듀서의 이름이 아니라 combineReducer
를 이용해서 하나의 리듀서로 합친다...
리듀서가 하나일땐 당연하게도 리듀서 이름이 아니라 상태명으로 가져오는거임🙄
마지막으로 useSelector()
는 redux의 코드가 아닌 react-redux의 코드이므로 사실 react의 useSyncExternalStore(...)
같은 훅을 이용해 만들었음이 자명하다. 따라서 내가 만든건 이도저도 아닌 얽혀있는 코드다.
개발을 공부한지 3개월도 채 되지 않았을때 만들어놓은 로직이라 오류가 상당한가보다ㅋㅋㅋ
이렇게 부끄러운 과거를 돌이켜보는것도 나름...🤮
아무튼 로직을 아래처럼 간단하게 바꾸어서 해결!
export const createStore = (reducer, middleware) => {
const state = observable(reducer(undefined, undefined));
const getState = () => Object.freeze(state);
const dispatch = (action) => {
if (getTag(action).includes("Function")) {
return middleware({ dispatch, getState })(dispatch)(action);
}
const nextState = reducer(getDeepCopy(state), action);
const stateKeys = Object.keys(state);
stateKeys.forEach((stateKey) => {
state[stateKey] = nextState[stateKey];
});
};
const useSelector = (selector) => {
const selectedState = selector(getState());
return selectedState;
};
return { dispatch, getState, useSelector };
};
바꾸는김에 useSelector(...)
의 작동방식도 바꾸고싶지만 일단 번들링이 목표였으니 여기까지 스톱.