1. 서문

create-react-app(이하 CRA) 으로 React 웹앱을 만들었다. 타입스크립트TypeScript도 쓰고 싶어서 만들 때 --template typescript 옵션도 주었다. 웹팩Webpack 설정은 어떻게 되어 있는지 살펴보고 싶어서 yarn eject 명령어도 실행했다.

(위 문단이 이해되지 않는다면 이 글이 다소 이해하기 어려울 수도 있다. 그럴 경우 create-react-app 공식 문서를 참고하자.)

이번에는 웹팩의 module 설정을 살펴보자.

2. module

module 설정은 프로젝트 내 여러 타입의 모듈들을 어떻게 다룰지 정의하는 옵션이다. 웹팩 공식 문서에는 아래와 같이 설명되어 있다.

These options determine how the different types of modules within a project will be treated.

(모듈Module은 기능 단위로 작게 나누어진 코드 뭉치다. ES6+에 익숙하다면 export 문으로 선언되고 import 문으로 사용되는 것이 모듈이라고 이해하면 되고, CommonJS에 익숙하다면 exports로 선언 및 정의되고 require()로 사용되는 것을 모듈이라고 이해하면 된다. 참고 문서)

CRA로 생성된 앱에는 module 설정에 strictExportPresencerules, 이렇게 두 가지 값이 설정되어 있다.

{
  // ...
  module: {
    strictExportPresence: true,
    rules: [
      // ...
    ]
  },
}

module.strictExportPresencetrue로 설정 시 모듈 내에 exports 문이 없을 때 에러를 발생시키도록 한다. (false라면 warning만 발생한다.)

module.rules는 웹팩이 모듈을 생성(초기화)할 때 사용하는 규칙들이다. 이 규칙들로 모듈이 생성되는 방법을 변경할 수 있다.

2.1. module.rules

CRA로 생성된 앱의 modules.rules에는 두 규칙이 정의되어 있다.

// ...
rules: [
  { parser: { requireEnsure: false } },
  {
    oneOf: [
      {
        /* ... */
      },
      {
        /* ... */
      },
      {
        /* ... */
      },
      {
        /* ... */
      },
      // ...
    ],
  },
];

첫번째는 아래와 같은 비교적 짧은 규칙이다.

{ parser: { requireEnsure: false } },

module.rules에 사용되는 규칙에는 보통 test 값이 포함된다. test는 문자열이나 정규표현식, 혹은 그것들의 배열값으로, test 조건에 일치하는 파일이 갖고 있는 모듈에 대해서 해당 규칙을 적용하라는 의미이다. (자세한 예는 아래에서 볼 수 있다.)

하지만 위 규칙에는 test 값이 없다. 이는 모든 유형의 파일에 대해 적용되는 공통 규칙이라는 의미다. 이 규칙은 이 앱의 모든 모듈에서require.ensure 기능을 쓰지 못하도록 하는 규칙이다. (require.ensure는 모듈을 동적으로 불러오는 CommonJS 문법이다. 자세한 사항은 문서 참고)

또다른 규칙은 oneOf라는 값을 갖는 객체다.

{
  oneOf: [
    {
      /* ... */
    },
    {
      /* ... */
    },
    {
      /* ... */
    },
    {
      /* ... */
    },
    // ...
  ];
}

oneOf는 규칙들의 배열인데, 이는 특정 모듈을 생성할 때 조건에 맞는 첫번째 규칙만 적용하도록 지정하는 규칙이다.

다시 정리하자면 모든 모듈은 위의 { parser: { requireEnsure: false } } 규칙이 일괄적으로 적용되고, oneOf 규칙 중에서는 첫 번째로 매칭되는 규칙만 적용된다.

2.2. 규칙의 조건

oneOf 배열에는 9개의 규칙이 포함되어 있다. 해당 규칙들의 조건들만 추려보면 다음과 같다.

{
  test: [/\.avif$/],
  // ...
},
{
  test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
  // ...
},
{
  test: /\.(js|mjs|jsx|ts|tsx)$/,
  include: paths.appSrc,
  // ...
},
{
  test: /\.(js|mjs)$/,
  exclude: /@babel(?:\/|\\{1,2})runtime/,
  // ...
},
{
  test: cssRegex,
  exclude: cssModuleRegex,
  // ...
},
{
  test: cssModuleRegex,
  // ...
},
{
  test: sassRegex,
  exclude: sassModuleRegex,
  // ...
},
{
  test: sassModuleRegex,
  // ...
},
{
  // `test` 값 없음
  exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
}

위 설정에 따르면, CRA 앱의 모듈은 아래와 같은 순서로 조건을 확인하고 조건에 맞는 규칙에 따라 모듈을 생성한다.

  1. .avif 파일인지 확인한다.
    • 조건이 일치하면 첫번째 규칙을 사용해 모듈을 생성한다.
    • 아니면 다음 규칙을 확인한다.
  2. .bmp, .gif, .jpg, .jpeg, .png 파일인지 확인한다.
    • 조건이 일치하면 두번째 규칙을 사용해 모듈을 생성한다.
    • 아니면 다음 규칙을 확인한다.
  3. .js, .mjs, .jsx, .ts, .tsx 파일인지 확인한다. 동시에 paths.appSrc 경로에 있는 파일인지도 확인한다.
    • 조건이 일치하면 세번째 규칙을 사용해 모듈을 생성한다.
    • 아니면 다음 규칙을 확인한다.
  4. .js, .mjs 파일인지 확인한다. @babel/runtime 패키지에 포함되지 않았는지도 확인한다.
    • 조건이 일치하면 네번째 규칙을 사용해 모듈을 생성한다.
    • 아니면 다음 규칙을 확인한다.
  5. .css 파일인지 확인한다. .module.css 파일이 아닌지도 확인한다.
    • 조건이 일치하면 다섯번째 규칙을 사용해 모듈을 생성한다.
    • 아니면 다음 규칙을 확인한다.
  6. .module.css 파일인지 확인한다.
    • 조건이 일치하면 여섯번째 규칙을 사용해 모듈을 생성한다.
    • 아니면 다음 규칙을 확인한다.
  7. .scss, .sass 파일인지 확인한다. .module.scss, module.sass 파일이 아닌지도 확인한다.
    • 조건이 일치하면 일곱번째 규칙을 사용해 모듈을 생성한다.
    • 아니면 다음 규칙을 확인한다.
  8. .module.scss, module.sass 파일인지 확인한다.
    • 조건이 일치하면 여덟번째 규칙을 사용해 모듈을 생성한다.
    • 아니면 다음 규칙을 확인한다.
  9. .js, .mjs, .jsx, .ts, .tsx, .html, .json 파일이 아닌지 확인한다.
    • 조건이 일치하면 마지막 규칙을 사용해 모듈을 생성한다.

2.3. 로더

loader 혹은 use 값은 모듈을 생성할 때 쓰일 로더를 지정할 때 쓰인다.

(로더Loader는 모듈 생성 시 전처리를 담당하는 라이브러리다. 일반적으로 웹팩에 포함되지 않는 라이브러리들이므로, 별도로 설치를 해주어야 한다. 공식 문서 참고.)

CRA로 생성된 앱에서는 이미지 파일을 모듈로 생성할 때는 url-loader, 자바스크립트 관련 파일에는 babel-loader, CSS 관련 파일에는 style-loader, css-loader, postcss-loader 등, 그 이외의 파일들에는 file-loader를 사용한다.

유형에 따라 어떻게 설정되었는지 살펴보자.

2.3.1. 이미지 모듈

2.2. 규칙의 조건에서 살펴보았던 규칙들 중 .avif 파일을 위한 첫번째 규칙, .bmp, .gif, .jpg, .jpeg, .png 파일들을 위한 두번째 규칙이 url-loader를 사용한다. 해당 규칙의 전문은 아래와 같다.

{
  test: [/\.avif$/],
  loader: require.resolve('url-loader'),
  options: {
    limit: imageInlineSizeLimit,
    mimetype: 'image/avif',
    name: 'static/media/[name].[hash:8].[ext]',
  },
},
{
  test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
  loader: require.resolve('url-loader'),
  options: {
    limit: imageInlineSizeLimit,
    name: 'static/media/[name].[hash:8].[ext]',
  },
},

loader 이외에도 options 값이 눈에 들어온다. 이 값은 웹팩이 직접 사용하는 값이 아니라 로더 라이브러리에 전달될 설정값이다.

첫번째 규칙에 의하면 .avif파일은 먼저 용량(단위: byte)이 imageInlineSizeLimit보다 큰지 검사한다. 만약 크다면 url-loader가 아닌 file-loader가 사용된다. 만약 작다면 url-loader에 의해 'image/avif' mimetype 에 맞춰 base64 URI 형식으로 변환된다. 변환된 파일은 번들링 결과가 저장되는 디렉토리에 static/media/[name].[hash:8].avif 형식으로 저장된다.

두번째 규칙도 첫번째 규칙과 거의 동일하다. 대상이 .bmp, .gif, .jpg, .jpeg, .png 파일들이라는 것과, mimetype 강제가 없다는 것만 다르다.

2.3.2. 자바스크립트 모듈

2.2. 규칙의 조건에서 살펴보았던 규칙들 중 .js, .mjs, .jsx, .ts, .tsx 파일들을 위한 세번째 규칙, 네번째 규칙은 babel-loader를 사용한다. 해당 규칙 전문은 아래와 같다.

{
  test: /\.(js|mjs|jsx|ts|tsx)$/,
  include: paths.appSrc,
  loader: require.resolve('babel-loader'),
  options: {
    customize: require.resolve(
      'babel-preset-react-app/webpack-overrides'
    ),
    presets: [
      [
        require.resolve('babel-preset-react-app'),
        {
          runtime: hasJsxRuntime ? 'automatic' : 'classic',
        },
      ],
    ],

    plugins: [
      [
        require.resolve('babel-plugin-named-asset-import'),
        {
          loaderMap: {
            svg: {
              ReactComponent:
                '@svgr/webpack?-svgo,+titleProp,+ref![path]',
            },
          },
        },
      ],
      isEnvDevelopment &&
        shouldUseReactRefresh &&
        require.resolve('react-refresh/babel'),
    ].filter(Boolean),
    cacheDirectory: true,
    cacheCompression: false,
    compact: isEnvProduction,
  },
},
{
  test: /\.(js|mjs)$/,
  exclude: /@babel(?:\/|\\{1,2})runtime/,
  loader: require.resolve('babel-loader'),
  options: {
    babelrc: false,
    configFile: false,
    compact: false,
    presets: [
      [
        require.resolve('babel-preset-react-app/dependencies'),
        { helpers: true },
      ],
    ],
    cacheDirectory: true,
    cacheCompression: false,

    sourceMaps: shouldUseSourceMap,
    inputSourceMap: shouldUseSourceMap,
  },
},

paths.appSrc(PROJECT_ROOT/src 디렉토리) 안에 있는 자바스크립트 파일들에 세번째 규칙이, 밖에 있는 자바스크립트 파일들에 네번째 규칙이 적용된다. options값을 통해 전처리 및 최적화를 다르게 하고 있다는 걸 알 수 있다. 관련된 자세한 내용은 추후 다른 글에서 다시 정리해보겠다.

2.3.3. 스타일시트 모듈

2.2. 규칙의 조건에서 살펴보았던 규칙들 중 .css, .module.css, .scss, .sass, .module.scss, module.sass 파일들을 위한 다섯번째 규칙부터 여덟번째 규칙까지는 스타일시트와 관련된 여러 로더를 사용한다. 해당 규칙 전문은 아래와 같다.

{
  test: cssRegex,
  exclude: cssModuleRegex,
  use: getStyleLoaders({
    importLoaders: 1,
    sourceMap: isEnvProduction
      ? shouldUseSourceMap
      : isEnvDevelopment,
  }),
  sideEffects: true,
},
{
  test: cssModuleRegex,
  use: getStyleLoaders({
    importLoaders: 1,
    sourceMap: isEnvProduction
      ? shouldUseSourceMap
      : isEnvDevelopment,
    modules: {
      getLocalIdent: getCSSModuleLocalIdent,
    },
  }),
},
{
  test: sassRegex,
  exclude: sassModuleRegex,
  use: getStyleLoaders(
    {
      importLoaders: 3,
      sourceMap: isEnvProduction
        ? shouldUseSourceMap
        : isEnvDevelopment,
    },
    'sass-loader'
  ),
  sideEffects: true,
},
{
  test: sassModuleRegex,
  use: getStyleLoaders(
    {
      importLoaders: 3,
      sourceMap: isEnvProduction
        ? shouldUseSourceMap
        : isEnvDevelopment,
      modules: {
        getLocalIdent: getCSSModuleLocalIdent,
      },
    },
    'sass-loader'
  ),
},

모든 규칙이 getStyleLoaders 함수를 쓰고 있는데, 이는 설정 파일 내에 선언된 함수이다. 해당 함수는 각 규칙에 알맞는 로더 설정을 반환한다. 첫번째 인자로 css-loader 에 쓰일 options 값을 받고 두번째 인자는 대상이 SASS 파일일 경우에만 받으며 sass-loader를 써달라는 의미로 쓰인다.

세부 옵션을 다 정리하면 너무 길어지니 간략히 하면 *.css 파일들은 아래 로더들을 순서대로 적용한다.

  • postcss-loader: PostCSS(CSS에 자바스크립트 플러그인을 적용할 수 있게 도와주는 라이브러리)가 적용된 코드를 순수한 CSS로 변환한다.
  • css-loader: CSS 안에 @import, url() 문을 해석(resolve)해 불러온다. 단, 이 로더의 결과는 자동으로 번들링에 반영되지 않는다. style-loader 등 다른 로더와 조합해서 써야 한다.
  • style-loader: CSS를 <style>태그로 감싸서 DOM에 삽입한다.
    • 프로덕션 빌드라면 style-loader 대신 MiniCssExtractPlugin.loader를 사용한다. 이 로더는 CSS를 import한 자바스크립트 파일과 맞춰서 여러 CSS 파일로 나누어 저장한다.

*.sass, *.scss 파일들은 아래 로더들이 순서대로 적용된다.

  • sass-loader: SASS를 CSS로 변환한다.
  • resolve-url-loader: 분산되어 저장되어있던 SASS 파일들의 상대 경로 관련 이슈를 해결해준다.
  • postcss-loader
  • css-loader
  • style-loader
    • 프로덕션 빌드라면 style-loader 대신 MiniCssExtractPlugin.loader를 사용한다.

sideEffect 옵션은 해당 파일이 다른 파일에도 영향을 주는지 알려주는 옵션이다. 만약 true 값이면 해당 규칙에 의해 불려오는 파일은 트리 셰이킹Tree Shaking에 영향받지 않고 무조건 로드된다. 자세한 사항은 트리 셰이킹 문서를 참고. (왜 .css.scss, .sass 규칙에만 sideEffect 옵션이 붙어있는지는 이 이슈를 참고.)

2.3.4. 기타 파일

2.2. 규칙의 조건에서 살펴보았던 규칙들 중 아홉번째 규칙은 위 여덟 개의 규칙의 조건에 해당하지 않은 기타 파일들을 위한 규칙이며 file-loader를 사용한다.

{
  loader: require.resolve('file-loader'),
  exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
  options: {
    name: 'static/media/[name].[hash:8].[ext]',
  },
},

file-loader는 특별한 처리 없이 해당 파일을 outputPath 로 복사한다.

또한 자바스크립트 관련 파일은(.js, .jsx, .mjs, .ts, .tsx) exclude 조건에 포함되었는데, 이는 일반적인 자바스크립트 관련 파일들은 세번째 네번째 규칙에서 모두 처리되었을 것이며 처리되지 않은 파일들은 CSS 관련 로더가 런타임에 동적으로 만든 파일일 가능성이 높기 때문이다.

더불이 .html.json도 제외되었는데 이 두 유형은 웹팩의 내장 로더가 처리하므로 별도의 설정이 필요 없기 때문이다.

3. 결론

CRA로 생성한 앱의 웹팩 module 설정은 이미지 파일, 자바스크립트 관련 파일, 스타일시트 파일, 그리고 기타 파일을 위한 로더 설정이 되어 있음을 간단히 살펴보았다.

어디까지나 훑어본 것이기 때문에 자바스크립트 관련 파일들에 관한 설정이나 스타일시트 파일들에 관한 설정을 자세히 살펴보지는 못했다. 이 설정들은 추후 다른 글을 통해 조금 더 자세히 살펴보도록 하겠다.