TypeScript 타입 에러가 빌드에서 안 잡힐 때
왜 이 문제를 만났나
TypeScript로 개발하면서 VSCode에서 빨간 밑줄로 타입 에러가 표시됐습니다. "나중에 고쳐야지"라고 생각하고 일단 빌드를 돌렸는데, 빌드가 성공했습니다.
// VSCode에서 에러 표시
const user: User = { name: 'John' }; // ❌ Property 'age' is missing
// 하지만 빌드는 성공
npm run build
✓ Build successful!
더 심각한 건 프로덕션에 배포한 후 런타임 에러가 났다는 겁니다. TypeScript를 쓰는 이유가 타입 안전성인데, 빌드 시에 타입 체크를 안 하면 의미가 없었습니다.
처음엔 "TypeScript가 자동으로 타입 체크를 하는 거 아닌가?"라고 생각했는데, 알고 보니 빌드 도구 설정에 따라 타입 체크를 스킵할 수 있었습니다.
처음엔 뭐가 이해가 안 갔나
TypeScript를 쓰면 당연히 타입 체크를 한다고 생각했습니다. 근데 이해가 안 갔던 부분들:
- 왜 VSCode에서는 에러가 보이는데 빌드는 성공하나?
- 왜 Vite/Webpack은 타입 에러를 무시하나?
tsc와 빌드 도구의 차이가 뭔가?
특히 "Next.js는 TypeScript를 지원한다고 하는데 왜 타입 체크를 안 하나?"라는 의문이 들었습니다.
어떤 포인트에서 이해가 됐나
이해의 전환점은 "빌드 도구는 속도를 위해 타입 체크를 스킵한다"는 걸 받아들였을 때였습니다.
빌드 도구의 TypeScript 처리
이걸 "번역과 교정"으로 비유하니까 이해가 됐습니다:
- TypeScript 컴파일러 (
tsc): 번역 + 교정. 문법 오류(타입 에러)를 찾아서 알려줌. 느림. - 빌드 도구 (Vite, esbuild, SWC): 번역만. 문법 오류는 무시하고 일단 번역. 빠름.
graph LR
A[TypeScript 코드] --> B{빌드 도구}
B --> C[tsc]
B --> D[Vite/esbuild]
C --> E[타입 체크 + 변환]
D --> F[타입 체크 없이 변환만]
E --> G[느리지만 안전]
F --> H[빠르지만 위험]
style E fill:#9f9,stroke:#333
style F fill:#f99,stroke:#333
대부분의 최신 빌드 도구(Vite, esbuild, SWC)는 타입 체크를 하지 않고 TypeScript를 JavaScript로 변환만 합니다. 이유는 속도 때문입니다.
tsc: 타입 체크 + 변환 → 느림 (수십 초)esbuild: 변환만 → 빠름 (수 초)
해결 방법
1. 빌드 스크립트에 타입 체크 추가 (권장)
package.json에 타입 체크를 별도로 실행:
{
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"type-check": "tsc --noEmit"
}
}
설명:
tsc --noEmit: 타입 체크만 하고 파일은 생성하지 않음&&: 타입 체크가 성공해야 빌드 진행
이제 타입 에러가 있으면 빌드가 실패합니다:
npm run build
# 타입 에러 발생 시
error TS2741: Property 'age' is missing in type '{ name: string; }'
# 빌드 중단!
2. tsconfig.json 엄격 모드
타입 체크를 더 엄격하게:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
3. Next.js 타입 체크
Next.js는 기본적으로 빌드 시 타입 체크를 하지 않습니다. 활성화하려면:
// next.config.js
module.exports = {
typescript: {
// ⚠️ 위험: 타입 에러를 무시하고 빌드
ignoreBuildErrors: false, // 기본값은 false이지만 명시적으로 설정
},
};
또는 빌드 스크립트에 추가:
{
"scripts": {
"build": "tsc --noEmit && next build"
}
}
4. Vite 타입 체크 플러그인
Vite는 기본적으로 타입 체크를 하지 않습니다. 플러그인을 사용:
npm install -D vite-plugin-checker
// vite.config.ts
import { defineConfig } from 'vite';
import checker from 'vite-plugin-checker';
export default defineConfig({
plugins: [
checker({
typescript: true, // 타입 체크 활성화
}),
],
});
이제 개발 중에도 타입 에러가 오버레이로 표시됩니다.
5. CI/CD에서 타입 체크
GitHub Actions 예시:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm install
- run: npm run type-check # 타입 체크 실행
- run: npm run build
이렇게 하면 PR을 머지하기 전에 타입 에러를 잡을 수 있습니다.
6. Pre-commit Hook
커밋 전에 타입 체크:
npm install -D husky lint-staged
npx husky install
npx husky add .husky/pre-commit "npx lint-staged"
// package.json
{
"lint-staged": {
"*.{ts,tsx}": [
"tsc --noEmit",
"eslint --fix"
]
}
}
이제 타입 에러가 있으면 커밋이 차단됩니다.
팁
1. 점진적 타입 체크
기존 프로젝트에 타입 체크를 추가할 때는 점진적으로:
// tsconfig.json
{
"compilerOptions": {
"strict": false, // 일단 false
"noImplicitAny": true, // 하나씩 활성화
"strictNullChecks": false
}
}
에러를 하나씩 고치면서 옵션을 늘려갑니다.
2. 타입 체크 캐싱
tsc는 느리므로 캐싱을 사용:
{
"compilerOptions": {
"incremental": true, // 증분 컴파일
"tsBuildInfoFile": ".tsbuildinfo"
}
}
이렇게 하면 변경된 파일만 타입 체크합니다.
3. 병렬 타입 체크
빌드와 타입 체크를 병렬로:
npm install -D concurrently
{
"compilerOptions": {
"strict": true
},
"scripts": {
"build": "concurrently \"tsc --noEmit\" \"vite build\""
}
}
하지만 타입 에러가 있어도 빌드가 진행되므로 주의하세요.
4. VSCode 설정
VSCode에서 타입 에러를 더 명확하게:
// .vscode/settings.json
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
실수했던 부분들
1. ignoreBuildErrors: true 사용
Next.js에서 타입 에러를 무시하도록 설정했다가 프로덕션에서 에러가 났습니다.
// ❌ 절대 하지 마세요!
module.exports = {
typescript: {
ignoreBuildErrors: true, // 위험!
},
};
교훈: 타입 에러는 반드시 고치세요. 무시하지 마세요.
2. @ts-ignore 남용
타입 에러를 임시로 무시하려고 @ts-ignore를 썼는데, 나중에 진짜 버그가 됐습니다.
// ❌ 나쁜 습관
// @ts-ignore
const user: User = { name: 'John' };
교훈: @ts-ignore는 최후의 수단입니다. 타입을 제대로 정의하세요.
3. any 타입 남용
타입 에러를 피하려고 any를 썼습니다.
// ❌ 타입 안전성 포기
const data: any = fetchData();
교훈: any 대신 unknown을 쓰고, 타입 가드를 사용하세요.
// ✅ 올바른 방법
const data: unknown = fetchData();
if (typeof data === 'object' && data !== null && 'name' in data) {
console.log(data.name);
}
4. skipLibCheck: true 남용
라이브러리 타입 체크를 스킵했다가 라이브러리 버전 충돌을 못 잡았습니다.
// ⚠️ 주의해서 사용
{
"compilerOptions": {
"skipLibCheck": true // node_modules 타입 체크 스킵
}
}
교훈: skipLibCheck는 빌드 속도를 위해 사용하되, 라이브러리 버전을 잘 관리하세요.
디버깅 팁
1. 타입 에러 위치 찾기
# 모든 타입 에러 출력
tsc --noEmit
# 특정 파일만 체크
tsc --noEmit src/components/Button.tsx
2. 타입 추론 확인
VSCode에서 변수에 마우스를 올리면 추론된 타입을 볼 수 있습니다.
const user = { name: 'John', age: 30 };
// 마우스 올리면: const user: { name: string; age: number; }
3. 타입 정의 찾기
// Cmd/Ctrl + 클릭으로 타입 정의로 이동
import { User } from './types';
한 줄 요약
TypeScript 타입 에러가 빌드에서 안 잡히는 이유는 빌드 도구가 타입 체크를 스킵하기 때문입니다. tsc --noEmit을 빌드 스크립트에 추가하세요.