원티드 프리온보딩 프론트엔드 코스의 첫 번째 기술 과제를 마쳤다. 이전에 개인 프로젝트에서 무한 스크롤 기능을 구현했어서 큰 어려움을 겪지는 않았다. 강사님 말씀으로는? 간단하게 실력을 평가하는 척도로 Infinite-scolling을 구현하는 과제라고 하셨다. 작동은 아래와 같다.
스타일 코드를 제외한 코드는 아래와 같다.(React, JS 사용)
import React, { useState, useEffect, useRef } from 'react';
import {
CommentContainer,
CommentBox,
InfiniteScrollContainer,
CommentDetail,
} from './InfiniteScrollListStyle';
function InfiniteScrollList() {
const [comments, setComments] = useState([]);
const [pageNumber, setPageNumber] = useState(1);
const [loading, setLoading] = useState(false);
const fetchComments = async (pageNumber) => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/comments?_page=${pageNumber}&_limit=10`,
);
const data = await res.json();
setComments((p) => [...p, ...data]);
setLoading(true);
};
useEffect(() => {
fetchComments(pageNumber);
}, [pageNumber]);
const pageEnd = useRef();
let num = 1;
useEffect(() => {
if (loading) {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
num++;
loadMore();
// num이 50이 되는 순간은 데이터 전체 개수인 500개가 되는 순간이기 때문에
// observer를 off 시켜준다.
if (num >= 50) observer.unobserve(pageEnd.current);
}
},
{ threshold: 1 },
);
observer.observe(pageEnd.current);
}
}, [loading, num]);
const loadMore = () => {
setPageNumber((prevPageNumber) => prevPageNumber + 1);
};
return (
<InfiniteScrollContainer>
{comments.map((comment, index) => (
<CommentContainer key={index}>
<CommentBox>
<div>Comment Id</div>
<span>{comment.id}</span>
</CommentBox>
<CommentBox>
<div>Email</div>
<span>{comment.email}</span>
</CommentBox>
<CommentDetail>
<div>Comment</div>
<p>{comment.body}</p>
</CommentDetail>
</CommentContainer>
))}
<div ref={pageEnd}></div>
</InfiniteScrollContainer>
);
}
export default InfiniteScrollList;
간단하게 정리하자면, Intersection Observer API를 사용하였다. Infinite Scrolling을 구현하는 방법이 기존의 스크롤 이벤트, 내가 사용한 방법 등 몇 가지 있는 걸로 아는데, 주워들은 얘기지만 현업에서는 Intersection Observer API를 사용한다고 들었다.
웹사이트를 개발할 때 특정 위치에 도달했을 때 어떤 액션을 취해야 한다면 어떻게 구현할 수 있을까? 보통 addEventListener()의 scroll 이벤트를 먼저 생각할 것이다. document에 스크롤 이벤트를 등록하고, 엘리먼트가 도달했을 때 실행할 콜백 함수를 등록하는 것이다.
하지만 scroll 이벤트는 단시간에 수백번, 수천번 호출될 수 있고 동기적으로 실행되기 때문에 메인 스레드(Main Thread) 영향을 준다고 한다. 또한 한 페이지 내에 여러 scroll 이벤트(무한 스크롤, 광고 배너, 애니메이션 등)가 등록되어 있을 경우, 각 엘리먼트마다 이벤트가 등록되어 있기 때문에 사용자가 스크롤할 때마다 이를 감지하는 이벤트가 끊임없이 호출된다.
디바운싱(Debouncing)과 쓰로틀링(Throttling)을 통해 이러한 문제를 개선시킬 수도 있기도 하고, 특정 지점을 관찰하기 위해서는 getBoundingClientRect() 함수를 사용해야 하는데, 이 함수는 리플로우(reflow) 현상이 발생한다는 단점이 있다.
해당 블로그에 따르면, Intersection Observer API(교차 관찰자 API)를 사용하면 위와 같은 문제를 해결할 수 있다고 한다. 결과적으로, 비동기적으로 실행되기 때문에 메인 스레드에 영향을 주지 않으면서 변경 사항을 관찰할 수 있다.
위의 코드 중 아래의 설명은 내가 개인 프로젝트를 진행했을 때 바로 이해하기 힘들었던 부분들이다.
observer의 콜백이 실행될 대상 요소의 가시성 퍼센티지를 나타내는 단일 숫자 혹은 숫자 배열입니다. 만일 50%만큼 요소가 보여졌을 때를 탐지하고 싶다면, 값을 0.5로 설정하면 됩니다. 혹은 25% 단위로 요소의 가시성이 변경될 때마다 콜백이 실행되게 하고 싶다면 [0, 0.25, 0.5, 0.75, 1] 과 같은 배열을 설정하세요.
기본값은 0이며(이는 요소가 1픽셀이라도 보이자 마자 콜백이 실행됨을 의미합니다). 1.0은 요소의 모든 픽셀이 화면에 노출되기 전에는 콜백을 실행시키지 않음을 의미합니다.
요약해서, 내가 작성한 threshold: 1
은 pageEnd
의 ref
가 있는 요소(div
태그) 전체가 보여야만 콜백함수(loadMore()
)을 실행시킬 수 있다는 말이다.
// MDN에서 제공되는 코드다.
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting -> 내가 사용한 부분
// entry.rootBounds
// entry.target
// entry.time
});
};
isIntersecting
프로퍼티는 target element가 교차 영역에 있다면 true
를 반환하고, 아니라면 false
를 반환한다.
Intersection Observer API에 대해 더 알 수 있던 좋은 기회였다.
안녕하세요! 이번 원티드 프리온보딩 프론트에 지원하였습니다! 궁금한것이 있는데 질문드려도 될까요?