브라우저 코드와 도구 코드를
서로 다른 규칙으로 검사한다
설정 파일이 셋인 이유가 이 페이지의 전부다. 같은 폴더 안에 실행 환경이 다른 두 종류의 코드가 있기 때문이다.
이 파일을 만든 이유
fe/ 안에는 성격이 다른 두 코드가 있다. src/App.tsx는 브라우저에서 돌고 document와 window를 쓴다. vite.config.ts는 Node.js에서 돌고 파일 경로를 다룬다.
하나의 설정으로 둘을 검사하면 문제가 생긴다. DOM type을 켜면 vite.config.ts에서 document를 써도 오류가 나지 않고, 끄면 App.tsx가 검사를 통과하지 못한다. 그래서 설정을 둘로 나누고, 최상단 파일이 둘을 가리킨다.
세 파일의 관계
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
"files": []
이 설정 자체는 아무 파일도 검사하지 않는다. 빈 배열이 그 뜻이다. 역할은 두 하위 설정을 묶는 것뿐이다.
"references" — project references
TypeScript가 여러 설정을 독립된 프로젝트로 다루게 한다. package.json의 tsc -b에서 -b가 바로 이 build mode이며, 참조를 따라가 각각을 검사한다. -b 없이 tsc만 실행하면 files: []를 보고 아무것도 검사하지 않은 채 성공한다.
tsconfig.app.json — 브라우저 코드
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"allowArbitraryExtensions": true,
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
"lib": ["ES2023", "DOM"] · "include": ["src"]
DOM이 있어야 document.getElementById가 main.tsx에서 type을 얻는다. 검사 대상은 src 폴더뿐이다.
"jsx": "react-jsx"
JSX를 어떻게 변환할지 정한다. react-jsx는 새 방식이라 import React가 필요 없다. App.tsx가 useState만 가져오고 React는 가져오지 않는 이유가 이것이다.
"noEmit": true
TypeScript는 검사만 하고 파일을 만들지 않는다. 실제 변환은 Vite가 한다. 두 도구가 각자 잘하는 일만 맡는 구조다.
"moduleResolution": "bundler" · "allowImportingTsExtensions": true
번들러가 있는 환경을 전제한 해석 방식이다. 덕분에 import App from './App.tsx'처럼 확장자를 붙인 import가 허용된다. Node 기본 규칙에서는 오류다.
"verbatimModuleSyntax": true
import type { ... }이라고 명시한 것만 type import로 다루고, 나머지는 그대로 남긴다. App.tsx가 import type { FormEvent, … }이라고 쓴 이유다. 무엇이 실행 코드로 남는지가 보는 그대로가 된다.
"types": ["vite/client"]
import './App.css'처럼 CSS를 가져오는 문법의 type을 제공한다. 이것이 없으면 “모듈을 찾을 수 없다”는 오류가 난다.
noUnusedLocals · noUnusedParameters · erasableSyntaxOnly
앞의 둘은 쓰지 않는 변수와 인수를 오류로 만든다. 개발 중에는 성가시지만 npm run build에서 죽은 코드를 걸러 준다.
erasableSyntaxOnly는 지우기만 하면 JavaScript가 되는 문법만 허용한다. enum처럼 실행 코드를 만들어 내는 TypeScript 기능을 금지하는데, Vite가 type을 “지우기만” 하기 때문에 그런 문법은 변환 결과가 깨진다.
tsconfig.node.json — 도구 코드
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"module": "nodenext",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
| 항목 | app 설정 | node 설정 | 이유 |
|---|---|---|---|
include | ["src"] | ["vite.config.ts"] | 검사 대상이 다르다 |
lib | ES2023, DOM | ES2023 | Node에는 document가 없다 |
types | vite/client | node | 각 환경의 전역 객체 |
module | esnext | nodenext | Node의 module 해석 규칙 |
jsx | react-jsx | 없음 | 설정 파일에 JSX가 없다 |
lib에서 DOM을 뺀 것이 핵심이다. 덕분에 vite.config.ts에서 실수로 window를 쓰면 그 자리에서 오류가 난다. Node에는 그런 것이 없으니 맞는 판정이다.
실제로 확인하는 방법
cd fe
npx tsc -b # 두 프로젝트를 모두 검사
npx tsc -b --force # 캐시를 무시하고 다시 검사
npx tsc # -b 없이 → files:[] 라 아무것도 검사하지 않는다
| 실험 | 방법 | 기대 결과 |
|---|---|---|
| 환경 분리가 동작한다 | vite.config.ts에 document.title를 한 줄 추가 | tsc -b가 오류를 낸다. DOM이 없기 때문 |
-b의 필요성 | npx tsc만 실행 | 오류가 있어도 통과한다 |
| 증분 빌드 | tsc -b를 두 번 실행 | 두 번째가 훨씬 빠르다. tsBuildInfoFile에 결과를 남긴다 |
noUnusedLocals | 쓰지 않는 변수를 하나 만든다 | npm run build가 실패한다 |
tsBuildInfoFile이 ./node_modules/.tmp/를 가리키는 것도 의도적이다. 소스 폴더를 더럽히지 않고, node_modules는 어차피 .dockerignore와 git에서 제외되므로 캐시가 저장소나 이미지에 섞이지 않는다.
자주 발생하는 오류
| 증상 | 원인 | 진단 |
|---|---|---|
| type 오류가 안 잡힘 | -b 없이 tsc 실행 | 최상단 설정은 files: []다 |
Cannot find module './App.css' | types: ["vite/client"] 누락 | CSS import의 type 선언이 필요하다 |
import React가 없다고 오류 | jsx가 react-jsx가 아님 | 옛 방식은 React import를 요구한다 |
.tsx 확장자 import 오류 | allowImportingTsExtensions 누락 | 번들러 환경 전용 설정이다 |
enum이 오류 | erasableSyntaxOnly | 실행 코드를 만드는 문법은 금지된다. 유니온 type으로 대체한다 |
편집기와 tsc 결과가 다름 | 편집기가 다른 설정을 봄 | 편집기를 재시작하거나 TypeScript 버전을 워크스페이스 것으로 맞춘다 |
설계 선택과 대안
설정 하나로 합치면?
가능하지만 lib에 DOM을 넣을지 말지를 정할 수 없다. 넣으면 도구 코드의 실수를 놓치고, 빼면 브라우저 코드가 검사를 통과하지 못한다. 두 실행 환경이 한 폴더에 있는 이상 나누는 것이 정답에 가깝다.
Vite가 검사하게 하면?
vite-plugin-checker 같은 도구로 개발 중에도 type 오류를 화면에 띄울 수 있다. 대신 개발 서버가 무거워진다. 8GB 환경에서는 build 때만 검사하는 지금 구성이 부담이 적다.
strict가 왜 없는가?
이 설정에는 strict: true가 명시돼 있지 않다. noUnusedLocals 같은 개별 항목만 켠 상태다. Vite 템플릿 기본값을 유지한 것이며, 엄격 검사를 켜면 null 처리 요구가 늘어난다. main.tsx의 ! 같은 표현이 어떻게 바뀌는지 확인해 볼 만한 실험이다.
이전 단계와 다음 파일
이 설정을 실행하는 명령은 package.json의 tsc -b다. 검사 대상은 App.tsx·main.tsx와 vite.config.ts이며, 검사를 통과한 뒤 실제 변환은 Vite가 맡는다.