타입스크립트

TypeScript Cannot find module 오류 해결: tsconfig paths와 moduleResolution 점검하기

2026.05.18·수정 2026.05.19·약 13분

이 글에서 정리하는 내용

TypeScript에서 Cannot find module 또는 TS2307 오류가 발생했을 때 import 경로만 고쳐서는 해결되지 않는 경우가 있습니다. 특히 tsconfig.jsonpaths, baseUrl, moduleResolution, 번들러 alias가 서로 다른 기준을 보고 있으면 에디터, 타입 검사, 개발 서버가 각각 다른 결과를 보여줍니다. 이 글은 TypeScript 5.x 기준으로 alias 경로 오류를 어디서부터 확인해야 하는지 정리합니다.

오류가 import 문제처럼 보이는 이유

TypeScript Cannot find module 오류에서 import 경로와 tsconfig 설정을 나누어 확인하는 진단 흐름

Cannot find module 오류는 화면상으로는 import 문 한 줄에 빨간 밑줄이 생기는 형태로 보입니다. 그래서 처음에는 파일명이 틀렸거나, 상대 경로에서 ../ 개수가 잘못된 문제처럼 느껴집니다. 실제로 그런 경우도 많지만, 경로가 맞는데도 계속 오류가 남는다면 TypeScript가 파일을 찾는 기준과 실제 실행 도구가 파일을 찾는 기준이 달라졌을 가능성을 봐야 합니다.

예를 들어 React나 Next.js 프로젝트에서 @/components/Button처럼 alias import를 쓰는 경우가 있습니다. 개발 서버는 정상적으로 뜨는데 VS Code만 빨간 줄을 표시하거나, 반대로 에디터에서는 멀쩡한데 빌드에서 실패하는 상황이 나올 수 있습니다. 이때는 한쪽 설정만 맞아 있고 다른 한쪽 설정이 빠져 있는 상태일 수 있습니다.

보이는 증상먼저 의심할 부분
VS Code에서만 빨간 줄이 남음tsconfig.json 인식, include, TypeScript 서버 캐시
tsc --noEmit에서 실패함paths, baseUrl, moduleResolution
개발 서버에서만 실패함Vite, Webpack, Next.js 등 번들러 alias 설정
로컬은 되는데 배포 빌드에서 실패함대소문자 불일치, 누락된 파일, CI 환경의 파일 시스템 차이

문제 해결은 오류 문구를 없애는 것보다 어느 단계에서 실패하는지 나누는 쪽이 먼저입니다. TypeScript 타입 검사에서 실패하는지, 개발 서버에서 실패하는지, 배포 빌드에서만 실패하는지에 따라 고쳐야 할 파일이 달라집니다.

실무에서 자주 헷갈리는 부분은 paths가 TypeScript의 경로 해석을 도와주는 설정이지, 모든 런타임 도구의 import 동작을 자동으로 바꾸는 설정은 아니라는 점입니다. 그래서 타입 검사는 통과하는데 Vite 빌드가 실패하거나, 반대로 Vite 개발 서버는 동작하는데 tsc에서 실패하는 상황이 생길 수 있습니다.

paths와 baseUrl 먼저 확인하기

paths는 TypeScript에게 특정 import 경로를 실제 파일 경로로 해석하는 방법을 알려주는 설정입니다. alias를 쓰지 않는 프로젝트라면 이 설정을 건드릴 일이 적지만, @/, ~/, @components/ 같은 경로를 쓰기 시작하면 거의 반드시 확인해야 합니다.

가장 흔한 실수는 alias 이름과 실제 폴더 구조가 조금씩 어긋나는 경우입니다. @/*["src/*"]로 연결해두면 @/components/Buttonsrc/components/Button을 기준으로 찾습니다. 그런데 실제 폴더가 app/components에 있거나, Components처럼 대문자가 섞여 있으면 환경에 따라 결과가 달라질 수 있습니다.

React/Vite 프로젝트에서 자주 쓰는 형태

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@/components/*": ["src/components/*"]
    },
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true
  },
  "include": ["src", "vite.config.ts"]
}

위 설정에서 baseUrl은 현재 프로젝트 루트를 기준으로 삼겠다는 의미입니다. @/*src/*로 연결되므로 @/lib/formatDate를 import하면 TypeScript는 src/lib/formatDate 근처에서 파일을 찾습니다. 이때 실제 파일명이 format-date.ts라면 import 경로도 그대로 맞춰야 합니다.

최근 TypeScript에서는 paths가 반드시 baseUrl에만 묶여 있다고 보기는 어렵지만, 프로젝트 루트 기준을 명확히 두고 싶다면 baseUrl: "."을 함께 두는 구성이 여전히 많이 사용됩니다. 다만 무작정 추가하기보다 현재 템플릿이 어떤 기준으로 생성되었는지 먼저 확인해야 합니다.

또 하나 놓치기 쉬운 부분은 include입니다. 파일은 존재하지만 tsconfig.json의 검사 범위 밖에 있으면 에디터나 타입 검사에서 정상적으로 잡히지 않을 수 있습니다. 프로젝트에서 src 밖에 설정 파일, 타입 선언 파일, 테스트 유틸 파일을 두었다면 검사 범위에 포함되어 있는지 확인해야 합니다.

moduleResolution을 실행 환경에 맞추기

moduleResolution은 TypeScript가 import 경로를 어떤 방식으로 해석할지 정하는 옵션입니다. 이름이 비슷해서 module과 같은 설정처럼 보이지만, 역할은 다릅니다. module은 출력되는 모듈 형식과 관련이 있고, moduleResolution은 어떤 파일을 찾아야 하는지 판단하는 방식에 가깝습니다.

프론트엔드 번들러를 쓰는 React, Vite, Next.js 계열 프로젝트에서는 보통 프로젝트 템플릿이 이미 적절한 설정을 넣어둡니다. 설정을 직접 바꿔야 한다면 현재 코드가 브라우저 번들링을 거치는지, Node.js 런타임에서 직접 실행되는지부터 나눠야 합니다. 아무 글에서 본 node16, nodenext, bundler 값을 그대로 복사하면 오히려 다른 오류가 생길 수 있습니다.

상황검토할 설정 방향
Vite, Webpack, Rollup 등으로 번들링하는 프론트엔드 앱moduleResolution: "bundler" 계열 설정을 검토
Node.js ESM 환경에서 직접 실행되는 코드modulemoduleResolutionNodeNext 기준으로 맞추는지 확인
기존 CommonJS 중심 프로젝트기존 설정과 패키지 형식을 먼저 확인한 뒤 변경

Node.js ESM 프로젝트에서 볼 수 있는 형태

{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "strict": true
  }
}

이 설정이 항상 정답이라는 의미는 아닙니다. 브라우저 앱인지, 라이브러리인지, 서버 코드인지에 따라 기준이 달라집니다. 다만 moduleResolution을 바꿀 때는 module과 패키지의 type 필드까지 같이 보아야 합니다. 하나만 바꾸고 나머지를 그대로 두면 기존 import 경로가 갑자기 깨질 수 있습니다.

예를 들어 Node.js ESM을 기준으로 맞춘 프로젝트에서는 상대 import 확장자 처리 방식이 프론트엔드 번들러 환경과 다르게 느껴질 수 있습니다. 반대로 Vite 같은 번들러 중심 프로젝트에서는 브라우저에서 실제로 실행될 코드가 번들러를 거쳐 정리되므로 bundler 쪽 설정이 더 자연스럽게 맞는 경우가 많습니다.

번들러 alias와 tsconfig paths를 같이 맞추기

tsconfig.json만 고쳤는데 개발 서버에서 계속 모듈을 찾지 못한다면 번들러 설정을 봐야 합니다. TypeScript의 paths는 타입 검사와 에디터 해석에 영향을 주지만, 모든 실행 도구가 이 값을 자동으로 그대로 따라가는 것은 아닙니다. Vite를 쓰는 프로젝트라면 vite.config.tsresolve.alias도 같이 맞춰야 하는 경우가 많습니다.

Vite alias 설정 예시

import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url))
    }
  }
});

이렇게 맞추면 TypeScript는 tsconfig.json을 기준으로 @/ 경로를 이해하고, Vite는 개발 서버와 빌드 과정에서 같은 alias를 기준으로 파일을 찾습니다. 둘 중 하나만 설정되어 있으면 한쪽에서는 정상이고 다른 쪽에서는 실패하는 모양이 나옵니다.

Vite alias에서 파일 시스템 경로를 가리킬 때는 상대 경로를 그대로 넣기보다 절대 경로로 변환하는 형태가 낫습니다. 그래서 new URL("./src", import.meta.url)fileURLToPath를 함께 쓰는 예시를 많이 볼 수 있습니다. 설정 파일이 프로젝트 루트에 있다는 전제까지 맞아야 하므로, 복사한 설정이 현재 폴더 구조와 일치하는지도 같이 확인해야 합니다.

Next.js처럼 프레임워크가 tsconfig.json 또는 jsconfig.json의 경로 alias를 읽어주는 경우도 있습니다. 그래도 오류가 난다면 프레임워크가 처리해줄 것이라고 넘기기보다, 현재 import 경로와 실제 파일 위치가 먼저 일치하는지 확인하는 게 좋습니다. 설정 자동 인식은 편하지만, 잘못된 경로까지 고쳐주지는 않습니다.

실제 작업에서 점검하는 순서

TypeScript paths, moduleResolution, Vite alias 설정을 순서대로 검증하는 해결 흐름

처음부터 설정 파일을 전부 바꾸면 원인을 놓치기 쉽습니다. 먼저 실제 파일이 있는지, 경로의 대소문자가 맞는지 확인합니다. Windows에서는 대소문자가 느슨하게 보일 수 있지만, 배포 환경이나 Linux 기반 CI에서는 Button.tsxbutton.tsx가 다른 파일처럼 취급될 수 있습니다.

그다음 TypeScript 자체가 문제를 보는지 확인합니다. 개발 서버 화면만 보지 말고 아래 명령으로 타입 검사 결과를 따로 확인하면 원인이 더 빨리 좁혀집니다.

npx tsc --noEmit

이 명령에서 실패한다면 tsconfig.json 쪽을 먼저 봅니다. paths, baseUrl, include, moduleResolution을 순서대로 확인합니다. 반대로 이 명령은 통과하는데 개발 서버에서 실패한다면 번들러 alias나 프레임워크 설정을 봐야 합니다.

어떤 설정 파일이 실제로 적용되고 있는지 애매할 때는 최종 적용 결과를 확인합니다. 루트의 tsconfig.json만 보고 있다고 생각했는데, 실제로는 tsconfig.app.json, tsconfig.node.json, extends로 연결된 설정을 함께 쓰는 프로젝트도 있습니다.

npx tsc --showConfig

경로 해석 과정 자체를 더 자세히 보고 싶다면 --traceResolution을 사용할 수 있습니다. 출력이 길기 때문에 매번 사용할 필요는 없지만, TypeScript가 어떤 파일 후보를 확인하다가 실패했는지 볼 수 있어 alias 충돌을 찾을 때 도움이 됩니다.

npx tsc --noEmit --traceResolution

VS Code에서만 오류가 남는 경우에는 TypeScript 서버가 오래된 설정을 들고 있을 수 있습니다. 파일 저장 후에도 계속 빨간 줄이 남으면 명령 팔레트에서 TypeScript 서버를 재시작하거나, 에디터를 다시 열어 확인합니다. 이 과정은 설정을 고치는 작업은 아니지만, 이미 해결된 문제를 계속 붙잡지 않게 해줍니다.

다음에 다시 볼 기준

Cannot find module 오류는 import 문만 보고 고치면 같은 문제가 반복됩니다. 먼저 파일 경로와 대소문자를 확인하고, 그다음 TypeScript가 보는 경로와 번들러가 보는 경로가 같은지 나누어 봐야 합니다. 특히 alias를 쓰는 프로젝트에서는 tsconfig.json과 번들러 설정을 한 쌍으로 관리하는 습관이 필요합니다.

  • 상대 경로 오류인지 alias 설정 오류인지 먼저 구분합니다.
  • paths의 왼쪽 alias와 오른쪽 실제 폴더 경로를 대소문자까지 맞춥니다.
  • moduleResolution은 프로젝트 실행 환경과 기존 템플릿 설정을 기준으로 조정합니다.
  • TypeScript 검사 결과와 개발 서버 결과를 npx tsc --noEmit으로 분리해서 확인합니다.
  • 설정이 여러 파일로 나뉘어 있으면 npx tsc --showConfig로 최종 적용값을 확인합니다.
  • 배포에서만 깨진다면 파일명 대소문자와 누락된 파일을 먼저 확인합니다.

작은 프로젝트에서는 상대 경로 몇 개를 직접 고치는 방식으로도 버틸 수 있습니다. 하지만 컴포넌트, 유틸 함수, 타입 파일이 늘어나면 경로 기준이 흔들릴 때마다 수정 범위가 커집니다. alias를 쓰기로 했다면 TypeScript, 번들러, 에디터가 같은 기준을 보게 만드는 것까지 설정의 일부로 보는 것이 좋습니다.

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

댓글 남기기