프로그래밍/React

[React] Github API를 이용하여 자동 팔로우 기능 만들기

라다디 2022. 10. 22. 20:25

🔧 삽질 기록입니다. 🔨

 

프로그래머스 데브코스 프론트엔드 3기에 합류하고 벌써 일주일이 지났네요.

깃허브에서 3기 분들 35명을 일일이 팔로우하다가 한 번에 팔로우 해주는 프로그램을 한 번 만들어봐야 겠다는 생각이 들었습니다.

 

아래는 완성본입니다. 

 

해당 계정은 테스트용으로 현재는 탈퇴 상태입니다.


깃허브 API

깃허브 REST API 공식 문서입니다. 

 

GitHub REST API - GitHub Docs

To create integrations, retrieve data, and automate your workflows, build with the GitHub REST API.

docs.github.com

 

Users 문서의 user 리스트, user 정보, 팔로워 가져오기 등의 API를 postman으로 확인했습니다. 아래는 user의 정보를 가져오는 API https://api.github.com/users/{username} 입니다.

 

리액트타입스크립트를 사용했고, 리액트 쿼리리액트 훅 폼을 함께 사용하였습니다.

npx create-react-app 내 앱 이름 --template typescript
npm i styled-components @types/styled-components react-router-dom 
@tanstack/react-query react-hook-form

 

구상한 흐름

1. 유저에게 이름 PAT(personal access token)을 받아와서 유저의 정보를 보여준다.

  • user의 이름과 PAT를 검증한다.
  • 해당 정보로 유저를 찾지 못했을 경우 유저가 존재하지 않는다는 메세지를 띄운다.

2. 유저의 정보가 출력이 되면 팔로우 버튼을 보여준다. 

3. 팔로우 버튼을 누르면 팔로잉 최대 30명을 아래에 보여준다.

 

1. 유저 정보 입력 폼

먼저 유저의 이름과 PAT를 받아야 합니다.

PAT 발급 방법은 블로그 혹은 공식 문서를 참고해주시면 될 것 같습니다.

권한은 user만 체크하시면 됩니다. 

 

폼을 좀 더 편리하게 사용하기 위해서 react-hook-form을 사용하였습니다. 

import { useForm } from "react-hook-form";
...

function Home() {
  const [userName, setUserName] = useState("");
  const [token, setToken] = useState("");

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<IInfo>();
  const onValid = ({ userName, token }: IInfo) => {
    setUserName(userName);
    setToken(token);
  };
  
    return (
    <StHome>
      <StTitle>프롱이 팔로우하기</StTitle>
      <StForm onSubmit={handleSubmit(onValid)}>
        <StInput
          {...register("userName", {
            required: "유저 이름을 입력해주세요!",
            pattern: {
              value: /^^[\w]+(?:-[\w]+)*$/,
              message: "유저 이름 형식이 잘못 되었습니다!",
            },
          })}
          placeholder="Enter your username"
        />
        <StSpan>{errors.userName?.message}</StSpan>
        <StInput
          {...register("token", {
            required: "토큰을 입력해주세요!",
            pattern: {
              value: /^ghp_[A-Za-z0-9]/,
              message: "토큰 형식이 잘못 되었습니다!",
            },
          })}
          placeholder="Enter personal access token"
        />
        <StSpan>{errors.token?.message}</StSpan>
        <StSubmit>유저 찾기</StSubmit>
      </StForm>
 ...

react-hook-form에서 useForm 훅을 import하고 register 함수의 반환 객체를 input에 props로 주었습니다. 

조금 더 자세히 살펴볼게요.

<StInput
  {...register("userName", {
    required: "유저 이름을 입력해주세요!",
    pattern: {
      value: /^^[\w]+(?:-[\w]+)*$/,
      message: "유저 이름 형식이 잘못 되었습니다!",
    },
  })}
  placeholder="Enter your username"
/>

입력창을 필수로 설정해줬고, 정규표현식을 이용해서 값을 검증하였습니다.

깃허브의 유저 이름은 알파벳, 숫자, '-'를 사용할 수 있지만 이름의 가장 앞과 뒤에는 '-'가 나와서는 안됩니다.

아래 블로그를 참고하여 정규표현식을 작성하였습니다. /^[\w]+(?:-[\w]+)*$/

 

정규식은 어떻게 사용되는 것일까?

필자는 지난 불규칙 속에서 규칙을 찾아내는 정규표현식 포스팅에서 정규식의 기본적인 사용 방법을 한 차례 설명한 바 있다. 그러나 아무리 정규식의 기본적인 사용 방법을 알고 있다고 해도

evan-moon.github.io

두 번째 입력창도 마찬가지로 구현했습니다. PAT는 ghp_로 시작하고 영어와 숫자가 들어갑니다. 

따라서 그 조건에 맞게 다음과 같은 정규표현식을 작성하였습니다.  /^ghp_[A-Za-z0-9]/

 

이제 사용자가 유저 찾기 버튼을 누르면 handleSubmit 함수가 호출되는데 이 함수는 2개의 인자를 받습니다. 

첫 번째 인자는 validation을 마쳤을 때 호출되는 함수이고, 두 번째 함수는 데이터가 유효하지 않을 때 호출되는 함수입니다. 두 번째 인자는 필수는 아닙니다. 

 

저는 첫 번째 인자로 onValid 함수를 넣어줬고 이 함수는 유저의 이름토큰을 state에 저장합니다. 

참고로 onValid의 IInfo는 다음과 같은 인터페이스입니다. api.ts 파일에 저장되어 있습니다.

export interface IInfo {
  userName: string;
  token: string;
}

2. 유저 정보 출력

이제 유저의 이름이 userName state에 담겼습니다.

const [userLoad, setUserLoad] = useState(false);
...

{userName && (
    <>
      <UserInfo
        userName={userName}
        token={token}
        userLoad={userLoad}
        setUserLoad={setUserLoad}
      />
      {userLoad && <Follow userName={userName} token={token} />}
    </>
)}

유저의 이름이 빈 문자열이 아니면 UserInfo 컴포넌트를 보여줍니다. 

props로는 유저의 이름과 토큰, 그리고 userLoad와 setter 함수를 보냅니다. 

 

userLoad는 유저의 정보가 잘 받아와 졌을 경우 true가 되고 그렇지 않으면 false가 됩니다. 

이를 상위 컴포넌트에서도 알기 위해서 여기서 state를 생성해줬습니다.

 

상위 컴포넌트에서 이 정보가 필요한 이유는 유저 정보가 있을 경우에만 Follow 컴포넌트를 보여주기 위함 입니다. 

유저 정보가 없다고 뜨는데 팔로우 하겠냐는 질문이 뜨는 건 이상하니까요.

유저 정보가 있을 경우 / 없을 경우(올바르지 않은 토큰)

 

먼저 유저의 정보를 받아오는 API fetch 코드를 api.ts에 작성했습니다.

export function getUser({ userName, token }: IInfo) {
  return fetch(`${BASE_PATH}/users/${userName}`, {
    headers: {
      Authorization: `token ${token}`,
    },
  }).then((response) => {
    if (!response.ok) throw new Error(response.statusText);
    return response.json();
  });
}

인증을 위해서 token을 header에 적을 때 주의할 점은 token이라는 문자열을 꼭 앞에 붙여야 한다는 점입니다.

 

이렇게 인증을 위해서 token을 적는 이유는 깃허브의 API 호출이 인증이 되지 않은 사용자는 시간당 최대 60회라는 제한이 걸려있기 때문입니다. 인증된 사용지는 시간당 5000회의 요청이 가능합니다. 

 

Rate limits for GitHub Apps - GitHub Docs

About rate limits for apps Rate limits for GitHub Apps and OAuth Apps depend on the plan for the organization where you install the application. For more information, see "GitHub's products" and "Types of GitHub accounts." Server-to-server requests Default

docs.github.com

이제 UserInfo에서 리액트 쿼리의 useQuery를 이용하여 데이터를 GET 하겠습니다.

const { data, isLoading } = useQuery<IUser>(
    ["user", userName],
    () => getUser({ userName, token }),
    {
      retry: 0,
      staleTime: 10000,
    }
);

참고로 리액트 쿼리는 요청에 실패할 경우 3번(default)까지 백그라운드 단에 요청을 보내고 모두 실패하였을시 화면에 표시하게 됩니다. 저는 재요청을 하지 않게 하기 위해서 retry를 0으로 설정하였습니다. 

 

리액트 쿼리에 대해 더 자세히 알고 싶으시면 다음 링크들을 참고하세요.

데이터가 로딩 중이라면 로딩중이라는 메세지를 보여주고, 로딩이 끝나게 되면 데이터가 잘 받아졌는지 확인해서 유저의 정보가 있는지 없는지를 알아야 합니다.

 

이를 위해서 useEffect를 이용하여 데이터가 없으면 상위 컴포넌트에서 받아온 setUserLoad를 이용하여 userLoad의 값을 false로 바꾸고, 데이터가 잘 받아졌으면 true로 바꿉니다. 

  useEffect(() => {
    !data ? setUserLoad(false) : setUserLoad(true);
  }, [data, setUserLoad]);

 

만일 이름과 토큰을 잘못 적어서 유저의 정보를 불러오지 못한다면 401 에러가 나게 됩니다. 

 

참고로 리액트 쿼리의 onError는 401, 404같은 에러가 아니라 정말 api 호출이 실패한 경우에만 호출이 됩니다. 그래서 강제로 에러를 발생시키기 위해 api 단에서 throw Error를 하였습니다.

 

Query Functions | TanStack Query Docs

A query function can be literally any function that returns a promise. The promise that is returned should either resolve the data or throw an error. All of the following are valid query function configurations:

tanstack.com

 

반환은 다음과 같습니다. 

function UserInfo({ userName, token, userLoad, setUserLoad }: IUserInfo) {
...
return (
    <>
      {isLoading ? (
        <StText>로딩중...</StText>
      ) : (
        <>
          {userLoad ? (
            <StUserInfo>
              <StName>{data?.name}</StName>
              <StAvatar src={data?.avatar_url} alt="avatar" width="100" />
              <StLink href={data?.html_url}>{data?.login}</StLink>
            </StUserInfo>
          ) : (
            <StText>유저 정보가 없습니다</StText>
          )}
        </>
      )}
    </>
  );
 }

3. 팔로우

드디어 팔로우 기능입니다. 아까 UserInfo에서 유저 정보가 있으면 userLoadtrue로 바꿨습니다.

Home의 이 코드로 인해 이제 Follow 컴포넌트가 보여지게 됩니다.

{userLoad && <Follow userName={userName} token={token} />}

Follow 컴포넌트에서는 유저가 버튼을 눌렀을 경우 팔로우를 하는 API를 호출합니다.

 

fetch 코드는 다음과 같습니다. 

export function followUser({ userName, token }: IInfo) {
  return fetch(`${BASE_PATH}/user/following/${userName}`, {
    method: "PUT",
    headers: {
      Authorization: `token ${token}`,
    },
  });
}

HTTP 메소드가 PUT이므로 리액트 쿼리의 useMutation을 사용하겠습니다. 

useMutation의 개념에 대해서는 다음 글을 참고하세요.

function Follow({ userName, token }: IInfo) {
  const [success, setSuccess] = useState(false);

  const { mutate } = useMutation(followUser, {
    onSuccess: () => {
      setSuccess(true);
    },
  });

  const followAllProng = () => {
    DEVCOURSE_FRONTEND_3TH_USERNAME.map((userName) =>
      mutate({ userName, token })
    );
  };

  return (
    <>
      {success ? (
        <FollowingList userName={userName} token={token} />
      ) : (
        <>
          <StText>프롱이들을 팔로우 하실 건가요?</StText>
          <StFollow onClick={() => followAllProng()}>Follow</StFollow>
        </>
      )}
    </>
  );
}

useMutation mutate를 호출해서 mutation을 실행시킬 수 있습니다. 

DEVCOURSE_FRONT_3TH_USERNAME는 3기 분들의 userName이 담긴 상수 배열입니다. 

 

참고로 특정 organization에서 회원들의 데이터를 받는 API도 있습니다. /orgs/{orgs}/members

다만 특정 팀(3기)에 속한 멤버들을 뽑는 방법은 제가 찾지 못한 건지 없는 것 같습니다.

 

아무튼 상수 배열에 담긴 유저 네임과 props로 받아 온 token을 mutate에 담아서 호출합니다.

mutationFn, 즉 followUser의 variables 는 mutate가 전달하는 객체가 됩니다. 

그리고 mutation 이 성공하고 결과를 전달할 때 onSuccess가 실행되는데 여기서 success state의 값을 true로 변경해주었습니다. 

 

successfalse였을 때는 프롱이들을 팔로우할 거냐는 질문이 나왔지만 이제 true가 되면 팔로잉 목록을 보여주는 FollowingList 컴포넌트가 보여집니다.

function FollowingList({ userName, token }: IInfo) {
  const { data } = useQuery<IUser[]>(["following"], () =>
    getFollowings({ userName, token })
  );

  return (
    <>
      <StLink href={`https://github.com/${userName}?tab=following`}>
        팔로잉 목록 보러가기
      </StLink>
      <StFollowing>
        {data?.map((user) => {
          return (
            <a href={user.html_url} key={user.id}>
              <StImg src={user.avatar_url} alt="avatar" width="50" />
            </a>
          );
        })}
      </StFollowing>
    </>
  );
}

팔로잉 목록을 GET하는 fetch 코드는 다음과 같습니다. 

export function getFollowings({ userName, token }: IInfo) {
  return fetch(`${BASE_PATH}/users/${userName}/following`, {
    headers: {
      Authorization: `token ${token}`,
    },
  }).then((response) => response.json());
}

여기까지가 코드 설명이었습니다.

 

이제 남은 것은 배포인데 저는 Vercel로 배포했습니다. 배포를 하니 환경 변수 문제로 에러가 발생했습니다. 

구글링을 하면서 하라는 대로 다했지만 되지 않았습니다..

환경 변수가 꼭 필요한 상황은 아니어서 지우는 게 가능했지만 나중을 대비해서라도 원인을 파악해야할 것 같습니다. 

 

에러의 근본 원인은 해결하지 못했지만 그래도 환경 변수에 대해 알아보며 참고했던 블로그 글 링크를 남겨둡니다.

리액트에서의 환경 변수는 꼭 REACT_APP_으로 시작해야 하고, npm start일 경우에는 env.development, npm rum build일 경우에는 env.production 파일의 내용을 사용한다는 사실을 알았습니다.

 

 

그 외 알게 된 것

1. npm rum build를 사용해본 적이 없었는데 이번에 처음 사용해봤습니다.

  1. 리액트 앱 빌드 npm run build
  2. 리액트 앱 배포 npm serve -s build

커밋하고 푸시하고 Vercel로 확인하고 그랬는데 의미 없는 커밋이 너무 많아져서 검색하다가 npm rum build를 하면 된다는 것을 알았습니다.

리액트(ReactJS) 배포하기 - npm run build, npx serve -s build

 

2. 리액트 쿼리 데브 툴은 production 환경일 때는 사용이 안됩니다.

https://tanstack.com/query/v4/docs/devtools

 

3. 너무 많은 API 요청이 너무 발생하면 429 에러가 발생합니다. 

Error: 429 Too Many Requests — You’ve been rate limited

 

 

조언이나 개선·보완할 점, 잘못된 점이 있다면 알려주시면 감사하겠습니다.