CRA webpack 분석 (2) - module 설정
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
설정에 strictExportPresence
와 rules
, 이렇게 두 가지 값이 설정되어 있다.
{
// ...
module: {
strictExportPresence: true,
rules: [
// ...
]
},
}
module.strictExportPresence
는 true
로 설정 시 모듈 내에 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 앱의 모듈은 아래와 같은 순서로 조건을 확인하고 조건에 맞는 규칙에 따라 모듈을 생성한다.
.avif
파일인지 확인한다.- 조건이 일치하면 첫번째 규칙을 사용해 모듈을 생성한다.
- 아니면 다음 규칙을 확인한다.
.bmp
,.gif
,.jpg
,.jpeg
,.png
파일인지 확인한다.- 조건이 일치하면 두번째 규칙을 사용해 모듈을 생성한다.
- 아니면 다음 규칙을 확인한다.
.js
,.mjs
,.jsx
,.ts
,.tsx
파일인지 확인한다. 동시에paths.appSrc
경로에 있는 파일인지도 확인한다.- 조건이 일치하면 세번째 규칙을 사용해 모듈을 생성한다.
- 아니면 다음 규칙을 확인한다.
.js
,.mjs
파일인지 확인한다. @babel/runtime 패키지에 포함되지 않았는지도 확인한다.- 조건이 일치하면 네번째 규칙을 사용해 모듈을 생성한다.
- 아니면 다음 규칙을 확인한다.
.css
파일인지 확인한다..module.css
파일이 아닌지도 확인한다.- 조건이 일치하면 다섯번째 규칙을 사용해 모듈을 생성한다.
- 아니면 다음 규칙을 확인한다.
.module.css
파일인지 확인한다.- 조건이 일치하면 여섯번째 규칙을 사용해 모듈을 생성한다.
- 아니면 다음 규칙을 확인한다.
.scss
,.sass
파일인지 확인한다..module.scss
,module.sass
파일이 아닌지도 확인한다.- 조건이 일치하면 일곱번째 규칙을 사용해 모듈을 생성한다.
- 아니면 다음 규칙을 확인한다.
.module.scss
,module.sass
파일인지 확인한다.- 조건이 일치하면 여덟번째 규칙을 사용해 모듈을 생성한다.
- 아니면 다음 규칙을 확인한다.
.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
설정은 이미지 파일, 자바스크립트 관련 파일, 스타일시트 파일, 그리고 기타 파일을 위한 로더 설정이 되어 있음을 간단히 살펴보았다.
어디까지나 훑어본 것이기 때문에 자바스크립트 관련 파일들에 관한 설정이나 스타일시트 파일들에 관한 설정을 자세히 살펴보지는 못했다. 이 설정들은 추후 다른 글을 통해 조금 더 자세히 살펴보도록 하겠다.