TypeORM 완전정복: 타입스크립트 개발자를 위한 초간단 ORM 활용 가이드 2025

콘텐츠 대표 이미지 - TypeORM 완전정복: 타입스크립트 개발자를 위한 초간단 ORM 활용 가이드 2025

 

 

안녕, 개발자 친구! 🚀 오늘은 타입스크립트 개발의 꽃이라 할 수 있는 TypeORM에 대해 함께 알아볼 거야. 2025년 현재 백엔드 개발에서 가장 핫한 ORM 도구 중 하나인 TypeORM을 마스터하면 데이터베이스 작업이 얼마나 편해지는지 곧 알게 될 거야!

📚 목차

  1. TypeORM이 뭐길래? 기본 개념 이해하기
  2. TypeORM 설치 및 프로젝트 셋업 방법
  3. 엔티티(Entity) 정의하기: 데이터베이스 테이블의 분신
  4. 관계 설정하기: 1:1, 1:N, N:M 관계 마스터하기
  5. 쿼리 빌더와 레포지토리 패턴 활용법
  6. 마이그레이션으로 데이터베이스 변경 관리하기
  7. 실전 프로젝트에 TypeORM 적용하기
  8. TypeORM의 성능 최적화 전략
  9. 2025년 TypeORM 최신 트렌드와 팁
  10. 마무리 및 다음 단계

1. TypeORM이 뭐길래? 기본 개념 이해하기 🤔

TypeORM은 타입스크립트와 자바스크립트를 위한 ORM(Object-Relational Mapping) 라이브러리야. 간단히 말하자면, 객체지향 코드와 관계형 데이터베이스 사이의 다리 역할을 해주는 도구지. 코드에서는 객체로 작업하고, 이 객체들이 자동으로 데이터베이스 테이블과 매핑되는 마법 같은 일이 일어나는 거야!

💡 ORM이란? Object-Relational Mapping의 약자로, 객체와 관계형 데이터베이스의 데이터를 자동으로 변환해주는 기술이야. SQL 쿼리를 직접 작성하는 대신 객체 지향적인 방식으로 데이터베이스를 다룰 수 있게 해줘.

TypeORM의 주요 특징

  1. 타입스크립트 지원 - 타입 안전성을 제공해서 개발 시 많은 실수를 방지할 수 있어 👍
  2. 다양한 데이터베이스 지원 - MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, MongoDB까지!
  3. 스키마 마이그레이션 - 데이터베이스 스키마 변경을 코드로 관리할 수 있어
  4. 관계 관리 - 1:1, 1:N, N:M 등 복잡한 관계도 쉽게 정의하고 관리
  5. 트랜잭션 지원 - 데이터 일관성을 유지하기 위한 트랜잭션 처리 가능
TypeScript 객체 User Post 데이터베이스 users posts TypeORM

TypeORM을 사용하면 SQL 쿼리를 직접 작성하는 대신 타입스크립트 코드만으로 데이터베이스 작업을 할 수 있어. 이게 얼마나 편한지는 써봐야 알 수 있어! 특히 프론트엔드 개발자 출신이라면 SQL에 익숙하지 않더라도 쉽게 데이터베이스 작업을 할 수 있다는 큰 장점이 있지.

요즘 재능넷과 같은 플랫폼에서도 백엔드 개발 재능을 공유할 때 TypeORM 같은 최신 기술 스택을 활용한 프로젝트가 인기가 많아. 왜냐하면 생산성이 엄청나게 향상되거든! 🚀

2. TypeORM 설치 및 프로젝트 셋업 방법 🛠️

자, 이제 본격적으로 TypeORM을 설치하고 프로젝트를 셋업해볼 거야. 2025년 기준 최신 방법으로 알려줄게!

기본 설치

먼저 npm이나 yarn을 사용해서 TypeORM과 필요한 의존성을 설치해야 해:

npm install typeorm reflect-metadata @types/node

그리고 사용할 데이터베이스 드라이버도 설치해야 해. 가장 많이 사용하는 몇 가지 예시를 들어볼게:

# MySQL 사용 시
npm install mysql2

# PostgreSQL 사용 시
npm install pg

# SQLite 사용 시
npm install sqlite3

tsconfig.json 설정

TypeORM을 제대로 사용하려면 타입스크립트 설정도 약간 손봐야 해:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "sourceMap": true,
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  }
}

emitDecoratorMetadata와 experimentalDecorators 옵션은 TypeORM의 데코레이터 기능을 사용하기 위해 꼭 필요해. 이 부분 빼먹으면 나중에 엄청 헤매게 될 거야! 😅

데이터베이스 연결 설정

이제 TypeORM에서 데이터베이스 연결을 설정해볼게. 2025년 현재는 두 가지 방식이 많이 사용돼:

1. data-source.ts 파일 사용 (권장)

// data-source.ts
import { DataSource } from "typeorm";

export const AppDataSource = new DataSource({
  type: "mysql",
  host: "localhost",
  port: 3306,
  username: "root",
  password: "password",
  database: "test_db",
  synchronize: true, // 개발 환경에서만 true로 설정!
  logging: true,
  entities: ["src/entity/**/*.ts"],
  migrations: ["src/migration/**/*.ts"],
  subscribers: ["src/subscriber/**/*.ts"],
});

⚠️ 주의사항: synchronize: true는 개발 환경에서만 사용하고, 프로덕션에서는 절대 사용하지 마! 데이터가 날아갈 수 있어. 프로덕션에서는 마이그레이션을 사용해야 해.

2. ormconfig.json 파일 사용 (레거시 방식)

// ormconfig.json
{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "root",
  "password": "password",
  "database": "test_db",
  "synchronize": true,
  "logging": true,
  "entities": ["src/entity/**/*.ts"],
  "migrations": ["src/migration/**/*.ts"],
  "subscribers": ["src/subscriber/**/*.ts"],
  "cli": {
    "entitiesDir": "src/entity",
    "migrationsDir": "src/migration",
    "subscribersDir": "src/subscriber"
  }
}

2025년 현재는 첫 번째 방식인 DataSource API를 사용하는 것이 권장돼. 더 타입 안전하고 유연한 설정이 가능하거든!

애플리케이션에서 연결 초기화

이제 애플리케이션이 시작될 때 데이터베이스 연결을 초기화하는 코드를 작성해볼게:

// index.ts
import "reflect-metadata";
import { AppDataSource } from "./data-source";

// 데이터베이스 연결 초기화
AppDataSource.initialize()
  .then(() => {
    console.log("데이터베이스 연결 성공! 🎉");
    // 여기서 Express 서버 등을 시작할 수 있어
  })
  .catch((error) => console.log("데이터베이스 연결 실패 😢", error));
TypeORM 프로젝트 구조 프로젝트 폴더 📄 package.json 📄 tsconfig.json 📄 data-source.ts 📁 src/ 📁 entity/ 📁 migration/ 데이터베이스 연결 흐름 data-source.ts index.ts 데이터베이스 Entity 클래스

이렇게 기본적인 TypeORM 설정이 완료됐어! 이제 본격적으로 엔티티를 정의하고 데이터베이스를 다루는 방법을 알아볼게. 😎

3. 엔티티(Entity) 정의하기: 데이터베이스 테이블의 분신 🧩

TypeORM에서 가장 중요한 개념 중 하나가 바로 엔티티(Entity)야. 엔티티는 데이터베이스 테이블과 매핑되는 클래스로, 테이블의 구조를 코드로 표현한 거라고 생각하면 돼.

기본 엔티티 만들기

간단한 User 엔티티를 만들어볼게:

// src/entity/User.ts
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

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

    @Column()
    firstName: string;

    @Column()
    lastName: string;

    @Column()
    age: number;

    @Column({ default: true })
    isActive: boolean;
}

이 코드는 다음과 같은 SQL 테이블을 자동으로 생성해:

CREATE TABLE "user" (
    "id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    "firstName" character varying NOT NULL,
    "lastName" character varying NOT NULL,
    "age" integer NOT NULL,
    "isActive" boolean NOT NULL DEFAULT true
);

정말 마법 같지? 😲 코드만 작성했는데 데이터베이스 테이블이 자동으로 생성되다니!

데코레이터 이해하기

TypeORM에서는 데코레이터(@)를 사용해서 메타데이터를 클래스와 프로퍼티에 추가해. 주요 데코레이터를 알아보자:

  1. @Entity() - 클래스가 엔티티임을 나타내는 데코레이터
  2. @PrimaryGeneratedColumn() - 자동 증가하는 기본 키 열
  3. @Column() - 일반 테이블 열
  4. @CreateDateColumn() - 생성 날짜를 자동으로 설정
  5. @UpdateDateColumn() - 업데이트 날짜를 자동으로 설정

열(Column) 옵션 설정하기

@Column 데코레이터에는 다양한 옵션을 설정할 수 있어:

// 다양한 열 옵션 예시
@Column({ type: "varchar", length: 100, nullable: false })
name: string;

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

@Column({ type: "text" })
description: string;

@Column({ type: "decimal", precision: 10, scale: 2, default: 0 })
price: number;

@Column({ enum: ["admin", "editor", "user"], default: "user" })
role: string;

2025년 현재 TypeORM은 더 많은 데이터 타입과 옵션을 지원하고 있어. 특히 JSON 타입이나 배열 타입도 쉽게 사용할 수 있지!

엔티티 이름 커스터마이징

기본적으로 엔티티 이름은 클래스 이름을 따라가지만, 원하는 테이블 이름을 직접 지정할 수도 있어:

@Entity("users") // 테이블 이름을 'users'로 지정
export class User {
    // ...
}

인덱스와 유니크 제약 조건

데이터베이스 성능을 위한 인덱스와 유니크 제약 조건도 쉽게 설정할 수 있어:

import { Entity, PrimaryGeneratedColumn, Column, Index, Unique } from "typeorm";

@Entity()
@Index(["firstName", "lastName"]) // 복합 인덱스
@Unique(["email"]) // 유니크 제약 조건
export class User {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

    @Column()
    @Index() // 단일 컬럼 인덱스
    email: string;
}
엔티티와 데이터베이스 테이블 매핑 TypeScript 엔티티 클래스 @Entity() class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() age: number; } 데이터베이스 테이블 id 1 2 3 4 name "John" "Alice" "Bob" "Emma" age 28 24 32 22 TypeORM

실전 엔티티 예제: 블로그 시스템

좀 더 실전적인 예제로 블로그 시스템의 Post 엔티티를 만들어볼게:

// src/entity/Post.ts
import { 
    Entity, 
    PrimaryGeneratedColumn, 
    Column, 
    CreateDateColumn, 
    UpdateDateColumn,
    Index
} from "typeorm";

@Entity("posts")
export class Post {
    @PrimaryGeneratedColumn("uuid")
    id: string;

    @Column({ length: 100 })
    @Index()
    title: string;

    @Column("text")
    content: string;

    @Column({ type: "simple-array", nullable: true })
    tags: string[];

    @Column({ type: "int", default: 0 })
    viewCount: number;

    @Column({ default: false })
    published: boolean;

    @CreateDateColumn()
    createdAt: Date;

    @UpdateDateColumn()
    updatedAt: Date;
}

이 예제에서는 UUID 기본 키, 텍스트 컬럼, 배열 타입, 기본값, 자동 날짜 설정 등 다양한 기능을 사용했어. 이런 기능들을 활용하면 복잡한 데이터 모델도 쉽게 구현할 수 있지!

엔티티를 잘 설계하는 것이 TypeORM 사용의 핵심이야. 다음 섹션에서는 이런 엔티티들 간의 관계를 어떻게 설정하는지 알아볼게! 🔄

4. 관계 설정하기: 1:1, 1:N, N:M 관계 마스터하기 🔗

데이터베이스에서 가장 중요한 개념 중 하나가 바로 테이블 간의 관계야. TypeORM에서는 이런 관계를 아주 직관적으로 설정할 수 있어. 2025년 현재 TypeORM은 관계 설정에 있어서 더욱 강력해졌어! 😎

1. 일대일(One-to-One) 관계

일대일 관계는 A 테이블의 한 레코드가 B 테이블의 딱 하나의 레코드와 연결되는 관계야. 예를 들어, 사용자와 프로필 정보의 관계가 이에 해당해.

// src/entity/User.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from "typeorm";
import { Profile } from "./Profile";

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

    @Column()
    name: string;

    @OneToOne(() => Profile, (profile) => profile.user)
    @JoinColumn() // 관계의 소유자 쪽에만 JoinColumn 데코레이터를 추가해
    profile: Profile;
}

// src/entity/Profile.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne } from "typeorm";
import { User } from "./User";

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

    @Column()
    gender: string;

    @Column()
    photo: string;

    @OneToOne(() => User, (user) => user.profile)
    user: User;
}

@JoinColumn() 데코레이터는 관계의 소유자 쪽에 추가해. 이 데코레이터가 있는 엔티티의 테이블에 외래 키 컬럼이 생성돼.

2. 일대다(One-to-Many) / 다대일(Many-to-One) 관계

일대다 관계는 A 테이블의 한 레코드가 B 테이블의 여러 레코드와 연결되는 관계야. 예를 들어, 한 명의 사용자가 여러 개의 게시글을 작성하는 경우가 이에 해당해.

// src/entity/User.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
import { Post } from "./Post";

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

    @Column()
    name: string;

    @OneToMany(() => Post, (post) => post.author)
    posts: Post[];
}

// src/entity/Post.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm";
import { User } from "./User";

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

    @Column()
    title: string;

    @Column()
    content: string;

    @ManyToOne(() => User, (user) => user.posts)
    author: User;
}

일대다 관계에서는 @OneToMany와 @ManyToOne을 함께 사용해. 외래 키는 항상 "다" 쪽(여기서는 Post)에 생성돼.

3. 다대다(Many-to-Many) 관계

다대다 관계는 A 테이블의 여러 레코드가 B 테이블의 여러 레코드와 연결되는 관계야. 예를 들어, 게시글과 태그의 관계가 이에 해당해. 한 게시글은 여러 태그를 가질 수 있고, 한 태그는 여러 게시글에 사용될 수 있지.

// src/entity/Post.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from "typeorm";
import { Tag } from "./Tag";

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

    @Column()
    title: string;

    @Column()
    content: string;

    @ManyToMany(() => Tag, (tag) => tag.posts)
    @JoinTable() // 관계의 소유자 쪽에만 JoinTable 데코레이터를 추가해
    tags: Tag[];
}

// src/entity/Tag.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm";
import { Post } from "./Post";

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

    @Column()
    name: string;

    @ManyToMany(() => Post, (post) => post.tags)
    posts: Post[];
}

다대다 관계에서는 @JoinTable() 데코레이터를 관계의 소유자 쪽에 추가해. 이 데코레이터는 두 테이블을 연결하는 중간 테이블을 자동으로 생성해줘.

TypeORM 관계 유형 일대일 (One-to-One) User Profile 일대다 (One-to-Many) User Post 1 Post 2 Post 3 다대다 (Many-to-Many) Post 1 Post 2 Tag 1 Tag 2 관계 코드 예시 // 일대일 관계 @OneToOne(() => Profile) @JoinColumn() profile: Profile; // 일대다 관계 @OneToMany(() => Post, post => post.author) posts: Post[]; // 다대일: @ManyToOne(() => User, user => user.posts) // 다대다 관계 @ManyToMany(() => Tag, tag => tag.posts) @JoinTable() tags: Tag[];

관계 옵션 설정하기

TypeORM에서는 관계에 다양한 옵션을 설정할 수 있어:

// 다양한 관계 옵션 예시
@ManyToOne(() => User, (user) => user.posts, {
    eager: true, // 항상 관련 엔티티를 함께 로드
    cascade: true, // 관련 엔티티도 함께 저장/업데이트/삭제
    onDelete: "CASCADE", // 부모 엔티티 삭제 시 자식도 삭제
    nullable: false // NOT NULL 제약 조건 추가
})
author: User;

💡 eager 로딩은 항상 관련 엔티티를 함께 로드하지만, 성능에 영향을 줄 수 있어. 필요한 경우에만 사용하는 것이 좋아. 대신 relations 옵션을 사용해서 필요할 때만 관련 엔티티를 로드하는 것이 더 효율적이야.

실전 관계 예제: 블로그 시스템

이제 좀 더 복잡한 블로그 시스템의 관계를 구현해볼게:

// src/entity/User.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, OneToOne, JoinColumn } from "typeorm";
import { Post } from "./Post";
import { Profile } from "./Profile";

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

    @Column()
    username: string;

    @Column()
    email: string;

    @OneToOne(() => Profile)
    @JoinColumn()
    profile: Profile;

    @OneToMany(() => Post, (post) => post.author)
    posts: Post[];
}

// src/entity/Post.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, ManyToMany, JoinTable, CreateDateColumn } from "typeorm";
import { User } from "./User";
import { Category } from "./Category";
import { Tag } from "./Tag";

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

    @Column()
    title: string;

    @Column("text")
    content: string;

    @CreateDateColumn()
    createdAt: Date;

    @ManyToOne(() => User, (user) => user.posts, { onDelete: "CASCADE" })
    author: User;

    @ManyToOne(() => Category, (category) => category.posts)
    category: Category;

    @ManyToMany(() => Tag, (tag) => tag.posts)
    @JoinTable()
    tags: Tag[];
}

// src/entity/Category.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
import { Post } from "./Post";

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

    @Column()
    name: string;

    @OneToMany(() => Post, (post) => post.category)
    posts: Post[];
}

// src/entity/Tag.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm";
import { Post } from "./Post";

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

    @Column()
    name: string;

    @ManyToMany(() => Post, (post) => post.tags)
    posts: Post[];
}

이 예제에서는 사용자-프로필(1:1), 사용자-게시글(1:N), 카테고리-게시글(1:N), 게시글-태그(N:M) 등 다양한 관계를 구현했어. 이런 관계들을 잘 설계하면 복잡한 애플리케이션도 효율적으로 구현할 수 있어!

관계 설정은 TypeORM의 가장 강력한 기능 중 하나야. 특히 재능넷과 같은 복잡한 플랫폼을 개발할 때 이런 관계 설정이 얼마나 중요한지 알 수 있지. 다음 섹션에서는 이렇게 설정한 엔티티와 관계를 이용해 실제로 데이터를 조작하는 방법을 알아볼게! 💪

5. 쿼리 빌더와 레포지토리 패턴 활용법 🔍

이제 TypeORM에서 데이터를 조회하고 조작하는 방법을 알아볼 차례야. TypeORM은 레포지토리 패턴쿼리 빌더라는 두 가지 주요 방식을 제공해. 2025년 현재는 이 두 방식을 적절히 조합해서 사용하는 것이 트렌드야! 👍

레포지토리 패턴

레포지토리 패턴은 엔티티에 대한 기본적인 CRUD(Create, Read, Update, Delete) 작업을 추상화한 인터페이스를 제공해. TypeORM에서는 각 엔티티마다 자동으로 레포지토리가 생성돼.

레포지토리 가져오기

// 레포지토리 가져오기
import { AppDataSource } from "./data-source";
import { User } from "./entity/User";

// 엔티티 레포지토리 가져오기
const userRepository = AppDataSource.getRepository(User);

기본 CRUD 작업

// 1. 생성 (Create)
const user = new User();
user.firstName = "John";
user.lastName = "Doe";
user.age = 25;
await userRepository.save(user);

// 2. 조회 (Read)
// 모든 사용자 조회
const allUsers = await userRepository.find();

// ID로 단일 사용자 조회
const user = await userRepository.findOneBy({ id: 1 });

// 조건으로 사용자 조회
const youngUsers = await userRepository.findBy({ age: LessThan(30) });

// 3. 업데이트 (Update)
user.age = 26;
await userRepository.save(user);

// 4. 삭제 (Delete)
await userRepository.remove(user);

// ID로 직접 삭제
await userRepository.delete(1);

관계 데이터 로드하기

// 관계 데이터 함께 로드하기
const userWithPosts = await userRepository.findOne({
    where: { id: 1 },
    relations: {
        posts: true,
        profile: true
    }
});

// 중첩 관계 로드하기
const userWithNestedRelations = await userRepository.findOne({
    where: { id: 1 },
    relations: {
        posts: {
            category: true,
            tags: true
        },
        profile: true
    }
});

2025년 현재 TypeORM은 중첩 관계 로드를 더욱 직관적으로 지원해. 이전 버전보다 훨씬 편리해졌어!

쿼리 빌더

쿼리 빌더는 좀 더 복잡한 쿼리를 체이닝 방식으로 구성할 수 있게 해줘. SQL과 비슷하지만 타입스크립트로 작성할 수 있어서 타입 안전성이 보장돼.

// 기본 쿼리 빌더 사용법
const users = await userRepository
    .createQueryBuilder("user")
    .where("user.age > :age", { age: 25 })
    .orderBy("user.firstName", "ASC")
    .getMany();

// JOIN 사용하기
const usersWithPosts = await userRepository
    .createQueryBuilder("user")
    .leftJoinAndSelect("user.posts", "post")
    .where("post.title LIKE :title", { title: "%TypeORM%" })
    .getMany();

// 집계 함수 사용하기
const userStats = await userRepository
    .createQueryBuilder("user")
    .select("user.age", "age")
    .addSelect("COUNT(user.id)", "count")
    .groupBy("user.age")
    .getRawMany();

// 서브쿼리 사용하기
const usersWithPostCount = await userRepository
    .createQueryBuilder("user")
    .leftJoinAndSelect("user.posts", "post")
    .loadRelationCountAndMap("user.postCount", "user.posts")
    .getMany();
TypeORM 데이터 접근 방식 비교 레포지토리 패턴 // 레포지토리 가져오기 const userRepo = AppDataSource .getRepository(User); // 데이터 조회 const users = await userRepo.find({ where: { age: MoreThan(25) }, relations: { posts: true } }); 쿼리 빌더 // 쿼리 빌더 사용 const users = await userRepo .createQueryBuilder("user") .where("user.age > :age", { age: 25 }) .leftJoinAndSelect("user.posts", "post") .orderBy("user.firstName", "ASC") .skip(10).take(5) .getMany(); 간단한 쿼리 복잡한 쿼리

고급 쿼리 기법

2025년 현재 TypeORM에서는 더 강력한 쿼리 기능들이 추가됐어. 몇 가지 유용한 기법을 소개할게:

1. 페이지네이션

// 페이지네이션 구현하기
const pageSize = 10;
const page = 2; // 2페이지 (0부터 시작)

const [posts, total] = await postRepository.findAndCount({
    skip: page * pageSize,
    take: pageSize,
    order: { createdAt: "DESC" }
});

// 또는 쿼리 빌더 사용
const [posts, total] = await postRepository
    .createQueryBuilder("post")
    .orderBy("post.createdAt", "DESC")
    .skip(page * pageSize)
    .take(pageSize)
    .getManyAndCount();

2. 트랜잭션 사용하기

// 트랜잭션 사용하기
await AppDataSource.transaction(async (transactionalEntityManager) => {
    // 트랜잭션 내에서 여러 작업 수행
    const user = new User();
    user.name = "John";
    await transactionalEntityManager.save(user);
    
    const post = new Post();
    post.title = "Hello TypeORM";
    post.author = user;
    await transactionalEntityManager.save(post);
    
    // 에러가 발생하면 모든 작업이 롤백됨
});

3. 커스텀 레포지토리

2025년에는 커스텀 레포지토리를 만드는 방식이 더 간편해졌어:

// src/repository/UserRepository.ts
import { AppDataSource } from "../data-source";
import { User } from "../entity/User";

export const UserRepository = AppDataSource.getRepository(User).extend({
    findByEmail(email: string) {
        return this.findOneBy({ email });
    },
    
    async findActiveUsersWithPosts() {
        return this.createQueryBuilder("user")
            .where("user.isActive = :isActive", { isActive: true })
            .leftJoinAndSelect("user.posts", "post")
            .getMany();
    }
});

// 사용 방법
import { UserRepository } from "./repository/UserRepository";

const user = await UserRepository.findByEmail("test@example.com");
const activeUsers = await UserRepository.findActiveUsersWithPosts();

💡 팁: 복잡한 프로젝트에서는 커스텀 레포지토리를 사용하면 코드 재사용성과 가독성이 크게 향상돼. 특히 비즈니스 로직이 복잡한 경우에 유용해!

4. 고급 필터링과 정렬

import { 
    Between, 
    Like, 
    In, 
    IsNull, 
    Not, 
    MoreThan, 
    LessThan 
} from "typeorm";

// 다양한 조건으로 필터링하기
const users = await userRepository.find({
    where: {
        // AND 조건
        firstName: Like("%John%"),
        age: Between(25, 35),
        role: In(["admin", "editor"]),
        deletedAt: IsNull(),
        
        // OR 조건
        // TypeORM 0.3.0부터 지원
        // 2025년에는 더 개선된 문법 지원
        or: [
            { lastName: Like("%Doe%") },
            { email: Like("%example.com%") }
        ]
    },
    order: {
        createdAt: "DESC",
        firstName: "ASC"
    }
});

실전 예제: 블로그 API 구현

이제 앞서 정의한 블로그 시스템의 엔티티를 이용해 실제 API를 구현해볼게:

// src/service/PostService.ts
import { AppDataSource } from "../data-source";
import { Post } from "../entity/Post";
import { User } from "../entity/User";
import { Tag } from "../entity/Tag";

export class PostService {
    private postRepository = AppDataSource.getRepository(Post);
    private userRepository = AppDataSource.getRepository(User);
    private tagRepository = AppDataSource.getRepository(Tag);
    
    // 게시글 목록 조회 (페이지네이션, 필터링, 정렬 지원)
    async getPosts(options: {
        page?: number;
        limit?: number;
        search?: string;
        categoryId?: number;
        tagIds?: number[];
        orderBy?: string;
        order?: "ASC" | "DESC";
    }) {
        const { 
            page = 0, 
            limit = 10, 
            search, 
            categoryId, 
            tagIds, 
            orderBy = "createdAt", 
            order = "DESC" 
        } = options;
        
        const queryBuilder = this.postRepository
            .createQueryBuilder("post")
            .leftJoinAndSelect("post.author", "author")
            .leftJoinAndSelect("post.category", "category")
            .leftJoinAndSelect("post.tags", "tag");
            
        // 검색 조건 추가
        if (search) {
            queryBuilder.andWhere(
                "(post.title LIKE :search OR post.content LIKE :search)",
                { search: `%${search}%` }
            );
        }
        
        // 카테고리 필터링
        if (categoryId) {
            queryBuilder.andWhere("category.id = :categoryId", { categoryId });
        }
        
        // 태그 필터링
        if (tagIds && tagIds.length > 0) {
            queryBuilder.andWhere("tag.id IN (:...tagIds)", { tagIds });
        }
        
        // 정렬
        queryBuilder.orderBy(`post.${orderBy}`, order);
        
        // 페이지네이션
        queryBuilder.skip(page * limit).take(limit);
        
        // 결과 조회
        const [posts, total] = await queryBuilder.getManyAndCount();
        
        return {
            data: posts,
            meta: {
                total,
                page,
                limit,
                pageCount: Math.ceil(total / limit)
            }
        };
    }
    
    // 게시글 상세 조회
    async getPostById(id: number) {
        const post = await this.postRepository.findOne({
            where: { id },
            relations: {
                author: true,
                category: true,
                tags: true
            }
        });
        
        if (!post) {
            throw new Error("Post not found");
        }
        
        // 조회수 증가
        post.viewCount += 1;
        await this.postRepository.save(post);
        
        return post;
    }
    
    // 게시글 생성
    async createPost(data: {
        title: string;
        content: string;
        authorId: number;
        categoryId: number;
        tagIds: number[];
    }) {
        const { title, content, authorId, categoryId, tagIds } = data;
        
        // 트랜잭션 사용
        return await AppDataSource.transaction(async (manager) => {
            // 작성자 조회
            const author = await manager.findOneBy(User, { id: authorId });
            if (!author) {
                throw new Error("Author not found");
            }
            
            // 태그 조회
            const tags = await manager.findBy(Tag, { id: In(tagIds) });
            
            // 게시글 생성
            const post = new Post();
            post.title = title;
            post.content = content;
            post.author = author;
            post.category = { id: categoryId } as any; // 간단한 참조
            post.tags = tags;
            
            return await manager.save(post);
        });
    }
    
    // 게시글 수정
    async updatePost(id: number, data: {
        title?: string;
        content?: string;
        categoryId?: number;
        tagIds?: number[];
    }) {
        const { title, content, categoryId, tagIds } = data;
        
        // 기존 게시글 조회
        const post = await this.postRepository.findOne({
            where: { id },
            relations: { tags: true }
        });
        
        if (!post) {
            throw new Error("Post not found");
        }
        
        // 데이터 업데이트
        if (title) post.title = title;
        if (content) post.content = content;
        if (categoryId) post.category = { id: categoryId } as any;
        
        // 태그 업데이트
        if (tagIds) {
            const tags = await this.tagRepository.findBy({ id: In(tagIds) });
            post.tags = tags;
        }
        
        return await this.postRepository.save(post);
    }
    
    // 게시글 삭제
    async deletePost(id: number) {
        const post = await this.postRepository.findOneBy({ id });
        
        if (!post) {
            throw new Error("Post not found");
        }
        
        await this.postRepository.remove(post);
        return { success: true };
    }
}

이 예제는 실제 프로젝트에서 사용할 수 있는 완전한 CRUD 서비스를 구현한 거야. 레포지토리 패턴과 쿼리 빌더를 적절히 조합해서 복잡한 비즈니스 로직을 구현했어. 특히 트랜잭션을 사용해서 데이터 일관성을 유지하는 부분이 중요해!

이런 방식으로 재능넷과 같은 플랫폼의 백엔드 API를 구현할 수 있어. 다음 섹션에서는 데이터베이스 스키마 변경을 관리하는 마이그레이션에 대해 알아볼게! 🚀

6. 마이그레이션으로 데이터베이스 변경 관리하기 📊

프로젝트가 발전함에 따라 데이터베이스 스키마도 변경되기 마련이야. TypeORM의 마이그레이션 기능을 사용하면 이런 변경사항을 코드로 관리하고 버전 관리할 수 있어. 2025년 현재는 마이그레이션이 더욱 강력해졌어! 🔄

마이그레이션이 필요한 이유

  1. 버전 관리 - 데이터베이스 스키마 변경을 Git과 같은 버전 관리 시스템으로 추적 가능
  2. 팀 협업 - 여러 개발자가 동일한 데이터베이스 스키마로 작업 가능
  3. 배포 자동화 - CI/CD 파이프라인에서 데이터베이스 변경 자동화 가능
  4. 롤백 - 문제 발생 시 이전 상태로 되돌릴 수 있음
  5. 프로덕션 안전성 - synchronize: true 옵션 대신 안전하게 스키마 변경 가능

⚠️ 주의사항: 프로덕션 환경에서는 절대로 synchronize: true 옵션을 사용하지 마! 데이터가 손실될 수 있어. 대신 마이그레이션을 사용해야 해.

마이그레이션 설정

마이그레이션을 사용하기 위해 data-source.ts 파일에 마이그레이션 설정을 추가해야 해:

// data-source.ts
import { DataSource } from "typeorm";

export const AppDataSource = new DataSource({
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "root",
    password: "password",
    database: "test_db",
    synchronize: false, // 마이그레이션 사용 시 false로 설정
    logging: true,
    entities: ["src/entity/**/*.ts"],
    migrations: ["src/migration/**/*.ts"],
    subscribers: ["src/subscriber/**/*.ts"],
});

마이그레이션 명령어

TypeORM CLI를 사용해 마이그레이션 관련 작업을 수행할 수 있어. 2025년 현재는 더 편리한 명령어가 추가됐어:

# package.json에 스크립트 추가
{
  "scripts": {
    "typeorm": "typeorm-ts-node-commonjs",
    "migration:generate": "npm run typeorm migration:generate -- -d ./src/data-source.ts",
    "migration:run": "npm run typeorm migration:run -- -d ./src/data-source.ts",
    "migration:revert": "npm run typeorm migration:revert -- -d ./src/data-source.ts"
  }
}

마이그레이션 생성하기

엔티티를 변경한 후 마이그레이션을 생성하는 방법이야:

# 마이그레이션 자동 생성
npm run migration:generate src/migration/AddUserEmailColumn

# 또는 직접 빈 마이그레이션 파일 생성
npm run typeorm migration:create src/migration/CustomMigration

자동 생성된 마이그레이션 파일은 다음과 같이 생겼어:

// src/migration/1714725600000-AddUserEmailColumn.ts
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddUserEmailColumn1714725600000 implements MigrationInterface {
    name = 'AddUserEmailColumn1714725600000'

    public async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`ALTER TABLE "user" ADD "email" varchar(255) NOT NULL`);
        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e12875dfb3b1d92d7d7c5377e2" ON "user" ("email")`);
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`DROP INDEX "IDX_e12875dfb3b1d92d7d7c5377e2" ON "user"`);
        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "email"`);
    }
}</void></void>

마이그레이션 파일에는 up()down() 두 개의 메서드가 있어. up()은 마이그레이션을 적용할 때 실행되고, down()은 마이그레이션을 되돌릴 때 실행돼.

마이그레이션 실행 및 되돌리기

# 마이그레이션 실행
npm run migration:run

# 마이그레이션 되돌리기 (가장 최근 마이그레이션 1개 되돌림)
npm run migration:revert
TypeORM 마이그레이션 흐름 1 2 3 4 5 초기 스키마 사용자 테이블 추가 이메일 컬럼 추가 인덱스 추가 외래 키 추가 마이그레이션 실행 과정 1. 엔티티 변경 2. 마이그레이션 생성 3. 마이그레이션 검토 4. 마이그레이션 실행 마이그레이션 명령어 $ npm run migration:generate src/migration/AddEmailColumn $ npm run migration:run $ npm run migration:revert $ npm run typeorm migration:show

수동으로 마이그레이션 작성하기

때로는 자동 생성된 마이그레이션만으로는 부족할 수 있어. 복잡한 데이터 변환이나 초기 데이터 삽입 등의 작업은 수동으로 마이그레이션을 작성해야 해:

// src/migration/1714725700000-SeedInitialData.ts
import { MigrationInterface, QueryRunner } from "typeorm";

export class SeedInitialData1714725700000 implements MigrationInterface {
    public async up(queryRunner: QueryRunner): Promise<void> {
        // 카테고리 초기 데이터 삽입
        await queryRunner.query(`
            INSERT INTO category (name) VALUES 
            ('Technology'), 
            ('Programming'), 
            ('Design'), 
            ('Business')
        `);
        
        // 태그 초기 데이터 삽입
        await queryRunner.query(`
            INSERT INTO tag (name) VALUES 
            ('TypeScript'), 
            ('JavaScript'), 
            ('React'), 
            ('Node.js'), 
            ('TypeORM')
        `);
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
        // 삽입한 데이터 삭제
        await queryRunner.query(`DELETE FROM tag`);
        await queryRunner.query(`DELETE FROM category`);
    }
}</void></void>

마이그레이션 상태 확인

현재 적용된 마이그레이션과 대기 중인 마이그레이션을 확인할 수 있어:

# 마이그레이션 상태 확인
npm run typeorm migration:show -- -d ./src/data-source.ts

마이그레이션 모범 사례

  1. 작은 단위로 나누기 - 하나의 마이그레이션에 너무 많은 변경사항을 포함하지 마
  2. 항상 down 메서드 구현하기 - 롤백이 가능하도록 down 메서드를 꼼꼼히 구현해
  3. 마이그레이션 테스트하기 - 프로덕션에 적용하기 전에 개발/스테이징 환경에서 테스트해
  4. 데이터 마이그레이션 주의하기 - 대량의 데이터를 변환할 때는 성능을 고려해
  5. 마이그레이션 파일 수정 금지 - 이미 적용된 마이그레이션 파일은 절대 수정하지 마

💡 팁: 2025년 현재는 TypeORM에서 마이그레이션 자동 생성 기능이 더욱 똑똑해져서 복잡한 변경사항도 잘 감지해. 하지만 항상 자동 생성된 마이그레이션을 검토하는 습관을 들이는 것이 좋아!

실전 마이그레이션 시나리오

실제 프로젝트에서 마이그레이션을 어떻게 활용하는지 시나리오를 통해 알아보자:

시나리오 1: 새로운 필드 추가

User 엔티티에 phoneNumber 필드를 추가하고 싶다고 가정해보자:

// User 엔티티 수정
@Entity()
export class User {
    // 기존 필드들...
    
    @Column({ nullable: true })
    phoneNumber: string;
}

이제 마이그레이션을 생성하고 실행해:

$ npm run migration:generate src/migration/AddUserPhoneNumber
$ npm run migration:run

시나리오 2: 테이블 이름 변경

Post 엔티티의 테이블 이름을 'post'에서 'blog_post'로 변경하고 싶다면:

// Post 엔티티 수정
@Entity('blog_post')
export class Post {
    // 필드들...
}

마이그레이션 생성 및 실행:

$ npm run migration:generate src/migration/RenamePostTable
$ npm run migration:run

시나리오 3: 복잡한 데이터 마이그레이션

사용자의 이름 필드를 firstName과 lastName으로 분리하는 경우:

// 수동 마이그레이션 작성
import { MigrationInterface, QueryRunner } from "typeorm";

export class SplitUserNameField1714725800000 implements MigrationInterface {
    public async up(queryRunner: QueryRunner): Promise<void> {
        // 새 컬럼 추가
        await queryRunner.query(`ALTER TABLE "user" ADD "firstName" varchar(255)`);
        await queryRunner.query(`ALTER TABLE "user" ADD "lastName" varchar(255)`);
        
        // 기존 데이터 마이그레이션
        await queryRunner.query(`
            UPDATE "user"
            SET 
                "firstName" = SUBSTRING_INDEX(name, ' ', 1),
                "lastName" = SUBSTRING_INDEX(name, ' ', -1)
        `);
        
        // 새 컬럼을 NOT NULL로 설정
        await queryRunner.query(`ALTER TABLE "user" MODIFY "firstName" varchar(255) NOT NULL`);
        await queryRunner.query(`ALTER TABLE "user" MODIFY "lastName" varchar(255) NOT NULL`);
        
        // 기존 컬럼 삭제
        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "name"`);
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
        // name 컬럼 다시 추가
        await queryRunner.query(`ALTER TABLE "user" ADD "name" varchar(255)`);
        
        // 데이터 복원
        await queryRunner.query(`
            UPDATE "user"
            SET "name" = CONCAT("firstName", ' ', "lastName")
        `);
        
        // name을 NOT NULL로 설정
        await queryRunner.query(`ALTER TABLE "user" MODIFY "name" varchar(255) NOT NULL`);
        
        // 분리된 컬럼 삭제
        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "firstName"`);
        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "lastName"`);
    }
}</void></void>

마이그레이션은 데이터베이스 스키마 변경을 안전하게 관리하는 필수 도구야. 특히 팀 프로젝트나 프로덕션 환경에서는 더욱 중요해. 재능넷과 같은 실제 서비스를 운영할 때 마이그레이션을 활용하면 데이터베이스 변경을 체계적으로 관리할 수 있어! 🚀

7. 실전 프로젝트에 TypeORM 적용하기 💼

이제 TypeORM의 기본 개념과 주요 기능을 모두 배웠으니, 실제 프로젝트에 어떻게 적용하는지 알아볼 차례야. 2025년 현재 가장 인기 있는 프레임워크와 TypeORM을 함께 사용하는 방법을 소개할게! 🔥

NestJS와 TypeORM 통합하기

NestJS는 2025년 현재 가장 인기 있는 Node.js 백엔드 프레임워크 중 하나야. TypeORM과 아주 잘 통합돼!

설치 및 설정

# NestJS 프로젝트에 TypeORM 설치
npm install @nestjs/typeorm typeorm mysql2
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user/entities/user.entity';
import { Post } from './post/entities/post.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'password',
      database: 'nestjs_db',
      entities: [User, Post],
      synchronize: process.env.NODE_ENV !== 'production',
    }),
    // 다른 모듈들...
  ],
})
export class AppModule {}

모듈 설정

// user.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

서비스 구현

// user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<user>,
  ) {}

  async findAll(): Promise<user> {
    return this.userRepository.find();
  }

  async findOne(id: number): Promise<user> {
    return this.userRepository.findOneBy({ id });
  }

  async create(createUserDto: CreateUserDto): Promise<user> {
    const user = this.userRepository.create(createUserDto);
    return this.userRepository.save(user);
  }

  // 다른 메서드들...
}</user></user></user></user>

컨트롤러 구현

// user.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './entities/user.entity';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  findAll(): Promise<user> {
    return this.userService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string): Promise<user> {
    return this.userService.findOne(+id);
  }

  @Post()
  create(@Body() createUserDto: CreateUserDto): Promise<user> {
    return this.userService.create(createUserDto);
  }

  // 다른 엔드포인트들...
}</user></user></user>

Express와 TypeORM 통합하기

Express는 여전히 많은 개발자들이 사용하는 인기 있는 프레임워크야. TypeORM과 함께 사용하는 방법을 알아보자:

설치 및 설정

# Express 프로젝트에 TypeORM 설치
npm install typeorm reflect-metadata mysql2 express body-parser
// src/data-source.ts
import "reflect-metadata";
import { DataSource } from "typeorm";
import { User } from "./entity/User";
import { Post } from "./entity/Post";

export const AppDataSource = new DataSource({
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "root",
    password: "password",
    database: "express_db",
    synchronize: process.env.NODE_ENV !== 'production',
    logging: true,
    entities: [User, Post],
    migrations: ["src/migration/**/*.ts"],
    subscribers: ["src/subscriber/**/*.ts"],
});

Express 앱 설정

// src/index.ts
import express from "express";
import bodyParser from "body-parser";
import { AppDataSource } from "./data-source";
import { userRouter } from "./routes/user.routes";
import { postRouter } from "./routes/post.routes";

// TypeORM 초기화
AppDataSource.initialize()
    .then(() => {
        console.log("Data Source has been initialized!");
        
        // Express 앱 생성
        const app = express();
        app.use(bodyParser.json());
        
        // 라우트 등록
        app.use("/api/users", userRouter);
        app.use("/api/posts", postRouter);
        
        // 서버 시작
        app.listen(3000, () => {
            console.log("Server is running on port 3000");
        });
    })
    .catch((err) => {
        console.error("Error during Data Source initialization", err);
    });

라우트 구현

// src/routes/user.routes.ts
import { Router } from "express";
import { UserController } from "../controllers/user.controller";

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

userRouter.get("/", userController.getAllUsers);
userRouter.get("/:id", userController.getUserById);
userRouter.post("/", userController.createUser);
userRouter.put("/:id", userController.updateUser);
userRouter.delete("/:id", userController.deleteUser);

컨트롤러 구현

// src/controllers/user.controller.ts
import { Request, Response } from "express";
import { AppDataSource } from "../data-source";
import { User } from "../entity/User";

export class UserController {
    private userRepository = AppDataSource.getRepository(User);
    
    getAllUsers = async (req: Request, res: Response) => {
        try {
            const users = await this.userRepository.find();
            return res.json(users);
        } catch (error) {
            console.error(error);
            return res.status(500).json({ message: "Internal server error" });
        }
    };
    
    getUserById = async (req: Request, res: Response) => {
        try {
            const id = parseInt(req.params.id);
            const user = await this.userRepository.findOneBy({ id });
            
            if (!user) {
                return res.status(404).json({ message: "User not found" });
            }
            
            return res.json(user);
        } catch (error) {
            console.error(error);
            return res.status(500).json({ message: "Internal server error" });
        }
    };
    
    createUser = async (req: Request, res: Response) => {
        try {
            const { firstName, lastName, age } = req.body;
            
            const user = new User();
            user.firstName = firstName;
            user.lastName = lastName;
            user.age = age;
            
            await this.userRepository.save(user);
            return res.status(201).json(user);
        } catch (error) {
            console.error(error);
            return res.status(500).json({ message: "Internal server error" });
        }
    };
    
    // 다른 메서드들...
}
TypeORM 프로젝트 아키텍처 클라이언트 API 레이어 (Express/NestJS) 컨트롤러 / 라우터 서비스 레이어 비즈니스 로직 TypeORM 레이어 레포지토리 / 엔티티 데이터베이스

실전 프로젝트 구조

대규모 프로젝트에서는 다음과 같은 구조로 TypeORM을 활용하는 것이 좋아:

src/
├── config/                 # 설정 파일
│   └── database.ts
├── entity/                 # 엔티티 정의
│   ├── User.ts
│   ├── Post.ts
│   └── ...
├── migration/              # 마이그레이션 파일
│   ├── 1714725600000-CreateUserTable.ts
│   └── ...
├── repository/             # 커스텀 레포지토리
│   ├── UserRepository.ts
│   └── ...
├── service/                # 비즈니스 로직
│   ├── UserService.ts
│   └── ...
├── controller/             # API 컨트롤러
│   ├── UserController.ts
│   └── ...
├── middleware/             # 미들웨어
│   ├── auth.middleware.ts
│   └── ...
├── dto/                    # 데이터 전송 객체
│   ├── CreateUserDto.ts
│   └── ...
├── util/                   # 유틸리티 함수
│   └── ...
├── data-source.ts          # TypeORM 데이터 소스
└── index.ts                # 애플리케이션 진입점

실전 프로젝트 팁

  1. 환경 변수 사용하기 - 데이터베이스 접속 정보는 환경 변수로 관리
  2. 트랜잭션 활용하기 - 여러 엔티티를 수정하는 작업은 트랜잭션으로 처리
  3. DTO 패턴 사용하기 - API 요청/응답에 DTO(Data Transfer Object) 패턴 적용
  4. 유효성 검증 추가하기 - class-validator와 함께 사용해 입력 데이터 검증
  5. 로깅 설정하기 - TypeORM 쿼리 로깅을 적절히 설정해 디버깅 용이하게
  6. 인덱스 최적화하기 - 자주 조회하는 필드에 인덱스 추가
  7. N+1 문제 해결하기 - relations 옵션이나 JOIN을 활용해 N+1 쿼리 문제 해결

💡 팁: 2025년 현재는 모듈형 아키텍처가 인기를 끌고 있어. 각 기능을 독립적인 모듈로 분리하고, 각 모듈이 자체 엔티티, 레포지토리, 서비스를 가지도록 설계하면 유지보수가 훨씬 쉬워져!

실전 예제: 소셜 미디어 API

간단한 소셜 미디어 API를 TypeORM으로 구현해보자:

엔티티 정의

// src/entity/User.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn, UpdateDateColumn } from "typeorm";
import { Post } from "./Post";
import { Comment } from "./Comment";

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

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

    @Column()
    password: string; // 실제로는 해시된 비밀번호를 저장해야 함

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

    @Column({ nullable: true })
    bio: string;

    @Column({ nullable: true })
    avatarUrl: string;

    @OneToMany(() => Post, post => post.author)
    posts: Post[];

    @OneToMany(() => Comment, comment => comment.author)
    comments: Comment[];

    @CreateDateColumn()
    createdAt: Date;

    @UpdateDateColumn()
    updatedAt: Date;
}

// src/entity/Post.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, CreateDateColumn, UpdateDateColumn } from "typeorm";
import { User } from "./User";
import { Comment } from "./Comment";

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

    @Column()
    title: string;

    @Column("text")
    content: string;

    @Column({ default: 0 })
    likes: number;

    @ManyToOne(() => User, user => user.posts, { onDelete: "CASCADE" })
    author: User;

    @OneToMany(() => Comment, comment => comment.post)
    comments: Comment[];

    @CreateDateColumn()
    createdAt: Date;

    @UpdateDateColumn()
    updatedAt: Date;
}

// src/entity/Comment.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn } from "typeorm";
import { User } from "./User";
import { Post } from "./Post";

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

    @Column("text")
    content: string;

    @ManyToOne(() => User, user => user.comments, { onDelete: "CASCADE" })
    author: User;

    @ManyToOne(() => Post, post => post.comments, { onDelete: "CASCADE" })
    post: Post;

    @CreateDateColumn()
    createdAt: Date;
}

서비스 구현

// src/service/PostService.ts
import { AppDataSource } from "../data-source";
import { Post } from "../entity/Post";
import { User } from "../entity/User";

export class PostService {
    private postRepository = AppDataSource.getRepository(Post);
    private userRepository = AppDataSource.getRepository(User);

    async getAllPosts(page: number = 0, limit: number = 10) {
        const [posts, total] = await this.postRepository.findAndCount({
            relations: { author: true },
            select: {
                author: {
                    id: true,
                    username: true,
                    avatarUrl: true
                }
            },
            skip: page * limit,
            take: limit,
            order: { createdAt: "DESC" }
        });

        return {
            data: posts,
            meta: {
                total,
                page,
                limit,
                pageCount: Math.ceil(total / limit)
            }
        };
    }

    async getPostById(id: number) {
        const post = await this.postRepository.findOne({
            where: { id },
            relations: {
                author: true,
                comments: {
                    author: true
                }
            },
            select: {
                author: {
                    id: true,
                    username: true,
                    avatarUrl: true
                },
                comments: {
                    id: true,
                    content: true,
                    createdAt: true,
                    author: {
                        id: true,
                        username: true,
                        avatarUrl: true
                    }
                }
            }
        });

        if (!post) {
            throw new Error("Post not found");
        }

        return post;
    }

    async createPost(authorId: number, title: string, content: string) {
        const author = await this.userRepository.findOneBy({ id: authorId });
        
        if (!author) {
            throw new Error("User not found");
        }

        const post = new Post();
        post.title = title;
        post.content = content;
        post.author = author;

        return await this.postRepository.save(post);
    }

    async likePost(id: number) {
        return await this.postRepository.increment({ id }, "likes", 1);
    }

    // 다른 메서드들...
}

이런 식으로 TypeORM을 활용하면 복잡한 비즈니스 로직도 깔끔하게 구현할 수 있어. 특히 관계가 많은 데이터 모델을 다룰 때 TypeORM의 강력함이 빛을 발해!

재능넷과 같은 플랫폼도 이와 비슷한 아키텍처로 구현되어 있을 거야. 사용자, 서비스, 리뷰, 결제 등 다양한 엔티티 간의 복잡한 관계를 TypeORM으로 효율적으로 관리할 수 있거든! 다음 섹션에서는 TypeORM의 성능을 최적화하는 방법을 알아볼게. 🚀

8. TypeORM의 성능 최적화 전략 🚀

TypeORM은 편리하지만, 잘못 사용하면 성능 문제가 발생할 수 있어. 2025년 현재 대규모 애플리케이션에서 TypeORM을 효율적으로 사용하는 방법을 알아보자! 💪

N+1 쿼리 문제 해결하기

N+1 쿼리 문제는 ORM을 사용할 때 가장 흔히 발생하는 성능 이슈야. 예를 들어, 10개의 게시글을 가져온 후 각 게시글의 작성자 정보를 개별적으로 조회하면 총 11번의 쿼리가 실행돼 (1 + 10 = 11).

문제가 있는 코드

// N+1 문제가 발생하는 코드
const posts = await postRepository.find();

// 각 게시글마다 별도의 쿼리가 실행됨
for (const post of posts) {
    const author = await userRepository.findOneBy({ id: post.authorId });
    console.log(`Post "${post.title}" by ${author.username}`);
}

해결 방법: relations 옵션 사용

// relations 옵션으로 해결
const posts = await postRepository.find({
    relations: { author: true }
});

// 추가 쿼리 없이 작성자 정보에 접근 가능
for (const post of posts) {
    console.log(`Post "${post.title}" by ${post.author.username}`);
}

해결 방법: 쿼리 빌더의 JOIN 사용

// 쿼리 빌더의 JOIN으로 해결
const posts = await postRepository
    .createQueryBuilder("post")
    .leftJoinAndSelect("post.author", "author")
    .getMany();

// 추가 쿼리 없이 작성자 정보에 접근 가능
for (const post of posts) {
    console.log(`Post "${post.title}" by ${post.author.username}`);
}

부분 로딩 최적화

항상 모든 필드를 로드할 필요는 없어. 필요한 필드만 선택적으로 로드하면 성능을 크게 향상시킬 수 있어:

// 필요한 필드만 선택적으로 로드
const users = await userRepository.find({
    select: {
        id: true,
        username: true,
        email: true
        // password 필드는 제외
    }
});

// 관계에서도 필요한 필드만 선택
const posts = await postRepository.find({
    relations: { author: true },
    select: {
        id: true,
        title: true,
        createdAt: true,
        author: {
            id: true,
            username: true
            // 다른 작성자 필드는 제외
        }
    }
});

페이지네이션 적용하기

대량의 데이터를 한 번에 로드하는 것은 메모리 사용량과 응답 시간에 악영향을 미쳐. 항상 페이지네이션을 적용하자:

// 페이지네이션 적용
const pageSize = 20;
const page = 0; // 첫 페이지

const [posts, total] = await postRepository.findAndCount({
    skip: page * pageSize,
    take: pageSize,
    order: { createdAt: "DESC" }
});

// 메타데이터 포함 응답
return {
    data: posts,
    meta: {
        total,
        page,
        pageSize,
        pageCount: Math.ceil(total / pageSize)
    }
};

인덱스 최적화

자주 조회하거나 필터링하는 필드에는 인덱스를 추가하는 것이 중요해:

// 엔티티에 인덱스 추가
import { Entity, PrimaryGeneratedColumn, Column, Index, CreateDateColumn } from "typeorm";

@Entity()
@Index(["title", "createdAt"]) // 복합 인덱스
export class Post {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    @Index() // 단일 컬럼 인덱스
    title: string;

    @Column("text")
    content: string;

    @Column()
    @Index()
    authorId: number;

    @CreateDateColumn()
    createdAt: Date;
}
TypeORM 성능 최적화 전략 최적화 기법 성능 향상 기본 쿼리 인덱스 추가 부분 로딩 JOIN 최적화 캐싱 적용 1x 1.5x 2.5x 3.2x 4.5x

쿼리 캐싱 활용하기

2025년 현재 TypeORM은 더 강력한 쿼리 캐싱 기능을 제공해. 자주 사용되는 쿼리 결과를 캐싱하면 데이터베이스 부하를 크게 줄일 수 있어:

// data-source.ts에 캐시 설정 추가
import { DataSource } from "typeorm";

export const AppDataSource = new DataSource({
    // 기본 설정...
    cache: {
        type: "redis",
        options: {
            host: "localhost",
            port: 6379
        },
        duration: 60000 // 캐시 유효 시간 (밀리초)
    }
});

// 쿼리에 캐시 적용
const users = await userRepository.find({
    cache: {
        id: "all_users",
        milliseconds: 30000
    }
});

// 특정 테이블의 캐시 무효화
await AppDataSource.queryResultCache.remove(["users"]);

벌크 작업 최적화

대량의 데이터를 처리할 때는 개별 작업보다 벌크 작업이 훨씬 효율적이야:

// 벌크 삽입
await userRepository
    .createQueryBuilder()
    .insert()
    .into(User)
    .values([
        { firstName: "John", lastName: "Doe", age: 25 },
        { firstName: "Jane", lastName: "Doe", age: 24 },
        { firstName: "Bob", lastName: "Smith", age: 30 }
    ])
    .execute();

// 벌크 업데이트
await userRepository
    .createQueryBuilder()
    .update(User)
    .set({ isActive: false })
    .where("lastLoginAt < :date", { date: new Date('2025-01-01') })
    .execute();

// 벌크 삭제
await userRepository
    .createQueryBuilder()
    .delete()
    .from(User)
    .where("isActive = :isActive", { isActive: false })
    .execute();

지연 로딩 vs 즉시 로딩

TypeORM에서는 관계 데이터를 로드하는 두 가지 방식이 있어:

  1. 지연 로딩(Lazy Loading) - 데이터에 접근할 때만 로드 (N+1 문제 발생 가능)
  2. 즉시 로딩(Eager Loading) - 처음부터 모든 관계 데이터를 함께 로드
// 지연 로딩 설정
@Entity()
export class Post {
    // ...
    
    @ManyToOne(() => User, user => user.posts, { lazy: true })
    author: Promise<user>; // Promise로 타입 지정
}

// 사용 방법
const post = await postRepository.findOneBy({ id: 1 });
const author = await post.author; // 이 시점에 추가 쿼리 실행

// 즉시 로딩 설정
@Entity()
export class Post {
    // ...
    
    @ManyToOne(() => User, user => user.posts, { eager: true })
    author: User;
}

// 사용 방법
const post = await postRepository.findOneBy({ id: 1 });
console.log(post.author); // 추가 쿼리 없이 바로 사용 가능</user>

💡 팁: 2025년 현재는 상황에 따라 적절한 로딩 전략을 선택하는 것이 권장돼. eager: true는 항상 관계를 로드하므로 필요하지 않은 경우에는 성능 저하를 가져올 수 있어. 대신 relations 옵션을 사용해 필요할 때만 관계를 로드하는 것이 더 효율적이야.

트랜잭션 최적화

트랜잭션을 효율적으로 사용하면 데이터 일관성을 유지하면서도 성능을 향상시킬 수 있어:

// 트랜잭션 최적화
await AppDataSource.transaction(async (transactionalEntityManager) => {
    // 트랜잭션 내에서 여러 작업을 한 번에 처리
    
    // 1. 사용자 생성
    const user = new User();
    user.username = "john_doe";
    user.email = "john@example.com";
    await transactionalEntityManager.save(user);
    
    // 2. 여러 게시글 생성
    const posts = [];
    for (let i = 0; i < 5; i++) {
        const post = new Post();
        post.title = `Post ${i + 1}`;
        post.content = `Content ${i + 1}`;
        post.author = user;
        posts.push(post);
    }
    
    // 벌크 삽입으로 최적화
    await transactionalEntityManager
        .createQueryBuilder()
        .insert()
        .into(Post)
        .values(posts)
        .execute();
});

Raw 쿼리 사용하기

극단적인 성능이 필요한 경우, TypeORM은 Raw SQL 쿼리를 직접 실행할 수 있는 기능도 제공해:

// Raw 쿼리 실행
const rawData = await AppDataSource.manager.query(`
    SELECT 
        p.id, p.title, p.created_at, 
        u.username as author_name,
        COUNT(c.id) as comment_count
    FROM post p
    JOIN user u ON p.author_id = u.id
    LEFT JOIN comment c ON c.post_id = p.id
    WHERE p.created_at > '2025-01-01'
    GROUP BY p.id, u.username
    ORDER BY p.created_at DESC
    LIMIT 10
`);

// 엔티티와 매핑
const posts = rawData.map(row => {
    const post = new Post();
    post.id = row.id;
    post.title = row.title;
    post.createdAt = row.created_at;
    post.commentCount = row.comment_count;
    
    const author = new User();
    author.username = row.author_name;
    post.author = author;
    
    return post;
});

실전 성능 최적화 사례

실제 프로젝트에서 성능을 최적화한 사례를 살펴보자:

사례 1: 대시보드 API 최적화

// 최적화 전: 여러 개의 개별 쿼리 실행
async function getDashboardData(userId: number) {
    const user = await userRepository.findOneBy({ id: userId });
    const posts = await postRepository.find({ where: { author: { id: userId } } });
    const comments = await commentRepository.find({ where: { author: { id: userId } } });
    const likes = await likeRepository.find({ where: { userId } });
    
    return { user, posts, comments, likes };
}

// 최적화 후: 단일 트랜잭션과 최적화된 쿼리
async function getDashboardData(userId: number) {
    return await AppDataSource.transaction(async (manager) => {
        // 1. 사용자 정보 (필요한 필드만)
        const user = await manager.findOne(User, {
            where: { id: userId },
            select: ['id', 'username', 'email', 'avatarUrl', 'createdAt']
        });
        
        if (!user) throw new Error("User not found");
        
        // 2. 통계 정보 (Raw 쿼리로 최적화)
        const stats = await manager.query(`
            SELECT
                (SELECT COUNT(*) FROM post WHERE author_id = $1) as post_count,
                (SELECT COUNT(*) FROM comment WHERE author_id = $1) as comment_count,
                (SELECT COUNT(*) FROM like WHERE user_id = $1) as like_count
        `, [userId]);
        
        // 3. 최근 게시글 (필요한 관계만 로드)
        const recentPosts = await manager.find(Post, {
            where: { author: { id: userId } },
            select: ['id', 'title', 'createdAt', 'likes'],
            order: { createdAt: "DESC" },
            take: 5
        });
        
        // 4. 최근 활동 (UNION으로 최적화)
        const recentActivities = await manager.query(`
            (SELECT 'post' as type, id, title as content, created_at
             FROM post WHERE author_id = $1 ORDER BY created_at DESC LIMIT 5)
            UNION ALL
            (SELECT 'comment' as type, id, content, created_at
             FROM comment WHERE author_id = $1 ORDER BY created_at DESC LIMIT 5)
            ORDER BY created_at DESC LIMIT 10
        `, [userId]);
        
        return {
            user,
            stats: stats[0],
            recentPosts,
            recentActivities
        };
    });
}

이런 최적화를 통해 API 응답 시간을 몇 초에서 몇 밀리초로 단축할 수 있어. 특히 대시보드처럼 여러 데이터를 한 번에 보여주는 API에서는 이런 최적화가 필수적이야!

성능 최적화는 재능넷과 같은 대규모 플랫폼에서 특히 중요해. 사용자가 많아질수록 데이터베이스 부하가 증가하기 때문에, 처음부터 성능을 고려한 설계가 필요해. 다음 섹션에서는 2025년 현재 TypeORM의 최신 트렌드와 팁을 알아볼게! 🚀

10. 마무리 및 다음 단계 🎯

축하해! 🎉 TypeORM의 기본부터 고급 기능까지 모두 살펴봤어. 이제 TypeORM을 활용해 강력한 백엔드 애플리케이션을 구축할 준비가 됐어. 마지막으로 배운 내용을 정리하고 다음 단계를 알아보자!

배운 내용 요약

  1. TypeORM 기본 개념 - ORM의 개념과 TypeORM의 주요 특징
  2. 설치 및 설정 - TypeORM 설치 방법과 데이터베이스 연결 설정
  3. 엔티티 정의 - 데이터베이스 테이블과 매핑되는 클래스 작성 방법
  4. 관계 설정 - 1:1, 1:N, N:M 관계 설정 및 관리 방법
  5. 쿼리 빌더와 레포지토리 - 데이터 조회 및 조작 방법
  6. 마이그레이션 - 데이터베이스 스키마 변경 관리 방법
  7. 실전 프로젝트 적용 - Express, NestJS 등과 함께 사용하는 방법
  8. 성능 최적화 - N+1 문제 해결, 캐싱, 인덱싱 등의 최적화 기법
  9. 최신 트렌드 - 2025년 TypeORM의 최신 기능과 트렌드
TypeORM 학습 로드맵 1 2 3 4 5 6 7 🏆 기본 개념 엔티티 정의 관계 설정 쿼리 작성 마이그레이션 성능 최적화 실전 프로젝트 마스터 당신은 여기에 있습니다!

다음 단계: 더 배워볼 만한 주제들

TypeORM을 마스터했다면, 다음과 같은 주제들을 더 탐구해볼 수 있어:

  1. 고급 데이터베이스 설계 - 샤딩, 파티셔닝, 인덱싱 전략 등
  2. GraphQL과 TypeORM 통합 - TypeGraphQL, Apollo Server와 함께 사용하기
  3. 마이크로서비스 아키텍처 - 여러 데이터베이스에 걸친 데이터 관리
  4. 이벤트 소싱과 CQRS - 복잡한 도메인 모델링을 위한 패턴
  5. 실시간 데이터 동기화 - WebSocket, Server-Sent Events와 통합
  6. 데이터베이스 보안 - SQL 인젝션 방지, 데이터 암호화 등
  7. 대규모 데이터 마이그레이션 - 대용량 데이터 마이그레이션 전략

TypeORM 커뮤니티 참여하기

TypeORM 생태계에 참여하고 최신 정보를 얻는 방법:

  1. GitHub - TypeORM 공식 리포지토리에서 이슈와 PR 확인하기
  2. Discord - TypeORM 공식 Discord 서버에 참여하기
  3. Stack Overflow - TypeORM 태그로 질문하고 답변하기
  4. 블로그와 튜토리얼 - 최신 튜토리얼과 모범 사례 학습하기
  5. 컨퍼런스와 밋업 - TypeScript 및 Node.js 관련 이벤트 참여하기

💡 팁: TypeORM에 기여하는 것도 좋은 학습 방법이야! 버그 수정이나 문서 개선 같은 작은 기여부터 시작해볼 수 있어.

마지막 조언

TypeORM을 마스터하기 위한 몇 가지 조언을 남기며 마무리할게:

  1. 실전 프로젝트 만들기 - 실제 문제를 해결하는 프로젝트를 만들어보는 것이 가장 좋은 학습 방법이야.
  2. 코드 리팩토링하기 - 기존 프로젝트를 TypeORM으로 리팩토링해보면 다양한 상황에 대처하는 방법을 배울 수 있어.
  3. 성능 테스트하기 - 다양한 쿼리 방식의 성능을 직접 측정해보면 최적화 감각이 생겨.
  4. 오픈 소스 코드 읽기 - TypeORM을 사용하는 오픈 소스 프로젝트의 코드를 분석해보면 많은 인사이트를 얻을 수 있어.
  5. 지속적으로 학습하기 - 데이터베이스와 ORM 기술은 계속 발전하니 최신 트렌드를 따라가는 것이 중요해.

TypeORM은 타입스크립트 백엔드 개발의 강력한 도구야. 이 글에서 배운 내용을 바탕으로 더 효율적이고 유지보수하기 쉬운 애플리케이션을 만들 수 있길 바라! 🚀

특히 재능넷과 같은 플랫폼을 개발할 때 TypeORM의 강력한 기능들이 큰 도움이 될 거야. 복잡한 관계 모델링, 효율적인 쿼리, 안정적인 마이그레이션 등 TypeORM의 장점을 최대한 활용해보자!

TypeORM과 함께하는 즐거운 개발 여정이 되길 바라! 😊

📚 목차

  1. TypeORM이 뭐길래? 기본 개념 이해하기
  2. TypeORM 설치 및 프로젝트 셋업 방법
  3. 엔티티(Entity) 정의하기: 데이터베이스 테이블의 분신
  4. 관계 설정하기: 1:1, 1:N, N:M 관계 마스터하기
  5. 쿼리 빌더와 레포지토리 패턴 활용법
  6. 마이그레이션으로 데이터베이스 변경 관리하기
  7. 실전 프로젝트에 TypeORM 적용하기
  8. TypeORM의 성능 최적화 전략
  9. 2025년 TypeORM 최신 트렌드와 팁
  10. 마무리 및 다음 단계

1. TypeORM이 뭐길래? 기본 개념 이해하기 🤔

TypeORM은 타입스크립트와 자바스크립트를 위한 ORM(Object-Relational Mapping) 라이브러리야. 간단히 말하자면, 객체지향 코드와 관계형 데이터베이스 사이의 다리 역할을 해주는 도구지. 코드에서는 객체로 작업하고, 이 객체들이 자동으로 데이터베이스 테이블과 매핑되는 마법 같은 일이 일어나는 거야!

💡 ORM이란? Object-Relational Mapping의 약자로, 객체와 관계형 데이터베이스의 데이터를 자동으로 변환해주는 기술이야. SQL 쿼리를 직접 작성하는 대신 객체 지향적인 방식으로 데이터베이스를 다룰 수 있게 해줘.

TypeORM의 주요 특징

  1. 타입스크립트 지원 - 타입 안전성을 제공해서 개발 시 많은 실수를 방지할 수 있어 👍
  2. 다양한 데이터베이스 지원 - MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, MongoDB까지!
  3. 스키마 마이그레이션 - 데이터베이스 스키마 변경을 코드로 관리할 수 있어
  4. 관계 관리 - 1:1, 1:N, N:M 등 복잡한 관계도 쉽게 정의하고 관리
  5. 트랜잭션 지원 - 데이터 일관성을 유지하기 위한 트랜잭션 처리 가능
TypeScript 객체 User Post 데이터베이스 users posts TypeORM

TypeORM을 사용하면 SQL 쿼리를 직접 작성하는 대신 타입스크립트 코드만으로 데이터베이스 작업을 할 수 있어. 이게 얼마나 편한지는 써봐야 알 수 있어! 특히 프론트엔드 개발자 출신이라면 SQL에 익숙하지 않더라도 쉽게 데이터베이스 작업을 할 수 있다는 큰 장점이 있지.

요즘 재능넷과 같은 플랫폼에서도 백엔드 개발 재능을 공유할 때 TypeORM 같은 최신 기술 스택을 활용한 프로젝트가 인기가 많아. 왜냐하면 생산성이 엄청나게 향상되거든! 🚀

2. TypeORM 설치 및 프로젝트 셋업 방법 🛠️

자, 이제 본격적으로 TypeORM을 설치하고 프로젝트를 셋업해볼 거야. 2025년 기준 최신 방법으로 알려줄게!

기본 설치

먼저 npm이나 yarn을 사용해서 TypeORM과 필요한 의존성을 설치해야 해:

npm install typeorm reflect-metadata @types/node

그리고 사용할 데이터베이스 드라이버도 설치해야 해. 가장 많이 사용하는 몇 가지 예시를 들어볼게:

# MySQL 사용 시
npm install mysql2

# PostgreSQL 사용 시
npm install pg

# SQLite 사용 시
npm install sqlite3

tsconfig.json 설정

TypeORM을 제대로 사용하려면 타입스크립트 설정도 약간 손봐야 해:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "sourceMap": true,
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  }
}

emitDecoratorMetadata와 experimentalDecorators 옵션은 TypeORM의 데코레이터 기능을 사용하기 위해 꼭 필요해. 이 부분 빼먹으면 나중에 엄청 헤매게 될 거야! 😅

데이터베이스 연결 설정

이제 TypeORM에서 데이터베이스 연결을 설정해볼게. 2025년 현재는 두 가지 방식이 많이 사용돼:

1. data-source.ts 파일 사용 (권장)

// data-source.ts
import { DataSource } from "typeorm";

export const AppDataSource = new DataSource({
  type: "mysql",
  host: "localhost",
  port: 3306,
  username: "root",
  password: "password",
  database: "test_db",
  synchronize: true, // 개발 환경에서만 true로 설정!
  logging: true,
  entities: ["src/entity/**/*.ts"],
  migrations: ["src/migration/**/*.ts"],
  subscribers: ["src/subscriber/**/*.ts"],
});

⚠️ 주의사항: synchronize: true는 개발 환경에서만 사용하고, 프로덕션에서는 절대 사용하지 마! 데이터가 날아갈 수 있어. 프로덕션에서는 마이그레이션을 사용해야 해.

2. ormconfig.json 파일 사용 (레거시 방식)

// ormconfig.json
{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "root",
  "password": "password",
  "database": "test_db",
  "synchronize": true,
  "logging": true,
  "entities": ["src/entity/**/*.ts"],
  "migrations": ["src/migration/**/*.ts"],
  "subscribers": ["src/subscriber/**/*.ts"],
  "cli": {
    "entitiesDir": "src/entity",
    "migrationsDir": "src/migration",
    "subscribersDir": "src/subscriber"
  }
}

2025년 현재는 첫 번째 방식인 DataSource API를 사용하는 것이 권장돼. 더 타입 안전하고 유연한 설정이 가능하거든!

애플리케이션에서 연결 초기화

이제 애플리케이션이 시작될 때 데이터베이스 연결을 초기화하는 코드를 작성해볼게:

// index.ts
import "reflect-metadata";
import { AppDataSource } from "./data-source";

// 데이터베이스 연결 초기화
AppDataSource.initialize()
  .then(() => {
    console.log("데이터베이스 연결 성공! 🎉");
    // 여기서 Express 서버 등을 시작할 수 있어
  })
  .catch((error) => console.log("데이터베이스 연결 실패 😢", error));
TypeORM 프로젝트 구조 프로젝트 폴더 📄 package.json 📄 tsconfig.json 📄 data-source.ts 📁 src/ 📁 entity/ 📁 migration/ 데이터베이스 연결 흐름 data-source.ts index.ts 데이터베이스 Entity 클래스

이렇게 기본적인 TypeORM 설정이 완료됐어! 이제 본격적으로 엔티티를 정의하고 데이터베이스를 다루는 방법을 알아볼게. 😎

3. 엔티티(Entity) 정의하기: 데이터베이스 테이블의 분신 🧩

TypeORM에서 가장 중요한 개념 중 하나가 바로 엔티티(Entity)야. 엔티티는 데이터베이스 테이블과 매핑되는 클래스로, 테이블의 구조를 코드로 표현한 거라고 생각하면 돼.

기본 엔티티 만들기

간단한 User 엔티티를 만들어볼게:

// src/entity/User.ts
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

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

    @Column()
    firstName: string;

    @Column()
    lastName: string;

    @Column()
    age: number;

    @Column({ default: true })
    isActive: boolean;
}

이 코드는 다음과 같은 SQL 테이블을 자동으로 생성해:

CREATE TABLE "user" (
    "id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    "firstName" character varying NOT NULL,
    "lastName" character varying NOT NULL,
    "age" integer NOT NULL,
    "isActive" boolean NOT NULL DEFAULT true
);

정말 마법 같지? 😲 코드만 작성했는데 데이터베이스 테이블이 자동으로 생성되다니!

데코레이터 이해하기

TypeORM에서는 데코레이터(@)를 사용해서 메타데이터를 클래스와 프로퍼티에 추가해. 주요 데코레이터를 알아보자:

  1. @Entity() - 클래스가 엔티티임을 나타내는 데코레이터
  2. @PrimaryGeneratedColumn() - 자동 증가하는 기본 키 열
  3. @Column() - 일반 테이블 열
  4. @CreateDateColumn() - 생성 날짜를 자동으로 설정
  5. @UpdateDateColumn() - 업데이트 날짜를 자동으로 설정

열(Column) 옵션 설정하기

@Column 데코레이터에는 다양한 옵션을 설정할 수 있어:

// 다양한 열 옵션 예시
@Column({ type: "varchar", length: 100, nullable: false })
name: string;

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

@Column({ type: "text" })
description: string;

@Column({ type: "decimal", precision: 10, scale: 2, default: 0 })
price: number;

@Column({ enum: ["admin", "editor", "user"], default: "user" })
role: string;

2025년 현재 TypeORM은 더 많은 데이터 타입과 옵션을 지원하고 있어. 특히 JSON 타입이나 배열 타입도 쉽게 사용할 수 있지!

엔티티 이름 커스터마이징

기본적으로 엔티티 이름은 클래스 이름을 따라가지만, 원하는 테이블 이름을 직접 지정할 수도 있어:

@Entity("users") // 테이블 이름을 'users'로 지정
export class User {
    // ...
}

인덱스와 유니크 제약 조건

데이터베이스 성능을 위한 인덱스와 유니크 제약 조건도 쉽게 설정할 수 있어:

import { Entity, PrimaryGeneratedColumn, Column, Index, Unique } from "typeorm";

@Entity()
@Index(["firstName", "lastName"]) // 복합 인덱스
@Unique(["email"]) // 유니크 제약 조건
export class User {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

    @Column()
    @Index() // 단일 컬럼 인덱스
    email: string;
}
엔티티와 데이터베이스 테이블 매핑 TypeScript 엔티티 클래스 @Entity() class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() age: number; } 데이터베이스 테이블 id 1 2 3 4 name "John" "Alice" "Bob" "Emma" age 28 24 32 22 TypeORM

실전 엔티티 예제: 블로그 시스템

좀 더 실전적인 예제로 블로그 시스템의 Post 엔티티를 만들어볼게:

// src/entity/Post.ts
import { 
    Entity, 
    PrimaryGeneratedColumn, 
    Column, 
    CreateDateColumn, 
    UpdateDateColumn,
    Index
} from "typeorm";

@Entity("posts")
export class Post {
    @PrimaryGeneratedColumn("uuid")
    id: string;

    @Column({ length: 100 })
    @Index()
    title: string;

    @Column("text")
    content: string;

    @Column({ type: "simple-array", nullable: true })
    tags: string[];

    @Column({ type: "int", default: 0 })
    viewCount: number;

    @Column({ default: false })
    published: boolean;

    @CreateDateColumn()
    createdAt: Date;

    @UpdateDateColumn()
    updatedAt: Date;
}

이 예제에서는 UUID 기본 키, 텍스트 컬럼, 배열 타입, 기본값, 자동 날짜 설정 등 다양한 기능을 사용했어. 이런 기능들을 활용하면 복잡한 데이터 모델도 쉽게 구현할 수 있지!

엔티티를 잘 설계하는 것이 TypeORM 사용의 핵심이야. 다음 섹션에서는 이런 엔티티들 간의 관계를 어떻게 설정하는지 알아볼게! 🔄