훌륭한 Nest.js 블로그 만들기: 게시물용 태그
Min-jun Kim
Dev Intern · Leapcell

이전 튜토리얼에서 블로그에 방문자 추적 기능을 추가하여 각 게시물의 인기를 시각적으로 확인할 수 있도록 했습니다.
블로그는 이미 상당히 완성된 것처럼 보이지만, 여전히 무언가 빠진 듯합니다. 블로그에 이미 많은 게시물이 있고 사용자들이 길을 잃을 수 있습니다... 그렇다면 사용자가 관심 있는 주제를 빠르게 찾는 방법은 무엇일까요?
맞습니다. 블로그에 이제 태그 기능이 필요합니다.
태그는 게시물의 주제와 콘텐츠를 표시하는 데 사용됩니다. 게시물에 여러 키워드(예: "기술 튜토리얼", "Nest.js", "데이터베이스")를 할당할 수 있습니다.
다음 두 개의 튜토리얼에서는 블로그 시스템에 태그 기능을 추가할 것입니다. 이번 튜토리얼에서는 먼저 게시물을 생성하고 편집할 때 태그를 설정하는 지원을 구현할 것입니다.
단계 1: 데이터 모델링 및 관계 구축
태그 엔티티 생성
먼저 이 새로운 개념에 대한 해당 모듈과 엔티티를 만듭니다.
nest generate module tags nest generate service tags
src/tags
디렉토리에 tag.entity.ts
를 생성합니다.
// src/tags/tag.entity.ts import { Entity, Column, PrimaryColumn, ManyToMany } from 'typeorm'; import { Post } from '../posts/post.entity'; @Entity() export class Tag { @PrimaryColumn({ type: 'uuid', default: () => 'gen_random_uuid()' }) id: string; @Column({ unique: true }) name: string; @ManyToMany(() => Post, (post) => post.tags) posts: Post[]; }
관계를 설정하기 위해 게시물 엔티티 업데이트
다음으로, 게시물과 태그 간의 연관성을 설정하기 위해 src/posts/post.entity.ts
파일을 업데이트해야 합니다.
게시물은 여러 태그를 가질 수 있고, 태그는 여러 게시물과 연관될 수 있습니다. 이것은 다대다(Many-to-Many) 관계입니다.
// src/posts/post.entity.ts import { Entity, Column, PrimaryColumn, CreateDateColumn, ManyToOne, ManyToMany, JoinTable } from 'typeorm'; import { Tag } from '../tags/tag.entity'; // ... 기타 가져오기 @Entity() export class Post { // ... 기타 필드 (id, title, content 등) @ManyToMany(() => Tag, (tag) => tag.posts, { cascade: true, // 게시물을 통해 새 태그를 생성할 수 있습니다. }) @JoinTable() // @JoinTable은 관계의 한쪽에 지정해야 합니다. tags: Tag[]; // ... 기타 관계 (댓글, 조회수 등) }
@JoinTable()
은 다대다 관계를 정의하기 위해 필요한 데코레이터입니다.Post
와Tag
간의 연관성을 관리하기 위해 조인 테이블post_tags_tag
를 생성해야 합니다.
데이터베이스 테이블 구조 업데이트
새 테이블과 필드를 생성하기 위해 다음 SQL 문을 실행합니다.
-- 태그 테이블 생성 CREATE TABLE "tag" ( "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "name" VARCHAR UNIQUE NOT NULL ); -- post_tags_tag 조인 테이블 생성 CREATE TABLE "post_tags_tag" ( "postId" UUID REFERENCES "post" ON DELETE CASCADE, "tagId" UUID REFERENCES "tag" ON DELETE CASCADE, PRIMARY KEY ("postId", "tagId") );
Leapcell에서 데이터베이스가 생성된 경우:
그래픽 인터페이스를 사용하여 SQL 문을 쉽게 실행할 수 있습니다. 웹사이트의 데이터베이스 관리 페이지로 이동하여 위의 문을 SQL 인터페이스에 붙여넣고 실행하기만 하면 됩니다.
단계 2: 백엔드 로직 구현
태그 생성 및 검색을 처리하는 Service를 작성하고, 게시물을 생성할 때 이러한 연관성을 처리하도록 PostsService
를 업데이트해야 합니다.
TagsService 작성
이 서비스의 로직은 비교적 간단하며, 주로 태그를 찾거나 생성하는 데 중점을 둡니다.
src/tags/tags.module.ts
를 열고 TypeOrmModule
을 등록합니다.
// src/tags/tags.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Tag } from './tag.entity'; import { TagsService } from './tags.service'; @Module({ imports: [TypeOrmModule.forFeature([Tag])], providers: [TagsService], exports: [TagsService], // 다른 모듈에서 사용할 서비스를 내보냅니다. }) export class TagsModule {}
src/tags/tags.service.ts
에서:
// src/tags/tags.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, In } from 'typeorm'; import { Tag } from './tag.entity'; @Injectable() export class TagsService { constructor( @InjectRepository(Tag) private tagsRepository: Repository<Tag> ) {} // 태그 찾기 또는 생성 async findOrCreate(tagNames: string[]): Promise<Tag[]> { const existingTags = await this.tagsRepository.findBy({ name: In(tagNames) }); const existingTagNames = existingTags.map((tag) => tag.name); const newTagNames = tagNames.filter((name) => !existingTagNames.includes(name)); const newTags = newTagNames.map((name) => this.tagsRepository.create({ name })); await this.tagsRepository.save(newTags); return [...existingTags, ...newTags]; } }
PostsService를 업데이트하여 연관성 처리
create
및 findOne
메서드를 수정해야 합니다.
먼저 PostsModule
로 TagsModule
을 가져옵니다.
// src/posts/posts.module.ts import { TagsModule } from '../tags/tags.module'; @Module({ imports: [TypeOrmModule.forFeature([Post]), CommentsModule, TrackingModule, TagsModule], // ... }) export class PostsModule {}
그런 다음 src/posts/posts.service.ts
를 업데이트합니다.
// src/posts/posts.service.ts import { Injectable } from '@nestjs/common'; import { TagsService } from '../tags/tags.service'; // ... 기타 가져오기 @Injectable() export class PostsService { constructor( @InjectRepository(Post) private postsRepository: Repository<Post>, private readonly tagsService: TagsService // TagsService 주입 ) {} // create 메서드 업데이트 async create(post: Omit<Partial<Post>, 'tags'> & { tags: string }): Promise<Post> { const tagNames = post.tags .split(',') .map((tag) => tag.trim()) .filter(Boolean); const tags = await this.tagsService.findOrCreate(tagNames); const newPost = this.postsRepository.create({ ...post, tags, }); return this.postsRepository.save(newPost); } // 관련 데이터 로드를 위해 findOne 메서드 업데이트 findOne(id: string): Promise<Post | null> { return this.postsRepository.findOne({ where: { id }, relations: ['tags'], // 태그 로드 }); } // ... 기타 메서드 }
단계 3: 프런트엔드 페이지 통합
마지막 단계는 게시물을 생성하고 편집할 때 태그를 설정하고 게시물 상세 페이지에 태그를 표시하도록 EJS 템플릿을 수정하는 것입니다.
새/편집 게시물 페이지 업데이트
views/new-post.ejs
를 열고 태그 입력을 위한 폼 필드를 추가합니다.
<form action="/posts" method="POST" class="post-form"> <div class="form-group"> <label for="tags">Tags (comma-separated)</label> <input type="text" id="tags" name="tags" /> </div> <button type="submit">Submit</button> </form>
편의상 현재 여러 태그에 대해 쉼표로 구분된 입력을 사용하고 있습니다. 실제 프로젝트에서는 사용자 경험을 개선하기 위해 별도의 태그 입력 구성 요소, 기존 태그 자동 일치 등과 같은 더 복잡한 UI 구성 요소 및 로직을 사용할 수 있습니다.
게시물 상세 페이지 업데이트
views/post.ejs
를 열고 게시물의 메타데이터에 태그를 표시합니다.
<article class="post-detail"> <h1><%= post.title %></h1> <small> <%= new Date(post.createdAt).toLocaleDateString() %> | Views: <%= viewCount %> </small> <div class="post-content"><%- post.content %></div> <% if (post.tags && post.tags.length > 0) { %> <div class="tags-section"> <strong>Tags:</strong> <% post.tags.forEach(tag => { %> <a href="/tags/<%= tag.id %>" class="tag-item"><%= tag.name %></a> <% }) %> </div> <% } %> </article>
실행 및 테스트
애플리케이션을 다시 시작합니다.
npm run start:dev
브라우저를 열고 다음으로 이동합니다: http://localhost:3000/
새 게시물을 만들면 하단에 태그 입력란이 표시됩니다.
태그를 쉼표로 구분하여 입력합니다. 예: Nest.js, Tutorial
을 입력한 다음 제출합니다.
제출 후 게시물 상세 페이지로 이동하면 게시물의 태그가 성공적으로 표시되는 것을 볼 수 있습니다.
이제 블로그에서 태그 생성 및 표시를 지원합니다. 그러나 사용자는 여전히 태그별로 기사를 필터링할 수 없습니다. 다음 튜토리얼에서 이 기능을 구현할 것입니다.
이전 튜토리얼: