React.js로 프로젝트를 진행하면 CRA(create-react-app)를 사용해 쉽게 프로젝트 세팅을 할 수 있다.
그런데 빌드 구성을 커스텀하거나 프로젝트의 구조가 다르면 Webpack을 직접 건드려줘야 하는 상황이 온다.
Webpack에 관련해서 정리해보려고 한다.
일단 먼저 웹팩이 뭔지부터 간단하게 살펴보고자 한다.
공식문서에서 웹팩은 다음과 같이 설명하고 있다.
webpack은 모던 JavaScript 애플리케이션을 위한 정적 모듈 번들러 입니다. webpack이 애플리케이션을 처리할 때, 내부적으로는 프로젝트에 필요한 모든 모듈을 매핑하고 하나 이상의 번들을 생성하는 디펜던시 그래프를 만듭니다.
웹팩에서는 애플리케이션을 구성하는 모든 모듈을 병합하고 압축해서 만들어진 하나 이상의 파일을 번들 이라고 하며, 이러한 동작을 모듈 번들링이라고 한다.
웹팩 정의에 나오는 정적 모듈 번들러란 모듈(HTML, CSS, Js 등)을 번들링해 정적인 파일로 만들어주는 도구라고 할 수 있다.
웹팩에 대해 깊게 알아보기 보다 내가 웹팩을 커스텀해야 하는 이유를 먼저 설명해보려고 한다.
내가 하려는 프로젝트에서는 하나의 react 프로젝트에서 여러 개의 페이지에 쓰이는 각각의 js, css 가 빌드되어 나와야 했다. (약간의 마이크로 프론트엔드 와 비슷한 개념(?))
간단히 정리하자면
페이지 종류 / build 파일
PAGE A / pageA.js, pageA.css
PAGE B / pageB.js, pageB.css
....
즉 하나의 프로젝트 내에서 여러개의 빌드 파일들이 나와야 하는 형태였다.
그래서 webpack에서는 아래와 같이 구성하게 되었다.
프로젝트 구조
img
js
apis
components
constants
hooks
pages
pageA.tsx
stores
uitls
pageA.tsx // entry point
html
pageA.html
pageB.html
css
PageA
PageB
types
const webpackConfig = (env: { page }) => {
const page = env.page || 'defaultPage';
const pages = {
pageA: {
entry: ['pageA tsx 위치', 'pageA, css 위치'],
outputJs: './js/pageA.js',
outputCss: './css/pageA.css',
html: '사용되는 html',
port: 'devserer 사용할 포트',
open: 'devserver 실행시 open 여부'
},
...
}[page]
const config = {
context: path.resolve(__dirname, 'src'),
mode: isDevelopment ? 'development' : 'production',
devtool: !isDevelopment ? 'hidden-source-map' : 'eval-source-map',
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
alias: {
'@': path.resolve(__dirname, './src/'),
// 필요할 경우 추가
},
},
entry: pages.entry,
module: {
rules: [
// 필요한 module rule 지정
],
},
plugins: '플러그인 리스트',
output: {
path: path.join(__dirname, 'dist'),
filename: pages.outputJs,
assetModuleFilename: '[path][name][ext]',
publicPath: 'auto',
},
devServer: {
host: 호스트명,
open: pages.open || '/',
historyApiFallback: {
index: pages.html,
rewrites: pages.rewrite,
},
port: pages.port || 3000,
},
}
return config;
}
export default webpackConfig;
const page = env.page || 'defaultPage';
const pages = {
pageA: {
entry: ['pageA tsx 위치', 'pageA css 위치'], // pageA의 tsx, css entry위치
outputJs: './js/pageA.js', // 빌드되어 나올 js
outputCss: './css/pageA.css', // 빌드되어 나올 css
html: '사용되는 html', // pageA의 index.html
port: 'devserer 사용할 포트',
open: 'devserver 실행시 open 여부'
},
...
}[page]
위처럼 하나의 프로젝트에 사용되는 page들을 객체로 관리했다. 그래서 npm script에서 실행하는 env.page에 따라 pages가 바뀌게 되도록 설정했다.
pacakge.json의 script를 예로 들자면 page=pageA를 넘겨 pageA를 webpack conifg에서 받게 되어 pages에는 pageA의 객체가 대입 되는 것이다.
package.json
"dev-pageA": "webpack serve --env page=pageA",
페이지별로 설정을 완료했다면 이제 페이지별로 빌드도 되어야 한다.
entry: pages.entry,
output: {
path: path.join(__dirname, 'dist'),
filename: pages.outputJs,
assetModuleFilename: '[path][name][ext]',
publicPath: 'auto',
},
entry 포인트를 지정해줬기 때문에 js와 css가 각 페이지별로 동적으로 빌드가 되도록 설정이 된다.
물론 plugin들도 필요한데 이는 아래에 설명하겠다.
빌드되는 파일 예시)
dist
css
pageA.css
pageB.css
...
img
js
pageA.js
pageB.js
...
public
pageA.html
pageB.html
...
플러그인 설정
위에서 플러그인은 생략해놨는데 그중 중요한 부분만 설명하려고 한다.
const 플러그인_리스트 = [
new HtmlWebpackPlugin({
// index.html 템플릿을 기반으로 빌드 결과물을 추가해줌
template: `.${pages.html}`,
filename: `.${pages.html}`,
publicPath: '/',
}),
new MiniCssExtractPlugin({ filename: pages.outputCss }),
];
HtmlWebpackPlugin은 HTML을 동적으로 생성할 수 있는 플러그인이다. 나는 build할때 html이 나오지 않아도 되서 필요없을 줄 알았는데, devserver를 돌리기 위해 필요한 플러그인이다.
MiniCssExtractPlugin 은 CSS를 별도의 파일로 추출해주는 플러그인이다. 즉 페이지별로 css를 추출해야하므로 필요한 플러그인이다.
이처럼 플러그인에도 page별 동적인 처리를 하여 구성하였다.
이외에 module이나 resolve의 설정도 추가하였지만 지금 중요하지 않은 내용이라 넘어가겠다.
마지막으로 build script 명령어를 보자면 serve 위와 차이가 없다.
마찬가지로 env에 page를 넘겨줘서 해당 page에 맞는 build를 수행하게 된다.
package.json
"build-pageA": "webpack --env page=pageA",
하나 더 마주한 과정이 있었는데 이는 각 페이지별로 devserver를 돌려 개발환경을 구성하는것 이였다.
(나는 여기서 가장 고생했다... react-router가 있어서 devserver에서 url path가 안맞는등 신경써줘야 할 것들이 너무나 많았다 😂😂😂)
devServer: {
host: 호스트명,
open: pages.open || '/',
historyApiFallback: {
index: pages.html,
rewrites: pages.rewrite,
},
port: pages.port || 3000,
},
webpack에서는 devserver를 돌려 개발환경을 구성할 수 있는데 여기도 마찬가지로 page에 따라 동적으로 넣어줘 사용했다. 여기서 중요한 설정이 historyApiFallback 이였다.
historyAPiFallback 이란?
개발환경에서 URL의 시작주소가 아닌 경로에서는 404 페이지로 내려주지만 해당 옵션을 사용해 가짜주소를 만들어주는 역할을 한다.
일반적인 react 프로젝트 구조라면 true로 설정하면 잘 잡아주지만 내가 진행한 프로젝트에서는 html도 여러개이며, entry포인트도 여러곳이라 historyApiFallback에서 제공해주는 index, rewrite 옵션을 사용해 잡아줬다.
공식문서에 잘 설명되어 있지만 간단하게 보자면 index는 기본 경로를 설정하는 것이고, rewrites는 from으로 들어온 경로를 to로 보내는 것이다.
rewrite: [
{ from: /\/pageA/, to: '/pageA.html'}
],
여기서 내가 힘들었던 점은 to에 들어가는 html이 상대경로로 들어가면 찾지 못한다는 것이다.
이것 때문에 정말 고생했다. './pageA.html' 로 했더니 계속 404 에러가 떠서 원인을 못찾다가 결국 절대경로로 수정하여 해결할 수 있었다. 위에서도 HtmlWebpackPlugin에서 .을 붙인 이유도 같은 이유에서이다.
정리하면 webpack을 직접 커스텀해서 어느정도 다루는데 익숙해질 수 있었던 프로젝트였다.
webpack에 정말 많은 option들이 있고 공식문서도 다가가기 힘들다고 생각하는데 이제는 조금이나마 잘 찾을 수 있을 것 같다
많은 도움이 되었습니다, 감사합니다.