서비스 학습
SOURCE · fe/tsconfig.json · tsconfig.app.json · tsconfig.node.json · 2026-09-06

브라우저 코드와 도구 코드를
서로 다른 규칙으로 검사한다

설정 파일이 셋인 이유가 이 페이지의 전부다. 같은 폴더 안에 실행 환경이 다른 두 종류의 코드가 있기 때문이다.

이 파일을 만든 이유

fe/ 안에는 성격이 다른 두 코드가 있다. src/App.tsx브라우저에서 돌고 documentwindow를 쓴다. vite.config.tsNode.js에서 돌고 파일 경로를 다룬다.

하나의 설정으로 둘을 검사하면 문제가 생긴다. DOM type을 켜면 vite.config.ts에서 document를 써도 오류가 나지 않고, 끄면 App.tsx가 검사를 통과하지 못한다. 그래서 설정을 둘로 나누고, 최상단 파일이 둘을 가리킨다.

세 파일의 관계

입구tsconfig.json파일 없음 · 참조만
브라우저 코드tsconfig.app.jsoninclude: src · lib에 DOM
도구 코드tsconfig.node.jsoninclude: vite.config.ts · types에 node
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ]
}
"files": []

이 설정 자체는 아무 파일도 검사하지 않는다. 빈 배열이 그 뜻이다. 역할은 두 하위 설정을 묶는 것뿐이다.

"references" — project references

TypeScript가 여러 설정을 독립된 프로젝트로 다루게 한다. package.jsontsc -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.getElementByIdmain.tsx에서 type을 얻는다. 검사 대상은 src 폴더뿐이다.

"jsx": "react-jsx"

JSX를 어떻게 변환할지 정한다. react-jsx는 새 방식이라 import React가 필요 없다. App.tsxuseState만 가져오고 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"]검사 대상이 다르다
libES2023, DOMES2023Node에는 document가 없다
typesvite/clientnode각 환경의 전역 객체
moduleesnextnodenextNode의 module 해석 규칙
jsxreact-jsx없음설정 파일에 JSX가 없다

lib에서 DOM을 뺀 것이 핵심이다. 덕분에 vite.config.ts에서 실수로 window를 쓰면 그 자리에서 오류가 난다. Node에는 그런 것이 없으니 맞는 판정이다.

실제로 확인하는 방법

cd fe
npx tsc -b                    # 두 프로젝트를 모두 검사
npx tsc -b --force            # 캐시를 무시하고 다시 검사
npx tsc                       # -b 없이 → files:[] 라 아무것도 검사하지 않는다
실험방법기대 결과
환경 분리가 동작한다vite.config.tsdocument.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가 없다고 오류jsxreact-jsx가 아님옛 방식은 React import를 요구한다
.tsx 확장자 import 오류allowImportingTsExtensions 누락번들러 환경 전용 설정이다
enum이 오류erasableSyntaxOnly실행 코드를 만드는 문법은 금지된다. 유니온 type으로 대체한다
편집기와 tsc 결과가 다름편집기가 다른 설정을 봄편집기를 재시작하거나 TypeScript 버전을 워크스페이스 것으로 맞춘다

설계 선택과 대안

설정 하나로 합치면?

가능하지만 libDOM을 넣을지 말지를 정할 수 없다. 넣으면 도구 코드의 실수를 놓치고, 빼면 브라우저 코드가 검사를 통과하지 못한다. 두 실행 환경이 한 폴더에 있는 이상 나누는 것이 정답에 가깝다.

Vite가 검사하게 하면?

vite-plugin-checker 같은 도구로 개발 중에도 type 오류를 화면에 띄울 수 있다. 대신 개발 서버가 무거워진다. 8GB 환경에서는 build 때만 검사하는 지금 구성이 부담이 적다.

strict가 왜 없는가?

이 설정에는 strict: true가 명시돼 있지 않다. noUnusedLocals 같은 개별 항목만 켠 상태다. Vite 템플릿 기본값을 유지한 것이며, 엄격 검사를 켜면 null 처리 요구가 늘어난다. main.tsx! 같은 표현이 어떻게 바뀌는지 확인해 볼 만한 실험이다.

이전 단계와 다음 파일

이 설정을 실행하는 명령은 package.jsontsc -b다. 검사 대상은 App.tsx·main.tsxvite.config.ts이며, 검사를 통과한 뒤 실제 변환은 Vite가 맡는다.

← 이전의존성과 명령다음 →Dockerfile 상세