Ch 01. Express + TypeScript 프로젝트 구성
서버 프로젝트는 프론트엔드와 출발점이 다릅니다. Vite 같은 올인원 도구가 없습니다. 직접 TypeScript 컴파일러를 설정하고, 개발 서버를 연결하고, 타입 선언 파일을 설치해야 합니다. 번거로워 보이지만 한 번 구성해두면 이후 모든 것이 편해집니다.
프로젝트 초기화
빈 폴더를 만들고 npm 프로젝트로 초기화합니다.
mkdir todo-apicd todo-apinpm init -y
이제 필요한 패키지를 설치합니다. Express 5는 현재 @latest 태그를 달고 있습니다.
# 런타임 의존성npm install express# 개발 의존성npm install -D typescript @types/node @types/express tsx nodemon
@types/express는 Express의 TypeScript 타입 선언 패키지입니다. Express 자체는 JavaScript로 작성되어 있어서, 별도로 타입 선언을 설치해야 TypeScript가 Express 객체를 이해합니다.
tsx는 TypeScript 파일을 빌드 없이 직접 실행해주는 도구입니다. ts-node보다 훨씬 빠르고, ESM과 CJS를 모두 지원합니다.
tsconfig.json 서버용 설정
프론트엔드와 백엔드의 tsconfig는 목적이 다릅니다. 브라우저 환경이 아니라 Node.js 환경이기 때문에 lib와 target이 달라집니다.
// 새 파일: tsconfig.json{ "compilerOptions": { "target": "ES2022", "module": "CommonJS", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "declaration": true, "declarationMap": true, "sourceMap": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"]}
주요 옵션을 하나씩 살펴봅니다.
target: "ES2022" — Node.js 18 이상은 ES2022를 완전히 지원합니다. 최신 문법을 트랜스파일 없이 사용할 수 있습니다.
module: "CommonJS" — Node.js의 기본 모듈 시스템입니다. Express와 대부분의 서버 패키지가 CJS를 기준으로 만들어졌습니다.
strict: true — 타입 안전한 서버를 만들려면 반드시 켜야 합니다. strictNullChecks, noImplicitAny 등이 모두 활성화됩니다.
declaration: true — .d.ts 파일을 생성합니다. 나중에 타입 패키지를 프론트엔드와 공유할 때 필요합니다.
프로젝트 구조 설계
todo-api/
├── src/
│ ├── routes/
│ │ └── todos.ts # Todo 라우트
│ ├── middleware/
│ │ └── errorHandler.ts # 에러 처리 미들웨어
│ ├── models/
│ │ └── todo.ts # 타입 정의
│ ├── db/
│ │ └── prisma.ts # Prisma 클라이언트
│ └── index.ts # 앱 진입점
├── prisma/
│ └── schema.prisma # 데이터베이스 스키마
├── package.json
└── tsconfig.json
서버 진입점 작성
// 새 파일: src/index.tsimport express from 'express';const app = express();const PORT = process.env.PORT ?? 3000;app.use(express.json());app.get('/health', (_req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() });});app.listen(PORT, () => { console.log(`서버가 http://localhost:${PORT} 에서 실행 중입니다.`);});export default app;
express.json() 미들웨어는 Content-Type: application/json 요청의 본문을 파싱해서 req.body에 담아줍니다. 이것이 없으면 POST/PUT 요청에서 req.body가 항상 undefined입니다.
?? 연산자는 Nullish Coalescing입니다. process.env.PORT가 undefined이거나 null일 때만 오른쪽 값을 씁니다. ||와 달리 0이나 빈 문자열은 그대로 통과시킵니다.
package.json 스크립트 설정
// 수정: package.json{ "name": "todo-api", "version": "1.0.0", "scripts": { "dev": "nodemon --watch src --ext ts --exec tsx src/index.ts", "build": "tsc", "start": "node dist/index.js", "typecheck": "tsc --noEmit" }, "dependencies": { "express": "^5.0.0" }, "devDependencies": { "@types/express": "^5.0.0", "@types/node": "^22.0.0", "nodemon": "^3.0.0", "tsx": "^4.0.0", "typescript": "^5.0.0" }}
dev 스크립트를 분석합니다.
--watch src—src폴더를 감시합니다.--ext ts—.ts파일이 변경될 때 재시작합니다.--exec tsx src/index.ts—tsx로 TypeScript 파일을 직접 실행합니다.
typecheck 스크립트는 컴파일 없이 타입 검사만 합니다. CI/CD에서 빠르게 타입 오류를 검출할 때 유용합니다.
개발 서버 실행 확인
npm run dev
터미널에 다음이 출력되면 성공입니다.
[nodemon] starting `tsx src/index.ts`
서버가 http://localhost:3000 에서 실행 중입니다.
src/index.ts 파일을 수정하고 저장하면 nodemon이 자동으로 서버를 재시작합니다. 콘솔에 [nodemon] restarting due to changes... 메시지가 보이면 핫 리로드가 동작하는 것입니다.
헬스 체크 엔드포인트를 브라우저나 curl로 확인합니다.
curl http://localhost:3000/health# {"status":"ok","timestamp":"2026-04-25T00:00:00.000Z"}
환경 변수 타이핑
process.env의 값은 항상 string | undefined입니다. 이를 그냥 쓰면 타입 오류가 생깁니다. 환경 변수를 한 곳에서 검증하고 타입을 확정하는 파일을 만들면 편합니다.
// 새 파일: src/config.tsfunction requireEnv(key: string): string { const value = process.env[key]; if (!value) { throw new Error(`환경 변수 ${key}가 설정되지 않았습니다.`); } return value;}export const config = { port: parseInt(process.env.PORT ?? '3000', 10), nodeEnv: (process.env.NODE_ENV ?? 'development') as 'development' | 'production' | 'test', databaseUrl: requireEnv('DATABASE_URL'),} as const;
requireEnv는 값이 없으면 즉시 서버를 종료합니다. 서버가 뜨는 순간 필수 환경 변수를 모두 검증하는 것이 좋습니다. 배포 후 한참 지나서 특정 기능을 쓸 때 오류가 나는 것보다, 시작 단계에서 바로 실패하는 편이 훨씬 낫습니다.
as const로 config 객체를 읽기 전용으로 만듭니다. nodeEnv는 string이 아니라 세 가지 리터럴 타입의 유니온으로 좁혀집니다.