Node.js 인터뷰 질문 53

질문: Node.js에서 TypeScript를 사용하는 방법과 그 이점은 무엇인가요?

답변:

TypeScript는 JavaScript의 상위 집합(Superset) 언어로, 정적 타입 지정과 객체 지향 프로그래밍 기능을 제공합니다. Node.js와 TypeScript를 함께 사용하면 개발 경험과 코드 품질을 크게 향상시킬 수 있습니다.

Node.js에서 TypeScript 설정 방법

1. 프로젝트 초기화 및 TypeScript 설치

# 프로젝트 폴더 생성 및 초기화
mkdir my-ts-node-app
cd my-ts-node-app
npm init -y

# TypeScript 및 Node.js 타입 정의 설치
npm install typescript ts-node @types/node --save-dev

2. TypeScript 구성 파일 (tsconfig.json) 생성

# TypeScript 구성 파일 생성
npx tsc --init

기본 생성된 파일을 프로젝트에 맞게 수정합니다:

{
  "compilerOptions": {
    "target": "ES2018",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.test.ts"]
}

3. 프로젝트 구조 설정

my-ts-node-app/
├── src/
│   └── index.ts
├── dist/         // 컴파일된 JavaScript 파일이 저장될 디렉토리
├── package.json
└── tsconfig.json

4. 스크립트 설정 (package.json)

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "ts-node src/index.js",
    "watch": "tsc -w"
  }
}

5. 첫 TypeScript 파일 작성 (src/index.ts)

// src/index.ts
import * as http from "http";

const server = http.createServer(
  (req: http.IncomingMessage, res: http.ServerResponse) => {
    res.statusCode = 200;
    res.setHeader("Content-Type", "text/plain");
    res.end("Hello, TypeScript with Node.js!");
  }
);

const port = process.env.PORT || 3000;

server.listen(port, () => {
  console.log(`Server running at http://localhost:${port}/`);
});

6. 실행 방법

개발 모드로 실행 (ts-node 사용):

npm run dev

빌드 후 실행:

npm run build
npm start

Express.js와 TypeScript 통합

Express.js는 Node.js의 가장 인기 있는 웹 프레임워크입니다. TypeScript와 함께 사용하는 방법을 살펴보겠습니다:

1. 필요한 패키지 설치

npm install express
npm install @types/express --save-dev

2. Express 애플리케이션 작성 (src/index.ts)

import express, { Request, Response, NextFunction } from "express";

// 애플리케이션 생성
const app = express();
const port = process.env.PORT || 3000;

// 미들웨어
app.use(express.json());

// 사용자 인터페이스 정의
interface User {
  id: number;
  name: string;
  email: string;
}

// 데이터 모델링
let users: User[] = [
  { id: 1, name: "홍길동", email: "hong@example.com" },
  { id: 2, name: "김철수", email: "kim@example.com" },
];

// 사용자 정의 에러 클래스
class AppError extends Error {
  statusCode: number;

  constructor(message: string, statusCode: number) {
    super(message);
    this.statusCode = statusCode;
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

// 라우트 핸들러
app.get("/users", (req: Request, res: Response) => {
  res.json(users);
});

app.get("/users/:id", (req: Request, res: Response, next: NextFunction) => {
  const id = parseInt(req.params.id);
  const user = users.find((u) => u.id === id);

  if (!user) {
    return next(new AppError("사용자를 찾을 수 없습니다", 404));
  }

  res.json(user);
});

app.post("/users", (req: Request, res: Response) => {
  const { name, email } = req.body;
  const newId = users.length > 0 ? Math.max(...users.map((u) => u.id)) + 1 : 1;

  const newUser: User = { id: newId, name, email };
  users.push(newUser);

  res.status(201).json(newUser);
});

// 에러 처리 미들웨어
app.use(
  (err: Error | AppError, req: Request, res: Response, next: NextFunction) => {
    console.error(err.stack);

    if (err instanceof AppError) {
      return res.status(err.statusCode).json({
        status: "error",
        message: err.message,
      });
    }

    res.status(500).json({
      status: "error",
      message: "서버 오류가 발생했습니다",
    });
  }
);

// 서버 시작
app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}/`);
});

TypeScript와 Node.js 모듈 패턴

1. 모듈화된 프로젝트 구조

my-ts-node-app/
├── src/
│   ├── index.ts
│   ├── controllers/
│   │   └── userController.ts
│   ├── models/
│   │   └── userModel.ts
│   ├── routes/
│   │   └── userRoutes.ts
│   └── utils/
│       └── errorHandler.ts
├── dist/
├── package.json
└── tsconfig.json

2. 모듈 작성 예시

// src/models/userModel.ts
export interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

export class UserModel {
  private users: User[] = [];

  constructor() {
    // 초기 데이터 설정
    this.users = [
      {
        id: 1,
        name: "홍길동",
        email: "hong@example.com",
        createdAt: new Date(),
      },
      {
        id: 2,
        name: "김철수",
        email: "kim@example.com",
        createdAt: new Date(),
      },
    ];
  }

  findAll(): User[] {
    return this.users;
  }

  findById(id: number): User | undefined {
    return this.users.find((user) => user.id === id);
  }

  create(userData: Omit<User, "id" | "createdAt">): User {
    const newId =
      this.users.length > 0 ? Math.max(...this.users.map((u) => u.id)) + 1 : 1;

    const newUser: User = {
      ...userData,
      id: newId,
      createdAt: new Date(),
    };

    this.users.push(newUser);
    return newUser;
  }
}
// src/controllers/userController.ts
import { Request, Response, NextFunction } from "express";
import { User, UserModel } from "../models/userModel";
import { AppError } from "../utils/errorHandler";

export class UserController {
  private userModel: UserModel;

  constructor() {
    this.userModel = new UserModel();
  }

  getAllUsers = (req: Request, res: Response): void => {
    const users = this.userModel.findAll();
    res.json(users);
  };

  getUserById = (req: Request, res: Response, next: NextFunction): void => {
    const id = parseInt(req.params.id);
    const user = this.userModel.findById(id);

    if (!user) {
      return next(new AppError("사용자를 찾을 수 없습니다", 404));
    }

    res.json(user);
  };

  createUser = (req: Request, res: Response): void => {
    const { name, email } = req.body;
    const newUser = this.userModel.create({ name, email });

    res.status(201).json(newUser);
  };
}
// src/utils/errorHandler.ts
export class AppError extends Error {
  statusCode: number;

  constructor(message: string, statusCode: number) {
    super(message);
    this.statusCode = statusCode;
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

export const errorMiddleware = (
  err: Error | AppError,
  req: any,
  res: any,
  next: any
): void => {
  console.error(err.stack);

  if (err instanceof AppError) {
    res.status(err.statusCode).json({
      status: "error",
      message: err.message,
    });
    return;
  }

  res.status(500).json({
    status: "error",
    message: "서버 오류가 발생했습니다",
  });
};
// src/routes/userRoutes.ts
import { Router } from "express";
import { UserController } from "../controllers/userController";

export const userRouter = Router();
const userController = new UserController();

userRouter.get("/", userController.getAllUsers);
userRouter.get("/:id", userController.getUserById);
userRouter.post("/", userController.createUser);
// src/index.ts
import express from "express";
import { userRouter } from "./routes/userRoutes";
import { errorMiddleware } from "./utils/errorHandler";

const app = express();
const port = process.env.PORT || 3000;

app.use(express.json());

// 라우트
app.use("/api/users", userRouter);

// 에러 처리 미들웨어
app.use(errorMiddleware);

// 서버 시작
app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}/`);
});

데이터베이스와 TypeScript 통합

Mongoose (MongoDB)와 TypeScript 통합

npm install mongoose
npm install @types/mongoose --save-dev
// src/models/userModel.ts
import mongoose, { Document, Schema } from "mongoose";

// 인터페이스 정의
export interface IUser extends Document {
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// 스키마 정의
const userSchema = new Schema<IUser>({
  name: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  createdAt: { type: Date, default: Date.now },
});

// 모델 생성 및 내보내기
export const User = mongoose.model<IUser>("User", userSchema);

TypeORM (SQL 데이터베이스)과 TypeScript 통합

npm install typeorm reflect-metadata pg
// src/entity/User.ts
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
} from "typeorm";

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column({ unique: true })
  email: string;

  @Column()
  password: string;

  @CreateDateColumn()
  createdAt: Date;
}
// src/index.ts
import "reflect-metadata";
import { createConnection } from "typeorm";
import express from "express";
import { User } from "./entity/User";

// 데이터베이스 연결
createConnection({
  type: "postgres",
  host: "localhost",
  port: 5432,
  username: "postgres",
  password: "password",
  database: "testdb",
  entities: [User],
  synchronize: true,
})
  .then(async (connection) => {
    // Express 애플리케이션 생성
    const app = express();
    app.use(express.json());

    // 사용자 추가 라우트
    app.post("/users", async (req, res) => {
      try {
        const userRepository = connection.getRepository(User);
        const newUser = userRepository.create(req.body);
        const result = await userRepository.save(newUser);
        res.status(201).json(result);
      } catch (error) {
        res.status(400).json({ error: error.message });
      }
    });

    // 사용자 목록 조회 라우트
    app.get("/users", async (req, res) => {
      const userRepository = connection.getRepository(User);
      const users = await userRepository.find();
      res.json(users);
    });

    // 서버 시작
    app.listen(3000, () => {
      console.log("Server started on port 3000");
    });
  })
  .catch((error) => console.log("TypeORM 연결 오류:", error));

TypeScript 장점과 모범 사례

Node.js에서 TypeScript를 사용하는 이점

  1. 정적 타입 체크:

    • 컴파일 타임에 타입 오류를 발견하여 런타임 오류 감소
    • 개발자의 실수를 조기에 발견하여 디버깅 시간 단축
  2. 코드 가독성과 유지보수성 향상:

    • 타입 주석을 통해 코드 의도를 명확히 표현
    • 리팩토링 시 안전성 제공
  3. 개발 도구 지원:

    • 자동 완성, 인텔리센스, 코드 탐색 등 풍부한 IDE 지원
    • API 문서 자동 생성 용이
  4. 객체 지향 프로그래밍 지원:

    • 클래스, 인터페이스, 제네릭, 열거형 등 고급 기능 지원
  5. 최신 JavaScript 기능 사용:

    • 최신 ECMAScript 기능을 이전 버전의 JavaScript로 컴파일

TypeScript 사용 모범 사례

  1. 타입 정의 최적화
// 잘못된 예: any 타입 사용
function processData(data: any): any {
  return data.map((item) => item.value);
}

// 올바른 예: 구체적인 타입 사용
interface DataItem {
  id: number;
  value: string;
}

function processData(data: DataItem[]): string[] {
  return data.map((item) => item.value);
}
  1. 인터페이스와 타입 별칭
// 객체 구조 정의에는 interface 사용 권장
interface User {
  id: number;
  name: string;
  email: string;
}

// 유니온 타입이나 기존 타입의 조합에는 type 사용 권장
type UserID = number | string;
type PartialUser = Omit<User, "id">;
  1. 유틸리티 타입 활용
interface CreateUserDTO {
  name: string;
  email: string;
  password: string;
}

// Partial: 모든 속성을 선택적으로 만듦
function updateUser(userId: number, data: Partial<CreateUserDTO>) {
  // 일부 필드만 업데이트 가능
}

// Pick: 특정 속성만 선택
type UserCredentials = Pick<CreateUserDTO, "email" | "password">;

// Omit: 특정 속성을 제외
type PublicUser = Omit<User, "password">;
  1. 비동기 코드 타입 지정
// Promise 반환 타입 명시
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error("사용자를 찾을 수 없습니다");
  }
  return response.json() as Promise<User>;
}
  1. 에러 처리
// 사용자 정의 에러 클래스
class APIError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
    this.name = "APIError";
    Object.setPrototypeOf(this, APIError.prototype);
  }
}

try {
  const result = await fetchData();
  return result;
} catch (error) {
  // 타입 가드를 사용한 에러 처리
  if (error instanceof APIError) {
    if (error.statusCode === 404) {
      return null;
    }
    throw error;
  }
  // 일반 오류 처리
  console.error("Unknown error:", error);
  throw new Error("서비스에 문제가 발생했습니다");
}

TypeScript + Node.js로 구축된 프로덕션 애플리케이션 구조

실제 프로덕션 환경에서 TypeScript와 Node.js를 사용하는 더 복잡한 프로젝트 구조를 살펴보겠습니다:

project-root/
├── src/
│   ├── config/             # 환경 설정
│   │   ├── index.ts
│   │   └── database.ts
│   ├── controllers/        # API 컨트롤러
│   │   ├── index.ts
│   │   ├── authController.ts
│   │   └── userController.ts
│   ├── middlewares/        # 미들웨어 함수
│   │   ├── auth.ts
│   │   ├── error.ts
│   │   └── validation.ts
│   ├── models/             # 데이터 모델
│   │   ├── index.ts
│   │   └── userModel.ts
│   ├── routes/             # 라우트 정의
│   │   ├── index.ts
│   │   ├── authRoutes.ts
│   │   └── userRoutes.ts
│   ├── services/           # 비즈니스 로직
│   │   ├── authService.ts
│   │   └── userService.ts
│   ├── types/              # 타입 정의
│   │   ├── index.ts
│   │   ├── auth.ts
│   │   └── user.ts
│   ├── utils/              # 유틸리티 함수
│   │   ├── logger.ts
│   │   └── errorTypes.ts
│   └── index.ts            # 애플리케이션 진입점
├── test/                   # 테스트 코드
│   ├── integration/
│   └── unit/
├── dist/                   # 컴파일된 JavaScript 파일
├── .env                    # 환경 변수
├── .env.example            # 환경 변수 예시
├── .gitignore
├── jest.config.js          # Jest 설정
├── package.json
├── tsconfig.json           # TypeScript 설정
└── README.md

Node.js + TypeScript 애플리케이션 개발 흐름

  1. 설정 및 구조화

    • 프로젝트 설정 (npm init, TypeScript 설치)
    • 적절한 프로젝트 구조 설계
    • linting, formatting 등 개발 환경 설정 (ESLint, Prettier)
  2. 타입 정의

    • 도메인 객체와 DTO 타입 정의
    • API 응답 및 요청 타입 정의
    • 서비스 인터페이스 정의
  3. 코드 작성

    • 모델 및 데이터 접근 계층 구현
    • 비즈니스 로직 서비스 구현
    • 컨트롤러 및 라우터 구현
    • 미들웨어 구현 (인증, 검증 등)
  4. 테스트

    • 단위 테스트 작성 (Jest, Mocha + Chai)
    • 통합 테스트 작성
    • 타입 검증을 통한 컴파일 타임 확인
  5. 빌드 및 배포

    • TypeScript를 JavaScript로 트랜스파일
    • 배포용 빌드 최적화
    • 지속적 통합/지속적 배포 (CI/CD) 설정

요약

Node.js에서 TypeScript를 사용하면 다음과 같은 주요 이점을 얻을 수 있습니다:

  1. 정적 타입 시스템으로 런타임 오류 감소 및 안정성 향상

  2. 개발 도구 지원 강화로 개발자 생산성 향상

  3. 코드 가독성과 유지보수성 향상으로 대규모 프로젝트 관리 용이

  4. 최신 ECMAScript 기능 조기 사용 가능

  5. 객체 지향 프로그래밍 패턴 지원으로 코드 구조화 개선

TypeScript를 Node.js 프로젝트에 도입하면 초기 설정과 학습 곡선이 있지만, 프로젝트 규모가 커지고 팀 협업이 필요한 환경에서 코드 품질과 유지보수성 측면에서 큰 이점을 제공합니다. 특히 복잡한 비즈니스 로직이나 대규모 애플리케이션 개발 시 TypeScript의 강력한 타입 시스템은 안정적인 코드베이스를 유지하는 데 큰 도움이 됩니다.

results matching ""

    No results matching ""