CRA webpack 분석 (1) - resolve.alias 설정
1. 서문
create-react-app(이하 CRA) 으로 React 웹앱을 만들었다. typescript도 쓰고 싶어서 만들 때 --template typescript
옵션도 주었다. webpack 설정을 직접 수정하고 싶어서 yarn eject
커맨드도 실행했다.
(위 문단이 이해되지 않는 사람이라면 이 글이 다소 이해하기 어려울 수도 있다. 그럴 경우 create-react-app 공식 문서를 참고하자.)
webpack 직접 설정하기 첫걸음으로, resolve.alias
를 설정해보자.
2. resolve.alias
설정하기
resolve.alias
옵션은 공식 문서 에서 아래와 같이 설명하고 있다.
Create aliases to import or require certain modules more easily. 특정 모듈을 더욱 쉽게 import 혹은 require 하기 위해 aliases 를 생성한다.
말하자면 상대 경로로 불편하게 import
해야 했던 것을 쉽게 import
할 수 있게 해주는 옵션이다.
/* webpack.config.js */
// ...
module.exports = {
//...
resolve: {
alias: {
Utilities: path.resolve(__dirname, "src/utilities/"),
},
},
};
/* someCode.js */
// // 위 resolve.alias 설정이 없었다면 이렇게 import 해야 하지만
// import Utility from '../../utilities/utility';
// 설정 덕분에 상대 경로 없이 편하게 import 할 수 있다.
import Utility from "utilities/utility";
CRA로 만든 앱에서도 이 설정을 이용하고 싶다. 어떻게 하면 효율적으로 적용할 수 있을까? 일단 기존 코드를 분석해보자.
2.1. 기존 resolve.alias
분석
CRA로 만든 앱의 기존 resolve.alias
는 아래와 같다.
// ...
resolve: {
// ...
alias: {
// 2.1.1. 'react-native'
// Support React Native Web
'react-native': 'react-native-web',
// 2.1.2. profiling
// Allows for better profiling with ReactDevTools
...(isEnvProductionProfile && {
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling',
}),
// 2.1.3. webpackAliases
...(modules.webpackAliases || {}),
}
// ...
}
2.1.1. 'react-native'
// Support React Native Web
'react-native': 'react-native-web',
이 설정은 react-native-web을 지원하기 위한 설정이다. 지금 우리의 관심사는 아니니 넘어가자.
2.1.2. profiling
// Allows for better profiling with ReactDevTools
...(isEnvProductionProfile && {
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling',
}),
여기서 isEnvProductionProfile
값은 아래와 같다.
const isEnvProductionProfile =
isEnvProduction && process.argv.includes("--profile");
프로덕션 환경이면서, webpack 실행 당시 --profile
옵션을 받았는지 여부를 확인하는 플래그이다.
즉 이 웹앱의 성능 분석을 하고자 할 때 사용하는 옵션이다. 자세한 사항은 React 성능 분석 관련 공식 문서(Introducing the React Profiler)를 참고하자.
2.1.3. webpackAliases
...(modules.webpackAliases || {}),
여기서 modules
는 아래와 같다.
const modules = require("./modules");
그럼 ./modules.js
파일을 찾아가 webpackAliases
값은 어떻게 지정되어 있는지 살펴보자.
return {
// ...
webpackAliases: getWebpackAliases(options),
// ...
};
options
와 getWebpackAliases
는 뭔지 살펴보자.
2.1.3.1. options
options
값은 아래와 같다.
let config;
if (hasTsConfig) {
const ts = require(resolve.sync("typescript", {
basedir: paths.appNodeModules,
}));
config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
} else if (hasJsConfig) {
/// ...
}
config = config || {};
const options = config.compilerOptions || {};
tsconfig.json
의 compilerOptions
값을 읽어온 것이 options
다.
2.1.3.2. getWebpackAliases
getWebpackAliases
내용도 보자.
function getWebpackAliases(options = {}) {
const baseUrl = options.baseUrl;
if (!baseUrl) {
return {};
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
if (path.relative(paths.appPath, baseUrlResolved) === "") {
return {
src: paths.appSrc,
};
}
}
인자로 받은 options
에 baseUrl
값이 있고, 해당 값이 path.appPath
와 같으면, { src: paths.appSrc }
를 반환하는 함수다.
여기서 baseUrl
은 타입스크립트 설정에서 쓰이는 값이다. 이 값은 webpack 설정의 resolve.alias
와 거의 같은 일을 한다. import
할 때 상대 경로를 쓰지 않고 baseUrl
부터 절대 경로로 쓸 수 있게끔 설정해준다.
/* tsconfig.json */
{
"compilerOptions": {
// ...
"baseUrl": "./"
}
}
// // 위 설정이 없었따면 아래처럼 import 해야 한다.
// import { Button } from '../../../components';
// 위 설정 덕에 아래처럼 import 가능하다.
import { Button } from 'src/components';
문제는 webpack 도 같이 설정을 해주어야 이 기능을 제대로 쓸 수 있다는 것이다. 그래서 위에 나왔던 getWebpackAliases
의 마지막 즈음 문장을 해석하자면
if (path.relative(paths.appPath, baseUrlResolved) === "") {
return {
src: paths.appSrc,
};
}
path.appPath
(앱의 루트 디렉토리의 절대 경로)와 baseUrlResolved
(baseUrl
의 절대 경로)가 같다면 webpack.alias.src
로 쓰일 값을 paths.appSrc
(앱의 ./src
의 절대 경로)로 해줘라, 라는 뜻이다.
한 마디로 ...(modules.webpackAliases || {}),
이 구문은, 타입스크립트의 baseUrl
설정값 사용을 webpack 에 적용하기 위한 설정이라고 이해하면 된다.
2.1.4 정리
정리하자면, --profile
옵션을 주지 않고 빌드 혹은 devServer 실행을 했다고 하면 resolve.alias
설정은 아래와 같이 된다.
resolve: {
alias: {
'react-native': 'react-native-web',
}
}
만약 여기에 tsconfig.json
에서 baseUrl
값을 설정해 주었다면 이렇게 설정될 것이다.
resolve: {
alias: {
'react-native': 'react-native-web',
src: paths.appSrc, // `./src` 디렉토리의 절대 경로
}
}
2.2. resolve.alias
설정 추가하기
그럼 이제 현재 설정을 해치지 않으면서 자연스럽게 resolve.alias
설정을 추가해보자.
이 글에서는 ./src/components
디렉토리와 ./src/themes
디렉토리를 alias 로 등록할 것이다.
타입스크립트 사용 시에는 tsconfig.json
도 같이 수정해주어야 적용이 문제 없이 되기 때문에, 위 2.1.3. 항목에서 보았던 것처럼, tsconfig.json
을 작성하면 자동으로 webpack 설정에도 적용되게끔 진행할 것이다.
2.2.1. tsconfig.json
수정
compilerOptions
에 baseUrl
과 paths
를 추가해주자.
{
"compilerOptions": {
// ...
"baseUrl": "./",
"paths": {
"components": ["src/components"],
"themes": ["src/themes"]
}
},
// ...
}
baseUrl
은 위에서 한 번 설명했고, paths
는 webpack 의 resolve.alias
와 같은 기능이라고 생각하면 된다. (세부 사항이 좀 다르긴 한데, 자세한 내용은 이 문서TypeScript - Module Resolution를 참고하자)
2.2.2. config/aliases.js
작성
기존의 설정 코드는 최대한 수정하지 않는 방향으로 진행하겠다. config/aliases.js
을 추가해서 설정을 작성하자. 기존 코드를 참고해서 작성할 것이기 때문에 코드 중복이 생기겠지만, 기존 설정 코드를 수정할 생각은 없기 때문에 코멘트만 남기는 정도로 넘어가겠다.
우선 config/modules.js
를 참고해서 tsconfig.json
설정 내용을 가져오는 함수를 작성한다.
// NOTE: duplicated with `getModules` in ./modules.js
function getCompilerOptions() {
const hasTsConfig = fs.existsSync(paths.appTsConfig);
if (!hasTsConfig) {
throw new Error("You don't have a tsconfig.json.");
}
const ts = require(resolve.sync("typescript", {
basedir: paths.appNodeModules,
}));
const config =
ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config || {};
return config.compilerOptions || {};
}
(타입스크립트를 쓸 것이기 때문에 자바스크립트 관련 코드는 삭제했다.)
가져온 tsconfig.json
의 설정을 가지고 alias 내용을 생성하는 코드도 작성한다.
function getAliases() {
const { baseUrl, paths: tsPaths } = getCompilerOptions();
// baseUrl 값이 없으면 tsconfig.json 의 paths 가 제대로 적용되지 않는다.
// 따라서 해당 값으로 aliases 를 설정할 필요도 없다.
if (!baseUrl) {
return {};
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
// 앱의 루트 디렉토리와 baseUrl 이 동일한 디렉토리가 아니라면
// alias를 설정하지 않는다.
// 이는 `config/modules.js` 에서도 사용하는 예외처리인데,
// convention over configuration 을 따르면서
// 복잡한 예외 처리를 하지 않으려는 의도 같다.
if (path.relative(paths.appPath, baseUrlResolved) !== "") {
return {};
}
// `"components": ["src/components"],` 형태를
// `"components": path.resolve(__dirname, `../src/components`)`
// 형태로 변환
return Object.keys(tsPaths).reduce((cfg, key) => {
cfg[key] = path.resolve(__dirname, `../${tsPaths[key][0]}`);
return cfg;
}, {});
}
이제 config/modules.js
와 같은 방식으로 exports 해주며 마무리하면 된다.
module.exports = getAliases();
2.2.3. webpack.config.js
수정
이제 작성한 config/aliases.js
파일을 webpack.config.js
안에서 사용해보자.
// 최상단의 온갖 코드를 require 하는 부분 마지막 부분에
// 적당히 끼워넣자.
const tsAliases = require('./aliases');
// ...
// resolve.alias 부분에 설정을 추가하자
alias: {
'react-native': 'react-native-web',
...(isEnvProductionProfile && {
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling',
}),
...(modules.webpackAliases || {}),
...tsAliases, // <- 이 코드를 추가하자
},
3. 결과
이제 tsconfig.json
파일의 compilerOptions.paths
값을 가지고 webpack 의 resolve.alias
옵션을 자동 설정할 수 있게 되었다. 실제 프로젝트에 적용된 코드가 궁금하다면 아래 소스 코드들을 참고하자.