TypeScript 프로젝트에서 테스트를 붙이기 시작하면, 처음 부딪히는 건 테스트 코드 자체가 아니라 실행 환경입니다. Jest가 TypeScript를 어떻게 처리할지, 모듈 시스템을 CommonJS로 둘지 ESM으로 둘지, 경로 별칭을 어떻게 맞출지 같은 문제들이 한 번에 튀어나옵니다.
간단한 예제에서는 대충 넘어가도 됩니다. 하지만 테스트가 CI에 들어가고, 모노레포에서 패키지가 늘어나면 설정이 애매한 부분부터 계속 흔들립니다.
여기서는 Jest + TypeScript 테스트 환경을 “돌아가게 만드는 것”보다 지속적으로 유지 가능한 구성으로 만드는 데 초점을 두었습니다.
선택지: ts-jest vs swc/esbuild 변환
Jest에서 TypeScript를 실행하는 방식은 크게 두 갈래입니다.
- ts-jest: TypeScript 컴파일러 기반으로 변환
- @swc/jest(또는 esbuild 기반): 빠른 트랜스파일
ts-jest는 타입과 tsconfig를 더 잘 따라옵니다. 대신 속도는 상대적으로 느립니다. @swc/jest는 빠르지만, 타입 체크는 별도로 tsc에 맡기는 구조로 가는 경우가 많습니다. 프로젝트 규모와 CI 시간 제약에 따라 선택이 달라질 수 있습니다.
설치: 가장 흔한 조합
CommonJS 기반 Node 백엔드에서 가장 많이 쓰는 조합을 기준으로 잡습니다.
npm i -D jest ts-jest @types/jest
추가로 path alias를 쓰거나, 환경 변수 로딩이 필요하면 패키지가 더 붙습니다. 이 글에서는 기본부터 잡고, 뒤에서 확장 포인트를 정리합니다.
Jest 설정 생성
ts-jest를 기준으로 설정을 생성합니다.
npx ts-jest config:init
생성된 jest.config.js(또는 ts)를 보면서 TypeScript 프로젝트에 맞게 최소 수정만 하는 편이 유지가 쉽습니다.
jest.config 예시
실무에서 자주 쓰는 형태는 다음과 같습니다.
// jest.config.cjs
module.exports = {
testEnvironment: 'node',
preset: 'ts-jest',
testMatch: ['<rootDir>/test/**/*.spec.ts', '<rootDir>/src/**/*.spec.ts'],
clearMocks: true,
resetMocks: true,
restoreMocks: true,
};
testMatch는 팀 구조에 맞게 하나로 통일하는 편이 좋습니다. src 옆에 테스트를 둘지, test 폴더로 뺄지는 팀 스타일에 따라 달라질 수 있습니다. 다만 섞어 쓰기 시작하면, 테스트 파일이 어디 있는지 찾는 비용이 계속 생깁니다.
tsconfig와 테스트의 관계
테스트는 보통 prod 코드보다 느슨한 설정이 필요할 때가 있습니다. 예를 들어 테스트에서만 쓰는 path alias, mock 파일, test 유틸 타입 등이 들어갑니다. 이때는 별도의 tsconfig.test.json을 두는 편이 깔끔합니다.
// tsconfig.test.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"]
},
"include": ["src", "test"]
}
ts-jest는 tsconfig를 따라가므로, 테스트 환경에서 필요한 types(jest)를 여기에 넣어두면 테스트 코드에서 describe, it 같은 전역이 자연스럽게 인식됩니다.
경로 별칭(path alias)을 쓰는 경우
tsconfig의 paths를 쓰는 프로젝트에서는 테스트에서 모듈 해석이 깨지는 경우가 흔합니다. Jest는 TypeScript의 paths를 자동으로 알지 못하기 때문입니다.
이 경우 moduleNameMapper를 맞춰줍니다.
// jest.config.cjs
module.exports = {
testEnvironment: 'node',
preset: 'ts-jest',
moduleNameMapper: {
'^@src/(.*)$': '<rootDir>/src/$1',
},
};
여기서 자주 생기는 문제는 tsconfig paths는 바뀌었는데 moduleNameMapper는 그대로인 상태입니다. 타입 체크는 통과하지만 테스트만 실패하는 패턴이 나옵니다. 이런 경우는 alias 정의를 한 곳에서 생성하도록 맞추거나, alias를 최소화하는 쪽으로 정리하는 경우도 있습니다.
실무에서 바로 쓰는 스크립트 구성
테스트 실행은 보통 다음 정도로 나눕니다.
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:ci": "jest --runInBand",
"typecheck": "tsc --noEmit"
}
}
CI에서는 --runInBand를 쓰는 경우가 많습니다. 병렬 실행이 빠를 것 같지만, DB/Redis 테스트나 포트 사용 테스트처럼 공유 리소스가 섞이면 간헐적으로 깨지는 케이스가 생깁니다. 이건 팀/환경에 따라 달라질 수 있습니다.
자주 겪는 문제
Jest + TypeScript 조합에서 흔히 나오는 문제는 비슷합니다.
- ESM/CJS 설정이 어긋나서 import가 깨진다
- paths 별칭은 타입 체크에서는 되는데 Jest에서만 안 된다
- 테스트에서만 쓰는 전역/타입이 tsconfig에 반영되지 않는다
- ts-jest가 느려서 CI 시간이 급격히 늘어난다
이 중에서 가장 많이 시간을 쓰는 건 모듈 시스템입니다. 특히 package.json의 type: module 설정, tsconfig module 설정, Jest 설정이 서로 다른 방향을 보고 있으면 테스트만 깨지고 본 코드는 정상인 상태가 됩니다. 모듈 시스템은 한 번 정하면 끝나는 게 아니라, 패키지 추가/업그레이드 때마다 다시 확인하게 되는 영역입니다.
속도 이슈가 생겼을 때 선택
프로젝트가 커지면 ts-jest의 속도가 부담이 됩니다. 이때는 다음처럼 분리하는 경우가 많습니다.
- 테스트 변환: @swc/jest로 빠르게
- 타입 체크: tsc --noEmit을 CI에서 강제
이 구성은 테스트 실행은 빨라지지만, 테스트 실행만으로는 타입 오류를 즉시 알 수 없습니다. 그래서 typecheck를 CI의 필수 단계로 두지 않으면 금방 흔들립니다.
Jest + TypeScript 테스트 환경은 설치보다 “모듈 해석과 타입 체크 책임을 어디에 둘지”를 정하는 과정에 가깝습니다. ts-jest는 설정이 직관적이고 tsconfig를 잘 따라옵니다. 대신 속도는 비용이 됩니다.
규모가 커지면 변환과 타입 체크를 분리하는 선택이 자연스럽게 나옵니다. 어떤 구성이든, 테스트가 CI에서 안정적으로 재현되고 타입 체크가 빠지지 않도록 묶어두는 것이 결국 유지 비용을 줄입니다.
'개발 > Typescript' 카테고리의 다른 글
| [TYPESCRIPT] TypeScript 프로젝트에서 절대 경로 설정을 운영 기준으로 잡는 방법 (0) | 2026.02.28 |
|---|---|
| [TYPESCRIPT] Vitest + TypeScript 통합: 빠르게 돌리되 흔들리지 않게 만드는 구성 (0) | 2026.02.27 |
| [TYPESCRIPT] ESLint + Prettier + TypeScript 설정을 운영 기준으로 잡는 방법 (0) | 2026.02.25 |
| [TYPESCRIPT] Babel과 TypeScript를 같이 쓰는 빌드 전략: 역할 분리와 실무 기준 (0) | 2026.02.24 |
| [TYPESCRIPT] ts-node-dev, swc, esbuild를 같이 써보고 나서 보이는 차이 (0) | 2026.02.23 |
