개요

TypeScript와 ESM에서 자주 보이는 세 가지 import 패턴

  • import … from ‘@…’
  • import … from ‘…’
  • import … from ‘#…’

표기만 비슷할 뿐, 해석 주체와 동작 범위가 다름

  • ‘@‘는 경로 별칭 또는 npm 스코프 패키지 의미 가능
  • ‘…‘는 상대·절대 경로로 파일 시스템 기준 해석
  • ‘#‘는 Node.js 패키지 imports 또는 브라우저 import maps에서의 별칭으로 사용

아래에서 각 패턴의 의미, 설정 지점, 주의사항을 정리함

‘@…’ 경로의 두 가지 의미

1) 경로 별칭 path alias

  • 의도: 길고 복잡한 상대 경로를 짧게 추상화
  • 설정 지점: tsconfig.json 의 compilerOptions.paths와 baseUrl
  • 예시
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@models/*": ["src/models/*"],
      "@utils/*": ["src/utils/*"]
    }
  }
}
import { User } from '@models/User'
import { calculate } from '@utils/math'
  • 장점
    • 상대 경로를 단순화, 가독성 및 리팩터링 내성 향상
  • 주의
    • tsconfig paths는 타입 체크러와 에디터가 이해하는 별칭일 뿐, 런타임 해석자는 아님
    • Node.js 런타임은 tsconfig paths를 모름. 번들러 설정별 alias 또는 전용 로더를 함께 구성 필요
    • 예: Vite, Webpack, ts-node, tsconfig-paths 등 도구별 설정 일치 필요

2) 스코프된 패키지 scoped package

  • 의미: npm 조직·팀·프로젝트 네임스페이스로 묶인 패키지 집합
  • 표기: @scope/package-name 형태
    • @nestjs/swagger, @angular/core 등
  • 사용 이유
    • 네임스페이스로 이름 충돌 회피
    • 관련 패키지의 그룹화와 공개·비공개 관리
  • 설치
npm install @nestjs/swagger
  • 해석
    • 이 경우 ‘@‘는 경로 별칭이 아닌 패키지 이름의 일부로 동작
    • tsconfig paths와 무관. Node/npm가 패키지 이름으로 직접 해석

‘…’ 상대·절대 경로 import

  • 상대 경로 ‘./’, ‘../’ 기준으로 현재 파일 위치에서 탐색
  • 절대 경로 ‘/path’는 실행 환경마다 기준이 다름
    • 브라우저 ESM에서는 오리진 기준 절대 URL 경로 해석
    • Node.js에서는 파일 시스템 루트 절대 경로로 해석되어 이식성 낮음
  • 예시
import { User } from './models/User'
import { calculate } from '../utils/math'
  • 장점
    • 추가 설정 없이 즉시 동작, 모든 환경 공통 동작 모델
    • 모듈 간 물리적 의존 관계가 드러남
  • 단점
    • 디렉터리 깊어질수록 ../../../ 형태로 복잡도 상승
    • 구조 변경 시 경로 대량 수정 발생

‘#…’ 경로의 의미

‘#{name}’ 표기는 두 가지 서로 다른 맥락에서 등장함. 혼동 주의

1) Node.js 패키지 imports

  • 개념: package.json 의 imports 필드로 패키지 내부 전용 별칭 제공
  • 특징
    • 키는 반드시 ‘#‘로 시작
    • 동일 패키지 내부 전용. 외부 소비자가 임의로 참조 불가
    • ESM 해석기에 의해 런타임에서 동작. 번들러 없이도 Node가 직접 이해
  • 설정 예시
{
  "name": "my-package",
  "type": "module",
  "imports": {
    "#utils": "./src/utils/index.js",
    "#models/*": "./src/models/*.js"
  }
}
  • 사용 예시
import { calc } from '#utils'
import { User } from '#models/User'
  • 장점
    • 런타임이 이해하는 공식 별칭. tsconfig paths와 달리 Node 해석 일관성 확보
    • 외부 공개 API와 내부 경로를 분리하는 캡슐화에 유리
  • 주의
    • 패키지 외부에서는 ‘#…’ 경로 사용 불가. 내부 전용 설계임
    • TypeScript는 nodenext 또는 node16 모듈 해석 설정에서 해당 매핑을 인지 가능. 도구별 지원 상태 확인 필요

2) 브라우저 import maps에서의 별칭 키

  • 개념: 브라우저가 script type=importmap 또는 별도 importmap.json을 통해 모듈 스펙ifier를 URL로 매핑
  • ‘#…‘는 필수 형식 아님. 임의의 bare specifier 사용 가능. 다만 충돌 방지를 위해 ‘#’ 접두를 쓰는 패턴이 존재
  • 예시
{
  "imports": {
    "#utils": "/src/utils/",
    "#models": "/src/models/"
  }
}
import { User } from '#models/User'
  • 주의
    • Node.js는 HTML 기반 import maps를 지원하지 않음. Node에서 import maps를 쓰고자 한다면 번들러 또는 서버 사이드 변환 계층 필요
    • 브라우저 타깃이 아닌 순수 Node 런타임에는 package.json imports를 고려하는 편이 적합

차이점 요약

  • ‘@…’ 경로

    • tsconfig paths 기반 별칭 또는 npm 스코프 패키지 이름 두 의미 존재
    • tsconfig paths는 타입 체커 전용 별칭. 런타임 동작을 위해 번들러·로더 설정 동기화 필요
    • 스코프 패키지는 패키지 네임스페이스 표기. 설정 없이 npm 해석
  • ‘…’ 경로

    • 상대·절대 경로를 직접 명시하는 기본 형태
    • 설정 불필요. 환경 공통 동작
    • 경로가 깊어지면 가독성 저하
  • ‘#…’ 경로

    • Node.js package.json 의 imports로 정의하는 내부 전용 별칭 또는 브라우저 import maps에서의 별칭 키로 사용
    • Node 런타임 관점에서는 package imports가 공식적이고 안정적이며, 외부 노출 불가라는 제약이 있음
    • 브라우저 import maps는 브라우저 전용 메커니즘. Node에서 직접 사용 불가

어떤 방식을 언제 쓸까

  • 작은 프로젝트 또는 간단한 모듈 트리

    • 상대 경로 유지가 가장 직관적
  • 대규모 코드베이스, 깊은 디렉터리 구조, 다수의 교차 참조

    • 경로 별칭 도입으로 가독성 확보
    • Node 런타임 우선이면 package.json imports의 ‘#…’ 채택 고려
    • 브라우저 우선이면 import maps 또는 번들러 alias 사용
  • 라이브러리 패키지 내부 캡슐화 강화 필요

    • ‘#…’ package imports로 내부 경로 보호 및 API 경계 명확화
  • 프레임워크·조직 단위 패키지 묶음 배포

    • ‘@scope/name’ 스코프 패키지로 네임스페이스 관리

실무 주의사항과 베스트 프랙티스

  • tsconfig paths를 쓰면 반드시 런타임 해석자와 동기화

    • 번들러 alias, 테스트 러너, ts-node, ESLint import resolver를 함께 맞춤
  • Node 런타임 기준 별칭 1순위는 package.json imports 고려

    • 런타임이 직접 이해하므로 환경 의존성 감소
    • 외부에 노출하지 않을 내부 경로에만 적용하는 것이 원칙
  • 브라우저 타깃에서 import maps 사용 시 폴리필·번들링 전략과 함께 설계

    • 구형 브라우저 호환성, 캐시 전략, 배포 경로 관리 동반 검토
  • 절대 경로 ‘/…’ 사용 자제

    • 브라우저와 Node에서 의미가 달라 이식성 낮음. 환경 단절 이슈 유발 가능
  • 혼용을 최소화하고 팀 컨벤션 수립

    • 동일 목적의 별칭 체계는 한 가지 메커니즘으로 통일
    • 예) 서버는 ‘#…’ imports, 클라이언트는 import maps, 공용 TypeScript는 상대 경로 또는 제한적 paths

간단 예시 스니펫

  • tsconfig paths로 ‘@utils/*’ 매핑
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": { "@utils/*": ["src/utils/*"] }
  }
}
import { calc } from '@utils/math'
  • Node package imports로 ‘#utils’ 매핑
{
  "type": "module",
  "imports": { "#utils": "./src/utils/index.js" }
}
import { calc } from '#utils'
  • 브라우저 import maps에서 ‘#models’ 매핑
{
  "imports": { "#models": "/src/models/" }
}
import { User } from '#models/User'

마무리

‘@’, ‘#’, 상대 경로는 표면적으로 비슷해 보여도 책임 주체와 동작 레이어가 다름

  • ‘@‘는 경로 별칭 또는 스코프 패키지 이름
  • ‘#‘는 Node의 package imports 또는 브라우저 import maps 별칭 키
  • 상대·절대 경로는 파일 시스템·URL 기준 직접 지정

런타임이 무엇을 실제로 해석하는지부터 결정한 뒤 별칭 전략을 선택하는 것이 유지보수 비용을 줄이는 가장 확실한 방법임

참고자료