
TypeScript 타입 에러가 빌드에서 안 잡힐 때
TypeScript 타입 에러가 개발 중에는 보이는데 빌드할 때는 무시되는 문제와 해결 방법을 정리했습니다.

TypeScript 타입 에러가 개발 중에는 보이는데 빌드할 때는 무시되는 문제와 해결 방법을 정리했습니다.
전처리(Preprocessing), 컴파일(Process), 어셈블리(Assembly), 링킹(Linking)의 4단계를 해부한다. 정적 라이브러리와 동적 라이브러리의 차이까지.

any를 쓰면 타입스크립트를 쓰는 의미가 없습니다. 제네릭(Generics)을 통해 유연하면서도 타입 안전성(Type Safety)을 모두 챙기는 방법을 정리합니다. infer, keyof 등 고급 기법 포함.

부모에서 전달한 props가 undefined로 나와서 앱이 크래시되는 문제 해결

Next.js 빌드 로그에 나오는 동그라미(○)와 람다(λ) 기호의 의미를 아시나요? 실수로 모든 페이지를 동적으로 만들어버리지 않는 방법을 확인하세요.

TypeScript로 개발하면서 VSCode에서 빨간 밑줄로 타입 에러가 표시됐습니다. "나중에 고쳐야지"라고 생각하고 일단 빌드를 돌렸는데, 빌드가 성공했습니다.
// VSCode에서 에러 표시
const user: User = { name: 'John' }; // ❌ Property 'age' is missing
// 하지만 빌드는 성공
npm run build
✓ Build successful!
더 심각한 건 프로덕션에 배포한 후 런타임 에러가 났다는 겁니다. TypeScript를 쓰는 이유가 타입 안전성인데, 빌드 시에 타입 체크를 안 하면 의미가 없었습니다.
처음엔 "TypeScript가 자동으로 타입 체크를 하는 거 아닌가?"라고 생각했는데, 알고 보니 빌드 도구 설정에 따라 타입 체크를 스킵할 수 있었습니다.
TypeScript를 쓰면 당연히 타입 체크를 한다고 생각했습니다. 근데 이해가 안 갔던 부분들:
tsc와 빌드 도구의 차이가 뭔가?
특히 "Next.js는 TypeScript를 지원한다고 하는데 왜 타입 체크를 안 하나?"라는 의문이 들었습니다.
이해의 전환점은 "빌드 도구는 속도를 위해 타입 체크를 스킵한다"는 걸 받아들였을 때였습니다.
이걸 "번역과 교정"으로 비유하니까 이해가 됐습니다:
tsc): 번역 + 교정. 문법 오류(타입 에러)를 찾아서 알려줌. 느림.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: 변환만 → 빠름 (수 초)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; }'
# 빌드 중단!
tsconfig.json 엄격 모드타입 체크를 더 엄격하게:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
Next.js는 기본적으로 빌드 시 타입 체크를 하지 않습니다. 활성화하려면:
// next.config.js
module.exports = {
typescript: {
// ⚠️ 위험: 타입 에러를 무시하고 빌드
ignoreBuildErrors: false, // 기본값은 false이지만 명시적으로 설정
},
};
또는 빌드 스크립트에 추가:
{
"scripts": {
"build": "tsc --noEmit && next build"
}
}
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, // 타입 체크 활성화
}),
],
});
이제 개발 중에도 타입 에러가 오버레이로 표시됩니다.
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을 머지하기 전에 타입 에러를 잡을 수 있습니다.
커밋 전에 타입 체크:
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"
]
}
}
이제 타입 에러가 있으면 커밋이 차단됩니다.
기존 프로젝트에 타입 체크를 추가할 때는 점진적으로:
// tsconfig.json
{
"compilerOptions": {
"strict": false, // 일단 false
"noImplicitAny": true, // 하나씩 활성화
"strictNullChecks": false
}
}
에러를 하나씩 고치면서 옵션을 늘려갑니다.
tsc는 느리므로 캐싱을 사용:
{
"compilerOptions": {
"incremental": true, // 증분 컴파일
"tsBuildInfoFile": ".tsbuildinfo"
}
}
이렇게 하면 변경된 파일만 타입 체크합니다.
빌드와 타입 체크를 병렬로:
npm install -D concurrently
{
"compilerOptions": {
"strict": true
},
"scripts": {
"build": "concurrently \"tsc --noEmit\" \"vite build\""
}
}
하지만 타입 에러가 있어도 빌드가 진행되므로 주의하세요.
VSCode에서 타입 에러를 더 명확하게:
// .vscode/settings.json
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
ignoreBuildErrors: true 사용Next.js에서 타입 에러를 무시하도록 설정했다가 프로덕션에서 에러가 났습니다.
// ❌ 절대 하지 마세요!
module.exports = {
typescript: {
ignoreBuildErrors: true, // 위험!
},
};
교훈: 타입 에러는 반드시 고치세요. 무시하지 마세요.
@ts-ignore 남용타입 에러를 임시로 무시하려고 @ts-ignore를 썼는데, 나중에 진짜 버그가 됐습니다.
// ❌ 나쁜 습관
// @ts-ignore
const user: User = { name: 'John' };
교훈: @ts-ignore는 최후의 수단입니다. 타입을 제대로 정의하세요.
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);
}
skipLibCheck: true 남용라이브러리 타입 체크를 스킵했다가 라이브러리 버전 충돌을 못 잡았습니다.
// ⚠️ 주의해서 사용
{
"compilerOptions": {
"skipLibCheck": true // node_modules 타입 체크 스킵
}
}
교훈: skipLibCheck는 빌드 속도를 위해 사용하되, 라이브러리 버전을 잘 관리하세요.
# 모든 타입 에러 출력
tsc --noEmit
# 특정 파일만 체크
tsc --noEmit src/components/Button.tsx
VSCode에서 변수에 마우스를 올리면 추론된 타입을 볼 수 있습니다.
const user = { name: 'John', age: 30 };
// 마우스 올리면: const user: { name: string; age: number; }
// Cmd/Ctrl + 클릭으로 타입 정의로 이동
import { User } from './types';
TypeScript 타입 에러가 빌드에서 안 잡히는 이유는 빌드 도구가 타입 체크를 스킵하기 때문입니다. tsc --noEmit을 빌드 스크립트에 추가하세요.