Skip to content
Go back

GitHub Discussions 자동 생성 시스템으로 Giscus 댓글 완성하기

Published:  at  09:00 PM

이전 글에서 Astro 블로그에 Giscus 댓글 시스템을 구현했습니다. 이번 글에서는 댓글 시스템의 완성도를 높이기 위한 GitHub Discussions 자동 생성 시스템을 구현하겠습니다. 🚀


🤔 왜 Discussion 자동 생성이 필요한가?

Giscus의 한계점

Giscus는 댓글을 표시하기 위해 해당 포스트와 매칭되는 GitHub Discussion이 존재해야 합니다.
Discussion이 없는 상태에서는 Giscus가 404 에러를 발생시켜 사용자 경험을 해치게 됩니다.
따라서 빌드 시 자동으로 Discussion을 생성하여 404 에러를 방지하고 사용자 경험을 개선합니다.

자동 생성의 필요성

  1. 404 에러 방지: Discussion이 없는 포스트에서 댓글 시스템 접근 시 에러 발생
  2. 사용자 경험 개선: 모든 포스트에서 일관된 댓글 시스템 제공
  3. 관리 효율성: 수동으로 Discussion을 생성할 필요 없음
  4. 확장성: 새 포스트 작성 시 자동으로 댓글 시스템 준비

🏗️ 시스템 아키텍처

전체 구조

+---------------+      +----------------+      +---------------------+
|      사용자     | ---> |   Astro 블로그   | ---> | GitHub Discussions  |
|  댓글 작성/조회   |      |   (Giscus)     |      |     댓글 저장/관리     |
+---------------+      +----------------+      +---------------------+
                              |
                              v
                      +----------------------+
                      |      빌드 시 자동       |
                      |   Discussion 생성     |
                      +----------------------+

핵심 구성 요소

  1. 빌드 후 스크립트: npm run build 완료 후 자동 실행
  2. 포스트 분석: 블로그의 모든 포스트 정보 수집
  3. Discussion 확인: 기존 Discussion 존재 여부 검사
  4. 자동 생성: 없는 Discussion에 대해 새로 생성
  5. 에러 처리: API rate limit 및 네트워크 오류 대응

🚀 GitHub GraphQL API 연동

0단계: Personal Access Token 생성

GitHub GraphQL API를 사용하기 전에 Personal Access Token을 생성해야 합니다.

토큰 생성 과정

  1. GitHub.comSettingsDeveloper settingsPersonal access tokensFine-grained tokens
  2. Generate new token 클릭
  3. Token name: blog-discussions-auto-generation
  4. Expiration: 1년 권장
  5. Repository access: Giscus가 사용하는 저장소만 선택
  6. Permissions: Discussions Read and write (필수)
  7. 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

환경 변수 설명:

저장소 및 카테고리 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.tscreate-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;
}

주요 특징


🤖 빌드 시 자동 실행 시스템

이제 구현한 유틸리티를 실제로 사용하여 빌드 시 자동으로 Discussion을 생성하는 시스템을 만들어보겠습니다.

1단계: package.json 스크립트 추가

{
  "scripts": {
    "post-build:discussions": "tsx scripts/create-discussions.ts",
    "discussions:manual": "tsx scripts/create-discussions.ts"
  }
}

스크립트 설명:

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 댓글 시스템의 완성도를 크게 높였습니다.

구현 완료된 기능들

시스템의 장점

  1. 완전 자동화: 개발자 개입 없이 자동 실행
  2. 사용자 경험 향상: 404 에러 완전 제거
  3. 관리 효율성: 수동 Discussion 생성 불필요
  4. 확장성: 새 포스트 자동 지원
  5. 안정성: 에러 처리 및 재시도 로직

📚 참고 자료


GitHub Discussions 자동 생성 시스템 구현을 통해 느낀 점: 자동화의 힘을 다시 한번 확인했습니다. 빌드 과정에 통합함으로써 개발자가 신경 쓸 필요 없이 댓글 시스템이 완벽하게 작동하는 것을 보니, 개발 효율성과 사용자 경험이 모두 향상되었다는 것을 느낍니다.

이제 모든 포스트에서 일관된 댓글 시스템을 제공할 수 있게 되었으며, 404 에러 없이 완벽한 사용자 경험을 제공할 수 있습니다! 🚀



Previous Post
Astro 블로그에 Giscus 댓글 시스템 구현하기