이번 시간에는 GitHub에서 제공해주는 API를 어떻게 사용하는지 (사실 다는 모름) 또는 IntersectionObserver를 사용한 인피니트 스크롤링은 어떻게 하는지에 대해 경험적으로다가 정리를 한다.
GitHub은 작성한 코드를 저장하고, 공유하고 뭐 그런 용도로만 사용하는줄 알았지만, 여러가지 API도 제공을 해주는건 얼마전에 알게 된 사실이다... (왜 그랬을까...)
깃헙 API 중 Issue에 해당되는 리스트를 불러올 예정이다.
또한, Issue의 댓글도 불러올 수 있다.
여기서 여러 정보를 확인 가능하다.
그리고 토큰을 발급받아서 더 많은 요청을 할 수도 있고, 그렇지 않을 경우에는 요청 횟수가 제한 되어있다. 귀찮더라도 토큰을 발급받아서 테스트 해보는 것을 추천한다.
토큰을 발급받는 과정 사용 과정도 알고보면 간단하다. 이런 간단한 발급절차를 빠르게 밟아보자.
위 이미지는 토큰 생성 페이지이다. Note에는 대략적으로 토큰이 어떤 목적으로 사용할지 이름을 적어주고, 아래 repo에 클릭한 후 Generate token 버튼을 클릭하여 생성하자.
Issue는 유명한 라이브러리나 프레임워크 깃헙페이지에 들어가면 많은 Issue에 대한 내용이 있다. 그 Issue들의 여러 데이터를 깃헙에서 제공해주는 API를 통해 불러올 수 있다.
여기서 확인 가능합니다.
아래 이미지는 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 await과 try 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을 사용한 무한 스크롤링을 구현해보았다.
Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다.
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);
}
그럼 타겟이 어떻게 정해지는지 확인해보자.
// ../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를 불러오면서 어떻게 무한스크롤링을 구현하는지 정말 잘 설명해준다. 영어지만 코드만 보고 이해할 수 있도록 보는 것을 추천한다.
이해가 어렵다면, 저 처럼 하나하나 콘솔로 찍어서 어떤 요소가 들어오고, 어떻게 반응하는지 추적을 해보는 것도 추천드립니다!
제가 설명하려니 뭔가 복잡하고, 정확한 설명을 하는지 감이 안옵니다. 틀린 부분이 있다면 말씀주시면 정말 감사드립니다!