본문 바로가기
Front-End Study/이건 우째해야 할까..

무한 스크롤.. 이건 우째해야 할까..

by 코딩기 2024. 6. 11.
728x90

초기 로직

✨게시글 댓글 페이지 구현을 하면서 limit 파라미터로 지정한 개수보다 댓글 수가 많아질 때 사용할 무한 스크롤에 대해 생각해보게 되었다.

🔖작성한 로직에 대한 고민

  • 난 상세 페이지와 그 안의 댓글 컴포넌트를 따로 분리했기 때문에, 상세 페이지에서 가져오는 API 호출 로직을 통해 백엔드 서버에 구현되어 있는 nextCursor 값 또한 가져와 사용하고 싶었다.
  • 하지만 분리되어 있기도 하고 이걸 어떤식으로 구현해야 할지 감이 잘 오지 않았다. 🫨 해결한 과정들을 하나씩 작성해보고자 한다. 👍
// 상세 페이지 로직
const ArticleWithComment = () => {
    ...
    const [comments, setComments] = useState<CommentResponse[]>([]);
    const [article, setArticle] = useState<ArticleResponse | null>(null);

    const getComments = async (id: string) => {
      const { list } = await getArticleComments(id, 10);
      setComments(list);
    };

    const getSingleArticle = async (id: string) => {
      const data: ArticleResponse = await getArticle(id);
      setArticle(data);
    };

    const setNewComment = (comment: CommentResponse) => {
      setComments((prevComments) => [comment, ...prevComments]);
    };

    useEffect(() => {
      if (id) {
        getComments(id);
        getSingleArticle(id);
      }
    }, [id]);

    // 이런 식으로 값을 props로 내려주는 형태
    return (
      ...
      <ArticleComment comments={comments} setNewComment={setNewComment} />
      ...
    );

// 댓글 컴포넌트 일부
function ArticleComment({ comments, setNewComment }: Props) {
    ...
    return (
        ...
        {comments.length !== 0 ? (
        comments.map((comment) => {
          return <CommentList key={comment.id} {...comment} />;
        })
      ) : (
        <Image
          className={styles["no-comment-img"]}
          src="/images/Articles/no-comment.png"
          alt="댓글이 없을 때 출력되는 이미지"
          width={151}
          height={195}
        />
      )}    
    );
  }

✨useIntersectionObserver 훅 구현

  • 우선 남아있는 댓글을 더 불러오기 위한 트리거를 만들어주기 위해, boolean 값을 리턴해주는 intersectionObserver 훅을 만들었다.
  • intersectionObserver는 웹 API로, 요소가 뷰포트나 지정된 부모 요소와 교차하는지 여부를 비동기적으로 관찰할 수 있게 해준다.
  • 난 이를 useEffect와 함께 사용하여 인자로 지정한 요소의 ref값을 받아, 지정한 요소를 관찰할때마다 true값이 된 isInteresting을 리턴하도록 구현했다.
// intersectionObserver
import { RefObject, useEffect, useState } from "react";

export default function useIntersectionObserver(ref: RefObject<HTMLDivElement>) {
  const [isIntersecting, setIsIntersecting] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) setIsIntersecting(true);
          else setIsIntersecting(false);
        });
      },
      { threshold: 0.3 }
    );

    if (ref.current) observer.observe(ref.current);

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

  return isIntersecting;
}

✨useIntersectionObserver 훅 사용

  • 다음으로 훅을 사용할 컴포넌트를 정해야했다. 먼저 API를 호출하고 있는 상세 페이지에 먼저 사용하보려 했지만.. 따로 빼둔 댓글 컴포넌트가 댓글 리스트 요소말고도 다른 여러 요소를 포함하고 있어 사용 위치가 애매해졌다.
  • 따라서 댓글 컴포넌트에 직접 사용하고, 상세 페이지의 관련 함수들을 props로 내려줘서 이를 사용 가능하도록 구현했다.
  <ArticleComment
     comments={comments}
     setNewComment={setNewComment}
     cursor={cursor}
     getComments={getComments}
     loading={loading}
   />

// 댓글 컴포넌트
  const loadingRef = useRef(null);
  const isInteresting = useIntersectionObserver(loadingRef);

  useEffect(() => {
    if (isInteresting && cursor !== null) {
      getComments(cursor);
    }
  }, [isInteresting]);

❗발생한 문제

  • 첫번째 문제는 API를 통해 list로 받은 배열을 바로 상태배열에 넣고 있어서 새로 받아온 배열을 넣을 시, 기존 배열에 배열을 또 넣는 꼴이라 에러가 발생했던 문제였다.
  • 하지만 비교적 간단하게 스프레드 문법을 사용해서 list 배열을 분해하여 넣어주었더니 잘 동작했다.
  const getComments = async (cursor: number | null) => {
    const { list, nextCursor } = await getArticleComments(id, LIMIT, cursor);
    if (list) {
        setComments((prevComments) => [...prevComments, list]); // 요게 기존 로직
        setComments((prevComments) => [...prevComments, ...list); // 새로 만든 로직
      }
      setCursor(nextCursor);
    }
  };
  • 두번째 문제는 prevComments를 통해 바로 list를 넣다보니, 처음 list를 할당할 때 prevComments 때문에 중복되어 list가 두번 들어가는 것과 같이 보이는 경우가 생겼다.
  • isFirstLoad 라는 상태값을 만들고, 조건식을 통해 처음에만 list를 온전히 할당, isFirstLoad를 false로 만들고 다음부터 prevComments를 사용하는 로직으로 변경했다.
  const [isFirstLoad, setIsFirstLoad] = useState(true);

  const getComments = async (cursor: number | null) => {
    const { list, nextCursor } = await getArticleComments(id, LIMIT, cursor);
    if (list) {
      setLoading(false);
      if (isFirstLoad) {
        setComments(list);
        setIsFirstLoad(false);
      } else {
        setComments((prevComments) => [...prevComments, ...list]);
      }
      setCursor(nextCursor);
    }
  };

🏷️ 추가적으로 고민해볼 만한 사항

  • 추가적으로 loading 상태값을 만들어 로딩되는 동안 페칭하도록 했는데, 댓글 수가 적어 불러오는 속도가 너무 빨라 잘 체감은 되지 않는다. 그리고 로직 자체를 잘 작성한 건지도 잘 모르겠어서 계속해서 사용자 경험을 긍정적으로 유도하기 위해 계속 고민해보는 것이 좋을 듯 하다.

  • 그리고 로직 자체도 잘 짠 건지 모르겠어서 계속 고민해보는 것이 좋겠다.

우째하긴~ 이렇게 하믄 된다! 👍