Github API를 호출해보자(ft. Octokit)

2025-01-13

깃헙에 백업해둔 옵시디언 노트들을 Github API(Octokit)을 사용해서 가지고 와보자.

How to start

API에 대한 정의는 공식 문서를 참고하자.

1. 패키지 설치

#npm
npm install @octokit/rest
 
#yarn
yarn add @octokit/rest

2. Github Access Token 발급

만약 Public Repository에만 접근을 한다던가, API rate limit을 신경쓰지 않아도 되는 경우라면 발급을 받지 않고 바로 사용해도 되지만, 나같은 경우에는 접근해야 하는 레포지토리가 Private으로 되어있어서 반드시 발급을 받아야 했다.

참고로 인증을 거치지 않고 API를 사용하려 한다면 IP주소로 rate limit을 체크하게 되고, 시간당 60회를 호출할 수 있다. 인증을 거친 유저는 사용하는 플랜에 따라 limit이 다른데, 일반 유저는 시간당 5000회의 요청을 날릴 수 있다. 더 자세한 내용은 여기서 확인할 수 있다.

이제 진짜 토큰을 발급받아보자.

Access Token은 Settings > Developer Settings > Personal access tokens에서 발급받을 수 있다.

해당 페이지에 가보면 이렇게 두 가지 종류가 있는데

|306x198 뭐가 나쁘고 좋고의 차이보다는 상황에 따라 다르게 사용하면 될 것 같다.

Fine-grained tokensTokens(classic)
권한 제어특정 레포나 작업에 대해서만 접근 권한을 부여하는 것도 가능넓은 범위의 접근 권한 부여 가능
보안성높음낮음
사용 예시특정 프로젝트에 한정된 자동화, CI/CD 등 제한된 영역에서 활용할 때계정 전체의 여러 레포에 대한 접근이 필요할 때 사용

나는 Obsidian 백업 레포에 한정해서 REST API를 사용하고 싶었기 때문에, Fine-grained tokens로 발급을 받아줬다.

3. Octokit 설정

Octokit을 사용하기 위한 기본 설정을 해보자.

octokit.ts
import { Octokit } from "@octokit/rest";
 
const octokit = new Octokit({
	auth: process.env.GITHUB_TOKEN,
});

위에서 발급받은 Github token을 auth에 넣어준다.

놀랍게도 준비 끝. 참 쉽죠? 더 많은 옵션들을 설정할 수도 있는데(timezone, baseUrl 등), 공식 문서에 자세하게 설명되어 있으니 참고하면 좋을 것 같다.

4. Content 불러오기

Github REST API는 다양한 API들을 지원하지만, 그 중에서 우리는 특정 경로에 있는 마크다운 컨텐츠를 불러와야 하기 때문에 getContent API를 사용할 것이다.

export const getContent = async (path: string) =>
	await octokit.repos.getContent({
		owner: obsidianConfig.owner //undefinedp,
		repo: obsidianConfig.repo //obsidian,
		path,
	});

각 포스트별로 경로가 전부 다르기 때문에 path를 파라미터로 넘겨주는 식으로 구현을 했다.

export async function fetchMarkdownFileContent(path: string): Promise<string> {
	try {
		const { data } = await getContent(path);
 
		if (!("content" in data)) {
			throw new Error("Content not found in response");
		}
	// content가 base64로 인코딩되어 넘어오기 때문에 디코딩 필요
		const decodedContent = Buffer.from(data.content, "base64").toString("utf8");
		return decodedContent;
	} catch (error) {
		console.error("Error fetching markdown file content:", error);
		return "Error: 컨텐츠를 불러올 수 없습니다.";
	}
}

5. 컴포넌트에 적용

.../post/[...slug]/page.tsx
export default async function PostPage({ params }: PostPageProps) {
	const {
		slug: [title],
	} = await params;
	
	const decodedSlug = decodeURIComponent(title as string);
	
	const slugMap = await getSlugMap();
	const matched = slugMap.get(decodedSlug);
 
	if (!matched) notFound();
	const content = await fetchMarkdownFileContent(matched.path);
 
	return (
		<MaxWidthWrapper>
			<PostTitle
				title={matched.title}
				date={ISODateFormatter(matched.createdAt)}
				tags={matched.tags}
			/>
			<hr className="my-5 border-t-[3px]" />
			<Markdown content={content} />
		</MaxWidthWrapper>
	);
}
  1. URL의 slug에서 첫 번째 요소를 가져와서 decodeURIComponent로 디코딩
  2. getSlugMap()으로 frontmatter 정보를 담은 Map을 불러오고, 이 Map에서 title과 일치하는 포스트를 찾음
  3. 찾은 포스트의 path를 fetchMarkdownFileContent()에 넘겨서 내용을 불러옴
  4. 불러온 콘텐츠를 Markdown 컴포넌트의 파라미터로 전달해 렌더링