[velog 클론 코딩 개발기 - 3 ] Intersection Observer로 무한 스크롤 기능 구현

Yeojin Choi·2021년 12월 14일
1

교차 상태를 감지할 타겟 요소 달아주기

  • IntersectionObserver 로 교차 상태를 감지하여 onIntersect 함수를 실행하기 위한 타겟 요소를 제일 하단에 위치시켜준다.
    -const targetRef = useRef<HTMLDivElement>(null); : useRef 를 사용하여 DOM에 접근하여 직접 조작할 수 있도록 한다.

insersection observer

onIntersect 함수 작성하기

게시글 불러오기

로딩 상태일때 Skeleton 보여주기

  • 로딩되는 동안 가짜 게시물 역할을 해줄 Skeleton Component 를 styled component 로 구현하였다.
  • shining 효과 줘서 반짝반짝 하게 해주기 ...

Server 에 Parameter 전송하기

axios({ baseURL: API_HOST, url: `/recentPosts/${loadPostCount.current}` })

client 에서 요청 횟수 (lodePostCount) 라는 변수를 url 경로에 담아

Node.js 기반 Server에서 데이터 15개씩 가져오기

1. parameter 로 클라이언트의 게시물 요청 횟수 받아오기

  • req.params API를 이용해 loadPostCount 라는 파라미터를 받아오도록 구현했다.
    req.params 는 라우팅 시 /:key 형식으로 지정이 가능하다.
  • 위 방식은 Get 방식의 Request 에서 Route Parameter를 사용하는 방법이다.
  • 다른 방법으로는 쿼리스트링(QueryString) 을 통해 Parameter를 전달할 수 있는데, 만약 이 방법을 사용한다면 다음과 같이 수정한다.
    - url : /recentPosts?loadPostCount=${loadPostCount.current}
    - parameter 접근 : req.param('loadPostCount')

2. SQL 문 작성

  • LIMIT 키워드를 사용해 게시글 데이터의 개수를 제한한다.

3. 응답

  • DB 에서 SQL 질의의 결과를 받아오는 과정은 비동기로 진행되기 때문에, 내가 원하는 형태로 가공해서 응답해주기 위해서 async/await 를 사용하였다.
  • async/await 를 사용하지 않으면 쿼리를 날리면서 데이터를 다 안받아왔지만 다음 라인으로 넘어가버리기 때문에, 위 코드의 line 8 에서 posts 변수가 undefined이게 된다.

Full Code

const RecentPostsPage = () => {
  const [posts, setPosts] = useState<PartialPost[]>();
  const [isLoading, setIsLoading] = useState<boolean>(true);
  const [isDone, setIsDone] = useState<boolean>(false);
  const targetRef = useRef<HTMLDivElement>(null);
  const loadPostCount = useRef<number>(0);

  const loadPosts = () => {
    setIsLoading(true);
    axios({ baseURL: API_HOST, url: `/recentPosts/${loadPostCount.current}` })
      .then(response => {
        if (Array.isArray(response.data) && !response.data.length) {
          setIsLoading(false);
          setIsDone(true);
          return;
        }

        if (response.data.length) {
          setPosts(prev => (prev ? [...prev, ...response.data] : response.data));
          loadPostCount.current += 1;
        }

        setIsLoading(false);
      })
      .catch(error => {
        console.error(error);
      });
  };

  useEffect(() => {
    loadPosts();
  }, []);

  const onIntersect = useCallback(
    ([entry]: IntersectionObserverEntry[]) => {
      if (!entry.isIntersecting || isDone) {
        return;
      }
      loadPosts();
    },
    [isDone]
  );

  useEffect(() => {
    if (!targetRef.current) {
      return;
    }
    const observer = new IntersectionObserver(onIntersect, {
      threshold: 0.4,
    });
    observer.observe(targetRef.current);

    return () => observer && observer.disconnect();
  }, [isLoading, onIntersect]);

  return (
    <>
      <div> 최신 글 페이지 입니다.</div>
      <PostCardGrid posts={posts} />
      {isLoading && !isDone && <PostCardGridSkeleton />}
      {!isLoading && (
        <div
          ref={targetRef}
          style={{
            width: '100%',
            height: '5rem',
          }}
        />
      )}
    </>
  );
};

export default RecentPostsPage;
profile
프론트가 좋아요

0개의 댓글