Nest.js 블로그 단계별 따라하기: 댓글 시스템
Takashi Yamamoto
Infrastructure Engineer · Leapcell

이전 튜토리얼에서 세션과 Passport.js를 기반으로 블로그를 위한 완벽한 사용자 인증 시스템을 통합했습니다. 이제 사용자는 블로그 시스템에 등록하고 로그인할 수 있으며, 접근하려면 로그인이 필요한 보호된 라우트가 있습니다.
이제 독자와 작성자를 명확하게 구분할 수 있으므로, 그들 간의 상호 작용을 위한 기능을 추가할 완벽한 시점이 아닐까요?
이 글에서는 블로그의 기본적이면서도 핵심적인 기능인 댓글 시스템을 추가하겠습니다.
구체적으로 다음 기능을 구현할 것입니다.
- 각 게시물 아래에 댓글 목록 표시
- 로그인한 사용자가 댓글을 게시하도록 허용
첫 번째 단계: 댓글 데이터 모델 생성
Post
및 User
엔티티와 마찬가지로 Comment
엔티티도 자체 데이터베이스 테이블과 해당 엔티티 파일이 필요합니다.
1. 데이터베이스 테이블 생성
먼저 PostgreSQL 데이터베이스에서 다음 SQL 문을 실행하여 comment
테이블을 생성합니다. 이 테이블은 postId
및 userId
를 통해 post
및 user
테이블과 관계를 설정합니다.
CREATE TABLE "comment" ( "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "content" TEXT NOT NULL, "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), "postId" UUID REFERENCES "post"("id") ON DELETE CASCADE, "userId" UUID REFERENCES "user"("id") ON DELETE CASCADE );
팁:
ON DELETE CASCADE
는Post
또는User
가 삭제될 때 데이터 일관성을 보장하기 위해 해당 댓글도 자동으로 삭제됨을 의미합니다.
2. 댓글 엔티티 생성
다음으로 Nest CLI를 사용하여 모든 댓글 관련 로직을 관리할 새로운 comments
모듈을 생성합니다.
nest generate module comments nest generate service comments
지금은 댓글 제출 및 표시에 대한 라우팅 로직이 게시물에 연결될 것이므로 댓글을 위한 별도의 컨트롤러를 만들 필요는 없습니다. 따라서 관련 라우팅 로직은 PostsController
와 나중에 생성할 CommentsController
에 배치할 것입니다.
src/comments
디렉토리에 comment.entity.ts
라는 이름의 파일을 생성합니다.
// src/comments/comment.entity.ts import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, ManyToOne } from 'typeorm'; import { User } from '../users/user.entity'; import { Post } from '../posts/post.entity'; @Entity() export class Comment { @PrimaryGeneratedColumn('uuid') id: string; @Column('text') content: string; @CreateDateColumn() createdAt: Date; @ManyToOne(() => User, (user) => user.comments) user: User; @ManyToOne(() => Post, (post) => post.comments) post: Post; }
3. 관련 엔티티 업데이트
댓글에서 사용자 및 게시물로의 ManyToOne
관계를 정의했습니다. 이제 User
및 Post
엔티티에서 반대 OneToMany
관계를 정의해야 합니다.
src/users/user.entity.ts
업데이트:
// src/users/user.entity.ts import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; import { Comment } from '../comments/comment.entity'; // Comment 가져오기 @Entity() export class User { @PrimaryGeneratedColumn('uuid') id: string; @Column({ unique: true }) username: string; @Column() password: string; @OneToMany(() => Comment, (comment) => comment.user) // 관계 추가 comments: Comment[]; }
src/posts/post.entity.ts
업데이트:
// src/posts/post.entity.ts import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, OneToMany } from 'typeorm'; import { Comment } from '../comments/comment.entity'; // Comment 가져오기 @Entity() export class Post { @PrimaryGeneratedColumn('uuid') id: string; @Column() title: string; @Column('text') content: string; @CreateDateColumn() createdAt: Date; @OneToMany(() => Comment, (comment) => comment.post) // 관계 추가 comments: Comment[]; }
두 번째 단계: 댓글 서비스 구현
이제 댓글 생성 및 쿼리를 처리하는 CommentsService
를 작성해 보겠습니다.
1. 댓글 엔티티 등록
src/comments/comments.module.ts
를 열고 TypeOrmModule
을 등록하고 CommentsService
를 내보내 다른 모듈에서 사용할 수 있도록 합니다.
// src/comments/comments.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CommentsService } from './comments.service'; import { Comment } from './comment.entity'; @Module({ imports: [TypeOrmModule.forFeature([Comment])], providers: [CommentsService], exports: [CommentsService], // 서비스 내보내기 }) export class CommentsModule {}
2. 서비스 로직 작성
src/comments/comments.service.ts
파일을 수정하여 댓글 생성 및 쿼리에 대한 메서드를 추가합니다.
// src/comments/comments.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Comment } from './comment.entity'; import { Post } from '../posts/post.entity'; import { User } from '../users/user.entity'; @Injectable() export class CommentsService { constructor( @InjectRepository(Comment) private commentsRepository: Repository<Comment> ) {} // 게시물 ID별 모든 댓글을 사용자 정보를 포함하여 찾기 findByPostId(postId: string): Promise<Comment[]> { return this.commentsRepository.find({ where: { post: { id: postId } }, relations: ['user'], // 키: 연관된 사용자 객체도 로드 order: { createdAt: 'ASC', // 생성 시간 기준으로 오름차순 정렬 }, }); } // 댓글 생성 async create(content: string, user: User, post: Post): Promise<Comment> { const newComment = this.commentsRepository.create({ content, user, post, }); return this.commentsRepository.save(newComment); } }
relations: ['user']
는 TypeORM의 강력한 기능입니다. 댓글을 쿼리할 때 외래 키를 통해 연관된user
객체를 자동으로 가져와 채우도록 TypeORM에 지시합니다. 이를 통해 댓글 작성자의 사용자 이름을 쉽게 가져올 수 있습니다.
세 번째 단계: 댓글 제출 및 표시
이제 게시물 페이지에 댓글 기능을 통합해야 합니다. 여기에는 댓글 제출을 처리하는 컨트롤러를 만들고 게시물 페이지에 댓글을 표시하도록 PostsController
를 업데이트하는 것이 포함됩니다.
1. CommentsController
생성
댓글 제출은 PostsController
에서 처리할 수도 있지만, 관심사 분리를 유지하기 위해 전용 컨트롤러를 만들겠습니다.
nest generate controller comments
src/comments/comments.controller.ts
를 수정합니다.
// src/comments/comments.controller.ts import { Controller, Post, Body, Param, Req, Res, UseGuards } from '@nestjs/common'; import { CommentsService } from './comments.service'; import { AuthenticatedGuard } from '../auth/authenticated.guard'; import { Response, Request } from 'express'; @Controller('posts/:postId/comments') export class CommentsController { constructor(private readonly commentsService: CommentsService) {} @UseGuards(AuthenticatedGuard) // 로그인한 사용자만 댓글을 달 수 있도록 보장 @Post() async create( @Param('postId') postId: string, @Body('content') content: string, @Req() req: Request, @Res() res: Response ) { // req.user는 Passport 세션에 의해 추가됨 const user = req.user as any; // 참고: 실제 애플리케이션에서는 postId가 존재하는지 확인해야 합니다. await this.commentsService.create(content, user, { id: postId } as any); res.redirect(`/posts/${postId}`); // 댓글을 작성한 후 게시물 페이지로 다시 리디렉션 } }
또한 이 새 컨트롤러를 src/comments/comments.module.ts
에 등록하는 것을 잊지 마십시오.
// src/comments/comments.module.ts // ... import { CommentsController } from './comments.controller'; @Module({ imports: [TypeOrmModule.forFeature([Comment])], controllers: [CommentsController], // 컨트롤러 추가 providers: [CommentsService], exports: [CommentsService], }) export class CommentsModule {}
2. PostsController
업데이트
게시물 세부 정보 페이지를 렌더링할 때 해당 게시물의 모든 댓글을 가져와 전달하도록 PostsController
의 findOne
메서드를 수정해야 합니다.
먼저 src/posts/posts.module.ts
에 CommentsModule
을 가져와 PostsModule
이 CommentsService
를 사용할 수 있도록 합니다.
// src/posts/posts.module.ts // ... import { CommentsModule } from '../comments/comments.module'; @Module({ imports: [TypeOrmModule.forFeature([Post]), CommentsModule], // CommentsModule 가져오기 controllers: [PostsController], providers: [PostsService], }) export class PostsModule {}
그런 다음 src/posts/posts.controller.ts
를 업데이트합니다.
// src/posts/posts.controller.ts import { Controller, Get, Render, Param, Request /* ... */ } from '@nestjs/common'; import { PostsService } from './posts.service'; import { CommentsService } from '../comments/comments.service'; // CommentsService 가져오기 // ... @Controller('posts') export class PostsController { constructor( private readonly postsService: PostsService, private readonly commentsService: CommentsService // CommentsService 주입 ) {} // ... 다른 메서드는 변경되지 않음 @Get(':id') @Render('post') async findOne(@Param('id') id: string, @Request() req) { const post = await this.postsService.findOne(id); const comments = await this.commentsService.findByPostId(id); // 댓글 가져오기 return { post, user: req.user, comments }; // 템플릿에 댓글 전달 } }
네 번째 단계: 프론트엔드 뷰 업데이트
마지막 단계는 댓글 목록과 댓글 폼을 표시하도록 EJS 템플릿을 수정하는 것입니다.
views/post.ejs
를 열고 게시물 내용 아래에 다음 코드를 추가합니다.
<a href="/" class="back-link">← 홈으로 돌아가기</a> <section class="comments-section"> <h3>댓글</h3> <div class="comment-list"> <% if (comments.length > 0) { %> <% comments.forEach(comment => { %> <div class="comment-item"> <p class="comment-content"><%= comment.content %></p> <small> 작성자: <strong><%= comment.user.username %></strong> on <%= new Date(comment.createdAt).toLocaleDateString() %> </small> </div> <% }) %> <% } else { %> <p>아직 댓글이 없습니다. 첫 번째 댓글을 남겨주세요!</p> <% } %> </div> <% if (user) { %> <form action="/posts/<%= post.id %>/comments" method="POST" class="comment-form"> <h4>댓글 남기기</h4> <div class="form-group"> <textarea name="content" rows="4" placeholder="여기에 댓글을 작성하세요..." required ></textarea> </div> <button type="submit">댓글 제출</button> </form> <% } else { %> <p><a href="/auth/login">로그인</a>하여 댓글을 남겨주세요.</p> <% } %> </section> <%- include('_footer') %>
페이지를 더 보기 좋게 만들려면 public/css/style.css
에 스타일을 추가할 수 있습니다.
/* ... 기타 스타일 ... */ .comments-section { margin-top: 3rem; border-top: 1px solid #eee; padding-top: 2rem; } .comment-list .comment-item { background: #f9f9f9; border: 1px solid #ddd; padding: 1rem; border-radius: 5px; margin-bottom: 1rem; } .comment-content { margin-top: 0; } .comment-item small { color: #666; } .comment-form { margin-top: 2rem; } .comment-form textarea { width: 100%; padding: 0.5rem; margin-bottom: 1rem; }
실행 및 테스트
애플리케이션을 다시 시작합니다.
npm run start:dev
이제 브라우저를 엽니다.
- 게스트로 게시물을 방문합니다. 댓글 섹션이 보이지만 "댓글을 남기려면 로그인하세요" 링크만 보입니다.
- 계정으로 로그인합니다.
- 다시 해당 게시물을 방문합니다. 이제 텍스트를 입력할 수 있는 댓글 상자가 표시됩니다.
- 댓글을 제출합니다. 페이지가 새로 고쳐지면 댓글 목록에 방금 게시한 내용이 표시됩니다.
축하합니다! Nest.js 블로그에 완벽하게 작동하는 댓글 시스템을 성공적으로 추가했습니다. 댓글 데이터 모델을 만들고, 백엔드 서비스 및 라우팅 로직을 구현했으며, 댓글을 표시하고 댓글 폼을 위한 프론트엔드 보기를 업데이트하고, Guard를 사용하여 로그인한 사용자만 댓글을 달 수 있도록 보장했습니다.
물론 현재 댓글 기능은 여전히 매우 기본적인 것입니다. 다음 글에서는 작성자가 댓글에 답글을 달 수 있는 기능을 구현하여 블로그의 상호 작용성을 한 단계 더 발전시켜 이 기능을 계속 강화할 것입니다.