프로젝트는 현재 코인고스트 앱 페이지에서 실제로 사용되고 있는, 블로고의 리스트페이지, 상세페이지 그리고 회원가입 페이지를 구현하는 것이다.
프로젝트 github 주소
NextJS ,React Hook ,typescript, Styled Components , SWR
getStaticProps , getStaticPath 이용한 pre-rendering
리스트 페이지는 api호출, 필터링 기능, infinte scroll 기능 구현으로 코드를 짜보니 결과적으로 다 이어져 있어 이러한 과제를 냈구나 라는 생각이 들었다.
//index.tsx
const [likesBtn, setLikesBtn] = useState(false);
const [allBtn, setAllBtn] = useState(true);
const handleFilter = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (e.currentTarget.id === 'likes') {
setAllBtn(false);
setLikesBtn(true);
}
if (e.currentTarget.id === 'all') {
setLikesBtn(false);
setAllBtn(true);
}
return;
};
일단 필터링 기능 구현을 위한 state와 filter함수를 만들어 주었다. tab 버튼에 기본적으로 id를 주고, 클릭 이벤트로 id를 가져오는 동시에 boolean값으로 필터를 변경해 주었다. likesBtn api 요청 시에 query 파라미터를 만들어주는 조건으로도 사용했다.
처음에는 swr로 api를 호출하고 있었지만 무한 스크롤을 구현하기 위해 검색하던 중, SWR공식 사이트에서 무한 스크롤과 페이지네이션을 위한 함수를 만들어 제공한다는 사실을 알게되었다.
import useSWRInfinite from 'swr/infinite'
// ...
const { data, error, isValidating, size, setSize } = useSWRInfinite(
getKey, fetcher?, options?
)
import { useSWRInfinite } from 'swr'로 불러온다.
useSWRInfinite의 리턴값을 하나하나 살펴보면
data : 데이터를 리턴해준다.
error : 에러 발생시 에러를 리턴해준다.
isValidating : 데이터를 다운받는 중인지 확인해준다. true면 로딩중 false면 완료
size : 페이지 인덱스를 나타낸다
setSize : 페이지 인덱스를 변경해주는 함수이다.
getKey는 api의 주소를 입력해주는 부분이고, fetcher는 request를 보내는 fetch 함수를 의미한다.
const getKey = (pageIndex, previousPageData) => {
if (previousPageData && !previousPageData.length) return null // reached the end
return `/users?page=${pageIndex}&limit=10` // SWR key
}
처음 써보는 만큼 검색도 많이 했고 필요한 부분만 사용하였다.
작성한 코드를 보자
//posts.tsx
import useSWRInfinite from 'swr/infinite';
...
const [target, setTarget] = useState<HTMLButtonElement | null | undefined>(
null,
);
const getKey = (pageIndex: number, previousPageData: any) => {
if (previousPageData && !previousPageData.data) return null;
let orderBy = '';
if (likesBtn) {
orderBy = '&orderBy=likes';
}
return `${API.BLOGS}?&page=${pageIndex + 1}&limit=10${orderBy}`;
};
const { data, setSize } = useSWRInfinite<Porps>(getKey, fetcher);
const posts = data ? data.map((data) => data?.data).flat() : [];
useEffect(() => {
if (!target) return;
const observer = new IntersectionObserver(onIntersect, {
threshold: 0.5,
});
observer.observe(target);
return () => observer && observer.disconnect();
}, [data, target]);
const onIntersect: IntersectionObserverCallback = ([entry]) => {
if (entry.isIntersecting) {
setSize((p) => p + 1);
}
};
return (
<>
{posts?.map((posts) => {
return <PostList key={posts.id} posts={posts} />;
})}
<TargetElement ref={setTarget} />
</>
);
getKey
는 페이지 인덱스와 이전 데이터를 받는다. 이전 데이터를 체크하여 마지막 페이지에 도달하면 null을 반환하고, 아닐 경우 페이지에 +1을 해주며 계속해서 데이터를 받아 온다.
위에 필터에서 받아 온 likesBtn이 true일 경우 orderBy query를 추가하여 인기글 api를 불러 왔다.
또한, 데이터가 쌓일 수 있게 data를 map함수와 flat함수를 이용하여 하나의 배열로 만들어 주었다.
여기서 끝이 아니다. 무한 스크롤을 구현하려면 스크롤 시에 이벤트를 주어야한다. 그렇기 위하여 Intersection Observer API
를 사용하였다.
intersection observer api는 타겟 요소와 함께 타겟 요소의 조상 요소나 최상위 document의 뷰포트가 상호작용을 일으키면 비동기적으로 변화를 관찰하는 기능이다.
사용방법
const io = new IntersectionObserver(callback, [options])
Intersection Observer 를 생성할 때는 옵션을 설정할 수 있다.
적용되는 순서를 보면
Intersection Observer 객체를 생성하면서, Callback Function 과 option 을 전달한다.
observe 로 구독할 Target Element 를 추가한다.
Target Element 가 options.threshold 로 정의한 Percent(%) 만큼 화면에 노출 혹은 제외 되면, entries 배열 에 추가하고, Callback Function 을 호출한다.
Callback Function 에서 전달 받은 entries 배열을 확인하면서, isIntersecting 으로 노출 여부를 확인한다.
결과적으로 TargetElement이라는 빈 컴포넌트에 target을 주고 감시가 되면 size(pageIndex)를 1씩 증가 시켜 무한으로 스크롤 기능이 일어나도록 구현을 하였다.
useEffect의 dependency array로 data와 target을 넣어 필터 기능이 일어나도 잘 작동이 되도록 했다.
성능적인 이슈를 생각한다면 사용할 일이 없어졌을 경우 연결 해제를 명시하는 경우가 좋다고 하여 observer.disconnect()을 넣어주어 인스턴스를 중지시켰다.(observe() 메서드가 호출되지 않는 이상 감시 콜백은 발동되지 않는다.)
상세 페이지의 getStaticProps , getStaticPath 이용한 pre-rendering 부분은 나누어 작성을 해야겠다.