교체 이유
neutrino를 교체하고자 하였으니, 저희가 사용 중인 neutrino 9버전에서는 typescript presets이 존재하지 않는다는 것을 알게 되었습니다. 뿐만 아니라 neutrino로 비롯한 매우 다양한 문제가 있었다.
1. neutrino-eslint를 사용하여 우리가 필요한 별도의 eslint 추가 구성을 하기에 불편했다.
+ 추가로 prettier-standard라는, lint와 formatter를 합친 지옥 같은 녀석이 neutrino와 맞물려 매우 불편했다.
2. webpack 코드를 구성해야 하는데 netrino는 이를 black box로 처리하여 커스텀하기 불편했다.
+ typescript 마이그레이션하는데 neutrino typescript plugin이 없었다. webpack 같았으면 후루룩 뚝딱하는데 neutrino라는 녀석 때문에 업계에서 통용되지 않는 녀석을 공부한다는 게 찜찜하고 불편했다. (https://github.com/neutrinojs/neutrino/issues/1269)
3. neutrino가 더 이상 발전하지 못하고 webpack 4에 머무르고 있으며 버전 업그레이드를 할 의지가 커뮤니티에서 보이지 않는다.
그래서 갈아 엎고 webpack으로 다시 만들기로 하였다.
우선 기존은 neutirno를 살펴보자.
./neutrino.js
const standard = require('@neutrinojs/standardjs')
const react = require('@neutrinojs/react')
const jest = require('@neutrinojs/jest')
const fbConfig = require('./firebase.conf')
const storybook = require('neutrino-preset-storybook-react')
const copy = require('@neutrinojs/copy')
const SentryWebpackPlugin = require('@sentry/webpack-plugin')
const DEV = process.env.TARGET_ENV === 'development'
const STAGING = process.env.TARGET_ENV === 'staging'
const PRODUCTION = process.env.TARGET_ENV === 'production'
const getEnv = () => process.env.TARGET_ENV || 'development'
const dotenv = require('dotenv').config({
path: `${__dirname}/.env.${getEnv()}`
})
const getReleaseVersion = () => {
const regexp = /refs[/]tags[/](.+)/g
const match = regexp.exec(process.env.RELEASE_VERSION)
if (!match) return process.env.RELEASE_VERSION
return match[1]
}
module.exports = {
options: {
root: __dirname
},
use: [
neutrino => {
if (STAGING || PRODUCTION) {
neutrino.config.plugin('sentry').use(
new SentryWebpackPlugin({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: 'business-canvas',
project: 'typed-app-react',
include: './build',
release: getReleaseVersion(),
ignore: ['node_modules', 'webpack.config.js', 'functions', 'api']
})
)
}
},
storybook({
addons: ['@storybook/addon-controls', '@storybook/addon-actions']
}),
standard(),
react({
env: {
TARGET_ENV: getEnv(),
firebase: PRODUCTION
? fbConfig.prod
: STAGING
? fbConfig.staging
: DEV
? fbConfig.dev
: fbConfig.local,
...dotenv.parsed,
...(process.env.RELEASE_VERSION
? { RELEASE_VERSION: process.env.RELEASE_VERSION }
: {}),
...(process.env.EXTENSION_MODE
? { EXTENSION_MODE: process.env.EXTENSION_MODE }
: {})
},
html: {
title: 'Typed',
favicon: 'favicon.ico',
template: 'index.ejs',
templateParameters: {
gtmId: process.env.GTM_ID,
hotjarSiteId: process.env.HOTJAR_SITE_ID,
heapId: process.env.HEAP_ID,
googleTrackingId: process.env.GOOGLE_TRACKING_ID,
googleOptimizerId: process.env.GOOGLE_OPTIMIZER_ID,
channelTalkPluginKey: process.env.CHANNELTALK_PLUGIN_KEY,
chaanelTalkAccessSecret: process.env.CHANNELTALK_ACCESS_SECRET
}
},
babel: {
plugins: [
'emotion',
[
'module-resolver',
{
alias: {
'@': './src'
}
}
],
'@babel/proposal-nullish-coalescing-operator'
]
},
devtool: 'source-map'
}),
copy({
patterns: ['*.html'],
pluginId: 'copy'
}),
jest({
setupFiles: ['./test/setup'],
setupFilesAfterEnv: ['./test/setup.integration'],
testPathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/viewer/',
'<rootDir>/functions/',
'<rootDir>/api/'
],
moduleNameMapper: {
'@/(.*)$': '<rootDir>/src/$1'
}
})
]
}
./webpack.config.js
const neutrino = require('neutrino')
module.exports = neutrino().webpack()
./package.json 중 기존 webpack 관련 패키지의 버전
"webpack": "4.44.1",
"webpack-cli": "^4.3.0",
"webpack-dev-server": "3.11.0"
기존 명령어들
"start": "webpack serve --mode development --port 7000",
"start:extension": "EXTENSION_MODE=true webpack-dev-server --mode development",
"build": "TARGET_ENV=production webpack --mode production",
"build:production": "npm run build",
"build:local": "webpack --mode development --watch",
"build:staging": "TARGET_ENV=staging webpack --mode production",
"build:dev": "TARGET_ENV=development webpack --mode production",
"build:pdf-viewer": "cd viewer/vendors/pdf.js && npx gulp generic",
"lint": "eslint --cache --format codeframe --ext mjs,jsx,js src test functions/src",
"preemulate": "mkdir -p local_db",
"emulate": "GOOGLE_APPLICATION_CREDENTIALS=fb-adminsdk-sa.json && ENVIRONMENT=local firebase emulators:start -P dev --import=./local_db --export-on-exit",
"deploy": "firebase deploy --only storage,firestore,functions",
"seed": "FIRESTORE_EMULATOR_HOST=localhost:8080 FIREBASE_AUTH_EMULATOR_HOST=localhost:9099 npm run --prefix functions register-schema:local && node ./seed",
"prepopulate-seed": "mkdir -p local_db",
"populate-seed": "firebase emulators:exec --only firestore,auth --import=./local_db \"npm run seed\" --export-on-exit",
"build-storybook": "build-storybook"
제일 간단한 jest를 분리하자.
./jest.config.js
module.exports = {
moduleDirectories: ['node_modules'],
testEnvironment: 'jsdom',
moduleNameMapper: {
'@/(.*)$': '<rootDir>/src/$1'
},
transform: {
'\\.[jt]sx?$': 'babel-jest',
'^.+\\.svg$': 'jest-svg-transformer'
},
transformIgnorePatterns: ['<rootDir>/node_modules/'],
setupFiles: ['./test/setup'],
setupFilesAfterEnv: ['./test/setup.integration'],
testPathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/viewer/',
'<rootDir>/functions/',
'<rootDir>/api/'
]
}
@neutrino/jest에서 이미 구성 되어 있던 설정들을 명시적으로 설정해주어야 함. babel 세팅이 안 되어 있어 추가 설정함.
module.exports = {
presets: ['@babel/preset-env'],
plugins: [['@babel/plugin-transform-runtime']]
}
어떻게 옮겨갈 것인가?
@neutrinojs/react 내부를 살펴보기보다는 빌드된 결과물을 확인하고 이에 맞춰 webpack을 구성하기로 함.
./build 경로에 assets으로 모두 번들링 됨.
sourcemap이 생성되고 있고, woff, svg, png, jpg에 대한 대응이 필요함.
우선 구동이 되게 하자! (webpack 4를 이용하여 우선은 예전처럼 서비스하는 것이 목표이다!)
// **** CAUTIONS ****
// html-webpack-plugin 4버전 사용할 것. 5버전 이상 webpack 5 요구
// copy-webpack-plugin 6버전 사용할 것. 6버전 이상 webpack 5 요구
// mini-css-extract-plugin 1버전 사용할 것.
// ts-loader 8버전 이하 사용할 것.
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyPlugin = require('copy-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const SentryWebpackPlugin = require('@sentry/webpack-plugin')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const ESLintPlugin = require('eslint-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin
const fbConfig = require('./firebase.conf')
const appBuildPath = path.resolve(__dirname, 'build')
const DEV = process.env.TARGET_ENV === 'development'
const STAGING = process.env.TARGET_ENV === 'staging'
const PRODUCTION = process.env.TARGET_ENV === 'production'
const getEnv = () => process.env.TARGET_ENV || 'development'
const dotenv = require('dotenv').config({
path: `${__dirname}/.env.${getEnv()}`
})
const getReleaseVersion = () => {
const regexp = /refs[/]tags[/](.+)/g
const match = regexp.exec(process.env.RELEASE_VERSION)
if (!match) return process.env.RELEASE_VERSION
return match[1]
}
const injectPlugins = targetEnv => {
if (targetEnv === 'development') {
return [
// https://v4.webpack.js.org/plugins/hot-module-replacement-plugin/
new webpack.HotModuleReplacementPlugin(),
new BundleAnalyzerPlugin({
openAnalyzer: false,
analyzerPort: 8888
})
]
} else {
return [
// https://github.com/getsentry/sentry-webpack-plugin
new SentryWebpackPlugin({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: 'business-canvas',
project: 'typed-app-react',
include: './build',
release: getReleaseVersion(),
ignore: ['node_modules', 'webpack.config.js', 'functions', 'api']
}),
// https://v4.webpack.js.org/plugins/mini-css-extract-plugin/
new MiniCssExtractPlugin({
filename: 'assets/[name].[contenthash:8].css'
}),
// https://github.com/johnagan/clean-webpack-plugin
new CleanWebpackPlugin()
]
}
}
const config = env => {
if (!(DEV | STAGING | PRODUCTION)) {
throw new Error('TARGET_ENV is not defined or typo error')
}
return {
target: 'web',
devtool: 'source-map',
context: path.resolve(__dirname),
stats: { children: false, entrypoints: false, modules: false },
node: { Buffer: false, fs: 'empty', tls: 'empty' },
entry: {
index: ['./src/index.tsx']
},
resolve: {
alias: { '@': path.join(__dirname, 'src') },
extensions: [
'.tsx',
'.ts',
'.jsx',
'.js',
'.mjs',
'.json',
'.web.jsx',
'.web.js',
'.wasm'
]
},
module: {
rules: [
{
test: /\.html$/i,
loader: 'html-loader',
options: {
attrs: ['img:src', 'link:href']
}
},
{
test: /\.(ts|tsx)?$/i,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true // disable type checker - we will use it in fork plugin
}
}
]
},
{
test: /\.(mjs|jsx|js)$/,
exclude: /node_modules/,
include: [
path.resolve(__dirname, 'src'),
path.resolve(__dirname, 'test')
],
use: [
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
debug: false,
useBuiltIns: false,
shippedProposals: true,
targets: {
browsers: [
'last 2 Chrome versions',
'last 2 Firefox versions',
'last 2 Edge versions',
'last 2 Opera versions',
'last 2 Safari versions',
'last 2 iOS versions'
]
}
}
],
[
'@babel/preset-react',
{ development: true, useSpread: true }
]
],
plugins: [
'emotion',
'@babel/plugin-proposal-class-properties',
[
'module-resolver',
{
alias: {
'@': './src'
}
}
],
'@babel/proposal-nullish-coalescing-operator'
],
cacheDirectory: true,
cacheCompression: false
}
}
]
},
{
oneOf: [
{
test: /\.module\.css$/,
use: [
DEV
? {
loader: 'style-loader'
}
: {
loader: MiniCssExtractPlugin.loader,
options: { esModule: true }
},
{
loader: 'css-loader',
options: { importLoaders: 0, modules: true }
}
]
},
{
test: /\.css$/,
use: [
DEV
? {
loader: 'style-loader'
}
: {
loader: MiniCssExtractPlugin.loader,
options: { esModule: true }
},
{
loader: 'css-loader',
options: { importLoaders: 0 }
}
]
}
]
},
{
test: /\.(eot|ttf|woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'file-loader', // https://v4.webpack.js.org/loaders/file-loader/
options: {
name: DEV
? 'assets/[name].[ext]'
: 'assets/[name].[hash:8].[ext]'
}
}
]
},
{
test: /\.(ico|png|jpg|jpeg|gif|svg|webp)(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'url-loader', // https://v4.webpack.js.org/loaders/url-loader/
options: {
limit: 8192,
name: DEV
? 'assets/[name].[ext]'
: 'assets/[name].[hash:8].[ext]'
}
}
]
}
]
},
plugins: [
// https://v4.webpack.js.org/plugins/progress-plugin/
new webpack.ProgressPlugin(),
// https://v4.webpack.js.org/plugins/environment-plugin/
new webpack.EnvironmentPlugin({
TARGET_ENV: getEnv(),
firebase: PRODUCTION
? fbConfig.prod
: STAGING
? fbConfig.staging
: DEV
? fbConfig.local // FIXME: 이후 branch 전략 수정이 곧 있을 것이라고 함. 일단은 target env = development라고 할지라도 local 환경 사용하기로 함
: fbConfig.dev,
...dotenv.parsed,
...(process.env.RELEASE_VERSION
? { RELEASE_VERSION: process.env.RELEASE_VERSION }
: {}),
...(process.env.EXTENSION_MODE
? { EXTENSION_MODE: process.env.EXTENSION_MODE }
: {}),
AMPLITUDE_API_KEY: process.env.AMPLITUDE_API_KEY
}),
// https://v4.webpack.js.org/plugins/html-webpack-plugin/
// https://github.com/jantimon/html-webpack-plugin
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'index.ejs'),
title: 'Typed',
filename: 'index.html',
hash: false,
inject: 'body',
scriptLoading: 'blocking',
compile: true,
favicon: 'favicon.ico',
minify: 'auto',
showErrors: true,
chunks: ['index'],
excludeChunks: [],
chunksSortMode: 'auto',
meta: { viewport: 'width=device-width, initial-scale=1' },
base: false,
appMountId: 'root',
lang: 'en',
templateParameters: {
gtmId: process.env.gtmId || '',
hotjarSiteId: process.env.hotjarSiteId || '',
heapId: process.env.heapId || '',
googleTrackingId: process.env.googleTrackingId || '',
googleOptimizerId: process.env.googleOptimizerId || '',
channelTalkPluginKey: process.env.channelTalkPluginKey || '',
chaanelTalkAccessSecret: process.env.chaanelTalkAccessSecret || ''
}
}),
// https://v4.webpack.js.org/plugins/copy-webpack-plugin/
new CopyPlugin({
patterns: [{ from: '*.html' }]
}),
// https://webpack.js.org/plugins/eslint-webpack-plugin/
new ESLintPlugin({
useEslintrc: true
}),
// https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#readme
new ForkTsCheckerWebpackPlugin(),
...injectPlugins(process.env.TARGET_ENV)
],
optimization: {
minimize: DEV ? false : true,
splitChunks: {
chunks: 'all',
maxInitialRequests: DEV ? Infinity : 5,
name: DEV ? true : false
},
runtimeChunk: 'single'
},
devServer: {
port: 5000,
hot: true,
historyApiFallback: true,
overlay: true,
stats: { all: false, errors: true, timings: true, warnings: true }
},
output: {
path: appBuildPath,
publicPath: '/',
filename: DEV ? 'assets/[name].js' : 'assets/[name].[contenthash:8].js'
}
}
}
module.exports = env => config(env)
간단히 설명하자면
html-loader -> Exports HTML as string. HTML is minimized when the compiler demands.
css-loader -> The css-loader interprets @import and url() like import/require() and will resolve them.
style-loader -> html header에 style 태그로 추가
일반적으로 다음 순서대로 적용함 ["style-loader", "css-loader", "postcss-loader", "sass-loader"],
ts-loader -> https://www.npmjs.com/package/ts-loader
babel-loader -> babel 돌려주는 친구임.
url-loader -> limit이하의 파일들을 base64로 변경
file-loader -> 이미지 파일의 이름을 만들고 폴더를 이동시키는 기능을 한다
추가적으로 뭔가 더 할 수 있는게 있지 않을까?
css prefix
postcss, postcss-loader
svg
compress
https://v4.webpack.js.org/plugins/compression-webpack-plugin/
빌드 속도 개선
1. speed-measure-webpack-plugin
https://www.npmjs.com/package/speed-measure-webpack-plugin
전자가 개발 환경이고 후자가 production 빌드이다.
1. babel-loader, ts-loader를 esbuild-loader로 바꿔보자.
https://github.com/privatenumber/esbuild-loader
2. cache-loader (현재 제거. 불필요)
https://v4.webpack.js.org/loaders/cache-loader/
3. node inspector
Important! If you need polyfills in your code, consider including core-js in your package.json. This is will configure @babel/preset-env to automatically include polyfills based on usage. *
https://v4.webpack.js.org/plugins/profiling-plugin/
bundle size 최적화
https://hyperconnect.github.io/2019/07/29/Optimize-webview-bundle-size-1.html
https://hyperconnect.github.io/2019/08/14/Optimize-webview-bundle-size-2.html
import { format } from 'date-fns' // BAD
import format from 'date-fns/format' // GOOD
내가 만났던 에러들
어노테이션 관련 에러들
1.
Error: Compiling RuleSet failed: A Rule must not have a 'loader' property when it has a 'use' property
2-1.
Error: Duplicate plugin/preset detected. If you'd like to use two separate instances of a plugin, they need separate names, e.g. (... 예시는 생략함) => 실수로 같은 presets을 사용함.
2-2.
A Rule must not have a 'options' property when it has a 'use' property (at ruleSet[1].rules[0].options: [object Object])
결론적으로 구성을 잘못한거임.
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-proposal-object-rest-spread']
}
}
}
]
}
4.
Can't resolve 'crypto' in '/Users/darrenkwon/typed-app/node_modules/crypto-js'
breaking change
webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.
If you want to include a polyfill, you need to:
- add a fallback 'resolve.fallback: { "crypto": require.resolve("crypto-browserify") }'
- install 'crypto-browserify'
If you don't want to include a polyfill, you can use an empty module like this:
resolve.fallback: { "crypto": false }
'🎁 Module bundler > 📖 Webpack' 카테고리의 다른 글
CRA 없이 TS react 환경 구성 (0) | 2020.09.28 |
---|---|
CRA 없이 webpack으로 Custom React 프로젝트 구성하기 (0) | 2020.09.28 |
웹팩을 기초부터 하나씩 (2) : webpack.config.js (0) | 2020.07.14 |
웹팩을 기초부터 하나씩 (1) : 모듈 번들러, 간단한 번들링 (0) | 2020.07.14 |
[Webpack] express 환경에서의 웹팩 사용법 (scss, es6 js 변환) (1) | 2020.03.19 |