Express + Prisma를 활용한 댓글 기능 구현

Server_side·2023년 10월 19일
1
post-custom-banner

Express, Prisma 간단 설명

Express는 Node.js 기반의 웹 프레임워크로 백엔드, 즉 서버 구축을 위한 것이다.
JAVA - Spring, Python - Django가 거의 당연하게 연결되듯이 Node.js하면 Express를 떠올리게 된다.
프레임워크란 간단히 말하자면 App을 만들기 위해 다양하고 편리한 라이브러리/미들웨어 등이 내장된 Package를 말한다. 이를 통해 개발에 있어 효율성이 오르며, 개발 규칙에 의해 코드 구조의 통일성이 향상된다는 장점이 있다.

Prisma는 이전 포스팅에서 설명한 ORM 중 하나다.(이전 포스팅 참고)
해당 기능을 구현한 프로젝트에서 DBMS로 Mysql을 활용하였기 때문에 개발의 편의성을 위해 Prisma를 채택하였다.


본격 댓글 기능 구현 돌입

  • TypeScript로 코드 작성
  • 아래에 작성된 내용은 모든 코드가 아닌 일부분임 (CRUD 중 CR만 포함, UD는 본문에서 따로 다루지 않았음)
    댓글 기능에 대한 로직 설명을 위한 내용이므로 실제 작동을 구현해보기 위해서는 부족분 채워야함
  • router, controller, service로 구분

댓글 Schema

model Comment {
  id            String    @id @default(uuid())
  author        User      @relation(fields: [authorId], references: [id], onDelete: Cascade)
  authorId      String
  diary         Diary     @relation(fields: [diaryId], references: [id], onDelete: Cascade)
  diaryId       String
  content       String
  nestedComment String?
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
  reply         Comment?  @relation("comment", fields: [nestedComment], references: [id], onDelete: Cascade)
  reComment     Comment[] @relation("comment")
}
  • id: 댓글 구분하는 댓글id
  • author: User와 관계 정의(User.id를 받아옴)
  • authorId: 받아온 User.id
  • diary: Diary와관계 정의(Diary.id를 받아옴)
  • diaryId: 받아온 diary.id
  • content: 댓글 내용
  • nestedComment: 댓글/대댓글 구분을 위함(댓글의 경우 null, 대댓글의 경우 원댓글id 저장)
  • reply, reComment: 클라이언트로 응답 시 댓글 내부에 대댓글을 포함시켜 응답하기 위한 self join

commentRouter.ts

commentRouter.post('/:diaryId', jwtAuthentication, createComment);
commentRouter.get('/:diaryId', getComment);
commentRouter.put('/:commentId', jwtAuthentication, updateComment);
commentRouter.delete('/:commentId', jwtAuthentication, deleteComment);
  • 댓글 CRUD를 위한 Router 정의
  • jwtAuthentication은 미들웨어로 jwt토큰으로 로그인 여부 확인을 위함

commentController.ts

export const createComment = async (
  req: IRequest,
  res: Response,
  next: NextFunction,
) => {
  try {
    const authorId = req.user.id;
    const inputData = req.body;
    const diary_id: string = req.params.diaryId;

    const comment = await createdComment(inputData, authorId, diary_id);
    res.json(comment);
  } catch (error) {
    next(error);
  }
};

export const getComment = async (
  req: Request,
  res: Response,
  next: NextFunction,
) => {
  try {
    const diary_id: string = req.params.diaryId;
    const page = Number(req.query.page) || 1;
    const limit = Number(req.query.limit) || 8;

    const comment = await getCommentByDiaryId(diary_id, page, limit);
    res.json(comment);
  } catch (error) {
    next(error);
  }
};
  • 댓글 작성
    토큰에서 user의 id 추출 -> authorId
    req.body로 댓글 작성을 위한 정보 확인(content, nestedComment) -> inputData
    req.params로 넘어온 댓글 작성된 diaryId 확인 -> diary_id
  • 댓글 조회
    req.params로 넘어온 댓글 조회를 위한 diaryId 확인 -> diary_id
    pagenation을 위해 req.query로 넘어온 page, limit 정보 확인(없을 경우 1, 8 적용) -> page, limit

commentService.ts

export async function createdComment(
  inputData: {
    content: string;
    nestedComment: string;
  },
  authorId: string,
  diary_id: string,
) {
  try {
    const { content, nestedComment } = inputData;
    const comment = await prisma.comment.create({
      data: { diaryId: diary_id, authorId, content, nestedComment },
    });

    const commentResponseData = plainToClass(commentResponseDTO, comment, {
      excludeExtraneousValues: true,
    });

    const response = successApiResponseDTO(commentResponseData);
    return response;
  } catch (error) {
    throw error;
  }
}

// 댓글 조회
export async function getCommentByDiaryId(
  diary_id: string,
  page: number,
  limit: number,
) {
  try {
    const comment = await prisma.comment.findMany({
      skip: (page - 1) * limit,
      take: limit,
      where: { diaryId: diary_id, nestedComment: null },
      select: {
        id: true,
        author: {
          select: {
            id: true,
            username: true,
            profileImage: true,
          },
        },
        diaryId: true,
        content: true,
        createdAt: true,
        updatedAt: true,
        reComment: {
          select: {
            id: true,
            author: {
              select: {
                id: true,
                username: true,
                profileImage: true,
              },
            },
            diaryId: true,
            content: true,
            createdAt: true,
            updatedAt: true,
          },
        },
      },
      orderBy: { createdAt: 'asc' },
    });

    if (comment.length == 0) {
      const response = emptyApiResponseDTO();
      return response;
    }

    const { totalComment, totalPage } = await calculatePageInfoForComment(
      limit,
      diary_id,
    );

    const pageInfo = { totalComment, totalPage, currentPage: page, limit };

    const commentResponseDataList = comment.map((comment) =>
      plainToClass(commentResponseDTO, comment, {
        excludeExtraneousValues: true,
      }),
    );

    const response = new PaginationResponseDTO(
      200,
      commentResponseDataList,
      pageInfo,
      '성공',
    );

    return response;
  } catch (error) {
    throw error;
  }
}
  • 댓글 작성
    prisma.comment.create를 통해 comment 테이블에 댓글 정보 저장
    해당 코드에서 DTO를 적용했는데 DTO(Data Transfer Object)는 간단히 말하자면 정보를 노출시키지 않고 데이터를 주고받기 위한 객체이다.

  • 댓글 조회
    prisma.comment.findMany를 이용하여 원하는 댓글정보 조회
    skip, take는 pagenation을 위한 속성
    where는 어떤 정보를 찾을 것인지임(nestedComment: null 인 이유는 대댓글의 경우, 댓글 내부에서 불러올 것이므로 대댓글 중복 조회 방지를 위함)
    select은 조회된 댓글에서 어떤 속성을 응답할 것인지를 정하는 것
    author는 user와 연결되어 있기 때문에 author.id = user.id 인 데이터를 user 테이블에서 찾음
    reComment는 self join으로 대댓글을 조회하기 위한 속성임


postman으로 테스트한 결과

댓글 조회

  • 댓글 내부에 reComment 속성으로 대댓글이 조회되는 것을 확인

마무리

이번 프로젝트에서 댓글 기능을 구현해보면서 pagenation, self join 등 처음 구현해본 것들이 꽤 있었다.
개념으로는 어느정도 알고 있었지만 역시 직접 손으로 코드를 작성하고 구현해봐야 제대로된 이해를 할 수 있다는 것을 다시 한번 깨달을 수 있었던 기회였다.
앞으로도 무언가를 보고 끄덕이지만 말고 직접 손으로 구현해봐야겠다라는 다짐을 하게된다.
그리고 코드 작성할때 머리를 아프게하는 경우들이.... 존재하지만 이를 해결하고 작동되는 것을 보니 짜릿한 쾌감이 느껴지는게... 코딩은 계속해서 나를 끌어당기는 매력이 있다는 것을 또한번 느끼게 되었다...

profile
아마도 난 백엔드 style?
post-custom-banner

0개의 댓글