Gatsby 블로그 관리자 기능 총정리 - Netlify CMS 활용편
서론
블로그를 운영하면서 가장 불편했던 점은 포스팅을 작성할 때마다 마크다운 파일을 직접 수정하고 Git에 커밋해야 한다는 것이었습니다. 솔직히 처음엔 괜찮았는데, 이런 번거로움이 쌓이다 보니 자연스럽게 글을 쓰는 빈도가 줄어들더군요.
그래서 Netlify CMS를 도입해서 웹 브라우저에서 편하게 글을 관리할 수 있는 시스템을 만들었습니다. 추가로 구독자 관리 기능도 직접 개발해서 통합했고, 생각보다 꽤 쓸만한 관리자 시스템이 되었습니다.
이번 포스팅에서는 이렇게 구축한 관리자 기능들을 정리해보려고 합니다.
Netlify CMS란
Netlify CMS는 정적 사이트를 위한 오픈소스 컨텐츠 관리 시스템입니다. 워드프레스처럼 별도 데이터베이스를 쓰는 게 아니라, Git 저장소를 그대로 데이터 저장소로 활용하는 방식이죠.
포스팅을 작성하면 CMS가 자동으로 Git에 커밋하고, Netlify가 감지해서 자동 배포합니다. 결국 마크다운 파일을 직접 수정하는 것과 동일한 결과지만, 웹 UI로 훨씬 편하게 작업할 수 있다는 게 장점입니다.
선택한 이유
실제로 사용해본 결과 다음과 같은 장점이 있었습니다.
Git 기반이라서 모든 변경 이력이 자동으로 관리됩니다. 실수로 뭔가 잘못 건드려도 언제든 되돌릴 수 있어서 마음이 편합니다.
서버 호스팅 비용이 들지 않습니다. Netlify 무료 플랜만으로도 충분히 운영 가능하고, 개인 블로그 수준에서는 비용 걱정이 전혀 없습니다.
오픈소스라서 커스텀 위젯을 추가할 수 있습니다. 실제로 구독자 관리 위젯을 직접 만들어서 통합했는데, 생각보다 간단했습니다.
포 스트 작성 기능
마크다운 에디터
Netlify CMS는 Rich Text 에디터와 마크다운 에디터를 모두 제공합니다. 저는 당연히 마크다운 에디터를 씁니다.
좌측에서 마크다운을 작성하면 우측에 실시간으로 렌더링된 결과가 표시됩니다. 이미지는 드래그 앤 드롭으로 업로드할 수 있고, 코드 블록도 하이라이팅이 잘 됩니다.
작성 중인 내용은 브라우저 로컬 스토리지에 자동 저장되어서, 실수로 창을 닫아도 내용이 날아가지 않습니다. 이게 은근히 유용한 기능입니다.
글 하단에는 썸네일 이미지 업로드, 카테고리 설정, Draft 상태 관리 등의 메타정보를 관리할 수 있습니다.
# static/admin/config.yml
fields:
- {
label: '제목',
name: title,
widget: string,
hint: '포스트 제목을 입력하세요',
}
- {
label: '본문',
name: body,
widget: markdown,
hint: '마크다운 형식으로 작성하세요',
}메타데이터 관리
각 포스트는 제목, 작성일, 카테고리, 태그, 썸네일 등의 메타데이터를 관리합니다.
제목은 주제명을 직접 작성하고, 카테고리는 frontmatter의 category 필드로 별도 관리합니다. 작성일은 포스트 정렬 기준이 되고, 카테고리는 Spring, Java, Javascript, ReactJS 등으로 구분됩니다.
썸네일은 1200x600px 크기를 권장합니다. Draft 상태로 저장하면 초안으로 남아있다가, 나중에 발행할 수 있습니다.
카테고리별 관리
블로그는 Spring, Java, React, JavaScript, Coding Test, Blog Development 등의 카테고리로 구성됩니다.
각 카테고리는 별도 폴더로 관리되고, CMS에서도 카테고리별로 콜렉션이 분리되어 있습니다. 좌측 사이드바에서 콜렉션을 선택하면 해당 카테고리의 모든 포스트 목록을 확인할 수 있어서, 관리하기가 편합니다.
# 카테고리별 설정 예시
- name: spring
label: 'Spring'
folder: content/blog/spring
create: true
fields:
- { label: '제목', name: title, widget: string }
- { label: '작성일', name: date, widget: datetime }
- { label: '본문', name: body, widget: markdown }미디어 관리
업로드한 모든 이미지를 한곳에서 관리할 수 있습니다. 이미지 업로드는 드래그 앤 드롭으로 할 수 있고, 검색과 필터링도 지원합니다.
# 미디어 파일 설정
media_folder: content/blog
public_folder: /blog저는 카테고리별로 별도 이미지 폴더를 만들어서 관리하고 있습니다. 나중에 특정 카테고리의 이미지만 찾을 때 편합니다.
구독자 관리 시스템
Newsletter 기능과 함께 구독자 관리 시스템을 직접 개발했습니다. Netlify CMS의 기본 기능이 아니라, 커스텀 위젯으로 구현한 기능이라 조금 손이 갔습니다.
CMS 내에서 구독자 정보를 실시간으로 확인할 수 있습니다. 이메일 주소, 구독 일시, 상태(active, pending, unsubscribed)를 볼 수 있고, 통계도 한눈에 확인 가능합니다. 전체, 활성, 대기, 해지 상태별 통계가 상단에 표시되고, 각 구독자마다 삭제 버튼도 붙어있습니다.
백엔드 구조
구독자 데이터는 별도의 Spring Boot 마이크로서비스에서 관리합니다. Gatsby는 정적 사이트라서 데이터베이스를 사용할 수 없기 때문에, 백엔드를 분리했습니다.
Spring Boot 3.x와 JPA를 사용했고, 데이터베이스는 MySQL입니다. Spring Mail로 구독 확인 이메일도 발송합니다. 현재 Google Cloud Run에 배포해서 운영 중입니다.
// SubscriberController.java
@RestController
@RequestMapping("/api/subscribers")
public class SubscriberController {
@GetMapping
public List<SubscriberDto> getAll() {
return subscriberService.findAll();
}
@DeleteMapping("/{id}")
public ResponseEntity<?> delete(@PathVariable Long id) {
subscriberService.delete(id);
return ResponseEntity.ok().build();
}
}CMS 위젯 통합
Netlify CMS 내부에서 구독자를 확인할 수 있도록 커스텀 위젯을 개발했습니다.
// src/cms/subscribersWidget.js
const SubscribersControl = () => {
const [subscribers, setSubscribers] = useState([])
const [stats, setStats] = useState({
total: 0,
active: 0,
pending: 0,
unsubscribed: 0,
})
useEffect(() => {
fetch('/api/subscribers')
.then(res => res.json())
.then(data => {
setSubscribers(data)
calculateStats(data)
})
}, [])
return (
<div className="subscribers-widget">
<div className="stats">
<div>전체: {stats.total}</div>
<div>활성: {stats.active}</div>
<div>대기: {stats.pending}</div>
</div>
<table className="subscribers-table">{/* 구독자 목록 테이블 */}<</table>
</div>
)
}
CMS.registerWidget('subscribers', SubscribersControl)React로 위젯을 만들고, CMS.registerWidget로 등록하면 CMS에서 사용할 수 있습니다.
사이트 설정 관리
사이트 설정과 About 페이지도 CMS에서 관리할 수 있습니다. 별도의 파일 편집 없이 웹 인터페이스에서 바로 수정이 가능합니다.
Newsletter 설정
- file: content/settings/site.json
label: '사이트 설정'
fields:
- {
label: 'Newsletter Provider',
name: newsletterProvider,
widget: select,
options: [Self-Hosted, Buttondown, Mailchimp, ConvertKit],
default: Self-Hosted,
}
- { label: 'Newsletter Action URL', name: newsletterAction, widget: string }현재는 Self-Hosted를 선택해서 직접 개발한 Spring Boot API를 사용합니다. 나중에 Buttondown이나 Mailchimp 같은 서비스로 바꾸고 싶으면 설정만 변경하면 됩니다.
About 페이지 관리
자기소개 페이지도 CMS에서 직접 수정 가능합니다.
- file: content/__about/resume-en.md
label: 'About Page'
fields:
- { label: 'Title', name: title, widget: string }
- { label: 'Content', name: body, widget: markdown }운영 중 마주한 이슈들
이미지 경로 문제
처음에는 이미지 경로 설정을 잘못해서 CMS에서 업로드한 이미지가 블로그에서 보이지 않는 문제가 있었습니다.
media_folder: '/static/thumbnail-images/spring/images'
public_folder: '/thumbnail-images/spring/images'media_folder는 Git 저장소 내 실제 경로이고, public_folder는 웹에서 접근할 때의 경로입니다. 이 둘을 정확히 맞춰야 이미지가 제대로 표시됩니다.
API 연동 CORS 이슈
로컬 개발 환경에서 localhost:8000에서 localhost:8080으로 API 요청을 보낼 때 CORS 에러가 발생했습니다.
개발 환경에서는 Gatsby의 proxy 미들웨어로 해결했습니다.
// gatsby-config.js
developMiddleware: app => {
const { createProxyMiddleware } = require('http-proxy-middleware')
app.use(
'/.proxy/newsletter',
createProxyMiddleware({
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/.proxy/newsletter': '' },
})
)
}프로덕션 환경에서는 Spring Boot에서 CORS 설정을 추가했습니다.
// Spring Boot CORS 설정
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://your-blog.com")
.allowedMethods("GET", "POST", "DELETE");
}
}Git Large File 문제
블로그에 이미지가 많아지면서 Git 저장소 크기가 계속 커지는 문제가 있었습니다.
일단 TinyPNG로 이미지를 미리 압축하고, CMS에서 업로드할 때 5MB 제한을 두었습니다. 장기적으로는 Git LFS 도입을 검토 중입니다.
// 이미지 크기 제한
if (file.size > 5 * 1024 * 1024) {
alert('이미지 크기는 5MB 이하여야 합니다.')
return false
}빌드 시간 증가
포스트가 늘어나면서 빌드 시간이 30초에서 2분까지 늘어났습니다.
정적 사이트의 trade-off입니다. 빌드 시간은 늘어나지만, 로딩 속도가 빠르고 SEO에 유리하며 CDN 캐싱도 잘 됩니다. 실시간 업데이트가 안 되는 단점은 있지만, 블로그 특성상 큰 문제는 아니었습니다.
Incremental Build를 활성화해서 변경된 페이지만 재빌드하도록 설정했습니다.
보안 설정
Netlify Identity
기본 인증 시스템으로 Netlify Identity를 사용합니다.
<!-- static/admin/index.html -->
<script src="https://identity.netlify.com/v1/netlify-identity-widget.js"><</script>이메일 + 비밀번호, GitHub OAuth, Google OAuth를 지원합니다. 저는 GitHub OAuth를 선택했습니다. 2FA 설정으로 보안성이 높고, 개발자 블로그라서 모든 사용자가 GitHub 계정을 가지고 있기 때문입니다.
Git Gateway
backend:
name: git-gateway
branch: master직접 Git에 접근하는 게 아니라 Git Gateway를 통해 접근합니다. Personal Access Token이 노출될 위험이 없고, Netlify 수준의 권한 관리를 사용할 수 있습니다.
접근 제어
Netlify Identity의 Invite-only 모드로 설정해서 관리자만 접근 가능하도록 제한했습니다.
AI Chat - 블로그 데이터 기반 대화형 검색
최근에 추가한 기능 중 가장 유용한 기능입니다. OpenAI GPT-4o-mini를 활용해서 블로그의 모든 포스트 데이터를 학습시켜, 자연어로 질문하면 정확한 답변을 받을 수 있습니다.
어떻게 작동하는가
기존에는 단순 통계 정보만 제공했지만, 이제는 전체 포스트 목록(제목, 카테고리, 태그, 날짜)을 모두 AI에게 Context로 제공합니다.
// netlify/functions/ai-chat.js
async function getBlogData() {
const fetch = (await import('node-fetch')).default
// MCP API에서 블로그 통계 + 전체 포스트 목록 가져오기
const [statsRes, postsRes] = await Promise.all([
fetch(`${mcpUrl}/api/stats`),
fetch(`${mcpUrl}/api/posts`)
])
// 포스트 목록을 간결하게 정리
const simplePosts = postsData.posts.map(p =>
`- ${p.title} (Category: ${p.category}, Date: ${p.date}, Tags: ${p.tags.join(', ')})`
).join('\n')
return `[블로그 통계]\n${stats}\n\n[전체 포스트 목록]\n${simplePosts}`
}이렇게 준비된 데이터를 System Prompt에 포함시켜서, AI가 블로그의 모든 정보를 기반으로 답변할 수 있도록 만들었습니다.
실제 사용 예시
“이 블로그의 총 포스트 갯수는?”
현재 블로그에는 총 73개의 포스트가 있습니다! 이런 주제에 대해 알고 싶으신가요?
정확한 숫자를 제공합니다. 기존에는 “대략 70개 정도…”처럼 애매하게 답했지만, 이제는 실제 데이터를 기반으로 정확히 답변합니다.
“최신 스프링 주제에 대해서 알려줘”
네, 최근 Spring 관련 포스트를 소개해드리겠습니다:
- 시니어도 헷갈리는 @Transactional 실수 5가지 (2025-11-20)
- Category: Spring
- Tags: Spring, Transaction, @Transactional 어노테이션
- 분산 트랜잭션 실전 가이드 - SAGA 패턴으로 데이터 정합성 지키기 (2025-11-20)
- Category: Architecture
- Tags: Distributed Transaction, SAGA Pattern
- 분산 환경에서 데이터 정합성 챙기기 - 사용자가 새로고침을 누르지 않게 (2025-11-20)
- Category: Architecture
- Tags: Data Consistency
포스트 제목, 날짜, 카테고리, 태그를 모두 포함해서 구체적으로 추천합니다. 사용자가 다음 질문으로 “1번은 무슨 내용인지 5줄로 요약해줘” 같은 후속 질문도 자연스럽게 할 수 있습니다.
기술적 개선 사항
1. 로컬 ↔ 프로덕션 자동 전환
개발 환경과 배포 환경을 자동으로 감지해서, 항상 블로그 데이터에 접근할 수 있도록 만들었습니다.
// MCP API URL 자동 선택
let mcpUrl = process.env.MCP_API_URL
if (!mcpUrl) {
// 로컬 서버가 있으면 로컬, 없으면 프로덕션 사용
const localCheck = await fetch(`${MCP_API_LOCAL}/`, { timeout: 1000 })
mcpUrl = (localCheck && localCheck.ok) ? MCP_API_LOCAL : MCP_API_PROD
}2. 마크다운 렌더링 지원
AI의 답변이 볼드체, 리스트, 코드 블록 같은 마크다운 형식으로 표시되도록 marked.js를 통합했습니다.
// static/admin/index.html
messages.innerHTML += `
<div class="markdown-body">
${marked.parse(reply)}
</div>
`이제 답변이 훨씬 더 읽기 편합니다.
3. 스마트한 System Prompt
const systemPrompt = `당신은 "Ramsbaby" 기술 블로그의 스마트한 AI 어시스턴트입니다.
[지침]
1. **정확성**: 제공된 "전체 포스트 목록"과 "통계"를 기반으로 답변하세요.
2. **추천**: 사용자가 특정 주제에 대해 물으면, 관련 포스트의 제목을 구체적으로 언급하세요.
3. **분석**: "최근 트렌드가 뭐야?" 같은 질문에는 최근 포스트를 분석하여 답변하세요.
4. **톤앤매너**: 친절하고 전문적인 개발자 동료처럼 대화하세요.
5. **형식**: 마크다운을 사용하여 가독성 있게 답변하세요.
`단순히 검색하는 게 아니라, 데이터를 분석하고 추천할 수 있도록 설계했습니다.
4. 프로필 데이터 추가
AI가 “이 블로그 주인은 누구야?”라는 기본적인 질문에도 제대로 답하지 못했습니다. 그래서 MCP API에 /api/profile 엔드포인트를 추가하고, AI에게 작성자 프로필 정보를 제공했습니다:
// MCP API에서 프로필 정보도 가져오기
const [statsRes, postsRes, profileRes] = await Promise.all([
fetch(`${mcpUrl}/api/stats`),
fetch(`${mcpUrl}/api/posts`),
fetch(`${mcpUrl}/api/profile`) // 추가!
])이제 경력과 전문성을 포함한 풍부한 답변을 받을 수 있습니다.
5. 연도별 통계 자동 집계
AI가 포스트 목록을 직접 세면서 연도별 통계를 계산했는데, 날짜 포맷이 제각각이라 오류가 자주 발생했습니다. 그래서 JavaScript로 정확히 계산된 연도별 통계를 AI에게 직접 제공하도록 변경했습니다. 이제 “연도별로 포스트가 몇 개씩 있어?”라고 물으면, 100% 정확한 답변을 받을 수 있습니다.
6. 응답 길이 & 품질 향상
max_tokens를 1000에서 2000으로, temperature를 0.7에서 0.8로 증가시켰습니다. 이제 AI가 최소 150단어 이상의 풍부한 답변을 제공하고, 관련 질문까지 유도할 수 있게 되었습니다.
활용 시나리오
블로그 관리자 입장:
- “최근 3개월 동안 Spring 관련 글을 몇 개 썼어?”
- “트랜잭션 관련 포스트가 있나?”
- “IoT 주제는 언제 마지막으로 썼지?”
방문자 입장 (향후 퍼블릭 공개 예정):
- “Spring Batch 성능 최적화 방법 알려줘”
- “이 블로그에서 추천하는 아키텍처 패턴은?”
현재는 관리자 페이지에서만 사용 가능하지만, 향후 블로그 메인 페이지에도 통합할 계획입니다.
향후 개선 계획
통계 대시보드
Google Analytics Reporting API를 연동해서 CMS 내에서 실시간 통계를 확인하고 싶습니다. 일간/주간/월간 방문자 수, 인기 포스트 Top 10, 유입 경로 분석, 검색 키워드 통계 같은 걸 보고 싶습니다.
포스트 예약 발행
GitHub Actions의 cron job을 활용해서 예약 발행 시스템을 만들 수 있을 것 같습니다.
# 예약 발행 필드
- {
label: '예약 발행 시간',
name: scheduledAt,
widget: datetime,
required: false,
}이미지 자동 최적화
Sharp를 이용해서 업로드하는 이미지를 자동으로 최적화하면 좋을 것 같습니다.
await sharp(input)
.resize(1200, 630)
.jpeg({ quality: 80 })
.toFile(output)Netlify Build Plugin으로 만들어서 배포 시 자동 실행되도록 하면 됩니다.
결론
Netlify CMS를 활용해서 Gatsby 블로그 관리 시스템을 만들면서 많은 걸 배웠습니다.
Git 기반이라서 버전 관리가 자동으로 되고, 카테고리별 콜렉션으로 체계적인 컨텐츠 관리가 가능합니다. 커스텀 위젯으로 구독자 관리 기능까지 추가했고, 마이크로서비스로 백엔드를 분리해서 확장성도 확보했습니다.
특히 최근에 추가한 AI Chat 기능은 블로그를 더욱 똑똑하게 만들어주었습니다. 전체 포스트 데이터를 학습시켜서 “최근 Spring 글 추천해줘” 같은 자연어 질문에 정확하게 답변하고, 관련 포스트를 구체적으로 추천할 수 있습니다. 단순한 키워드 검색이 아니라, 맥락을 이해하고 분석하는 진짜 AI 어시스턴트를 만든 셈입니다.
정적 사이트의 성능 장점을 유지하면서도 CMS로 관리 편의성을 확보한 게 가장 큰 수확입니다. 프로덕션 환경에서 발생한 이슈들을 해결하면서 시스템 안정성도 개선했고, 앞으로 통계 대시보드 같은 추가 개선도 계획하고 있습니다.
참고 :
https://www.netlifycms.org/docs/intro/
https://www.gatsbyjs.com/docs/how-to/sourcing-data/sourcing-from-netlify-cms/
https://github.com/netlify/netlify-cms
https://identity.netlify.com/
