TypeORM으로 데이터 처리하기

API 기획하고 회원가입 만들기

데이터베이스

ORM

TypeORM

  • SQLite
  • Postgres
  • MySQL
  • MongoDB

Mongoose

  • MongoDB

아무거나 사용해도 된다.

여기서는 TypeORM과 SQLite를 사용할 것. 마지막에는 Postegres로 변경할 계획

Entity

우리는 AppModule 내부에[ 두가지 모듈(users, reports)을 만들기로 계획했었다. 이 모듈에는 각각 User Entity와 ReportEntity 파일을 만들 것이다.

Entity 파일은 애플리케이션 내부에 저장하려는 한 종류의 리소스나 항목을 정의하고, 갖고 있는 모든 속성을 나열하는 리스트이다. 예를 들어 User 모듈에는 사용자가 입력한 email과 password가 있어야 하는데, User Entitiy에 나와 있어야 한다.

Repository

TypeORM은 각 모듈에 Repository를 자동으로 생성해준다. 저번에 수동으로 만들지 않은 이유가 이 때문이다.

DB 연결하기

터미널에서 현재 프로젝트의 디렉토리(mycv)로 가서

npm install @nestjs/typeorm typeorm sqlite3를 입력하여 orm과 DB를 설치한다.

src의 app.module.ts 파일을 수정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; // 추가 코드 
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { ReportsModule } from './reports/reports.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({ // 추가된 코드
      type: 'sqlite', // DB 유형
      database: 'db.sqlite', // DB 이름
      entities: [], // 나중에 user entity와 report entity를 넣어줄 것
      synchronize: true, // 나중에 알게 될 것
    }),
    UsersModule,
    ReportsModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

(이 과정에서 뜨는 오류가 거슬리면 .eslintrc.js 파일에서 module.exports 의 내부를 모두 주석처리하자.)

이제 터미널에서 npm run start:dev를 입력하여 실행해보자.

에러없이 실행되면 루트 디렉토리에 db.sqlite라는 파일이 비어있는 채로 새롭게 생성된다. SQLite는 파일 기반 데이터베이스이기 떄문이다.

Entity 생성하기

3단계로 구성된다.

  1. Entity 파일을 만들고 entity가 가질 모든 속성을 나열하는 클래스를 생성한다.
  2. entity를 부모 모듈과 연결한다. 여기서 Repository가 생성된다.
  3. entity를 app module의 root connection과 연결한다.

1단계

src 내 user 디렉토리에 user.entity.ts 파일을 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; // decorator들

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

    @Column()
    email: string;

    @Column()
    password: string;
}

2단계

user 모듈로 가서 연결한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// user.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; // 추가된 코드 1
import { UsersController } from './users.controller';
import { UsersService } from './users.service'; 
import { User } from './user.entity'; // 추가된 코드 2

@Module({
  imports: [TypeOrmModule.forFeature([User])], // 추가된 코드 3
  controllers: [UsersController],
  providers: [UsersService]
})
export class UsersModule {}

3단계

App.module.ts 로 가서

import { User } from ;./users/user.entity; 를 추가하고

비워놨던 entities 애 User를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { ReportsModule } from './reports/reports.module';
import { User } from './users/user.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'sqlite',
      database: 'db.sqlite',
      entities: [User],
      synchronize: true,
    }),
    UsersModule,
    ReportsModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

서버를 실행했을 때 오류가 발생하면 안된다.

위 과정을 Reports 모듈에도 적용하면서 연습해보자

  1. Entity 파일 만들기
1
2
3
4
5
6
7
8
9
10
11
12
// reports.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

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

  @Column()
  price: number;
}

  1. Report module에 연결하기
1
2
3
4
5
6
7
8
9
10
11
12
13
// reports.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ReportsController } from './reports.controller';
import { ReportsService } from './reports.service';
import { Report } from './reports.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Report])],
  controllers: [ReportsController],
  providers: [ReportsService],
})
export class ReportsModule {}
  1. App module에 연결하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { ReportsModule } from './reports/reports.module';
import { User } from './users/user.entity';
import { Report } from './reports/reports.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'sqlite',
      database: 'db.sqlite',
      entities: [User, Report],
      synchronize: true,
    }),
    UsersModule,
    ReportsModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

db.sqlite 파일 내용 보기

여기까지 완료된 후 db.sqlite 파일을 열어보면 아까는 비어있는 파일이 열린 것과 달리 열리지 않는다.

확장 프로그램을 통해 무엇이 들어갔는지 확인해보자. sqlite 라는 확장프로그램을 설치하자.

설치 후 컨트롤 + 쉬프트 + P (맥북은 커맨드 + P)를 누르고 sqlite를 입력하여 SQLite: Open Databases를 선택하자

아무 변화가 없는것처럼 보일수 있지만 왼쪽 exploer를 보면 OUTLINE, TIMELINE 밑에 SQLITE EXPLORER이 새로 열렸다. 열어보자.

들어가보면 우리가 작성한대로 데이터베이스가 잘 만들어졌음을 볼 수 있다.

이미지0

코드 이해하기

app.moudle.ts 파일에 추가한 코드부터 살펴보자

1
2
3
4
5
6
7
8
@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'sqlite', // 우리가 선택한 데이터베이스
      database: 'db.sqlite', // 데이터 저장할 파일 지정
      entities: [User, Report], // 로드할 entity
      synchronize: true,
    }),

동기화(synchronization)의 이유

SQL 데이터베이스는 행, 열이 정확하게 갖추어져있다. 여서 구조를 변경할때(열이나 행 추가 혹은 삭) Migration을 실행한다.

이 동기화는 개발환경에서만 실행해야 하는데, entity, decorator를 통해 모든 구조를 파악고 자동으로 데이터베이스 구조를 업데이트한다.

이런 일은 흔치 않아서 보통은 데이터베이스 구조를 변경할때 마이그레이션 파일을 작성해야 한다.

저장소에 대해 이해해보기

Repository API

api 링크

method description
create() 새로운 entity 인스턴스를 생성. DB에 유지되진 않음
save() 기록을 DB에 추가하거나 업데이트
find() 쿼리를 실행하고 entity 목록을 반환
findOne() 쿼리를 실행하여 탐색값과 일치하는 첫번째 기록을 반환
remove() DB에서 기록을 제

외울 필요 없다. Document에는 미세한 차이(remove VS delete, insert vs save) 까지 나와있으니 참고하자.

Routes 만들기

처음 API를 계획할 떄, User와 관련되어 새로운 유저의 회원가입과 로그인, 기존 유저의 로그인 2가지를 기획했다.

TypeORM을 연습하기 위해 이를 더 세분하게 쪼개서 비록 불필요해보이는 기능일지라도 만들어보자.

만들 것은 다음과 같다.

Mothod and Route Body or Query String Description Controller Method Service Method
POST /auth/signup Body - { email, password } 새로운 유저 생성 createUser create
GET /auth/:id - id로 한 명의 유저 찾기 findUser findOne
GET /auth?email=… - email로 모든 유저 찾기 findAllUsers find
PATCH /auth/:id Body - { email, password } id로 유저 업데이트하기 updateUser update
DELETE /auth/:id - id로 유저 삭제하 removeUser remove

Body validation

가장 첫번쨰 route를 만들어보자.

우선 user controller 파일을 수정한다.

1
2
3
4
5
6
7
8
// user.controller.ts
import { Controller, Post} from '@nestjs/common';

@Controller('auth') // route가 모두 /auth/이니 user에서 바꿔주자
export class UsersController {
    @Post('/signup') // decorator
    createUser() {}
}

유저 생성 요청이 들어오면 Body의 유효성을 검사해야한다. 이전에 공부한 DTO를 만들어보자(기억이 안나면 잠시 돌아가보자)

main.ts 파일로 이동한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common'; // 추가한 코드 1
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes( // 추가한 코드 2
    new ValidationPipe({
      whitelist: true // 이 코드의 의미는 이따 설명할 예정이다.
    }),
  );
  await app.listen(3000);
}
bootstrap();

다시 src의 user 디렉토리로 돌아가 dtos 폴더를 만들고 내부에 create-user.dto.ts를 만든다.

1
2
3
4
5
6
7
8
9
10
// create-user.dto.ts
import { IsEmail, IsString } from 'class-validator'; // 유효성 검사를 위한 decorator들이 많았다는 것을 기억하자.

export class CreateUserDto {
    @IsEmail()
    email: string;

    @IsString()
    password: string;
}

패키지가 설치되어있지 않아 위에서 오류가 발생한다면, 서버를 멈추고

npm install class-validator class-transformer

를 입력여 라이브러리를 설치하자.

만든 dto를 컨트롤러에 적용할 차례.

1
2
3
4
5
6
7
8
9
10
11
12
// users.controller.ts
import { Controller, Post, Body } from '@nestjs/common'; // 들어오는 request의 body를 추출하기 위해 Body도 import 한다.
import { CreateUserDto } from './dtos/create-user.dto'; // dto import하기

@Controller('auth')
export class UsersController {
    @Post('/signup')
    createUser(@Body() body: CreateUserDto) { // body decorator로 추출하고 검사한다.
        console.log(body);
    }
}

확인해보기

API Client를 이용하여 확인해보자. 이번에도 postman 안쓰고 Rest client를 쓰겠다.

user 디렉토리 안에 requests.http 파일을 만들고 다음과 같이 입력하고 요청을 보내보자

1
2
3
4
5
6
7
8
### Create a new user
POST http://localhost:3000/auth/signup
content-type: application/json

{
    "email": "junsoopooh@naver.com",
    "password": "19940411"
}

이미지1

잘 만들어졌다. 콘솔창에도 받은 정보가 뜬다.

DTO를 통한 validation의 작동 확인을 위해 잘못된 이메일 형식을 보내보자

이미지 2

password를 빼고 보내보자.

이미지3

굿

whitelist

DTO 설정시 main.ts파일에 whitelist: true라고 표시했었다.

이 코드는 들어오는 요청의 body에 예상 외의 속성이 들어오더라도 포함지 않도록 하는 것이다.

예를 들어, 위에서 email과 password만 들어오도록 되어있지만

1
2
3
4
5
{
    "email" : "junsoopooh@naver.com",
    "password": "19940411",
    "age": "30"
}

이라는 본문이 들어오면 age 속성은 제거한채 email과 password만 들어온다. 보안과도 관련있는 코드이다.