본문 바로가기
NestJS

[NestJS] 모듈 네스팅

by Programmer.Junny 2025. 3. 12.

모듈 네스팅이란?

모듈 네스팅은 하나의 모듈이 다른 모듈을 import하여 계층적인 구조를 구성하는 것을 의미한다.

모듈 네스팅의 장점

  • 관심사의 분리
    • 기능별로 모듈을 분리하면 코드 관리가 수월해진다.
  • 재사용성
    • 한 번 정의된 모듈은 여러 다른 모듈에서 재사용할 수 있다.
  • 의존성 관리
    • 모듈 간의 의존성을 명확하게 관리할 수 있으며, 순환 의존성 문제를 forwardRef() 등의 기법으로 해결할 수 있다.

Comments (댓글) 구현해보기

1. cli 로 posts 내부에 comments 모듈 구현하기

nest g resource posts/comments

2. Comments Entity 생성하기

import { IsNumber, IsString } from "class-validator";
import { BaseModel } from "src/common/entity/base.entity";
import { PostsModel } from "src/posts/entity/posts.entity";
import { UsersModel } from "src/users/entity/users.entity";
import { Column, Entity, ManyToOne } from "typeorm";

@Entity()
export class CommentsModel extends BaseModel {
    @ManyToOne(() => UsersModel, (user) => user.comments)
    author: UsersModel;

    @ManyToOne(() => PostsModel, (post) => post.comments)
    post: PostsModel;

    @Column()
    @IsString()
    comment: string;

    @Column({
        default: 0,
    })
    @IsNumber()
    likeCount: number;
}

댓글은 하나의 사용자에서 여러 개가 생성될 수 있으며, 하나의 포스트에서 여러 개가 생성될 수 있으므로, author, postManyToOne 으로 구현한다.

entities: [
        PostsModel,
        UsersModel,
        ImageModel,
        ChatsModel,
        MessagesModel,
        CommentsModel,
      ],

모델을 사용하려면 app.module.ts 에 entities에 등록해줘야 한다. (스키마 구성)

@Module({
  imports: [
    TypeOrmModule.forFeature([
      CommentsModel,
    ]),

Repository를 사용하기 위해선 forFeature에 모델을 등록한다.

3. Paginate Comment API 구현하기

3.1. 모듈 등록

@Module({
  imports: [
    TypeOrmModule.forFeature([
      CommentsModel,
    ]),
    CommonModule,
    AuthModule,
    UsersModule,
    PostsModule,
  ],

CommonServiceUsersService, PostsService 등을 사용하기 때문에 모듈들을 imports 해준다.

3.2. 생성자 주입

@Injectable()
export class CommentsService {
    constructor(
    @InjectRepository(CommentsModel)
    private readonly commentsRepository: Repository<CommentsModel>,
    private readonly commonService: CommonService,
    ) {}

CommentsModel 과 CommonService를 사용하기 위해 주입하여 준다.

3.3. PaginateCommentsDto 구현

import { BasePaginationDto } from "src/common/dto/base-pagination.dto";

export class PaginateCommentsDto extends BasePaginationDto {

}

굳이 PaginateCommentsDto가 필요없으나, 추후에 추가될 수 있는 프로퍼티들을 위해 구현한다.

3.4. paginateComments 를 Service에 작성하기

async paginateComments(dto: PaginateCommentsDto, postId: number) {
        return this.commonService.paginate(
            dto,
            this.commentsRepository,
            {
                where: {
                    post: {
                        id: postId,
                    }
                },
                ...DEFAULT_COMMENT_FIND_OPTIONS,
            },
            `posts/${postId}/comments`,
            );
    }

CommentsService에 postId에 해당하는 모든 Comment(댓글)을 불러오도록 구현한다.

3.5. FindOptions 공통 설정

import { FindManyOptions } from "typeorm";
import { CommentsModel } from "../entity/comments.entity";

export const DEFAULT_COMMENT_FIND_OPTIONS: FindManyOptions<CommentsModel> = {
    relations: {
        author: true,
    },
    select: {
        author: {
            id: true,
            nickname: true,
        }
    }
}

relations 를 공통적으로 적용하기 위해 FindManyOptions 를 구현하여 적용한다.

3.6. Controller 구현

@Get()
  getComments(
    @Param('postId', ParseIntPipe) postId: number,
    @Query() query: PaginateCommentsDto
  ) {
    return this.commentsService.paginateComments(query, postId);
  }

서비스에서 만든 paginateComments를 적용할 getComments 라우터를 만든다.

3.7. 포스트맨 테스트

4. ID 기반으로 하나의 Comment를 가져오는 API 구현하기

4.1. 서비스 코드 작성

async getCommentById(id: number) {
        const comment = await this.commentsRepository.findOne({
            where: {
                id,
            },
            ...DEFAULT_COMMENT_FIND_OPTIONS,
        });

        if (!comment) {
            throw new BadRequestException(
                `id: ${id} Comment는 존재하지 않습니다.`
            )
        }

        return comment;
    }

CommentsModel에서 id로 검색하면 Comment를 찾을 수 있다.

4.2. 라우터 구현

@Get(':commentId')
  getComment(
    @Param('commentId', ParseIntPipe) commentId: number
  ) {
    return this.commentsService.getCommentById(commentId);
  }

4.3. 포스트맨 테스트

5. POST Comment 구현하기

5.1. CreateCommentsDto 구현

import { PickType } from "@nestjs/mapped-types";
import { CommentsModel } from "../entity/comments.entity";

export class CreateCommentsDto extends PickType(CommentsModel, [
    'comment'
]) {
    
}

CommentsModel에서 comment만을 사용하는 CreateCommentsDto를 구현한다.

5.2. 서비스 구현

async createComment(
        dto: CreateCommentsDto,
        postId: number,
        author: UsersModel,
    ) {
        return this.commentsRepository.save({
            ...dto,
            post: {
                id: postId,
            },
            author,
        });
    }

5.3. 라우터 구현

@Post()
  @UseGuards(AccessTokenGuard)
  postComment(
    @Param('postId', ParseIntPipe) postId: number,
    @Body() body: CreateCommentsDto,
    @User() user: UsersModel,
  ) {
    return this.commentsService.createComment(body, postId, user);
  }

포스트를 생성하기 위해선 인증이 되어있어야 하므로 Guard를 사용한다.

5.4. 포스트맨 테스트

6. PATCH Comment 구현하기

6.1. UpdateCommentsDto 구현

import { PartialType } from "@nestjs/mapped-types";
import { CreateCommentsDto } from "./create-comments.dto";

export class UpdateCommentsDto extends PartialType(CreateCommentsDto) {
    
}

6.2. 서비스 구현

async updateComment(
        dto: UpdateCommentsDto,
        commentId: number,
    ) {
        const comment = await this.commentsRepository.findOne({
            where: {
                id: commentId,
            }
        });

        if(!comment) {
            throw new BadRequestException(
                `존재하지 않는 Comment 입니다. ${commentId}`
            );
        }
        
        const prevComment = await this.commentsRepository.preload({
            id: commentId,
            ...dto,
        });

        const newComment = await this.commentsRepository.save(
            {
                ...prevComment,
            }
        );

        return newComment;
    }

preload는 DB에 변경사항을 저장하지 않고 임시로 변경한 데이터로 반환한다. (save를 하여야 반영)

6.3. 라우터 구현

  @Patch(':commentId')
  @UseGuards(AccessTokenGuard)
  patchComment(
    @Param('commentId', ParseIntPipe) commentId: number,
    @Body() body: UpdateCommentsDto,
  ) {
    return this.commentsService.updateComment(body, commentId);
  }

6.4. 포스트맨 테스트

4번 댓글을 변경해보도록 하자.

PATCH로 구현하여 comment 필드의 내용을 변경하였다.

GET으로 확인 시 4번 댓글의 내용이 잘 적용된 것을 볼 수 있다.

7. DELETE Comment 구현하기

7.1. 서비스 구현

    async deleteComment(commentId: number) {
        const comment = await this.commentsRepository.findOne({
            where: {
                id: commentId,
            }
        });

        if(!comment) {
            throw new BadRequestException(
                `존재하지 않는 Comment 입니다. ${commentId}`
            );
        }

        return await this.commentsRepository.delete(commentId);
    }

7.2 라우터 구현

  @Delete(':commentId')
  @UseGuards(AccessTokenGuard)
  deleteComment(
    @Param('commentId', ParseIntPipe) commentId: number,
  ) {
    return this.commentsService.deleteComment(commentId);
  }

7.3. 포스트맨 테스트

8. Path의 Post가 존재하는지 검증하는 Middleware 생성하기

CommentController에서 postId가 존재하지 않는 경우에 대한 에러처리를 미들웨어(혹은 가드)로 구현할 수 있다.

import { BadRequestException, Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response, NextFunction } from "express";
import { PostsService } from "src/posts/posts.service";

@Injectable()
export class PostExistsMiddleware implements NestMiddleware {
    constructor(
        private readonly postService: PostsService,
    ) {}

    async use(req: Request, res: Response, next: NextFunction) {
        const postId = req.params.postId;

        if(!postId) {
            throw new BadRequestException(
                'Post ID 파라미터는 필수입니다.'
            );
        }

        const exists = await this.postService.checkPostExistsById(parseInt(postId));

        if(!exists) {
            throw new BadRequestException(
                'Post가 존재하지 않습니다.'
            );
        }

        next();
    }
}

9. Middleware 적용하기 

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { CommentsService } from './comments.service';
import { CommentsController } from './comments.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CommentsModel } from './entity/comments.entity';
import { CommonModule } from 'src/common/common.module';
import { AuthModule } from 'src/auth/auth.module';
import { UsersModule } from 'src/users/users.module';
import { PostExistsMiddleware } from './middleware/post-exists.middleware';
import { PostsModule } from '../posts.module';

@Module({
  imports: [
    TypeOrmModule.forFeature([
      CommentsModel,
    ]),
    CommonModule,
    AuthModule,
    UsersModule,
    PostsModule,
  ],
  controllers: [CommentsController],
  providers: [CommentsService],
})
export class CommentsModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(PostExistsMiddleware).forRoutes(CommentsController);
  }
}

comments.module.ts 의 CommentsModuleNestModuleimplements를 적용하여 미들웨어를 실행시킬 수 있다.

'NestJS' 카테고리의 다른 글

[NestJS] Authorization Guard  (0) 2025.03.14
[NestJS] RBAC (Role Base Access Controll)  (0) 2025.03.13
[NestJS] SocketIO 심화  (1) 2025.03.12
[NestJS] SocketIO 일반  (0) 2025.03.12
[NestJS] Middleware  (0) 2025.03.11

최근댓글

최근글

skin by © 2024 ttuttak