CRA는 기본적으로 webpack + babel로 구현되어 있음.
$ create-react-app $MY_APP_NAME --scripts-version=react-scripts-ts
create-react-app CLI로 react-app 빌드환경을 쉽게 생성할 수 있다.
$ cd $MY_APP_NAME
$ npm run eject
eject 명령으로 빌드 환경을 뽑아낼 수 있다. 단 되돌릴 수 없다. 그런데 요새 되돌리는 프로젝트가 있었던 것 같다.
eject를 실행하게 되면 복잡한 디펜던시와 많은 파일들이 생기기 때문에 그냥 사용할 것을 권장한다. 다만 webpack을 이용하여 typescript를 컴파일 하는 과정을 보고싶어서 eject로 create-react-app의 빌드 스크립트를 정리한다.
Webpack
create-react-app은 webpack-cli를 이용하여 빌드하지 않고, 코드레벨에서 webpack 패키지를 이용하여 커스터마이즈 했다. 설정만으로 해결되는 환경이라면 Webpack-cli + config 파일로 뚝딱뚝딱 끝나게 된다. 다만, 그렇지 않은 매우 복잡한 환경이라면 create-react-app과 같이 webpack 패키지를 import 해서 커스터마이즈 하게 된다.
Webpack3 config
entry
-> loader
-> plugins
-> output
const webpack = require('webpack');
module.exports = {
// entry : 웹팩이 파일을 읽어들이기 시작하는 부분.
// 하나 이상의 진입점 설정 가능
entry: [
require.resolve('./polyfills'),
'./src/app.js'
]
// entry에서 설정한 의존성 트리를 바탕으로 결과물을 반환하는 설정.
output: {
// 결과물이 저장될 경로
path: '',
// 결과물이 저장될 이름
filename: '',
// app이 제공될 URL.
publicPath: '',
},
// 모듈의 해석 방식 설정
resolve: {
// 어디서 require를 찾을거냐
modules: ['node_modules'],
// 어떤 파일들을 찾을거냐
extensions: ['.js', '.json', '.jsx', '.css'],
},
// 모듈 처리 방식
module: {
rules: [
{
// 정규표현식으로 적용되는 파일 설정
test: /\.(js|jsx|mjs)$/,
// 로더 지정
loader: require.resolve('source-map-loader'),
enforce: 'pre',
// 컴파일할 폴더지정
include: paths.appSrc,
},
]
},
// 번들링 후 결과물의 처리 방식 등을 처리할 다양한 플러그인들을 설정
plugins: [
new InterpolateHtmlPlugin(env.raw),
new HtmlWebpackPlugin({
inject: true,
template: paths.appHtml,
}),
new webpack.NamedModulesPlugin(),
],
// 최적화
optimization: {},
};
Webpack plugin
- 플러그인은 웹팩의 뼈대다.
- 플러그인은
apply
메서드가 있는 js 객체다. apply
메서드는 webpack 컴파일러에서 호출한다.- create-react-app에서 일부 편의 기능을 제공하기 위해 별도의 플러그인을 만들어서 사용중임.
/// react-dev-utils/InterpolateHtmlPlugin.js
const escapeStringRegexp = require('escape-string-regexp');
class InterpolateHtmlPlugin {
constructor(replacements) {
this.replacements = replacements;
}
apply(compiler) {
compiler.plugin('compilation', compilation => {
compilation.plugin(
'html-webpack-plugin-before-html-processing',
(data, callback) => {
// Run HTML through a series of user-specified string replacements.
Object.keys(this.replacements).forEach(key => {
const value = this.replacements[key];
data.html = data.html.replace(
new RegExp('%' + escapeStringRegexp(key) + '%', 'g'),
value
);
});
callback(null, data);
}
);
});
}
}
start script
scripts/start.js
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
// create-react-app 에서 사용하는 환경변수와 path 설정값이 들어있음.
// env에서 .env 파일 읽는 부분이 추가됨.
require('../config/env');
const fs = require('fs');
const chalk = require('chalk');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const clearConsole = require('react-dev-utils/clearConsole');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const {
choosePort,
createCompiler,
prepareProxy,
prepareUrls,
} = require('react-dev-utils/WebpackDevServerUtils');
const openBrowser = require('react-dev-utils/openBrowser');
const paths = require('../config/paths');
const config = require('../config/webpack.config.dev');
const createDevServerConfig = require('../config/webpackDevServer.config');
const useYarn = fs.existsSync(paths.yarnLockFile);
const isInteractive = process.stdout.isTTY;
// 필요한 파일이 있는지 확인
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
const HOST = process.env.HOST || '0.0.0.0';
if (process.env.HOST) {
console.log(
chalk.cyan(
`Attempting to bind to HOST environment variable: ${chalk.yellow(
chalk.bold(process.env.HOST)
)}`
)
);
console.log(
`If this was unintentional, check that you haven't mistakenly set it in your shell.`
);
console.log(`Learn more here: ${chalk.yellow('http://bit.ly/2mwWSwH')}`);
console.log();
}
// 포트 선택. 이미 사용중이라면 다른 포트를 사용하도록.
choosePort(HOST, DEFAULT_PORT)
.then(port => {
if (port == null) {
// We have not found a port.
return;
}
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const appName = require(paths.appPackageJson).name;
const urls = prepareUrls(protocol, HOST, port);
// webpack.config.dev 파일에 정의한 설정으로 webpack 컴파일러를 만든다.
// 이벤트에 따른 메시지 처리를 위해 커스터마이즈한 스크립트를 사용.
const compiler = createCompiler(webpack, config, appName, urls, useYarn);
// 프록시 설정. API를 다른 사이트로 요청하는 기능?
const proxySetting = require(paths.appPackageJson).proxy;
const proxyConfig = prepareProxy(proxySetting, paths.appPublic);
// WebpackDevServer 설정을 만든다. webpackDevServer.config 파일 내용 전달됨
// 프록시, URL 설정으로 webpack 개발 서버 설정을 만든다.
const serverConfig = createDevServerConfig(
proxyConfig,
urls.lanUrlForConfig
);
// 커스터마이징 된 webpack 컴파일러와 webpack 개발 서버 설정을 기반으로
// WebpackDevServer 인스턴스를 생성한다.
const devServer = new WebpackDevServer(compiler, serverConfig);
// 개발서버 실행
devServer.listen(port, HOST, err => {
if (err) {
return console.log(err);
}
if (isInteractive) {
// 콘솔을 정리한다.
clearConsole();
}
console.log(chalk.cyan('Starting the development server...\n'));
// 브라우저를 실행시킨다.
openBrowser(urls.localUrlForBrowser);
});
['SIGINT', 'SIGTERM'].forEach(function(sig) {
process.on(sig, function() {
devServer.close();
process.exit();
});
});
})
.catch(err => {
if (err && err.message) {
console.log(err.message);
}
process.exit(1);
});
config/webpack.config.dev.js
'use strict';
const autoprefixer = require('autoprefixer');
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const getClientEnvironment = require('./env');
const paths = require('./paths');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
// Webpack은 `publicPath`를 app을 제공하기 위한 장소로 결정한다.
// 개발모드에서는 항상 루트로 설정한다. 이렇게 하면 설정이 쉬워진다.
const publicPath = '/';
// `publicUrl`은 `publicPath`와 유사하지만, index.html에 `%PUBLIC_URL%`,
// JS에서 `process.env.PUBLIC_URL` 기능을 제공하기 위해 사용된다.
const publicUrl = '';
// 앱에 삽입하기 위한 환경 변수를 얻는다.
const env = getClientEnvironment(publicUrl);
// 개발 설정이고, 개발환경과 빠른 재빌드에 초점이 맞춰져 있다.
// 프로덕션 설정은 분리되어 있다.
module.exports = {
// source-map을 제공해서 브라우저에서 컴파일 전의 소스를 볼 수 있다.
// See the discussion in https://github.com/facebookincubator/create-react-app/issues/343.
devtool: 'cheap-module-source-map',
// 어플리케이션 진입점이다. JS가 번들링 되는 루트.
entry: [
// Promise, Promise rejection-tracking, whatwg-fetch 폴리필을 포함한다.
require.resolve('./polyfills'),
// WebpackDevServer 대체 클라이언트를 포함한다.
// 클라이언트의 작업은 소켓을 통해 WebpackDevServer에 연결하고
// 변경 사항에 대한 알림을 받는다.
// CSS를 변경하면 핫 업데이트를 적용,
// JS를 변경하면 페이지를 새로 고침.
// 사용자가 syntax error를 발생시키면, 페이지에 오버레이 시킨다.
// WebpackDevServer의 클라이언트를 사용하고 싶으면 아래 주석을 해제.
// require.resolve('webpack-dev-server/client') + '?/',
// require.resolve('webpack/hot/dev-server'),
require.resolve('react-dev-utils/webpackHotDevClient'),
// 앱 JS 진입파일
paths.appIndexJs,
// 앱 JS 진입파일을 마지막으로 포함시켜서 초기화 하는 동안
// 런타임 오류가 발생하더라도 WebpackDevServer 클라이언트를 터트리지 않고,
// JS code가 새로고침을 트리거 하도록 한다.
],
output: {
pathinfo: true,
// 실제파일은 아니고, 가상 경로에 WebpackDevServer가 제공한다.
// JS 번들파일이고, Webpack 런타임에 생성된다.
filename: 'static/js/bundle.js',
// 코드 분할을 사용하면 추가 chunk 파일도 있다.
chunkFilename: 'static/js/[name].chunk.js',
publicPath: publicPath,
// 소스맵의 원래 디스크 위치를 가르킨다.
devtoolModuleFilenameTemplate: info =>
path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
},
resolve: {
// 이렇게 하면 webpack이 모듈을 찾는데 fallback을 설정할 수 있다.
// node_modules를 처음으로 찾고, 다음으로 appNodeModules를 찾도록.
// https://github.com/facebookincubator/create-react-app/issues/253
modules: ['node_modules', paths.appNodeModules].concat(
// `env.js`에서 설정할 수 있기 때문에, 존재할 수 밖에 없다.
process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
),
// Node 생태계에서 지원하는 합당한 기본값이다.
// JSX 지원을 위한 일부 확장자도 추가했지만, 사용하지 않는것이 좋다.
// see: https://github.com/facebookincubator/create-react-app/issues/290
// `web` React Native를 지원하기 위해 web 접두사가 추가되었다.
extensions: [
'.mjs',
'.web.ts',
'.ts',
'.web.tsx',
'.tsx',
'.web.js',
'.js',
'.json',
'.web.jsx',
'.jsx',
],
alias: {
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
'react-native': 'react-native-web',
},
plugins: [
// 사용자가 src/ 외부에서 파일을 가져올 수 없게 한다.
// src/에 있는 파일만 바벨로 프로세싱 하기 때문에 혼란을 만든다.
// node_modules에 추가하고 가져오는 식으로 해결하세요.
// 소스 파일이 처리되지 않으므로, 컴파일 되도록 해야 한다.
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
new TsconfigPathsPlugin({ configFile: paths.appTsConfig }),
],
},
module: {
strictExportPresence: true,
rules: [
{
test: /\.(js|jsx|mjs)$/,
loader: require.resolve('source-map-loader'),
enforce: 'pre',
include: paths.appSrc,
},
{
// "oneOf"는 아래 로더들을 차례로 선회하고, 요구사항에 일치하는
// 로더가 없으면 최종적으로 file loader를 사용한다.
oneOf: [
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve('url-loader'),
options: {
limit: 10000,
name: 'static/media/[name].[hash:8].[ext]',
},
},
{
test: /\.(js|jsx|mjs)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
compact: true,
},
},
{
test: /\.(ts|tsx)$/,
include: paths.appSrc,
use: [
{
loader: require.resolve('ts-loader'),
options: {
transpileOnly: true,
},
},
],
},
// "postcss" 로더는 우리 CSS에 autoprefixer를 적용( vender prefix 삽입 )
// "css" 로더는 CSS 패스를 해결하고, assets 를 디펜던시에 삽입한다.
// "style" 로더는 CSS를 JS 모듈에 삽입할 수 있게 해준다.
// 프로덕션에서는 CSS를 파일로 추출하지만,
// 개발시에는 "style"로더로 hot editing을 가능하게 한다.
{
test: /\.css$/,
use: [
require.resolve('style-loader'),
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
},
},
{
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebookincubator/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
autoprefixer({
browsers: [
'>1%',
'last 4 versions',
'Firefox ESR',
'not ie < 9', // React doesn't support IE8 anyway
],
flexbox: 'no-2009',
}),
],
},
},
],
},
{
exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/],
loader: require.resolve('file-loader'),
options: {
name: 'static/media/[name].[hash:8].[ext]',
},
},
],
},
// ** STOP ** 새로운 로더를 넣을거에요?
// "file" 로더 전에 삽입하세요. 여기는 아니에요.
],
},
plugins: [
// 환경변수를 `index.html`에서 사용 가능하게 만들어 준다.
// 개발환경에서는 빈 문자열 상태.
new InterpolateHtmlPlugin(env.raw),
// <script>가 삽입된 `index.html` 파일을 생성한다.
new HtmlWebpackPlugin({
inject: true,
template: paths.appHtml,
}),
// 팩토리 함수에 모듈 이름을 추가해서 브라우저 프로파일러에 표시.
new webpack.NamedModulesPlugin(),
// 환경변수를 JS 코드에서 사용할 수 있게 해준다.
new webpack.DefinePlugin(env.stringified),
// 최신 업데이트를 내보내는데 필요하다. (현재는 CSS만)
new webpack.HotModuleReplacementPlugin(),
// Watcher가 경로를 잘못 입력하면 제대로 동작하지 않으므로, 오류를 출력
// See https://github.com/facebookincubator/create-react-app/issues/240
new CaseSensitivePathsPlugin(),
// 누락된 모듈을 참조했을 때 `npm install`을 실행하면,
// 웹팩이 참조하게 하기 위해, 서버를 재시작해야한다.
// 이 플러그인은 자동으로 재시작 해준다.
// See https://github.com/facebookincubator/create-react-app/issues/186
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
// Moment.js같은 유명 라이브러리는 큰 local 파일들이 같이 번들링 된다.
// 전체 locale을 가져오지 않도록 하고, 특정 locale을 import 하는 실용적인 방법이다.
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
// Moment.js 안쓰면 삭제해도 된다.
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// 컴파일 속도를 높이기 위해 별도의 프로세스에서 유형 검사 및 linting 수행
// typescript 설정이 여기서 참조된다. __중요__
new ForkTsCheckerWebpackPlugin({
async: false,
watch: paths.appSrc,
tsconfig: paths.appTsConfig,
tslint: paths.appTsLint,
}),
],
// 일부 라이브러리는 노드 모듈을 가져오지만, 브라우저에서는 사용하지 않는다.
// webpack에 빈 mock 을 제공하도록 알려준다.
node: {
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
},
// 속도 향상을 위해 분할이나 축소를 수행하지 않으므로, 개발시 성능 힌트를 끈다.
performance: {
hints: false,
},
};