모듈 네스팅이란?
모듈 네스팅은 하나의 모듈이 다른 모듈을 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, post는 ManyToOne 으로 구현한다.
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,
],
CommonService와 UsersService, 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 의 CommentsModule 에 NestModule를 implements를 적용하여 미들웨어를 실행시킬 수 있다.
'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 |