개발 · 2026. 8. 8.13

태그 기능 개발 일지

devlog-porklog(1/5)

  1. 1.태그 기능 개발 일지
  2. 2.블로그 포스팅 검색기능 개선 작업
  3. 3.관리자용 통계 화면 개발일지
  4. 4.E2E 테스트 적용기
  5. 5.데이빗또, 이건 기초적인 블로그 기능이다. (예약발행 및 비공개 기능 추가)
목차

왜 태그가 필요했나

PorkLog는 카테고리 탭(개발/일상/운동/밥)으로 글을 나누고 있었지만, 카테고리 하나만으로는 부족한 지점이 있었다. 예를 들어 "개발" 카테고리 안에서도 React 글과 인프라 글이 섞여 있으면, 특정 주제만 모아 보기가 어렵다. README에도 "태그 기반 분류"를 예정 항목으로 남겨둔 채 미뤄왔던 이유이기도 하다.

검색은 이미 제목 기준으로 되고 있었지만, 제목에 키워드가 없으면 찾을 수 없다는 한계가 있었다. 카테고리보다 한 단계 세밀한 분류가 필요했고, 그게 태그였다.

설계: 단순함을 먼저 선택하기

가장 먼저 부딪힌 질문은 데이터 모델이었다. 태그처럼 다대다 관계가 자연스러운 데이터는 보통 tags 테이블과 post_tags 조인 테이블로 정규화하는 게 "정석"이다. 태그명을 한 번에 바꿀 수 있고, 태그별 글 수 집계도 깔끔하다.

하지만 기존 posts.category 컬럼을 보면 이미 답이 나와 있었다. 카테고리도 별도 테이블 없이 text 컬럼 하나로 관리되고 있었다. 개인 블로그 규모에서 조인 테이블까지 가는 건 과설계라고 판단했고, 같은 결로 tags 컬럼을 배열로 추가하기로 했다.

// src/db/schema.ts
tags: text('tags').array().default([]).notNull(),

트레이드오프는 명확히 있다. 태그명을 나중에 일괄로 바꾸려면 전체 글을 순회하며 UPDATE 해야 하고, 오타나 대소문자 혼용을 막을 registry도 없다. 후자는 저장 시점에 trim().toLowerCase()로 정규화하는 걸로 최소한의 안전장치를 뒀다. 자동완성 기능도 고민했지만, 개인 블로그에서 태그 종류가 폭발적으로 늘어날 일이 없어서 과한 기능이라고 판단하고 뺐다.

카테고리와의 관계도 정리해뒀다. 태그는 카테고리의 완전한 하위 집합은 아니다 — DB 레벨에서 "이 태그는 이 카테고리에서만 쓸 수 있다"를 강제하지 않는다. react 태그를 실수로 다른 카테고리 글에 붙이면 그대로 섞여 나온다. 이 정도는 직접 관리하는 선에서 감수하기로 했다.

구현: 4단계로 쪼개서

한 번에 다 만들지 않고 단위를 쪼갰다.

1. 스키마 · 검증 · 서버 액션. posts.tags 컬럼 추가 후 pnpm db:push로 반영. post-schema.ts의 zod 스키마에 태그 개수 상한(10개), 글자수 상한(30자) 검증과 중복 제거를 추가했다.

tags: z
  .array(
    z.string().trim().min(1).max(MAX_TAG_LENGTH).transform((v) => v.toLowerCase()),
  )
  .max(MAX_TAGS)
  .transform((tags) => Array.from(new Set(tags))),

2. 작성 폼 UI. 태그 자동완성 대신 엔터로 추가되는 칩 입력을 붙였다. 입력값을 정규화한 뒤 중복이면 무시하고, 상한을 넘으면 에러 메시지를 띄운다.

3. 노출. 카드 목록에는 최대 3개까지만 배지로 보여주고 나머지는 "+N"으로 접는다. 상세 페이지는 본문 하단에 전체 태그를 노출한다. 공용 컴포넌트(TagBadges)로 분리해서 두 군데서 재사용했다.

4. 필터. 새 라우트를 만들지 않고 기존 홈 화면의 쿼리 파라미터 패턴(?category=, ?q=)에 ?tag=를 얹었다. 검색·카테고리와 마찬가지로 한 번에 한 가지 모드만 적용되도록 우선순위를 뒀다 — 검색 > 태그 > 카테고리.

const whereClause = isSearching
  ? ilike(posts.title, `%${query}%`)
  : activeTag
    ? arrayContains(posts.tags, [activeTag])
    : activeCategory
      ? eq(posts.category, activeCategory)
      : undefined;

트러블슈팅: 뱃지가 그냥 텍스트처럼 보인 이유

기능은 다 붙었는데 막상 화면에서 보니 태그가 뱃지가 아니라 그냥 문자열처럼 보였다. shadcn에서 막 추가한 Badge 컴포넌트를 그대로 썼는데, 기본 variant(bg-secondary 등)가 참조하는 CSS 변수가 PorkLog의 GitHub Primer 기반 커스텀 팔레트에는 정의돼 있지 않았던 게 원인이었다.

해결은 오히려 더 간단한 방향이었다. Resume 페이지의 기술 스택 뱃지(skill-stack.tsx)를 보니 애초에 shadcn Badge를 쓰지 않고 span에 직접 색상 클래스를 입힌 방식이었다. 같은 패턴을 따라가서 Badge 컴포넌트 의존을 걷어내고, 스킬 스택의 "주력" 등급에 쓰는 accent-fg 톤을 그대로 가져다 썼다. 결과적으로 새 컴포넌트를 억지로 끼워 맞추기보다 이미 있는 디자인 시스템을 그대로 재사용하는 쪽이 더 깔끔했다.

남은 것

README에 같이 적어뒀던 예정 항목 중 본문 전문 검색은 아직 남아 있다. 지금은 제목만 검색되는데, 태그로 세분화가 가능해졌으니 우선순위가 조금 낮아지긴 했다. 다음 사이드 프로젝트 시간에 이어서 붙여볼 생각이다.