728x90

저번에 User와 관련한 Entity를 만들었었다.

@Entity('users')
@Unique(['email'])
export class Users{

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @Column()
    email: string;

    @Column()
    password: string;
}

 

이 Entity를 등록했었고, 이거를 바탕으로 nest repository를 서비스에 만들어보자.

 

@Injectable()
export class UsersService {
    
    constructor(@InjectRepository(Users) private readonly userRepository: Repository<Users>) { }
}

이렇게 UserService에 UserRepository의 의존성을 등록해준다.

 

컨트롤러의 내용은 제외하고 서비스의 코드만 채워볼것이다.

  • Create

우선 create부터 만들어보자.

async create(createUserDto: CreateUserDto) {
        const user = this.userRepository.create({
            name: createUserDto.name,
            email: createUserDto.email,
            password: createUserDto.password
        });
        
        return await this.userRepository.save(user);
    }

 

create와 save를 사용했는데, create가 데이터베이스에 값을 create하는 메서드는 아니다.

해당 entity 구조를 바탕으로 새로운 entity를 만들어주는 메서드이다.

save가 데이터베이스에 해당 값을 저장해주는 메서드이다.

 

사실 아래와 같은 방법으로도 저장은 가능하다.

this.userRepository.save({name: createUserDto.name, email: createUserDto.email,  password: createUserDto.password});

하지만 그럼에도 불구하고 entity를 생성해서 저장하는 이유는 entity에 비즈니스 로직들이 들어있으며, 데코레이터등을 사용해 값을 미리 검증할 수 있다.

그리고 insert, update와 같은 작업 이후에 특정 메서드가 동작하도록 할 수 있다.

 

    @AfterInsert()
    logInsert(){
        console.log(`user inserted id: ${this.id}`);
    }

이렇게 AfterInsert()를 사용하면 데이터베이스 저장 이후에 로그가 출력되도록 할 수 있다.

 

이렇게 entity의 기능들과 hook을 사용할 수 있도록, 직접 객체 리터럴로 저장하는 것이 아닌 create로 엔티티를 생성해서 저장하도록 하자.

 

  • Read

이번엔 read이다.

우선 id를 사용해 하나의 entity만 가져오는 메서드이다.

    async findOne(id: number){
        return this.userRepository.findOne({where: {id: id}});
    }

이렇게 findOne where절 내에 조건을 작성해주면 된다.

id는 PK이기 때문에 하나만 값을 가져와 findOne() 메서드를 사용했다.

여기서 만약 찾지 못하는 경우를 대비해 404 에러를 반환할수 있도록 하자.

 

    async findOne(id: number): Promise<Users> {
        const user = await this.userRepository.findOne({where: {id: id}});
        if(!user){
            throw new NotFoundException('User not found');
        }
        return user;
    }

 

이번에는 name을 사용해 많은 데이터들을 검색하는 메서드이다.

    async findAll(name: string): Promise<Users[]> {
        return await this.userRepository.find({where: {name}});
    }

역시 where절의 조건으로 name을 추가해주었고, 하나의 엔티티만 가져오는 것이 아니기 때문에 find() 메서드를 사용한다.

여기서는 만약 해당하는 값이 없다면 404에러가 아닌 빈 배열이 반환되게 될 것이다.

 

  • Update
    async update(id: number, userAttrs: Partial<Users>): Promise<Users> {
        const user = await this.findOne(id);
        Object.assign(user, userAttrs);
        return await this.userRepository.save(user);
    }

여기서 Partial 타입을 사용한 것을 볼 수 있는데

Users의 부분적인 속성들을 가지는 클래스이다.

{name}, {name, email} ... 등등 Users 클래스 내의 속성들을 부분적으로 가지고 있는 클래스이다.

그 값을 Object.assign을 통해 기존의 user 데이터에 덮어쓰고, save() 메서드를 통해 데이터베이스에 저장한다.

 

export class UpdateUserDto {

    @IsString()
    @IsOptional()
    name: string;

    @IsEmail()
    @IsOptional()
    email: string;

    @IsString()
    @IsOptional()
    password: string;
}

dto에서 @IsOptional()을 사용하면, json에서 선택적으로 값을 넣어 요청할 수 있다.

 

  • Delete
    async delete(id: number): Promise<Users> {
        const user = await this.findOne(id);
        return this.userRepository.remove(user);
    }

 

저번과 마찬가지로 조회 후 값을 확인하고 삭제한다.

remove()는 아이디가 아닌 entity를 넣어서 삭제한다.

 

 

그럼 여기서 궁금한점이 있다.

save() 메서드는 언제 insert로 동작하고, 언제 update로 동작하는걸까?

다음과 같은 3가지 상황을 만들어서 save()를 호출해봤다.

모두 find를 호출하지 않은 상태로 save()를 호출하는 것이다.

  • save()로 넘긴 entity에 id가 없는 경우
  • entity에 id가 있지만 데이터베이스에는 해당 id가 없는 경우
  • entity에 id가 있으며, 데이터베이스에도 해당 id가 존재하는 경우

우선 현재 데이터베이스에는 해당 값만 존재한다.

 

첫번째 경우는 역시 성공한다.

POST http://localhost:3000/users/test
Content-Type: application/json

{
  "name": "승규",
  "email": "sda1fs@gmail.com",
  "password": "1234"
}

그냥 평범하게 insert 쿼리가 날아간 것을 볼 수 있다.

 

두번째 경우도 성공한다.

POST http://localhost:3000/users/test
Content-Type: application/json

{
  "id": 123,
  "name": "승규",
  "email": "sda1123fs@gmail.com",
  "password": "1234"
}

 

쿼리를 보면 신기하게도 미리 조회를 해보고, insert 한 것을 볼 수 있다.

 

세번째 경우에서는 중복 에러가 발생할 줄 알았지만 성공했다.

POST http://localhost:3000/users/test
Content-Type: application/json

{
  "id": 123,
  "name": "승규",
  "email": "sda112123s@gmail.com",
  "password": "1234"
}

 

이렇게 요청을 하니

이거도 미리 조회를 하고, 값이 있기에 update로 바꾼 것을 볼 수 있었다.

 

save() 메서드는 id가 존재하는 경우에 데이터베이스에 2번 접근하는 것을 알 수 있었다.

728x90

우선 필요한 라이브러리들을 가져와준다.

npm install @nestjs/typeorm typeorm mysql2

 

그리고 AppModule에 우리가 사용할 데이터베이스의 연결정보를 추가해줘야 한다.

 

imports에 TypeOrmModule.forRoot()로 다음과 같이 추가해준다.

@Module({
  imports: [TypeOrmModule.forRoot({
    type: 'mysql',
    host: 'localhost',
    port: 3306,
    username: 'root',
    password: '1204',
    database: 'usedCars',
    entities: [__dirname + '/**/*.entity{.ts,.js}'],
    synchronize: true
  }), UsersModule, ReportsModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

mysql을 사용하기 때문에 type은 mysql이며, database로 스키마를 나타낸다.

synchronize를 사용하면 변경된 entity 정보가 데이터베이스의 스키마에도 반영된다.

개발 환경에서만 사용하고, 배포 환경에서는 사용하지 말도록 하자.

 

entities에 우리가 작성할 entity들을 넣어준다.

작성한 entity들을 하나하나 추가할 수 있지만, 많은 양의 entity를 작성하기 때문에 convention에 따라 .entity.ts로 끝나는 파일들을 모두 추가해주도록 path를 지정해주자.

 

이제 예시로 entity를 하나 만들어보자.

nest에서는 entity를 다음과 같은 과정으로 생성한다.

 

1번으로 UserEntity를 만들어보도록 하겠다.

@Entity('users')
@Unique(['email'])
export class Users{

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    email: string;

    @Column()
    password: string;
}

@Entity를 붙여 Entity임을 알려주고, 사용할 테이블의 이름을 적는다.

@Unique()에는 unique 속성을 가질 column 명을 적어준다.

@PrimaryGeneratedColumn을 사용하면 autoincrement의 int column이 생성된다.

 

그 다음에는 2번으로 UserModule에 UserEntity를 추가한다.

@Module({
  imports: [TypeOrmModule.forFeature([Users])],
  controllers: [UsersController],
  providers: [UsersService]
})
export class UsersModule {}

 

3번도 해야하지만, 해당 path의 모든 entity를 등록했기에 우리는 할 필요가 없다.

 

일단 이렇게 해서 서버를 실행해보면, synchronize가 true이기 때문에 데이터베이스에 다음과 같이 테이블이 생성된 것을 볼 수 있다.

 

UserEntity는 성공적으로 작성 한 것 같다.

 

728x90

스프링에서도 최근에는 모듈간의 의존성을 만들어서 멀티모듈로 개발을 했었다.

그렇게 개발하는 방법이 중복되는 코드를 줄일 수 있기 때문이다.

 

Nest에서도 해당 방법으로 개발을 해보도록 하자.

 

우선 생성할 프로젝트의 구조는 다음과 같다.

Cpu module과 Disk module에서 Power module을 가져와서 사용하고, 최종적으로는 그 모듈들을 Computer Module에서 사용하는 것이다.

단순히 기능만 사용하는 것이기 때문에 Controller는 Computer module에만 생성하면 될 것이다.

 

모듈들은 미리 생성을 해두었고 우선 가장 기본이 되는 Power module부터 만들어보자.

nest g service power

 

그리고 해당 서비스에 간단한 메서드를 추가해보았다.

@Injectable()
export class PowerService {

    supplyPower(amount: number){
        console.log(`${this.constructor.name} 클래스에서 ${amount}의 전력을 공급했습니다.`)
    }
}

가장 기본적으로 전력을 공급하는 역할을 할 것이고, 해당 클래스의 정보를 확인하기 위해 값을 추가해보았다.

 

그리고 PowerModule에서 해당 서비스를 exports 해준다.

@Module({
  providers: [PowerService],
  exports: [PowerService]
})
export class PowerModule {}

이렇게 하면 PowerModule을 import한 다른 모듈에서 PowerService를 사용할 수 있게 된다.

 

마찬가지로 Cpu module과 Disk module에서 해당 모듈들을 가져가 사용해보자.

먼저 Disk module이다.

 

Disk module에서는 Power module을 가져오고, DiskService를 export 해야하니 다음과 같이 작성한다.

@Module({
  providers: [DiskService],
  imports: [PowerModule],
  exports: [DiskService]
})
export class DiskModule {}

 

그리고 service에서는 그냥 같은 모듈이라고 생각하며 의존성을 가져온다.

@Injectable()
export class DiskService {

    constructor(
        private readonly powerService: PowerService
    ) {}

    getData(): number[]{
        this.powerService.supplyPower(10);
        
        return [Math.floor(Math.random() * 10),  Math.floor(Math.random() * 10)];
    }
}

 

CpuService에서는 그냥 다음과 같이 만들어줬다.

@Injectable()
export class CpuService {
    constructor(private readonly powerService: PowerService) { }
    
    calculate(a: number, b: number): number {
        this.powerService.supplyPower(20);
        return a + b;
    }
}

 

이제 이 모든 것을 사용할 Computer module이다.

 

사용할 모듈들은 imports에 추가하고, 컨트롤러를 빼준다.

@Module({
  providers: [ComputerService],
  controllers: [ComputerController],
  imports: [CpuModule, DiskModule]
})
export class ComputerModule {}

 

@Injectable()
export class ComputerService {

    constructor(private readonly cpuService: CpuService,
                private readonly diskService: DiskService) {}

    useCom(): number{
        const numbers = this.diskService.getData()
        return this.cpuService.calculate(numbers[0], numbers[1]);
    }

}

코드를 다음과 같이 작성하고 요청을 해보니

 

이렇게 Power module의 서비스를 이용하는 것을 볼 수 있다.

 

같은 인스턴스인지 궁금해서 랜덤값으로 값을 부여하고 출력해보았는데, 같은 값이 나왔다.

같은 서버 내에서는 imports 하더라도 싱글톤으로 같은 인스턴스를 공유하고 있는 것을 알 수 있었다.

 

 

 

이렇게 다른 모듈에서 export, import 하게 되면 컨테이너에서 해당 import 클래스들을 가져올 수 있게 된다.

728x90

결국 Nest도 프레임워크이기 때문에 제어의 역전과 런타임 중 클래스를 주입하는 DI를 사용하게 된다.

스프링과 동일한 개념이겠지만, 어차피 마지막으로 정리하는 것일테니 자세하게 정리하고 가자.

 

우선 IOC(Inversion of Control Principle, 제어의 역전)은 다음을 말한다.

클래스가 의존성이 필요한 클래스를 본인이 직접 인스턴스를 생성하는 것이 아니라, 알아서 주입이 되도록 제어의 권한을 프레임워크에 위임하는 것을 말한다.

 

기존에는 생성자에 의존성이 있는 클래스를 다음과 같이 생성했었다.

@Controller('messages')
export class MessagesController {

    private messagesService: MessagesService;

    constructor(){
        this.messagesService = new MessagesService();
    }
}

이거를 IOC의 DI로 바꾸어야 한다는 것이다.

 

여기에서 messageService를 이용할 수 있도록 하는 방법은 3가지가 있을 것이다.

 

  • 인스턴스 직접 생성

이번에 만들었던 방법대로 직접 인스턴스를 생성하는 것이다.

@Controller('messages')
export class MessagesController {

    private messagesService: MessagesService;

    constructor(){
        this.messagesService = new MessagesService();
    }
}

 

  • 생성자를 통해 외부에서 주입
@Controller('messages')
export class MessagesController {

    private messagesService: MessagesService;

    constructor(messagesService: MessagesService){
        this.messagesService = messagesService;
    }
}

 

  • 해당 클래스의 추상화를 주입
interface Service{
	....
}

@Controller('messages')
export class MessagesController {

    private messagesService: Service;

    constructor(messagesService: Service){
        this.messagesService = messagesService;
    }
}

 

당연히 아래로 갈수록 의존성이 낮아지기에 좋은 방법이다.

다른 구현체로 변경하기가 쉽기 때문이다.

예를들면 실제 배포에서는 우리가 만들었던 서비스를 주입하고, 테스트 환경에서는 해당 인터페이스를 모킹해서 구현한 서비스를 주입하는 작업을 할 수 있기 때문이다.

 

결국 모든 목적은 컨트롤러가 될 것이다.

컨트롤러를 만들어야 적절한 API를 제공하기 때문이다.

 

그렇게 컨트롤러의 의존성에 대해 필요한 클래스들을 싱글톤으로 생성하여 등록해두고 의존성이 있는 클래스들에 대하여 해당 클래스들을 공유한다.

 

순서는 다음과 같다.

1. 시작하면 모든 클래스들을 컨테이너에 등록한다.

2. 컨테이너는 각 클래스들의 의존성들을 파악한다.

3 컨테이너에게 각 클래스들의 인스턴스 생성을 요청한다.

4. 컨테이너는 필요한 모든 의존성을 생성하고, 우리에게 인스턴스를 제공한다.

5. 컨테이너는 모든 클래스들을 싱글톤으로 생성하고, 의존성이 있는 각 클래스들에 공유한다.

 

이제 이렇게 DI를 사용하는 방법으로 코드를 리펙토링 해보자.

우선 의존성으로 가져가야 하는 클래스들에 @Injectable()을 붙여준다.

그러면 이 클래스들이 생성한 인스턴스로 컨테이너에 등록된다.

 

그리고 그 중 제공되어야 하는 클래스들을 알리기 위해 @Module()에 providers로 등록한다.

 

이렇게하면 providers에서 의존성으로 필요한 클래스들을 가져가서 컨테이너까지 생성하게 된다.

 

실행을 해보면 정상적으로 실행되는 것을 볼 수 있다.

 

 

728x90

저번에 사용했던 그림으로, Service는 Repository에서 데이터를 받아와서 비즈니스 로직을 다루는 layer이다.

 

Repository Layer를 대충 만들기는 했지만, 그래도 사용은 되기 때문에 이 Repository를 바탕으로 Service를 만들어보자.

 

우선 nest의 convention에 따라 service를 생성한다.

 

service에서는 repository에서 데이터를 받아오기 때문에, service가 repository에게 의존하는 관계가 된다.

 

내부에서 repository를 사용해야 하기 때문에, 생성자를 통해서 repository를 만들어서 가져오자.

    constructor(private readonly messagesRepository: MessagesRepository) {
        this.messagesRepository = new MessagesRepository();
    }

 

우선 이렇게 만들기는 했지만, 이후에는 절대 이렇게 사용하지 않는다.

messagesRepository를 직접 만들어서 사용하는 것이 아니라, DI를 통해 주입받도록 만들어야 한다.

constructor에서 messagesRepository를 생성하지 않는 것이 목표다.

일단 이렇게 만들고 service layer의 개발에 집중해보자.

 

service에서 만들 메서드는 다음과 같다.

export class MessagesService {

    constructor(private readonly messagesRepository: MessagesRepository) {
        this.messagesRepository = new MessagesRepository();
    }

    async findOne(id: string){}

    async findAll(id: string){}

    async create(id: string){}

}

내부 코드는 작성하지 않았지만, 이런 의문이 들 수도 있다.

어차피 이거 repository에 있는 내용이랑 똑같은데, 굳이 service를 만들어야 해?

 

하지만 스프링에서도 같은 구조를 사용했었고, 그렇기에 왜 이렇게 사용하는지는 알 수 있을 것이다.

Service Layer에서는 이런 repository의 메서드들 조합해 비즈니스 로직을 만들고, 이런 부분에 대해 공통 관심사인 트랜잭션 처리등의 역할을 하기 때문에 service Layer는 굉장히 중요하다.

 

export class MessagesService {

    constructor(private readonly messagesRepository: MessagesRepository) {
        this.messagesRepository = new MessagesRepository();
    }

    async findOne(id: string){
        return this.messagesRepository.findOne(id);
    }

    async findAll(id: string){
        return this.messagesRepository.findAll();
    }

    async create(content: string){
        return this.messagesRepository.create(content);
    }

}

결국 repository layer의 메서드들을 그대로 사용하는 코드들이지만, 그래도 만들어보도록 하자.

 

그리고 controller에 연결 한 후, http 요청을 보내보자.

controller에 작성한 코드는 간단하기에 생략하겠다.

 

이렇게 POST 요청을 보내니

messages.json 파일에 이렇게 생성이 되고

 

GET으로 해당 데이터를 가져올 수 있었다.

 

마지막으로 여기에 예외의 경우를 추가해보자.

당연히 없는 경우의 id로 조회할 수 있다.

 

이렇게 해당 데이터가 없는 경우에도 404 에러가 아닌, 정상적인 200 코드를 넘기고 있는 것을 볼 수 있다.

 

이런 경우에 NotFoundException을 사용해 예외를 추가해보자.

    @Get('/:id')
    async getMessage(@Param('id') id: string){
        const message = await this.messagesService.findOne(id)

        if(!message){
            throw new NotFoundException('Message not found');
        }

        return message
    }

이렇게 Null인 경우에 NotFoundException을 throw하면 nest에서 해당 예외에 맞는 에러코드로 변경해준다.

 

이렇게 해당 경우에 맞는 에러를 반환해주는 것을 볼 수 있다.

 

이번 글에서는 생성자에서 해당 의존성 클래스들을 생성해서 사용했었다.

다음 글에서는 이 부분을 바꿔보도록 하겠다.

728x90

우선 제목부터 나와있지만 해당 글에서는 데이터베이스를 사용해서 repository를 만드는 것이 아닌, 별도의 파일에 데이터들을 저장하고 그것을 repository로 불러오고 저장할 것이다.

 

우선 repository에 다음과 같은 메서드들을 만들것이다.

export class MessagesRepository {
    async findOne(id: string){
        
    }

    async findAll(){

    }

    async create(message: string){

    }
}

 

우선 구조만 이렇게 만들어두고, messages의 데이터들을 저장하기 위해 루트 디렉토리에 다음과 같은 파일도 생성한다.

이제 read, write로 해당 json 파일에 데이터를 저장할 것이다.

 

이제 repository의 남은 부분을 완성해보자.

 

우선 데이터는 이렇게 저장되어 있을 것이다.

{
	1: {
    	"id": 1,
        "content": "hi"
    }
}

그러면 우리는 1을 검색 할 때, [1]로 가져오면 되는 것이다.

 

이렇게 readFile로 메시지들을 읽어오고, Json으로 파싱한 후 해당 아이디의 값을 가져온다.

    async findOne(id: string){
        const messages = await readFile(`messages.json`, 'utf8');
        const messagesJson = JSON.parse(messages);

        return messagesJson[id];
    }

 

findAll()은 그냥 Json으로 파싱하고, 모든 데이터를 가져오면 된다.

    async findAll(){
        const messages = await readFile(`messages.json`, 'utf8');
        return JSON.parse(messages);
    }

 

마지막으로 가장 어려운 create이다.

 

아이디는 동시성 문제 때문에 우선 random으로 생성한다.

 

그리고 모두 Json으로 불러오고, 해당 Json에 추가하는 값을 넣은 후 다시 파일에 작성하는 방법으로 데이터를 저장한다.

사실 어차피 파일 시스템이라 동시성 문제가 발생하기는 한다...

    async create(content: string){
        const messages = await readFile(`messages.json`, 'utf8');

        const messagesJson = JSON.parse(messages);

        const id = (Math.random() * 9999)

        messagesJson[id] = {id, content};

        await writeFile(`messages.json`, JSON.stringify(messagesJson), 'utf8');
    }

 

이렇게 임시로 repository layer를 만들어보았다.

 

당연히 원래는 데이터베이스로 만들어야 하지만, 임시로 텍스트 파일을 사용했으며 테스트는 service layer까지 만들어보고 진행할 예정이다.

728x90

우선 Post를 통해 요청되는 값 자체를 콘솔로 찍어보자.

값을 이렇게 보내면, 위의 부분이 Header이고 아래부분이 Body이다.

 

다음과 같은 핸들러로 요청을 출력해보니

    @Post()
    async createMessage(@Body() body: any, @Headers() headers: Record<string, string>) {
        console.log('📌 Headers:');
        for (const [key, value] of Object.entries(headers)) {
            console.log(`  ${key}: ${value}`);
        }

        console.log('📌 Body:');
        console.log(JSON.stringify(body, null, 2));
    }

 

그냥 그대로 나오는 것을 볼 수 있다.

그럼 우리는 이제 사용자의 요청에 대하여, 이 헤더와 바디를 통해서 처리를 하도록 할 것이다.

 

그럼 일단 body부터 원하는 데이터로 가져올 수 있도록 하자.

해당 데이터를 이렇게 any 타입이 아닌, 우리가 원하는 값으로 가져오기 위해 DTO를 작성한다.

 

export class CreateMessageDto {
    content: string
}

현재는 가져오는 값이 content 밖에 존재하지 않기 때문에, content 필드 하나만 가지고 있는 DTO를 생성한다.

 

    @Post()
    async createMessage(@Body() body: CreateMessageDto) {
        console.log(`This is content: ${body.content}`);
    }

그리고 body의 타입을 CreateMessageDto로 직접 지정해준 후에 재요청해보자.

 

그러면 이렇게 타입이 적절하게 지정된 것을 볼 수 있다.

 

하지만 이렇게 정상적인 값이 아니라

POST http://localhost:3000/messages
Content-Type: application/json

{
}

이렇게 본문이 비어있거나, 

POST http://localhost:3000/messages
Content-Type: application/json

{
  "content": null
}

null인 값이 있어도 

위와 같이 정상 응답을 주게 된다.

 

우리는 이렇게 잘못된 요청에 대하 BadRequest인 400 응답을 주어야 한다.

 

이렇게 자동으로 체크를 해서 응답을 해줄 수 있도록, validation pipe를 사용해보자.

 

사용자의 요청과 응답이 서버에서 위와 같은 순서로 처리가 되는데, 이 중 가장 먼저 이루어지는 데이터의 유효성 체크이다.

 

현재 CreateMessageDto는 content를 string 타입으로 받고 있다.

그렇기에 content가 string인 것을 확인하고 나서야 요청 처리를 시작해야 한다.

 

class-validator를 사용해보자.

우선 이런 타입에 대한 체크는 모든 핸들러에서 이루어지기 때문에, 전역으로 validation pipe를 추가한다.

 

main.ts에서 app에 다음 속성을 추가하자.

  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true
  }));

이러면 모든 request에 대하여 dto에 타입체크가 시작된다.

 

이제 dto에도 데코레이터를 추가하자.

export class CreateMessageDto {
    @IsString()
    content: string
}

이렇게 string 타입이기 때문에 @IsString() 데코레이터를 추가해준다.

 

그렇게해서 잘못된 요청을 보내보자.

그렇게 요청해보니, 잘못된 요청이라며 400에러가 나오는 것을 볼 수 있었다.

 

근데 여기서 궁금한 부분이 있다.

우리가 만든 TypeScript는 JavaScript로 변환되어 동작하게 되는데, 그럼 이 validation pipe는 어떻게 동작하는 것인가.

 

tsconfig.json을 보면 다음과 같은 부분이 있다.

 

이 설정을 통해서 데코레이터의 정보가 자바스크립트에도 포함이 되게 된다.

변환된 자바스크립트의 dto를 보면 다음과 같이 나와있다.

 

__decorate([
    (0, class_validator_1.IsString)(),
    __metadata("design:type", String)
], CreateMessageDto.prototype, "content", void 0);

데코레이터로 파라미터가 string 타입임을 알리고 있는 것이다.

 

이렇게 자바스크립트에서도 타입에 관하여 동작할 수 있도록 한다.

728x90

우선 명령어를 입력한다.

사실 이 nest 프로젝트를 생성해주는 명령어가 있다고 한다.

 

nest new .

 

이 명령어를 터미널에 입력하면, 현재 폴더에 nest 프로젝트가 생성된다.

 

그러면 모든 nest 프로젝트에 필요한 파일들이 설치된다.

 

여기서 우리는 app module을 사용할 것은 아니기에 main.ts를 제외하고 모두 삭제해도 된다.

 

이제 컨트롤러와 모듈을 추가해야 하는데, 스프링에서는 직접 파일들을 만들면서 연결해주었지만 nest는 터미널 명령어를 사용해 연결한다.

 

  우선 message 모듈을 작성할 것이기에 messages 모듈을 만들어보자.

 

nest g module messages

여기서 g 옵션은 generate이며, messages라는 모듈을 생성하라는 명령어이다.

 

이어서 컨트롤러도 생성해보자.

nest g controller messages

이러면 controller의 테스트 파일과 controller가 생성된다.

 

 

messages 모듈이 3000번 포트에서 동작할 수 있도록, main.ts에 추가해주고

async function bootstrap() {
  const app = await NestFactory.create(MessagesModule);
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

 

message module을 확인해보면, 다음과 같이 컨트롤러가 추가된 것을 볼 수 있다.

 

컨트롤러를 확인해보기 위해, 간단한 Get 메서드를 작성해보고

@Controller('messages')
export class MessagesController {

    @Get()
    listMessages(): string{
        return "hi Seungkyu!!"
    }
}

 

다음과 같이 http 파일을 작성해서 테스트 해보았다.

GET http://localhost:3000/messages

 

일단은 이렇게 정상적인 응답이 오는 것을 보아서는, 아직까지는 성공한 것 같다!

+ Recent posts