코로 넘어져도 헤딩만 하면 그만

Cat API으로 무한 스크롤 복습(feat. Intersective Observer) 본문

스터디/야 나두(새로운 기술 학습) - 2023.08~

Cat API으로 무한 스크롤 복습(feat. Intersective Observer)

꼬드리 2024. 2. 19. 21:54

일전에 포켓몬 Api를 활용한 사이트에서 next Page 버튼으로 데이터를 넘기는 방식에서 더 나아가 무한 스크롤로 데이터를 추가하는 리펙토링을 하려 했는데, 중간에 어디선가 로직이 꼬이며 데이터 요청이 마음처럼 안 되었다(...) 

 

그래서 이참에 react Query를 통한 데이터 사용과 Intersective Observer을 다시 복습하기로 했다. 무료로 제공되는 Cat Api를 활용하여 고양이_무한제공_사이트를 간단하게 만들어보았다. 기존에 쓰던 poketmon api 보다 불러오는 방식과 불려온 데이터 구조가 간단하여 연습에 도움이 될 것으로 보인다.

 

 

🍬이번에 사용한...

- React

- TypeScript

- react Query

- styled component

- intersective Observer


Cat api: https://thecatapi.com/

1) 사이트에 간단 가입한다. 입력한 메일주소로 온 개인 key를 준비할 것.

2) 상단 Documentation을 가보면 어떻게 사용하는지 나와있다. 

3) https://api.thecatapi.com/v1/images/search?limit=10&breed_ids=beng&api_key=REPLACE_ME 의 예시와 같이 REPLACE_ME 부분을 메일을 통해 받은 key로 대체해준다. breed_ids는 필수가 아닌 쿼리 파라미터다. 이 주소로 get 요청을 보내면 귀여운 고양이들🐱이 가득한 데이터를 불러올 수 있다.

 

일단 가장 중요한 것은 요청을 보낼 때 쿼리 파라미터를 limit=10로 지정하면 10개씩 추가로 불러올 수 있다는 부분같다. 

 

🚩레이아웃 세팅 및 useQuery로 데이터 받아오기

나는 해당 페이지에서 기본적으로 그리드를 사용하여 가로 5개의 사진을 보여주고 싶기 때문에 데이터를 한번에 15개씩 불러올 수 있게 세팅해주기로 했다. 사실 display:flex;를 자주 써서 레이아웃을 잡는 편이지만, 이처럼 페이지에서 카드 형태를 반복적으로 배치할 경우에는 경험상 grid를 쓰는 쪽이 보기 좋았다.

flex를 쓰게 되면 justify-contents: space-between; 을 써서 좌우에 딱 알맞게 붙일 수 있다. 하지만 만약 애매하게 수가 남는다면(ex 한줄에 5개씩 배치했으나 데이터가 정확히 나누어지지 않아 마지막 줄에 2개가 있는 경우) 좌측으로 예쁘게 정렬되지 않고 엉뚱하게 좌우에 하나씩 붙는 경우가 발생했다. 이런 현상을 막기 위해 나는 grid를 사용한다.

자주 사용하는 grid 코드를 적어둔다.

  display: grid;
  grid-template-columns: repeat(5,1fr); 
  gap:40px;

 

문서에 적혀있던 대로 아래 주소에 요청을 보내니 데이터가 넘어오는 것을 확인할 수 있었다. 키값은 개인마다 다르고 노출되면 안 되기 때문에 이렇게만 적어둔다.

const API_URL =`https://api.thecatapi.com/v1/images/search?limit=${limit}&api_key=(고유 키값)`

 

interface catInterface {
  id: string;
  url: string;
}

function App(): JSX.Element {  
  const {isLoading, error, data, refetch} = useQuery('cats', async()=>{
    const API_URL =`https://api.thecatapi.com/v1/images/search?limit=15&api_key=live_41tiYuEDSioJw9mVOsS9Hk9NdOySLhzQ6kqN4P11bR7DsQ9HIZXXmnzrxqKuTZHq`;
    const response = await fetch(API_URL);
    const jsonData = await response.json();
    const catData = jsonData.map((catImage: {id: string, url: string}) => ({
      id: catImage.id,
      url: catImage.url
    }))
    return jsonData;
  });

  if(error) return <div>error</div>
  return (
    <>
      <Header>
        <h1>🐱고양이 무한제공 사건🐱</h1>
      </Header>
      <MainContainer>
      {isLoading ? (
        <CardsContainer><img src="./src/assets/image/spinner.gif" alt='loading'></img></CardsContainer>
      ) : (
        <CardsContainer>
          {data.map((item: catInterface) => (
            <Cards key={item.id} data={item} />
          ))}
        </CardsContainer>
        
      )}
      </MainContainer>
      <button onClick={handleNextLimit}>증가시키기</button>
      <Target id="target"></Target>
      </>
  );
}
export default App;

 

하단에는 무한스크롤 용도의 Target으로 삼을 div를 하나 마련해두었다. 

 

호출한 데이터가 잘 들어오는 것을 확인했다. 각각의 Cards에 배열 형태로 들어오는 데이터 개수만큼 map을 돌려서, 내가 보여줄 데이터에 관한 Props를 적절하게 내려준 뒤 전체적인 레이아웃까지 확인했다. 

 

 

🚩새로 불러온 데이터를 useState에 추가하기

본격적으로 매번 새로 불러와지는 데이터를 추가해보자. 어쨌거나 무한 스크롤을 만들기 위해서는 호출 때마다 추가되는 데이터를 intersective observer를 사용하여 보여줘야 하기 때문에 이 부분부터 작업해야 한다. 

const {isLoading, error, refetch} = useQuery('cats', async()=>{
    const API_URL =`https://api.thecatapi.com/v1/images/search?limit=${limit}&api_key=키값`;
    const response = await fetch(API_URL);
    const jsonData = await response.json();
    const catData = jsonData.map((catImage: {id: string, url: string}) => ({
      id: catImage.id,
      url: catImage.url
    }))
    setCatArr(((prevCatArr)=>[...prevCatArr, ...catData]))
    return jsonData;
  });

 

우선 전체 데이터를 담아놓을

const [catArr, setCatArr] = useState<catInterface[]>([])

를 하나 만들어준다. 그리고 새롭게 호출할 때마다 들어온 데이터를 setCatArr을 통해 이어 붙이는 코드를 작성한다. 

 

이후 작동 때마다 limit을 늘려주고 refetch를 통해 새롭게 데이터를 불러올 함수를 하나 만들었다. 

const handleNextLimit = () => {
  setLimit((prevLimit) => prevLimit + 15);
  const newLimit = limit + 15;
  refetch({ queryKey: ['cats', newLimit] });
};

여기 이 limit 때문에 사소하게 문제가 하나 발생하는데...... (지금 보면 사소하지만 예상과 다른 작동에 헤맸다.)

 

 

✨limit을 이용한 새 데이터 불러오기?

잠시 내가 했던 이전 프로젝트 얘기를 안 할 수가 없다....... 

전에 무한 스크롤 구현했던 메인 프로젝트에서는 요청 주소의 쿼리 파라미터인 page가 1씩 증가하면 15개씩 다음 데이터를 받아오는 방식이었다. 한 페이지마다 15개씩 데이터가 저장되어 있었고 프론트 쪽인 나는 page에 +1 씩 해주면서 추가로 데이터를 가져오는 구조였다. 또 포켓몬 사이트를 만들며 다루었던 poketmon api는 쿼리 파라미터인 limit을 통해 한번에 불려올 개수를 조절하고, 이후 쿼리 파라미터 offset으로 배열의 [0]번째 데이터로 몇번째 포켓몬을 둘 지 지정해 원하는 만큼 데이터를 불러오는 방식이었다. 

 

저런 방식의 API들을 사용했기 때문에, 이번에도 useState에 limit부터 넣고 시작했다. limit의 초기값은 15이고, 이후 호출을 할 때마다 15씩 오르면 되겠거니... 판단했던 것이다. 

catArr은 추가되는 데이터들도 담아줄 전체 데이터의 배열이다. 

  const [catArr, setCatArr] = useState<catInterface[]>([])
  const [limit, setLimit] = useState<number>(15);
  
    const {isLoading, error, refetch} = useQuery(['cats', limit], async()=>{
    const API_URL =`https://api.thecatapi.com/v1/images/search?limit=${limit}&api_key=고유의 키값`
    const response = await fetch(API_URL);
    const jsonData = await response.json();

    const catData = jsonData.map((catImage: {id: string, url: string}) => ({
      id: catImage.id,
      url: catImage.url
    }))
    setCatArr(((prevCatArr)=>[...prevCatArr, ...catData]))
    return jsonData;
  });

 

이제 버튼을 하나 만들고, 이 버튼을 눌렀을때 handleNextLimit이 실행되도록 onClick={handleNextLimit}로 설정해주었다. 이후 무한스크롤로 개선할 것이지만 일단 데이터를 잘 받아오는지 확인하기 위해서다. 아래처럼 handleNextLimit 함수가 작동할 때 limit에 +15가 더해지고, 새롭게 15를 추가한 newLimit으로 cats키를 가진, 고양이 데이터를 불러오는 react query에 refetch요청을 보내 보았다. 

const handleNextLimit = () => {
  setLimit((prevLimit) => prevLimit + 15);
  const newLimit = limit + 15;
  refetch({ queryKey: ['cats', newLimit] });
};

 

그런데, 기이한 현상이 발생했다. 첫 호출에는 정상적으로 15개의 데이터가 catArr에 담긴다. 그 다음 30개의 데이터가(여기까지는 좋았다.), 다음 호출에는 60개, 다음에는 105개... 이런 식으로 원치않게 catArr에 담긴 데이터 개수가 급증했다. 15개씩만 추가하려고 했는데.......이게 어떻게 된 일이지? 

각각 콘솔을 찍어보니 limit는 한번 클릭시 15씩 늘어나는데 catArr만 훌쩍 증가하니, 당황스러운 노릇이었다.

 

지금 생각하면 당연한 원리다. 내가 이전에 다루던 API들과 달리, 이 Cat API는 데이터에 순서가 정해져 있는 것이 아니라 매번 limit로 설정한 '개수만큼' 랜덤사진을 보내주는 방식으로 작동했다. 즉 내가 처음 생각한대로 useState로 limit을 늘려가며 요청을 보낼 필요가 없었다. 매번 limit=15만 보내도, 15개만큼 새 랜덤 사진을 제공해주는 API였던 것. (API마다 다르다는 걸 새삼 느낀다 잘 파악하자

위에서 작성한 방식이면 limit가 +15씩 늘어나니, api 요청시 데이터를 limit 개수만큼, 즉 늘어난 만큼 더 달라고 요청한 것이나 다름없었다. 

이걸 깨닫고 const [limit, setLimit] = useState<number>(15);를 빼버린 뒤 const limit = 15로 고정시켰다. 원하는대로 버튼을 한번 누르면 데이터가 기존 catArr에 15개씩 잘 추가되는 결과를 볼 수 있다. 

catArr에 콘솔을 찍어둠


🚩교차 관찰자Intersective Observer로 무한스크롤

추가 데이터를 불러오는 것까지 잘 설정해뒀으니, 이제 무한스크롤은 그렇게 어렵지 않을 것이다. 

 

" Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법 / 출처 MDN "

 

Web API중 하나인 Intersective Observer는 그 이름대로 현재 관찰중인 요소가 브라우저의 화면 영역, 즉 뷰 포트와 노출되는지 여부를 감지하는 역할을 한다. 간략하게 다음과 같은 과정을 통해 작동한다. 

 

1) 관찰할 요소를 정한다. 무한스크롤의 경우, 데이터가 불려온 하단에 div를 만들어 타겟으로 두는 형식을 많이 쓴다.

2) new IntersectiveObserver(callback, options)로 관찰자 객체를 하나 생성해준다. 저기서 callback은 관찰대상이 뷰포트에 들어온 순간 어떤 함수를 실행할 것인지 넣어주면 된다. 옵션은 root, rootMargin, threshold 세 가지가 있으며 필수는 아니다. threshold가 중요한데, 기본값 0(0%)에서 1(100%)사이의 소수점으로 둘 수 있으며 대상이 화면에 얼마나 들어왔을 시 호출할 지 결정해준다. 지금은 기본값인 0으로 두겠다.

3) observer.observe(관찰할 엘리먼트); 를 통해 observer 객체의 observe함수에 등록한다.

 

위에서 이미 div에 id=target을 주어 타겟을 하나 만들어두었다. 이제 이 div를 활용해볼 생각이다.

const observer = new IntersectionObserver(handleObserver, { threshold: 0 });
const observerTarget = document.getElementById("target");

우선 observer과 target을 하나 만든다. threshold는 0으로 줬으며 콜백으로 받을 handleObserver을 만들어주었다.

 

관찰시 실행될 함수 내부에는 미리 만들어두었던 다음 Limit을 요청하는 함수 handleNextLimit을 넣었다. 데이터 증가시키기 버튼은 필요가 없어졌으니 기존 코드에서 삭제했다.

  const handleObserver = (entries: IntersectionObserverEntry[]) => {
    if(entries[0].isIntersecting){
    handleNextLimit();}
    console.log("불러오는 중")
  }

여기서 entriesobserver가 감시하고 있는 대상들을 받아온다. 원한다면 등록한 여러 요소를 동시에 감시할 수도 있지만 지금 필요한 건 observerTarget 하나 뿐이므로, entires[0]가 보일 경우 handleNextLimit를 실행하도록 조건을 주었다. 이렇게 조건을 주지 않고 handleNextLimit만 넣을 경우 감시대상이 화면에 나타나지 않았는데 계속 handleNextLimit을 반복 실행하는 황당한 경우가 생기게 된다. IntersectionObserver은 초기화 될 때부터 대상이 보이지 않아도 handleObserver은 실행할 수 있기 때문이다(...직접 해봤다...)

  if (observerTarget) {
    observer.observe(observerTarget);
  } else {
    console.error('Element not found.');
  }

해당 observerTarget은 observer객체의 observe함수에 등록해서 앞으로 이 대상을 관찰할 것이라고 한다. 요소가 존재하지 않아 null일 경우가 있으니(타입스크립트가 빨간 줄을 띄웠다..) null이 아닐 경우에만 작동하도록 조건을 준다.

 

이제 페이지의 하단에 닿을 경우 Target을 감지하여 15개의 cat data를 이어붙인다. 가벼운 무한스크롤 완성!!

하단의 div는 잘 보이도록 잠시 backgroundColor:"red"를 주었다.

 

 

 

Comments