-
무한스크롤 기능 첫 도전🎉
카카오 엔터프라이즈의 코드 따라해보기 : 배경지식이 0이기 때문에 카카오 엔터프라이즈의 코드를 빌렸으며 '동작의 흐름 파악'에 의의를 두었습니다.scroll 이벤트를 활용한 무한스크롤 기능을 구현하던 중,
화면상에서는 스크롤이 있음에도 불구하고, 전체 컨텐츠의 높이인
offsetHeight
가 화면 높이인innerHeight
와 같은 현상이 발생했습니다.// ThumbnailWrapper.tsx import React, { useState, useEffect, useCallback } from 'react'; import { Thumbnail } from '../Thumbnail/Thumbnail'; import { StThumbnailWrapper } from './ThumbnailWrapper.styles'; import { ResponseData, ReviewsList } from 'api/reviewsApi'; import { axiosInstance } from 'api/api'; export const ThumbnailWrapper = () => { const type = 'likes'; const [page, setPage] = useState(1); const [reviews, setReviews] = useState<ReviewsList[]>([]); const [isFetching, setFetching] = useState(false); const [hasNextPage, setNextPage] = useState(true); const fetchReviews = useCallback(async () => { const { data } = await axiosInstance.get<ResponseData>('/reviews', { params: { type, page }, }); setReviews(reviews.concat(data.content)); setPage(page + 1); setNextPage(!data.last); setFetching(false); }, [page]); useEffect(() => { const handleScroll = () => { const { scrollTop, offsetHeight } = document.documentElement; if (window.innerHeight + scrollTop >= offsetHeight) { setFetching(true); } }; setFetching(true); window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, []); useEffect(() => { if (isFetching && hasNextPage) fetchReviews(); else if (!hasNextPage) setFetching(false); }, [isFetching]); return ( <StThumbnailWrapper> {reviews.map((review) => ( <Thumbnail key={review.id} id={review.id} roadName={review.roadName} img_url={review.img_url} likeCnt={review.likeCnt} tag={review.tag} profileImg_url={review.profileImg_url} /> ))} {isFetching && <div>Loading...</div>} </StThumbnailWrapper> ); };
innerHeight
와scrollTop
을 더한 값이offsetHeight
와 같거나 크다면 isFetching을 true로 하고, 만약hasNextPage가 true라면 fetchReviews함수를 실행하여 다음 페이지가 로드되어야합니다.하지만 전체 컨텐츠의 총 높이인
offsetHeight
의 정보를 제대로 가져오지 못해서 이벤트가 동작하지 않았습니다.01 가리키는 요소가 잘못된 경우
scrollTop
과offsetHeight
가 가리키는document.documentElement
는 전체 컨텐츠여야 하는데, 혹시 다른 요소를 가리키고 있는 것은 아닌가? 곧바로 확인했지만 document.documentElement는 올바르게 요소를 가리키고 있었습니다.하지만 콘솔에는 여전히 667로
offsetHeight
와innerHeight
가 동일하게 출력됩니다.02 문제의 페이지에 포함된 컴포넌트의 height 100% 또는 100vh
stackoverflow에서 비슷한 문제를 발견했는데, 원인은 Style Component에서의 높이 설정이 문제였습니다.
Homepage, ThumbnailWrapper, GlobalStyle 3곳에서는 높이 설정을 하지 않았으므로 남은 곳은 CommonLayout.
StLayoutBody
에 높이가 설정되어 있는 것을 확인했습니다. 해당 부분을 주석 처리하고 다시 실행해보았습니다.// HomePage.tsx import React from 'react'; import { Input } from 'components/common'; import { CommonLayout, NavBar } from 'components/layout'; import { ThumbnailWrapper } from 'components/homePage'; export const HomePage = () => { return ( <CommonLayout header={ <NavBar btnLeft={'logo'} btnRight={'mypage'}> <Input /> </NavBar> } > <ThumbnailWrapper /> </CommonLayout> ); };
// CommonLayout.styles.ts // 생략 export const StLayoutBody = styled.div` position: relative; width: 100vw; max-width: 390px; /* height: 100vh; */ max-height: 850px; box-shadow: 0 0 8px rgba(0, 0, 0, 0.1); `; // 생략
// CommonLayout.tsx // 생략 export const CommonLayout: React.FC<CommonLayoutProps> = ({ children, header, footer, headerHeight = '50px', hideHeader = true, }) => { // 생략 return ( <StLayoutOuter> <StLayoutBody> {header && <StSlidingHeader $isShow={isShow}>{header}</StSlidingHeader>} <StLayoutSection ref={scrollRef} $headerHeight={headerHeight}> {children} </StLayoutSection> {footer && <FloatingFooter>{footer}</FloatingFooter>} </StLayoutBody> </StLayoutOuter> ); };
아래와 같이 잘 실행되는 것을 확인했습니다.
하지만 최종적으로 고정 값이 있는
CommonLayout
이 필요했습니다. 어떻게 하면 무한스크롤과 고정된 높이를 동시에 적용할 수 있을까요?카카오 엔터프라이즈의 글에서도 소개하고 있지만,
무한스크롤을 검색해보면
IntersectionObserver
라는 API가 대부분을 차지합니다.IntersectionObserver
API는 루트 요소와 타겟 요소의 교차점을 관찰하고, 교차 여부를 구별합니다.앞서 사용했던
scroll
이벤트는 동기적으로 실행되는 반면,IntersectionObserver
API는 비동기적으로 실행됩니다. 전자의 경우 만약 짧은 시간 내에 수 천 번의 이벤트가 동기적으로 실행된다면 많은 양의 콜백이 실행되며, 큰 부하를 줄 수 있습니다. 또IntersectionObserver
API는scroll
이벤트와는 다르게 reflow를 발생시키지 않습니다.이러한 측면에서
IntersectionObserver
API가 성능이 뛰어남을 알 수 있습니다.그러니
scroll
이벤트 관련된 기능을 구현할 때IntersectionObserver
API를 쓰지 않을 이유가 없네요 !!저의 케이스는 CommonLayout의 스타일 적용으로 인해서 offsetHeight가 제대로 인식되지 않는다는 것이 문제였습니다. 무한스크롤 첫 도전이라,
IntersectionObserver
를 이용하지 않고scroll
이벤트만으로 구현해보며, 동작의 흐름을 익히는 것이 이번 목표였는데요.IntersectionObserver
API를 이용할 경우,offsetHeight
대신div
같은 사각형 요소를 target으로 하여 계산되기 때문에 문제를 해결할 수 있다는 생각이 들었고,scroll
이벤트만으로 구현할 경우 성능저하 등의 부작용이 있기 때문에 방향을 바꾸게 되었습니다.// useIntersect.ts import { useEffect, useRef, useCallback } from 'react'; interface IntersectionObserverInit { root?: Element | Document | null; rootMargin?: string; threshold?: number | number[]; } type IntersectHandler = ( entry: IntersectionObserverEntry, observer: IntersectionObserver ) => void; export const useIntersect = ( onIntersect: IntersectHandler, options?: IntersectionObserverInit ) => { const ref = useRef<HTMLDivElement>(null); const callback = useCallback( (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => { entries.forEach((entry) => { if (entry.isIntersecting) onIntersect(entry, observer); }); }, [onIntersect] ); useEffect(() => { if (!ref.current) return; const observer = new IntersectionObserver(callback, options); observer.observe(ref.current); return () => observer.disconnect(); }, [ref, options, callback]); return ref; };
이렇게 하면 바로 잘 작동할 줄 알았는데,
IntersectionObserver
적용하기 전과 같은 결과였습니다.이유는
IntersectionObserver
적용 전offsetHeight
의 문제를 해결해보려고 이리저리 테스트 코드를 넣었는데요. 스타일에flex: 1;
을 적용했던 것을 지우지 않았던 것이었습니다..!원인을 찾기 위해 많은 시간을 들였는데, style쪽의 문제라서 조금 허무했던...
그래도 실무에서 이런 일이 발생하지 않으리란 법은 없죠 !
배운 점: 등잔 밑이 어둡다. 코드에 문제가 없는데 작동이 잘 안된다면, style 코드를 유심히 살펴보자 !
해당 부분을 삭제해보니 문제없이 잘 동작하였습니다🎉
'Troubleshooting > Issues' 카테고리의 다른 글
이미지 fetchpriority로 빠르게 불러오기 (0) 2023.08.25 scroll event와 style (0) 2023.08.17 댓글