글 작성자: bbangson
반응형

개요

기업 과제로 간단한 무한 스크롤링을 React로 구현하였습니다. Intersection Observer API를 사용하였습니다.
이 포스팅은 프로젝트 소개와 회고, 간단한 Intersection Observer API 설명으로 구성되어있습니다.


 


프로젝트 소개

간단하게 진행된 API 호출을 통한 댓글 '무한 스크롤' 구현 토이 프로젝트입니다.
기업 과제에서 요구한 내용은 아래 '과제 요구 사항 보기' 버튼을 클릭하여 확인해주시기 바랍니다.

더보기

Requirement

  • Implement the user's comment data list with infinite scrolling by getting more 10 comments repeatedly.

Data API

  • The user's dummy comment data can be called through the API below.
  • The following parameters are supported:
    • _page
      • it starts at 1.
    • _limit
      • Please set the _limit parameter to 10.
  • Example of the first comment page
  • Sample data
  • [
    	{ 
    	"postId": 1, 
            "id": 1, 
            "name": 
            "id labore ex et quam laborum", 
            "email": "Eliseo@gardner.biz", 
            "body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\\ntempora quo necessitatibus\\ndolor quam autem quasi\\nreiciendis et nam sapiente accusantium" 
         }, 
     ]
  • You can use id as comment id, email as user's email and body as comment text


화질이 조금 깨지지만 프로젝트 결과 화면입니다.

프로젝트 결과



 


프로젝트 설명 및 회고

폴더 구조, 구현 방법, 이슈 순서로 설명하겠습니다.

 

 

□ 폴더 구조

 

폴더 / 파일 설명
api / getComment.js 댓글 리스트를 읽어오는 API 호출(GET) 컴포넌트입니다.
constants / index.js 상수를 저장하고 export하는 목적의 파일입니다.
const API_ENDPOINT const MAX = 10; 를 저장했습니다.
style / GlobalStyle.js 글로벌 styled-reset 컴포넌트입니다.
App.js data를 갖고오기 위한 API호출과 각 State를 저장하는 최상위 컴포넌트입니다.
Comment.js 댓글을 그려주는 컴포넌트입니다.
FetchMore.js IntersectionObserver API를 사용하는 컴포넌트입니다.

 

 

□ 구현 방법

getComment.js

import { API_ENDPOINT, MAX } from '../constants/index';

export const getComments = async (page) => {
  const response = await fetch(`${API_ENDPOINT}?_page=${page}&_limit=${MAX}`, {
    method: 'GET',
  }).then((res) => res.json());
  return response;
};

API 호출 컴포넌트입니다. MAX는 10을 저장하는 상수입니다. 한번의 호출로 10개의 데이터를 불러옵니다.

App.js

import React, { useState, useEffect } from 'react';
import Comment from './Comment';
import styled from 'styled-components';
import { getComments } from './api/getComment';
import FetchMore from './FetchMore';

export default function App() {
  const [data, setIsdata] = useState([]);
  const [loading, setIsLoading] = useState(false);
  const [page, setPage] = useState(1);

  const getItems = async () => {
    setIsLoading(true);
    const fetchData = await getComments(page);
    setIsdata((prev) => prev.concat(fetchData));
    setIsLoading(false);
  };

  useEffect(() => {
    getItems();
  }, [page]);

  return (
    <Container>
      <Wrap>
        {data.map(({ id, email, name }) => {
          return <Comment id={id} email={email} name={name} key={id} />;
        })}
      </Wrap>
      {loading ? 'Loading...' : <FetchMore setPage={setPage} />}
    </Container>
  );
}

 가장 기본이 되는 App.js 컴포넌트입니다. API 호출을 통한 데이터를 저장할 state와 loadingpage state를 관리합니다.

 

 보통 무한 스크롤을 구현할 때 가장 머릿속으로 많이 떠올리는 로직은 "현재 보고있는 브라우저 화면의 스크롤을 최하단으로 드래그할 시, API를 다시 호출한다." 일 것입니다.

 저는 위 로직을 구현하기 위해 호출한 데이터를 감싸는 <Wrap> 스타일 태그를 지정하였고 그 밑에 <FetchMore> 컴포넌트를 두었습니다. 로딩 상태면 로딩을 나타내는 UI, 아니면 <FetchMore> 컴포넌트를 통해 page를 업데이트했습니다. 페이지가 업데이트 되면 useEffect를 호출하여 다시 API를 재호출하는 로직입니다. 


FetchMore.js

import React, { useRef, useEffect } from 'react';

const FetchMore = ({ setPage }) => {
  const target = useRef(null);

  const callback = (entries) => {
    const target = entries[0];

    if (target.isIntersecting) {
      setPage((prev) => prev + 1);
      console.log('화면에서 노출됨');
    } 
  };

  const observer = new IntersectionObserver(callback, { threshold: 0.5 });

  useEffect(() => {
    observer.observe(target.current);
    return () => {
      if (target.current) {
        observer.unobserve(target.current);
      }
    };
  }, []);

  return <div className={'loading'} ref={target} />;
};

export default FetchMore;

 무한 스크롤링의 핵심이 되는 컴포넌트라고 생각합니다.

<div> 태그에 ref 요소를 지정하였습니다. App.js 컴포넌트의 <Wrap> 태그는 10개씩 불러온 데이터를 묶어주는 스타일 요소입니다. 스크롤을 끝까지 내리면 사용자는 존재하지만 보이지 않는(사이즈가 없기 때문) <FetchMore> 컴포넌트를 발견하게 되고, <FetchMore> 컴포넌트의 Callback 함수를 동작하여 page가 변경됩니다. 

 

 

□ 이슈

 

 프로젝트 진행중에 몇가지 마주쳤던 이슈를 소개하겠습니다. 

"Encountered two children with the same key~" 

 

 현재는 API 호출을 통해  받아온 data를  App.js 컴포넌트에서 map 함수로 탐색하는 로직입니다. 하지만 기존에는 App.js 컴포넌트가 아닌 Comment.js 컴포넌트에서 map 함수를 구현하였고, App.js 컴포넌트는 Comment.js를 렌더링할 뿐이었습니다. Comment 컴포넌트에서 props로 받아온 data의 id로 key 값을 설정했었습니다.

 그래서 위와 같은 오류가 발생했습니다.  id는 index와 같은 역할을 하여 각 id는 다른 값을 가지고 있을거라 생각하고 기존 로직을 이렇게 구상했지만, 동일한 페이지를 App 컴포넌트에서 여러번 렌더링하는 이슈로 인해 오류가 발생한 것이라 생각합니다. 

 

 "TypeError: Failed to execute 'unobserve' on 'IntersectionObserver': parameter 1 is not of type 'Element.'"

이슈2

 위의 오류는 FetchMore.js 컴포넌트에서 발생했습니다. 아래 코드는 FetMore 컴포넌트의 일부분을 가져왔습니다.

  useEffect(() => {
    observer.observe(target.current);
    return () => {
    // if문으로 감싸주어서 이슈 해결.
      if (target.current) {
        observer.unobserve(target.current);
      }
    };
  }, []);

 결론부터 말씀드리자면 useRef Hook 함수로 선언된 target 상수가 null 값으로 초기화해주었기 때문에 발생했던 이슈였습니다. 기존에는 if문을 사용하지 않은 코드였습니다. 다시 말해서, if문으로 observer.unobserve(target.current); 코드를 감싸줌으로써 이슈를 해결하였습니다.  

 

 

 


Intersection Observer API

 이 포스팅에서는 Intersection observer api에 대해 간단하게 설명하겠습니다. 이 개념은 추후에 따로 포스팅으로 더 자세하게 다루는 것이 좋을 것 같습니다. 

 

 Intersectino observer api는 타겟 요소와 상위 요소 또는 root 요소와의 변경 사항(노출, 제외)을 비동기적으로 관찰하는 방법을 제공합니다. 

 

사용법.

  const observer = new IntersectionObserver(callback, { threshold: 0.5 });

 

 위 코드에서 callback은 뷰포트(상위 요소)와 타겟이 겹쳤을 때 실행되는 함수이고, 객체로 넣어준 threshold는 options에 속한 객체중 하나입니다. options에서는 뷰포트로 사용될 root와 겹치는 범위를 설정해줄 수 있습니다.  

 

 즉, Intersection observer 객체를 생성하면서 observe함수로 관찰할 타겟을 파라미터로 넣어줍니다. 비동기로 타겟을 관찰합니다. options에서 정의한 범위만큼 대상이 변경된다면(노출 혹은 제외) callback 함수를 호출하고 entries 배열에 대상을 추가합니다. 결국 isIntersecting 함수를 통해 target이 노출된 것인지 제외된 것인지 판별하게 되고 true를 반환한다면 대상이 노출된 것입니다. 

 

 

 

 

 

 

 


 생각보다 까다로운 프로젝트였습니다. 무한 스크롤을 구현하는 것도 쉽지 않은데, Intersection observer api도 이해하기 쉽지 않았습니다. 여러번 반복 학습이 필요한 개념인 것 같습니다.



링크

https://github.com/kwak-bs/wanted_hayanmind

 

GitHub - kwak-bs/wanted_hayanmind

Contribute to kwak-bs/wanted_hayanmind development by creating an account on GitHub.

github.com


https://www.youtube.com/watch?v=hVcriryAVbg

 

반응형