All Articles

CRA with Typescript로 Webpack 분석하기( 구버전 )

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,
  },
};