
교육생 블로그에 댓글을 달아주고 싶어서 블로그 플랫폼을 만들었습니다

시작은 단순했습니다
교육생이 블로그 글을 쓰면 무조건 댓글을 달아주자는 생각이 출발점이었습니다.
개발을 가르치면서 느끼는 건데, 초보 개발자가 첫 블로그 글을 쓰는 건 꽤 용기가 필요한 일입니다. 글을 올렸는데 반응이 없으면, 두 번째 글은 안 쓰거든요.
그런데 댓글을 달려면 일단 그 글이 눈에 보여야 합니다. velog나 tistory에 올린 글을 하나하나 찾아다니는 건 한계가 있고, 수강생이 늘어나면 사실상 불가능합니다.
교육생 A → velog에 글 작성 → 강사가 velog 방문 → 댓글 교육생 B → tistory에 글 작성 → 강사가 tistory 방문 → 댓글 교육생 C → 아직 블로그가 없음 → ... → 이걸 수강생 50명으로 확장하면? 불가능.
FullStackFamily(fullstackfamily.com)에 블로그 기능이 있으면 어떨까. 교육생이 글을 쓰면 피드에 바로 뜨고, 거기서 댓글을 달 수 있으면. "글 쓰면 선생님이 읽어주는" 환경이 되는 거죠.
전체 구조: 7번의 Phase에 걸친 작업
블로그 기능을 완성하기까지 7개의 Phase를 거쳤습니다. 처음엔 "그냥 글쓰기 기능 추가하면 되지 않나" 싶었는데, 파고 들어갈수록 신경 써야 할 것이 많았습니다.
Phase 82 블로그 기본 시스템 ← 핵심 뼈대 Phase 83 관리자 기능 + 권한 처리 ← 운영 도구 Phase 94 메인 피드 통합 + 리디자인 ← 발견 가능성 Phase 95 블로그 자동 생성 ← 진입 장벽 제거 Phase 96 시리즈 + 모바일 글쓰기 ← 글쓰기 경험 Phase 99 OG 썸네일 + 태그 최적화 ← 공유와 탐색 Phase 101 작성자 프로필 + 블로그 연결 ← 커뮤니티 연결
순서대로 짚어보겠습니다.
Phase 82: 기존 시스템 위에 블로그 얹기
FullStackFamily에는 이미 UnifiedPost라는 통합 게시물 시스템이 있었습니다. 커뮤니티 게시판, Q&A, 수업 게시판이 전부 이 테이블을 공유하고, 댓글, 태그, 투표(좋아요), 파일 첨부도 다 여기에 붙어 있거든요.
블로그를 처음부터 새로 만들 수도 있었는데, 기존 인프라를 재사용하기로 했습니다.
┌──────────────────────────────────────────────────────────┐ │ unified_post │ │ (게시판, Q&A, 수업, 블로그 — 모든 글의 공통 저장소) │ ├──────────────────────────────────────────────────────────┤ │ id, title, content, author_id, status, board_id, ... │ └──────────────┬───────────────────────┬───────────────────┘ │ │ ┌─────────┴─────────┐ ┌────────┴────────┐ │ unified_post_blog │ │ unified_post_tag │ │ (블로그 확장 정보) │ │ (태그 — 기존) │ │ blog_id, category │ └─────────────────┘ └───────────────────┘
unified_post_blog라는 확장 테이블 하나만 추가하면 블로그 글도 기존 댓글, 태그, 투표 시스템을 그대로 쓸 수 있습니다. 뉴스 게시판을 통합할 때(Phase 77)도 같은 패턴을 썼으니 검증된 방식이기도 하고요.
블로그의 핵심 테이블 관계
┌──────────────┐ ┌──────────────────┐ │ users │──1:1─│ blog │ │ │ │ slug, displayName│ └──────────────┘ │ oneLiner, ... │ └───────┬──────────┘ │ ┌──────────────────┼──────────────────┐ │ │ │ ┌──────┴───────┐ ┌──────┴───────┐ ┌──────┴───────┐ │blog_linked │ │blog_category │ │blog_series │ │_account │ │name, slug │ │name, desc │ │(소셜 계정) │ │(글 분류) │ │(연재물 묶음) │ └──────────────┘ └──────────────┘ └──────────────┘
사용자 한 명에게 블로그 하나. 블로그에는 카테고리(글 분류)와 시리즈(연재물 묶음)가 붙고요. blog_linked_account는 카카오, 네이버, 구글 등 여러 소셜 계정을 하나의 블로그에 묶어주는 테이블입니다.
@slug URL 설계
블로그 URL은 /@urstory 형태입니다. GitHub, dev.to, velog에서 익숙한 그 패턴이죠.
블로그 메인 fullstackfamily.com/@urstory 글 상세 fullstackfamily.com/@urstory/posts/123 About 페이지 fullstackfamily.com/@urstory/about 글쓰기 fullstackfamily.com/@urstory/write
Next.js에서 @로 시작하는 URL은 직접 라우팅이 안 돼서, Middleware에서 rewrite로 처리합니다.
브라우저: /@urstory/posts/123 │ ▼ (Next.js Middleware) 내부 경로: /(blog)/urstory/posts/123 │ ▼ (generateMetadata) canonical: fullstackfamily.com/@urstory/posts/123
redirect가 아니라 rewrite만 씁니다. 브라우저에 보이는 URL은 항상 /@slug이고, 내부적으로만 /(blog)/slug로 바뀌는 거죠. canonical URL도 /@slug 기반으로 통일했습니다.
Phase 94-95: "글을 쓰면 누군가 읽어주는" 환경
블로그 시스템을 만들고 나서 황당한 걸 발견했습니다. 블로그 글이 메인 피드에 안 나옵니다.
HomeService.getFeed() → boardRepository.findGlobalActiveBoards() ← isGlobal=true인 보드만 → 블로그 보드는 isGlobal=false로 생성됨 → 결과: 블로그 글이 피드에서 제외
블로그를 만들었는데 아무도 안 읽어주면 의미가 없잖아요. 글이 피드에 안 뜨면 찾아갈 수조차 없으니까요.
Phase 94에서 피드 쿼리를 수정해서 BLOG preset 게시물도 포함시키고, 메뉴도 피드 | 커뮤니티 | 배우기 | 랭킹으로 개편했습니다. 이제 피드에 들어가면 교육생의 블로그 글이 바로 보입니다. 인기순 정렬과 태그 필터도 같이 넣었고요.
진입 장벽 제거: 가입하면 블로그가 생긴다
초기 블로그 생성 흐름은 이랬습니다.
1. 사용자 → 관리자에게 "블로그 만들고 싶어요" 요청 2. 관리자 → canCreateBlog = true 설정 3. 사용자 → 슬러그 입력하고 블로그 생성
dev.to, velog, Medium은 가입하면 바로 블로그가 있잖아요. 2026년에 "관리자 승인 후 생성"은 좀 아닙니다. 교육생에게 "먼저 저한테 요청하세요"라고 하면 그 시점에서 절반은 포기합니다.
Phase 95에서 OAuth2 로그인 플로우에 블로그 자동 생성을 끼워 넣었습니다.
OAuth2 로그인 완료 │ ├─ 기존 사용자 → 블로그 있는지 확인 │ ├─ 있음 → 통과 │ └─ 없음 → 자동 생성 │ └─ 신규 사용자 → 회원 생성 → 블로그 자동 생성
슬러그는 이메일에서 자동으로 뽑아냅니다.
toto@gmail.com → @toto john.doe@company.com → @john-doe kakao_12345@kakao.usr → @kakao-12345 한글닉네임@kakao.usr → @user (폴백)
마음에 안 들면 블로그 설정 페이지에서 바꿀 수 있고, 입력하는 동안 실시간으로 사용 가능 여부도 보여줍니다.
중요한 설계 원칙이 하나 있었는데, 블로그 생성에 실패해도 로그인은 성공해야 한다는 것이었습니다.
try { blogAutoCreationService.createBlogForNewUser(user); } catch (Exception e) { log.warn("블로그 자동 생성 실패: userId={}", user.getId()); // 로그인은 정상 진행 — 다음에 다시 시도 }
부가 기능의 실패가 핵심 기능에 영향을 줘서는 안 됩니다.
Phase 96: velog 스타일 글쓰기 경험
블로그가 자동으로 생기고 피드에 노출되니까, 이제 "글쓰기 경험"이 중요해졌습니다.
시리즈 기능
"Spring Boot 입문 1편, 2편, 3편..." 이렇게 연재물을 쓰는 교육생이 있습니다. 이런 글은 묶어서 보여줄 수 있어야죠.
┌──────────────────────────────────────────┐ │ 📚 Spring Boot 입문 시리즈 ▼/▲ │ │ ─────────────────────────────────────── │ │ 1. 프로젝트 생성하기 │ │ 2. REST API 만들기 ← 현재 글 (강조) │ │ 3. JPA로 DB 연동하기 │ │ ─────────────────────────────────────── │ │ [◀ 이전 글] [다음 글 ▶] │ └──────────────────────────────────────────┘
글 상세 페이지에서 작성자 정보 바로 아래에 이 시리즈 박스가 들어갑니다. 접기/펼치기가 되고, 이전/다음 글로 바로 넘어갈 수 있고요.
발행 모달
글을 다 쓰고 "출간하기"를 누르면 발행 설정 모달이 뜹니다. 카테고리, 시리즈, 공개/비공개를 한 곳에서 고르는 방식인데, velog 발행 플로우를 참고했습니다.
글 작성 화면 │ ├─ [나가기] → 목록으로 ├─ [임시저장] → DRAFT 상태로 저장 └─ [출간하기] → 발행 모달 열림 │ ├─ 카테고리 선택 ├─ 시리즈 선택/생성 ├─ 공개 설정 └─ [출간] → PUBLISHED 상태로 저장
에디터 툴바
마크다운에 익숙하지 않은 교육생을 위해 툴바를 넣었습니다. H1~H4, Bold, Italic, 이미지, 링크 버튼이 있고, "마크다운 문법 도움말" 모달도 만들었습니다.
Phase 99-101: 기본이 되니까 빈 곳이 보입니다
기본 기능이 돌아가기 시작하면 오히려 아쉬운 부분이 눈에 들어옵니다.
OG 이미지 문제
블로그 글을 카카오톡이나 슬랙에 공유하면 미리보기 이미지(OG 이미지)가 나옵니다. 그런데 글에서 사진을 삭제하면, OG 이미지가 이미 없는 사진을 가리키고 있는 겁니다.
글이 저장될 때 본문에서 첫 번째 이미지 URL을 뽑아서 thumbnailUrl 필드에 넣도록 고쳤습니다. 이미지가 아예 없으면 기본 OG 이미지를 씁니다.
태그 카운트 최적화
태그별 글 수를 보여줘야 하는데, 매번 GROUP BY + COUNT 쿼리를 날리면 글이 많아질수록 느려지거든요.
# 이렇게 하면 안 됩니다 SELECT tag_name, COUNT(*) FROM unified_post_tag GROUP BY tag_name # 비정규화 테이블에서 바로 가져옵니다 SELECT tag_name, post_count FROM global_tag_count WHERE post_count > 0 ORDER BY post_count DESC
blog_tag_count와 global_tag_count 비정규화 테이블을 만들어서, 글이 발행/수정/삭제될 때 카운트를 같이 갱신하도록 바꿨습니다.
작성자 프로필 카드
글을 읽다가 "이 사람 다른 글도 보고 싶다" 싶을 때가 있잖아요. 작성자 이름을 클릭하면 그 사람 블로그로 갑니다. 글 하단에는 프로필 카드도 넣었는데, 프로필 이미지, 한 줄 소개, 팔로우 버튼이 들어가 있습니다.
┌─────────────────────────────────────────────┐ │ ┌──────┐ │ │ │ 👤 │ 홍길동 │ │ │ 96px │ "Spring Boot와 React를 공부중" │ │ └──────┘ 팔로워 5 · 팔로잉 3 [+ 팔로우] │ └─────────────────────────────────────────────┘
커뮤니티 게시판, Q&A, 피드 어디서든 작성자 이름을 누르면 블로그로 갈 수 있습니다. 이게 들어가고 나서 "다른 사람 블로그 구경하기"가 자연스러워졌습니다.
현재 블로그가 제공하는 것들
7번의 Phase를 거쳐 완성된 현재의 모습은 이렇습니다.
| 기능 | 설명 |
|---|---|
| 자동 블로그 생성 | 가입하면 바로 /@slug 블로그가 생김 |
| 마크다운 에디터 | 툴바 + 문법 도움말 + 이미지 업로드 |
| 시리즈 | 연재물을 묶어서 순서대로 탐색 |
| 해시태그 | 글마다 태그, 태그 팔로우, 트렌딩 태그 |
| 피드 통합 | 블로그 글이 메인 피드에 자동 노출 |
| SEO | canonical URL, OG 메타태그, 동적 sitemap |
| 댓글 + 좋아요 | 댓글, 대댓글, 좋아요 |
| 프로필 카드 | 작성자 이름 클릭하면 블로그로 이동 |
| 팔로우 | 관심 있는 작성자를 팔로우 |
| 임시저장 | DRAFT 상태로 저장, 나중에 이어 쓰기 |
| 슬러그 변경 | 블로그 URL 변경 가능 |
만들면서 고민했던 것들
UnifiedPost 확장 패턴
새 게시물 유형이 생길 때마다 테이블을 새로 만들지 않습니다. unified_post를 중심에 두고, 유형별 확장 테이블(unified_post_blog, unified_post_news 등)을 1:1로 붙이는 방식이죠.
┌─────────────────────────────────────────────┐ │ 장점 │ │ · 댓글, 태그, 투표, 파일 — 전부 재사용 │ │ · 피드에서 모든 유형의 글을 하나의 쿼리로 조회 │ │ · 새 유형 추가가 테이블 하나 + FK 하나로 끝남 │ │ │ │ 주의점 │ │ · status 검증을 확장 테이블이 아닌 │ │ unified_post에서 해야 함 (DELETED 글 노출 방지)│ │ · soft delete 시 확장 테이블 정리를 │ │ 애플리케이션에서 직접 해야 함 │ └─────────────────────────────────────────────┘
슬러그 예약어 관리
admin, api, login, settings, search 같은 단어를 슬러그로 쓰면 URL이 충돌합니다. 40개 정도의 예약어 목록을 서비스 레이어 상수로 관리하면서, 블로그 생성이나 슬러그 변경 시 걸러냅니다.
예약어 목록 (일부): admin, api, settings, search, login, logout, register, profile, notifications, messages, terms, privacy, books, qna, my, mypage, learn, lessons, playgrounds, uiux, images...
보안: AdSense/Analytics ID 검증
블로그 설정에서 Google AdSense나 Analytics ID를 입력할 수 있는데, 이 필드가 XSS 공격에 취약할 수 있어서 정규식으로 형식을 잡아둡니다.
허용 형식: G-XXXXXXXXXX (Google Analytics 4) UA-XXXXXXX-Y (Universal Analytics) ca-pub-XXXXXXXXXX (AdSense) 그 외 문자열 → 거부
정규식은 보조 수단일 뿐이고, 출력 시 이스케이프 처리가 보안의 핵심입니다.
돌아보며
처음 "블로그 기능 추가하자"고 했을 때는 Phase 한두 번이면 될 줄 알았습니다. 실제로는 7번에 걸쳐 테이블 수십 개, API 수십 개, 테스트 수백 개를 만들었습니다.
그래도 핵심은 안 변했습니다. 교육생이 글을 쓰면, 그 글이 바로 보이고, 거기에 댓글을 달 수 있는 환경. 나머지는 전부 이 한 가지를 위한 것이었습니다.
아직 남은 숙제도 있긴 합니다.
· 블로그 글 URL에 제목 기반 슬러그 넣기 (/posts/123 → /posts/spring-boot-guide) · 블로그 내 검색 기능 · 무한 스크롤 · 슬러그 변경 이력 + 이전 URL 리다이렉트 · About 프로필 섹션 고도화 (경력, 기술 스택, 프로젝트)
그래도 지금 교육생이 글을 쓰면 피드에서 바로 보이고, 거기서 댓글을 달 수 있습니다. 처음에 하고 싶었던 건 되고 있습니다.






댓글
댓글을 작성하려면 이 필요합니다.