개요
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 기준 직접 지정
런타임이 무엇을 실제로 해석하는지부터 결정한 뒤 별칭 전략을 선택하는 것이 유지보수 비용을 줄이는 가장 확실한 방법임