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),
  // ...
};

optionsgetWebpackAliases는 뭔지 살펴보자.

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.jsoncompilerOptions 값을 읽어온 것이 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,
    };
  }
}

인자로 받은 optionsbaseUrl값이 있고, 해당 값이 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 수정

compilerOptionsbaseUrlpaths를 추가해주자.

{
  "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 옵션을 자동 설정할 수 있게 되었다. 실제 프로젝트에 적용된 코드가 궁금하다면 아래 소스 코드들을 참고하자.