Logo

타입스크립트의 경로 맵핑 (baseUrl, paths)

자바스크립트 프로젝트에서 아래와 같은 형태로 모듈을 불러오는 코드를 마주친 적이 있으신가요? 머리 속에서 상위 디렉토리를 하나씩 차례대로 올라갔다가 다시 내려오다보면 혈압이 오르는 경험할 수 있는데요. 🤬

import { whereAmI } from "../../../../../../where/the/hell/are/you";

이번 포스팅에서는 상대 경로나 절대 경로를 통해서 내부 모듈을 불러올 때 겪을 수 있는 문제점에 대해서 알아보겠습니다. 그리고 경로 별칭을 통해서 좀 더 깔끔하게 내부 모듈을 불러올 수 있도록 타입스크립트를 설정하는 방법에 대해서 배우겠습니다.

상대 경로

타입스크립트 프로젝트에서 내부 모듈을 불러올 때 가장 쉽게 볼 수 있는 방식은 import 문에서 from 키워드 다음에 상대 경로를 적는 것입니다.

test/common/utilities/math/add.test.ts
import { add } from "../../../../src/common/utilities/math";

보시다시피 상대 경로를 사용하면 해당 파일이 프로젝트 내에서 정확히 어디에 있는지 파악한는 게 상당히 고통스러울 수 있습니다. 뿐만 아니라 코드 작성자 입장에서도 내부 모듈의 파일 위치를 상대 경로로 적는 것이 코드 편집기의 도움이 없으면 쉽지 않죠.

더 큰 문제는 내부 모듈을 불러오는 파일을 다른 디렉토리로 옮길 때 모든 import 문을 새로운 파일 위치에 맞게 수정해줘야 한다는 것입니다. 상대 경로는 말 그대로 현재 모듈을 불러오는 파일의 위치에 따라서 상대적으로 결정되기 때문입니다.

예를 들어, 파일을 부모 디렉토리로 옮긴다면 다음과 같이 수정을 해줘야하고,

import { add } from "../../../src/common/utilities/math";

파일을 자식 디렉토리로 옮긴다면 다음과 같이 수정을 해줘야할 것입니다.

import { add } from "../../../../../src/common/utilities/math";

이 마저도 코드 편집기의 도움을 받을 수 있지만 분명히 코드 리팩토링(refactoring)이 까다로워지는 요인이 됩니다. 이 때문에 개발자들이 파일을 적절한 위치로 옮기는 것을 기피하게 되는 부작용이 생길 수도 있습니다.

절대 경로

그렇다면 개발자의 경험을 해칠 수 있는 상대 경로 대신에 절대 경로를 통해서 내부 모듈을 불러오면 어떨까요?

import { add } from "/Users/John/Projects/src/common/utilities/math";

이 것이 이론적으로 불가능한 것은 아니지만, 혼자 개발하는 프로젝트가 아닌 이상 현실적으로 적용하기 어려운 전략입니다. 왜냐하면 개발자들은 본인의 컴퓨터에 해당 프로젝트를 재각기 다른 경로에 위치시킬 것이기 때문입니다.

예를 들어, macOS을 사용하는 개발자 John는 /Users/John/Projects/our-project에 프로젝트를 두고, Windows을 사용하는 개발자 Jane은 C:\Users\Jane\Documents\our-project에 프로젝트를 두었다면, 두 개발자는 동일한 파일을 불러오기 위해서 서로 다른 절대 경로를 사용하게 될 것입니다.

경로 별칭

경로 별칭(path alias)은 상대 또는 절대 경로를 대체할 수 있는 단축된 경로를 의미하는데요. 경로 별칭을 활용하면 마치 npm 내려받은 외부 모듈을 불러오듯이 기억하기 쉬운 이름을 통해 내부 모듈을 불러올 수 있습니다.

예를 들어, Zod라는 외부 모듈을 불러올 때, 아래와 같이 절대 경로나 상대 경로를 사용하시는 분은 없죠?

import { z } from "/Users/John/Projects/our-project/node_modules/zod";
import { z } from "../../../node_modules/zod";

외부 모듈을 불러올 때는 패키지 이름만을 사용하는 것이 일반적입니다.

import { z } from "zod";

node_modules 폴더 아래의 패키지의 실제 경로 대신에 패키지 이름으로 외부 모듈을 불러올 수 있는 것처럼, 경로 별칭을 사용하면 내부 모듈에 미리 부여해 놓은 간결한 이름으로 불러올 수 있습니다.

즉, 아래와 같이 실제 파일의 절대 경로나 상대 경로로 내부 모듈을 불러오는 대신에,

import { add } from "/Users/John/Projects/src/common/utilities/math";
import { add } from "../../../../common/utilities/math";

다음과 같이 간단한 문자열을 통해서 내부 모듈을 불러올 수 있습니다.

import { add } from "utilities/math";
import { add } from "utils/math";
import { add } from "math-utils";

심지어 이모지(emoji)를 통해서도 내부 모듈을 불러올 수 있게되죠.

import { add } from "🤓";

현업 프로젝트에서는 별칭 경로를 한 눈에 알아볼 수 있도록 ~, @, #, $와 같은 특수 기호를 앞에 붙이는 경우가 많습니다.

import { add } from "~utils/math";
import { add } from "@utils/math";
import { add } from "#utils/math";
import { add } from "$utils/math";

paths 옵션

타입스크립트 프로젝트에서 경로 별칭은 tsconfig.json 파일의 paths 옵션을 통해서 등록할 수 있습니다. 경로 별칭은 키(key)로 모듈 경로를 값(value)으로 하는 객체를 paths 옵션에 설정해주면 되는데요.

예를 들어, 아래와 같이 두 개의 경로 별칭을 등록해두면,

tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "#utils/*": ["./src/common/utilities/*"],
      "#math-utils/*": ["./src/common/utilities/math/*"],
    }
  }
}

경로 별칭을 통해서 내부 모듈을 불러올 수 있게 됩니다.

import { shuffle } from "#utils/random";
// import { shuffle } from "<프로젝트 최상위 경로>/src/common/utilities/random";

import { add } from "#math-utils";
// import { add } from "<프로젝트 최상위 경로>/src/common/utilities/math";

baseUrl 옵션

규모가 그리 크지 않은 프로젝트에서는 경로 별칭까지는 필요 없을 수 있습니다. 단지 내부 모듈을 불러올 때 상대 경로나 절대 경로만을 피하고 싶다면 tsconfig.json 파일의 baseUrl 옵션을 사용할 수 있습니다.

예를 들어서, 소스 코드를 모두 src 폴더 아래에 두는 프로젝트에서는 baseUrl 옵션을 ./src로 지정해줄 수 있습니다.

tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./src"
  }
}

그러면 맨 앞에 있는 ./src를 생략한 체로 내부 모듈의 파일 위치를 적어줄 수 있습니다.

import { shuffle } from "common/utilities/random";
// import { shuffle } from "<프로젝트 최상위 경로>/src/common/utilities/random";

import { add } from "common/utilities/math-utils";
// import { add } from "<프로젝트 최상위 경로>/src/common/utilities/math";

baseUrl 옵션 + paths 옵션

baseUrl 옵션에 paths 옵션을 함께 설정해주면 좀 더 유연하고 섬세한 경로 맵핑이 가능해집니다.

예를 들어, baseUrl 옵션으로 이미 ./src을 지정해준 경우, paths 옵션에서 ./src를 생략할 수 있습니다.

tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "#utils/*": ["common/utilities/*"],
      "#math-utils/*": ["common/utilities/math/*"]
    }
  }
}

그러면 경로 별칭을 사용할 때 내부적으로 두 단계로 경로 맵핑이 일어납니다. 첫 번째는 paths에 따라 맵핑이 일어나고, 두 번째는 baseUrl에 따라 맵핑이 일어나죠.

import { shuffle } from "#utils/random";
// import { shuffle } from "common/utilities/random";
// import { shuffle } from "<프로젝트 최상위 경로>/src/common/utilities/random";

import { add } from "#math-utils";
// import { add } from "common/utilities/math/*";
// import { add } from "<프로젝트 최상위 경로>/src/common/utilities/math";

경로 별칭 설정 팁

새로운 디렉토리가 생길 때 마다 경로 별칭을 등록해주는 것이 번거롭거나 까먹기 쉬운 작업이 될 수 있는데요. 이럴 때는 다음과 같이 내부 모듈을 대표할 수 있는 특수 기호를 딱 하나만 등록해놓는 방법도 있습니다.

tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "#/*": ["*"]
    }
  }
}

위와 같이 설정을 해주면 #/로 시작하는 문자열을 통해서 프로젝트 내의 모든 내부 모듈을 불러올 수 있게 됩니다.

import { add } from "#/common/utilities/math";
// import { add } from "common/utilities/math/*";
// import { add } from "<프로젝트 최상위 경로>/src/common/utilities/math";

마치면서

지금까지 내부 모듈을 불러올 때 상대 경로나 절대 경로 대신에 경로 별칭을 사용할 수 있도록 타입스크립트를 설정하는 방법에 대해서 알아보았습니다. 리팩토링이 용이하고 이해하기 쉬운 코드를 짜는데 본 포스팅에서 다룬 요령들이 도움이 되었으면 좋겠습니다.