Skip to content
푸땡로그
Go back

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

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


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

Giscus의 한계점

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

자동 생성의 필요성

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

🏗️ 시스템 아키텍처

전체 구조

flowchart TD
  A[사용자<br/>댓글 작성 / 조회] --> B[Astro 블로그<br/>Giscus 위젯]
  B --> C[GitHub Discussions<br/>댓글 저장 / 관리]
  D[Astro Build] -->|빌드 시 자동 생성| C

핵심 구성 요소

flowchart TD
  A[npm run build 완료] --> B[포스트 분석<br/>모든 포스트 정보 수집]
  B --> C{Discussion<br/>존재 여부 확인}
  C -->|있음| D[스킵]
  C -->|없음| E[GraphQL API로<br/>Discussion 생성]
  E --> F{성공?}
  F -->|성공| G[다음 포스트 처리]
  F -->|실패| H[에러 로깅 후<br/>다음 포스트 처리]
  D --> G
  H --> G

🚀 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 에러 없이 완벽한 사용자 경험을 제공할 수 있습니다! 🚀


Share this post on:

Previous Post
Astro 블로그에 Giscus 댓글 시스템 구현하기
Next Post
Claude Code Agent Teams 완벽 가이드: 에이전트 협업으로 생산성 극대화하기