이전 글에서 Astro 블로그에 Giscus 댓글 시스템을 구현했습니다. 이번 글에서는 댓글 시스템의 완성도를 높이기 위한 GitHub Discussions 자동 생성 시스템을 구현하겠습니다. 🚀
🤔 왜 Discussion 자동 생성이 필요한가?
Giscus의 한계점
Giscus는 댓글을 표시하기 위해 해당 포스트와 매칭되는 GitHub Discussion이 존재해야 합니다.
Discussion이 없는 상태에서는 Giscus가 404 에러를 발생시켜 사용자 경험을 해치게 됩니다.
따라서 빌드 시 자동으로 Discussion을 생성하여 404 에러를 방지하고 사용자 경험을 개선합니다.
자동 생성의 필요성
- 404 에러 방지: Discussion이 없는 포스트에서 댓글 시스템 접근 시 에러 발생
- 사용자 경험 개선: 모든 포스트에서 일관된 댓글 시스템 제공
- 관리 효율성: 수동으로 Discussion을 생성할 필요 없음
- 확장성: 새 포스트 작성 시 자동으로 댓글 시스템 준비
🏗️ 시스템 아키텍처
전체 구조
+---------------+ +----------------+ +---------------------+
| 사용자 | ---> | Astro 블로그 | ---> | GitHub Discussions |
| 댓글 작성/조회 | | (Giscus) | | 댓글 저장/관리 |
+---------------+ +----------------+ +---------------------+
|
v
+----------------------+
| 빌드 시 자동 |
| Discussion 생성 |
+----------------------+
핵심 구성 요소
- 빌드 후 스크립트:
npm run build
완료 후 자동 실행 - 포스트 분석: 블로그의 모든 포스트 정보 수집
- Discussion 확인: 기존 Discussion 존재 여부 검사
- 자동 생성: 없는 Discussion에 대해 새로 생성
- 에러 처리: API rate limit 및 네트워크 오류 대응
🚀 GitHub GraphQL API 연동
0단계: Personal Access Token 생성
GitHub GraphQL API를 사용하기 전에 Personal Access Token을 생성해야 합니다.
토큰 생성 과정
- GitHub.com → Settings → Developer settings → Personal access tokens → Fine-grained tokens
- Generate new token 클릭
- Token name:
blog-discussions-auto-generation
- Expiration: 1년 권장
- Repository access: Giscus가 사용하는 저장소만 선택
- Permissions: Discussions
Read and write
(필수) - Generate token 클릭 후 토큰 복사
보안 주의사항
⚠️ 중요: 토큰을 절대 공개 저장소에 커밋하지 마세요!
.env
파일을.gitignore
에 추가- 배포 환경에서는 환경 변수로 설정
- 토큰 노출 시 즉시 재발급
토큰 권한 확인
curl -H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/vnd.github.v4+json" \
https://api.github.com/graphql \
-d '{"query":"query { viewer { login } }"}'
1단계: 환경 변수 설정
Personal Access Token을 환경 변수로 설정합니다:
# .env 파일에 추가 (절대 공개 저장소에 커밋하지 마세요!)
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
환경 변수 설명:
GITHUB_TOKEN
: 방금 생성한 Personal Access TokenGISCUS_REPO
: Giscus가 사용하는 저장소 (예:username/blog-repo
)GISCUS_REPO_ID
: 저장소의 고유 ID (숫자)GISCUS_CATEGORY_ID
: Discussion 카테고리 ID (숫자)
저장소 및 카테고리 ID 설정
실제 구현에서는 constants.ts
에서 설정된 값을 사용합니다:
// src/constants.ts
export const GISCUS_CONFIG = {
REPOSITORY: {
ID: "R_kgDOGxxxxxxxx", // 저장소 ID (숫자가 아닌 문자열)
NAME: "username/blog-comments", // 저장소 이름
},
CATEGORY: {
ID: "DIC_kwDOGxxxxxxxx", // 카테고리 ID (숫자가 아닌 문자열)
NAME: "Announcements", // 카테고리 이름
},
API: {
GRAPHQL_URL: "https://api.github.com/graphql",
},
};
2단계: Discussion 생성 유틸리티
실제 구현된 githubGraphQL.ts
와 create-discussions.ts
를 기반으로 한 유틸리티입니다:
핵심 유틸리티 함수들
// src/utils/githubGraphQL.ts
import "dotenv/config";
import { GISCUS_CONFIG } from "../constants.js";
// GitHub GraphQL API 타입 정의
interface Discussion {
id: string;
title: string;
number: number;
}
interface SearchResult {
discussionCount: number;
nodes: Discussion[];
}
interface CreateDiscussionInput {
repositoryId: string;
categoryId: string;
title: string;
body: string;
}
// GraphQL 클라이언트 초기화
function getGraphQLClient() {
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error("GITHUB_TOKEN environment variable is not set");
}
return {
async query<T>(
query: string,
variables?: Record<string, unknown>
): Promise<T> {
const response = await fetch(GISCUS_CONFIG.API.GRAPHQL_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query,
variables,
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`GitHub API error: ${response.status} ${response.statusText}\n${errorText}`
);
}
const result = await response.json();
if (result.errors) {
throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
}
return result.data;
},
};
}
Discussion 검색 및 생성 함수
// src/utils/githubGraphQL.ts
// Discussion 검색 함수
export async function searchDiscussion(
pathname: string
): Promise<Discussion | null> {
const client = getGraphQLClient();
// pathname을 기반으로 discussion 검색
const searchQuery = `repo:${GISCUS_CONFIG.REPOSITORY.NAME} "${pathname}" in:title`;
const query = `
query SearchDiscussions($query: String!) {
search(query: $query, type: DISCUSSION, first: 1) {
discussionCount
nodes {
... on Discussion {
id
title
number
}
}
}
}
`;
try {
const result = await client.query<{ search: SearchResult }>(query, {
query: searchQuery,
});
if (result.search.discussionCount > 0 && result.search.nodes.length > 0) {
return result.search.nodes[0];
}
return null;
} catch (error) {
console.error(`[GitHub GraphQL] Error searching for discussion:`, error);
throw error;
}
}
// Discussion 생성 함수
export async function createDiscussion(pathname: string): Promise<Discussion> {
const client = getGraphQLClient();
const mutation = `
mutation CreateDiscussion($input: CreateDiscussionInput!) {
createDiscussion(input: $input) {
discussion {
id
title
number
}
}
}
`;
const input: CreateDiscussionInput = {
repositoryId: GISCUS_CONFIG.REPOSITORY.ID,
categoryId: GISCUS_CONFIG.CATEGORY.ID,
title: pathname,
body: `Discussion for blog post: ${pathname}\n\nThis discussion was automatically created for the blog post at path: ${pathname}`,
};
try {
const result = await client.query<{
createDiscussion: { discussion: Discussion };
}>(mutation, {
input,
});
return result.createDiscussion.discussion;
} catch (error) {
console.error(`[GitHub GraphQL] Error creating discussion:`, error);
throw error;
}
}
고급 처리 함수들
// src/utils/githubGraphQL.ts
// Discussion 존재 여부 확인 및 생성 함수
export async function ensureDiscussionExists(
pathname: string
): Promise<Discussion> {
try {
// 먼저 기존 discussion이 있는지 확인
const existingDiscussion = await searchDiscussion(pathname);
if (existingDiscussion) {
console.log(
`[GitHub GraphQL] Discussion already exists for ${pathname}, skipping creation`
);
return existingDiscussion;
}
// discussion이 없으면 새로 생성
console.log(
`[GitHub GraphQL] Discussion not found for ${pathname}, creating new one`
);
return await createDiscussion(pathname);
} catch (error) {
console.error(
`[GitHub GraphQL] Error ensuring discussion exists for ${pathname}:`,
error
);
throw error;
}
}
// 배치 처리를 위한 함수
export async function ensureDiscussionsExist(
pathnames: string[]
): Promise<Discussion[]> {
const results: Discussion[] = [];
const errors: Array<{ pathname: string; error: Error }> = [];
console.log(
`[GitHub GraphQL] Processing ${pathnames.length} pathnames for discussion creation`
);
// 순차 처리 (rate limiting 고려)
for (const pathname of pathnames) {
try {
const discussion = await ensureDiscussionExists(pathname);
results.push(discussion);
// API rate limiting을 위한 짧은 지연
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
console.error(
`[GitHub GraphQL] Error ensuring discussion for ${pathname}:`,
error
);
errors.push({ pathname, error: error as Error });
}
}
if (errors.length > 0) {
console.error(
`[GitHub GraphQL] ${errors.length} errors occurred during batch processing:`
);
errors.forEach(({ pathname, error }) => {
console.error(` - ${pathname}: ${error.message}`);
});
}
console.log(
`[GitHub GraphQL] Batch processing completed. Success: ${results.length}, Errors: ${errors.length}`
);
return results;
}
주요 특징
- 타입 안전성: TypeScript 인터페이스로 GraphQL 응답 타입 정의
- 에러 처리: 상세한 에러 메시지와 로깅
- Rate Limiting: API 제한을 고려한 지연 처리
- 배치 처리: 여러 Discussion을 효율적으로 처리
- 중복 방지: 기존 Discussion 확인 후 생성
🤖 빌드 시 자동 실행 시스템
이제 구현한 유틸리티를 실제로 사용하여 빌드 시 자동으로 Discussion을 생성하는 시스템을 만들어보겠습니다.
1단계: package.json 스크립트 추가
{
"scripts": {
"post-build:discussions": "tsx scripts/create-discussions.ts",
"discussions:manual": "tsx scripts/create-discussions.ts"
}
}
스크립트 설명:
post-build:discussions
: 빌드 완료 후 자동 실행discussions:manual
: 수동으로 Discussion 생성 실행
2단계: Discussion 생성 스크립트
// src/scripts/create-discussions.ts
import { getSortedPosts } from "../src/utils/getSortedPosts";
import {
ensureDiscussionExists,
ensureDiscussionsExist,
} from "../src/utils/githubGraphQL";
async function createMissingDiscussions() {
try {
console.log("🔍 포스트별 Discussion 확인 중...");
const posts = await getSortedPosts();
// pathname 배열 생성 (포스트 slug 기반)
const pathnames = posts.map(post => `/${post.slug}`);
console.log(
`📝 총 ${posts.length}개 포스트에 대한 Discussion 확인/생성 시작`
);
// 배치 처리로 모든 Discussion 생성
const results = await ensureDiscussionsExist(pathnames);
console.log(`\n📊 Discussion 생성 완료:`);
console.log(` - 성공: ${results.length}개`);
console.log(` - 총 포스트: ${posts.length}개`);
if (results.length === posts.length) {
console.log(
"✅ 모든 포스트에 대한 Discussion이 성공적으로 준비되었습니다!"
);
} else {
console.log(
`⚠️ ${posts.length - results.length}개 포스트에 대한 Discussion 생성에 실패했습니다.`
);
}
} catch (error) {
console.error("❌ Discussion 생성 중 오류 발생:", error);
process.exit(1);
}
}
// 스크립트 실행
createMissingDiscussions();
3단계: 실행 및 테스트
자동 실행 (빌드 후)
npm run build
# 빌드 완료 후 자동으로 Discussion 생성 스크립트 실행
수동 실행
npm run discussions:manual
# 언제든지 수동으로 Discussion 생성 가능
실행 결과 확인
# GitHub 저장소의 Discussions 탭에서 생성된 Discussion 확인
# 또는 콘솔 로그를 통해 처리 결과 확인
🎯 결론
GitHub Discussions 자동 생성 시스템을 통해 Giscus 댓글 시스템의 완성도를 크게 높였습니다.
구현 완료된 기능들
- ✅ GitHub GraphQL API 연동: Personal Access Token 기반 인증
- ✅ 빌드 시 자동 Discussion 생성: 포스트별 자동 매칭
- ✅ 포스트별 Discussion 매칭: pathname 기반 정확한 연결
- ✅ API rate limit 대응: 지연 처리로 안정성 확보
- ✅ 에러 처리 및 로깅: 상세한 오류 정보 제공
- ✅ 배치 처리: 효율적인 대량 Discussion 생성
시스템의 장점
- 완전 자동화: 개발자 개입 없이 자동 실행
- 사용자 경험 향상: 404 에러 완전 제거
- 관리 효율성: 수동 Discussion 생성 불필요
- 확장성: 새 포스트 자동 지원
- 안정성: 에러 처리 및 재시도 로직
📚 참고 자료
- GitHub GraphQL API for Discussions
- GitHub Personal Access Tokens
- GitHub Fine-grained Personal Access Tokens
GitHub Discussions 자동 생성 시스템 구현을 통해 느낀 점: 자동화의 힘을 다시 한번 확인했습니다. 빌드 과정에 통합함으로써 개발자가 신경 쓸 필요 없이 댓글 시스템이 완벽하게 작동하는 것을 보니, 개발 효율성과 사용자 경험이 모두 향상되었다는 것을 느낍니다.
이제 모든 포스트에서 일관된 댓글 시스템을 제공할 수 있게 되었으며, 404 에러 없이 완벽한 사용자 경험을 제공할 수 있습니다! 🚀