원티드 프리온보딩 인턴쉽에 참여하면서, 2주차에 깃허브에서 제공하는 API를 이용하여, 특정 레포지토리의 이슈 목록 데이터를 받아와 렌더링하고, 해당 이슈에 대한 상세 페이지를 구현하는 과제를 받았다. 이를 구현하기 위해 데이터 Fetching시 평소 사용하던 Axios
를 이용할까 했으나, Github
에서 본인들의 API에 맞춰 제공하는 라이브러리가 있었으며, 그것이 바로 이번 포스팅에서 설명할 Octokit
이다.
Octokit
(옥토킷) 이란 GitHub API
를 JavaScript
및 TypeScript
로 더 쉽게 사용할 수 있게 해주는 깃허브의 공식 클라이언트 라이브러리다. 옥토킷을 사용하면 깃허브의 데이터를 가져오거나, 이슈를 생성하거나, 풀 리퀘스트를 만들고, 기타 깃허브 관련 작업을 프로그래밍 방식으로 편리하게 수행할 수 있도록 도와준다.
Octokit
을 사용하는 방법에 대해서는 깃허브 API 공식문서에서 친절하게 설명해준다. 해당 링크로 접속해보면 옥토킷을 사용하여 API를 호출할 때 필요한 매개변수나 사용 예시 및 response
결과를 코드를 통해 보여준다. 이를 참고해 나의 프로젝트에 적용시켜보자.
옥토킷을 사용하기 위해서는 터미널에서 명령어를 통해 모듈을 설치해주어야 한다. 아래 명령어를 통해 설치하도록 하자.
npm install @octokit/rest
또, 나는 타입스크립트를 사용했기 때문에 타입 관련 모듈도 설치해주었다.
npm istall @octokit/type
Octokit
을 사용해 git API를 호출해보자. 그러기 위해 우선 파일을 하나 만들어 기본 베이스가 되는 코드를 아래와 같이 작성했다.
//octokit.ts
import { Octokit } from '@octokit/rest';
const OCTOKIT_TOKEN = process.env.REACT_APP_OCTOKIT_TOKEN;
if (!OCTOKIT_TOKEN) {
throw new Error('.env 파일의 git hub token이 잘못되었습니다.');
}
export const octokit = new Octokit({
auth: OCTOKIT_TOKEN,
});
위와 같이 /api/octokit.ts 파일을 만들어 베이스 로직을 작성해주었다. 여기서 OCTOKIT_TOKEN
은 깃허브에서 발급받는 개인 토큰값을 넣어주면 된다. 토큰 값이 없어도 사용은 가능하나, API 요청이 1시간에 60회로 제한이 걸리게 된다. 개발 환경에서는 API 요청을 많이 하게되기 때문에 토큰을 넣어 제한을 풀어주었다. 또한, 토큰값은 .env
파일에 작성하여 환경변수로 관리하였고, 토큰이 잘못되었을 시 에러처리 로직을 작성해주었다.
베이스 로직을 모두 작성하고, 별도의 파일을 만들어 API를 요청하는 로직을 작성해주었다.
//getIssueDate.ts
import { Endpoints } from '@octokit/types';
import { octokit } from './octokit';
// 해당 레포지토리의 모든 이슈 데이터를 받아옴
export const getAllIssues = async (
owner: string,
repo: string,
page: number,
): Promise<Endpoints['GET /repos/{owner}/{repo}/issues']['response']['data']> => {
const result = await octokit.issues.listForRepo({
owner,
repo,
state: 'open',
sort: 'comments',
direction: 'desc',
per_page: 100,
page,
});
return result.data;
};
// id 값을 통해 이슈의 상세 데이터를 받아옴
export const getIssueById = async (
owner: string,
repo: string,
issue_number: number,
): Promise<Endpoints['GET /repos/{owner}/{repo}/issues/{issue_number}']['response']['data']> => {
const result = await octokit.issues.get({
owner,
repo,
issue_number,
});
return result.data;
};
위와 같이 전체 데이터를 받아오는 get 요청 로직과 :id
값을 이용해 상세 데이터를 받아오는 get 요청 로직을 작성해주었다. 타입스크립트를 사용했기 때문에 타입 지정을 위해 옥토킷의 Endpoints
을 import 하여 response
타입을 지정해주었다. 위 코드를 보면 owner
, repo
, state
, sort
등의 속성들이 있다. 이 속성에 값을 지정해줌으로써 필요한 데이터를 받아올 수 있다.
export const getAllIssues = async (): Promise<Endpoints['GET /repos/{owner}/{repo}/issues']['response']['data']> => {
const result = await octokit.issues.listForRepo({
owner: 'facebook',
repo: 'react',
state: 'open',
sort: 'comments',
direction: 'desc',
per_page: 100,
page,
});
return result.data;
};
예를 들어, 위와 같이 작성할 경우 facebook 이 작성한 react 라는 레포지토리의 issue 데이터를 불러오게된다. 이 때 state
를 'open' 으로 지정하여, open 상태인 이슈들만 불러왔으며, sort
: 'comments', per_page
: 100 속성을 부여하여, 데이터를 댓글이 많은 순으로 정렬하고 최대한의 데이터를 불러왔다. per_page
속성을 지정해주지 않으면 기본적으로 불러오는 데이터는 30개이다.
마지막으로 아래와 같이 페이지 헤더에 들어갈 레포지토리의 작성자와 레포지토리 이름 데이터를 받아오기 위한 로직을 별도의 파일에 분리하여 작성했다.
// getRepoData.ts
import { Endpoints } from '@octokit/types';
import { octokit } from './octokit';
const getRepoData = async (
owner: string,
repo: string,
): Promise<Endpoints['GET /repos/{owner}/{repo}']['response']['data']> => {
const result = await octokit.repos.get({
owner,
repo,
});
return result.data;
};
export default getRepoData;
위에서 작성한 옥토킷 베이스 로직과 API 요청 로직을 기반으로, 실제 데이터를 요청하고 받아오는 함수를 작성했다. 이는 useIssueData
라는 커스텀 훅을 만들어 함수를 사용하는 컴포넌트를 간결하게 하고, 관심사를 분리하여 관리가 용이하게 하기 위함이었다.
// useIssueData.ts
import { useState, useEffect } from 'react';
import { getAllIssues, getIssueById } from '../api/issue';
import { Endpoints } from '@octokit/types';
import useLoading from './useLoading';
import useError from './useError';
export const useIssueData = () => {
const [issues, setIssues] = useState<
Endpoints['GET /repos/{owner}/{repo}/issues']['response']['data']
>([]);
const [issue, setIssue] = useState<
Endpoints['GET /repos/{owner}/{repo}/issues/{issue_number}']['response']['data'] | null
>(null);
const [page, setPage] = useState<number>(1);
const [hasMore, setHasMore] = useState<boolean>(true);
const { loading, setLoading } = useLoading();
const { error, setError, isError, setIsError } = useError();
const loadMoreIssues = async () => {
setLoading(true);
try {
const data = await getAllIssues('facebook', 'react', page);
if (data.length === 0) {
setHasMore(false);
return;
}
setIssues(prevIssues => [...prevIssues, ...data]);
setPage(prevPage => prevPage + 1);
} catch (error: any) {
console.error('이슈 데이터를 불러오는데 실패했습니다. : ', error);
setIsError(true);
setError({
message: error.message || '이슈 데이터를 불러오는데 실패했습니다.',
code: error.response?.status,
});
} finally {
setLoading(false);
}
};
const loadIssueById = async (id: number) => {
setLoading(true);
try {
const data = await getIssueById('facebook', 'react', id);
setIssue(data || null);
return data || null;
} catch (error: any) {
console.error('이슈 데이터를 불러오는데 실패했습니다. : ', error);
setIsError(true);
setError({
message: error.message || '이슈 데이터를 불러오는데 실패했습니다.',
code: error.response?.status,
});
return null;
} finally {
setLoading(false);
}
};
useEffect(() => {
loadMoreIssues();
}, []);
return {
issues,
loading,
loadMoreIssues,
hasMore,
isError,
error,
loadIssueById,
issue,
setIssue,
};
};
필요한 파일들을 import 해오고, useState
를 이용해 데이터를 담을 상태들을 만들었다. 그리고 해당 상태들을 이용해서 로직 내에 try / catch
문을 통해 데이터 fetching 과 error 처리를 해주었다. 해당 로직들은 추후 명령형, 절차형에서 벗어난 선언형 방식으로 리팩토링을 진행할 예정이다. . .
// IssueDetailPage.tsx
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useIssueData } from '../hooks/useIssueData';
import IssueAuthor from '../components/Issue/IssueAuthor';
import styled from 'styled-components';
import IssueBodyContent from '../components/Issue/IssueBodyContent';
const IssueDetailPage = () => {
const { id } = useParams<{ id: string }>();
const { loadIssueById, issue, setIssue } = useIssueData();
useEffect(() => {
(async () => {
const fetchedIssue = await loadIssueById(Number(id));
setIssue(fetchedIssue);
})();
}, []);
if (!issue) return null;
return (
<IssueDetailLayout>
<h1 style={{ margin: 0 }}>#{issue.number}</h1>
<h2>{issue.title}</h2>
<IssueAuthor
author={issue.user}
created_at={issue.created_at}
commentCount={issue.comments}
/>
{issue.body && <IssueBodyContent body={issue.body} />}
</IssueDetailLayout>
);
};
export default IssueDetailPage;
const IssueDetailLayout = styled.div`
padding: 20px;
border: 1px solid #e1e4e8;
border-radius: 6px;
`;
작성한 함수들을 필요한 컴포넌트에서 import 하여 사용했다. 데이터의 상세 페이지를 표시하는 IssueDatailPage
에서는 ur lParams
를 이용하여 해당 issue의 id 값을 추출하고 넘겨주는 방식을 채택했다. 이슈 목록 페이지의 경우 무한스크롤을 적용하였는데, 이는 무한스크롤 포스팅을 통해 더 자세하게 다룰 예정이다.
항상 데이터 fetching 시엔 Axios 만을 사용해왔는데, 이번에 새롭게 Octokit을 사용해볼 수 있는 경험을 하게되어 좋았다. 깃허브에서 자체적으로 개발하여 제공하는 것이라 그런지, 깃허브의 Rest API 를 이용할 때는 Axios 를 이용하는 것 보다 Octokit 을 이용하는게 데이터 fetching 속도가 더 빨랐다. 다른 분들도 깃허브 API 를 사용할 일이 있다면 한 번 쯤 사용을 고려해보길 바란다.