React - IntersectionObserver , github API

chu·2021년 6월 14일
2

이번 시간에는 GitHub에서 제공해주는 API를 어떻게 사용하는지 (사실 다는 모름) 또는 IntersectionObserver를 사용한 인피니트 스크롤링은 어떻게 하는지에 대해 경험적으로다가 정리를 한다.


GitHub API

GitHub은 작성한 코드를 저장하고, 공유하고 뭐 그런 용도로만 사용하는줄 알았지만, 여러가지 API도 제공을 해주는건 얼마전에 알게 된 사실이다... (왜 그랬을까...)

깃헙 API 중 Issue에 해당되는 리스트를 불러올 예정이다.
또한, Issue의 댓글도 불러올 수 있다.

여기서 여러 정보를 확인 가능하다.

그리고 토큰을 발급받아서 더 많은 요청을 할 수도 있고, 그렇지 않을 경우에는 요청 횟수가 제한 되어있다. 귀찮더라도 토큰을 발급받아서 테스트 해보는 것을 추천한다.

토큰 발급 받기

토큰을 발급받는 과정 사용 과정도 알고보면 간단하다. 이런 간단한 발급절차를 빠르게 밟아보자.

  1. 로그인 후 우측 상단 아바타 아이콘을 클릭하여, setting 클릭
  2. setting 페이지에서 좌측 메뉴 하단에 Developer Setting 클릭
  3. 좌측 메뉴에서 Personal Access Tokens 클릭
  4. Generate New Token 클릭하여 토큰을 생성해주자

위 이미지는 토큰 생성 페이지이다. Note에는 대략적으로 토큰이 어떤 목적으로 사용할지 이름을 적어주고, 아래 repo에 클릭한 후 Generate token 버튼을 클릭하여 생성하자.

Issue API Page

Issue는 유명한 라이브러리나 프레임워크 깃헙페이지에 들어가면 많은 Issue에 대한 내용이 있다. 그 Issue들의 여러 데이터를 깃헙에서 제공해주는 API를 통해 불러올 수 있다.

여기서 확인 가능합니다.

Issue List 불러오기

아래 이미지는 API페이지에서 보여주는 URL 중 path 경로이다. 이게 뭘까? 싶지만 코드로 설명하겠다.

// 아래는 기본 형태로 보면 되겠으며, 해당하는 페이지를 입력해주면 된다.
// 중괄호 부분만 선택적으로 넣어주면 된다.
/repos/{owner}/{repo}/issues/{issue_number}

// Code Sample #1
// 설명 - octocat 깃헙의 hello-word 리포지토리 Issue의 42번 Issue 불러오기
https://api.github.com/repos/octocat/hello-world/issues/42

// Code Sample #2
// 설명 - facebook 깃헙의 react 리포지토리 Issue 모두 불러오기
https://api.github.com/repos/facebook/react/issues

위에 대략적으로 설명을 해보았다.{issue_number}를 통해 번호에 해당되는 Issue만 불러올 수 있고, 그렇지 않으면 여러개의 Issue를 불러온다.

여기서 여러개의 Issue를 불러올 경우에는 한번에 최대 30개의 리스트를 불러올 수 있다. 이건 기본값이기 때문에 원하는 리스트 수, 페이지 수 등 설정이 가능하다.

또한, sort(정렬) 기능도 다 구현이 되어 있기 때문에 차근차근 설명 해보겠다.

선택적 기능

  • number : 해당 Issue 넘버만 불러오기
// 21673번 issue 불러오기
https://api.github.com/repos/facebook/react/issues/21673
  • sort : query string으로 정렬하기
// ex) 댓글 많은 수
https://api.github.com/repos/facebook/react/issues?sort=comments
  • per_page : 페이지당 결과 목록 수 / 기본값 = 30, 최대값 = 100
// 10개의 목록 가져오기
https://api.github.com/repos/facebook/react/issues?per_page=10
  • page : 요청한 결과의 페이지 번호
// facebook/react/issues의 2번 page
https://api.github.com/repos/facebook/react/issues?page=2

더 많은 기능이 구현 되어있지만, 이정도로 설명을 하며 더 알고 싶다면 Github API 페이지로 접속하여 필요한 기능을 찾아 사용하시면 됩니다.

토큰 사용 방법

토큰도 발급을 받았고, API에 대한 설명도 진행했으니 실제 토큰을 사용하여 원하는 목록을 불러오자.

아래는 커스텀훅처럼 api를 불러올 수 있도록 구현한 컴포넌트이다.
비동기 통신을 위한 axios를 설치하였고, async awaittry catch문으로 응답과 에러를 처리하였다.

// ../api/api.jsx
// hasMore, loading 등 상태값을 제거한 상태로 올리겠습니다.
import { useEffect, useState } from 'react';
import axios from 'axios';

const useIssueList = ( pageNumber ) => {
  const [issue, setIssue] = useState([]); // 데이터 저장
  const [error, setError] = useState(false);
  
  useEffect(() => {
    const issueListApi = async () => {
      try {
        const response = await axios.get(`https://api.github.com/repos/facebook/react/issues?per_page=30&page=${pageNumber}&sort=comments`, {
          headers: { Authorization: '발급받은 토큰' },
        });
        setIssue((prevIssue) => {
          return [...prevIssue, ...response.data];
        });
      } catch(error) {
        console.log(error);
        setError(true);
      }
    }
    issueListApi()
  }, [pageNumber]);

  return { issue, error }
}   

export default useIssueList;

위 github API는 per_page로 30개씩 목록을 가져오고, page에 props로 받은 pageNumber를 통해 추가적으로 목록을 더 가져오도록 설정하였다. 또한 댓글이 많은 수로 가져오도록 sort를 사용했다.

// ../src/home.jsx
import React, { useState, useRef, useCallback } from 'react';
import { IssueWrapper } from './styles';
import useIssueList from '../api/api';
import IssueItem from '../components/IssueItem';
import AdBanner from '../components/adBanner';

const Home = () => {
  const [pageNumber, setPageNumber] = useState(1);
  const {issue, error} = useIssueList(pageNumber);
  const ob = useRef();
  
  // IntersectionObserver 활용한 무한 스크롤링
  // 자세한 설명은 아래에서 설명하겠습니다.
  const lastElementObserver = useCallback(node => {
    if (ob.current) {
      ob.current.disconnect();
    }
    ob.current = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting && issue.length < 120) {
        setPageNumber(prev => prev + 1);
      }
    });
    if (node) {
      ob.current.observe(node);
    }
  }, [pageNumber, ob]);
    
  return (
    <>
        <IssueWrapper>
            <ul>
            {issue.map((item, index) =>
              (index+1) % 10 === 0 // 10의 배수번째에는 광고배너 추가 삽입
                  ? (
                      <div key={item.id}>
                          <AdBanner
                              lastElementObserver={lastElementObserver}
                              length={issue.length} index={index}
                          />
                          <IssueItem item={item} />
                      </div>
                    )
                  : <IssueItem key={item.id} item={item} />
             )}
            </ul>
        </IssueWrapper>
    </>
    )
}
export default Home;

위 코드에서 설명드리고자 하는 건 return 문은 아니고, 무한 스크롤링에 대한 내용이다.
평소같으면 스크롤값을 구해서 무한 스크롤링을 구현하겠지만, IntersectionObserver을 사용한 무한 스크롤링을 구현해보았다.

IntersectionObserver

Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다.

MDN에서 참고

const ob = useRef();

const lastElementObserver = useCallback(node => {
  if (ob.current) {
    console.log('ob.current :', ob.current);
    ob.current.disconnect();
  }
  ob.current = new IntersectionObserver((entries) => {
    console.log('entries :', entries);
    if (entries[0].isIntersecting && hasMore && issue.length < 120) {
      setPageNumber(prev => prev + 1);
    }
  });
  if (node) {
    console.log('node :', node);
    ob.current.observe(node);
  }
}, [pageNumber, ob]);

해외 영상이지만, 작업하는 코드만 보고 따라해본 뒤 콘솔로 해당 코드마다 어떤 변화와 역할을 하는지 확인하여 이해를 해보았고, 경험을 통해 설명을 하려고 한다.

영상주소

lastElementObserver

변수명만 봐도 마지막 요소를 감시한다는 대략적인 의미이다. 실제 마지막 요소에 이 변수를 props를 통해 전달하여, 해당 요소 ref에 넣어줄 예정이다.

그 전에 lastElementObserver의 내부를 하나씩 뜯어서 확인해보자.

// useRef hook을 사용해서 변수를 만들어주자.
const ob = useRef();

// 마운트가 됐을 때 undefinde 이기 때문에 작동하지 않는다.
// 만약 ob안에 요소가 들어가 있을 경우에는 disconnect로 감시대상 연결을 중지한다.
if (ob.current) {
  console.log('ob.current :', ob.current);
  ob.current.disconnect();
}

// 아래는 ob에 요소에 IntersectionObserver 생성하기
// entries[0]에는 감시 타겟의 정보를 가지고 있다.
// isIntersecting은 타겟이 브라우저 뷰포트에 들어오게되면 false -> true
// true면 setPageNumber(prev => prev + 1)로 pageNumber를 1씩 증가시켜 목록을 더 불러오도록 설정한다.
ob.current = new IntersectionObserver((entries) => {
  console.log('entries :', entries);
  if (entries[0].isIntersecting) {
    setPageNumber(prev => prev + 1);
  }
});

// node는 lastElementObserver 함수의 매개변수로 들어온다.
// 즉, node가 발견되면 ob.current 요소를 감시대상으로 정하는 역할이다.
if (node) {
  console.log('node :', node);
  ob.current.observe(node);
}

그럼 타겟이 어떻게 정해지는지 확인해보자.

IntersectionObserver target

// ../src/home.jsx의 return 영역
return (
    <>
        <IssueWrapper>
            <ul>
            {issue.map((item, index) =>
              (index+1) % 10 === 0
                  ? (
                      <div key={item.id}>
                          <AdBanner // 바로 이 컴포넌트로 props 전달
                              lastElementObserver={lastElementObserver}
                              length={issue.length} index={index}
                          />
                          <IssueItem item={item} />
                      </div>
                    )
                  : <IssueItem key={item.id} item={item} />
             )}
            </ul>
        </IssueWrapper>
    </>
)

// ../components/adBanner.jsx
import React from 'react';

const AdBanner = ({ lastElementObserver, length, index }) => {
  // length - 현재 불러온 리스트 목록의 길이
  // index - 현재 불러온 리스트 index 번호이며 length와 비교이므로, +1을 해준다.
  // length와 (index+1)이 같다면, 제일 하단에 있는 요소를 뜻한다.
  // 그럼 그 요소에게 ref로 lastElementObserver 넣어준다. 즉, 감시대상 설정
  if (length === index + 1) {
    return (
      <div ref={lastElementObserver}>
          <a href="https://thingsflow.com/ko/homea">
              <img src="url..." />
          </a>
      </div>
    )
  }
  return(
    <div>
        <a href="https://thingsflow.com/ko/homea">
            <img src="url..." />
        </a>
    </div>
  )
}

export default AdBanner;

흐음... 위에 링크로 걸어둔 영상을 한번쯤 보는것을 추천한다. API를 불러오면서 어떻게 무한스크롤링을 구현하는지 정말 잘 설명해준다. 영어지만 코드만 보고 이해할 수 있도록 보는 것을 추천한다.

이해가 어렵다면, 저 처럼 하나하나 콘솔로 찍어서 어떤 요소가 들어오고, 어떻게 반응하는지 추적을 해보는 것도 추천드립니다!

제가 설명하려니 뭔가 복잡하고, 정확한 설명을 하는지 감이 안옵니다. 틀린 부분이 있다면 말씀주시면 정말 감사드립니다!

profile
한 걸음 한걸음 / 현재는 알고리즘 공부 중!

0개의 댓글