안녕하세요 오늘은 TypeScript + React에서 무한스크롤을 구현하는 방법에 대해서 알아보도록 하겠습니다. 흔히 무한스크롤은 여러 사이트에서 발견할 수 있습니다. 전체 목록을 불러오는 것이 아닌, 페이징화 되어있는 서버의 리스트 데이터들을 이용하여 일정량만 계속해서 가져오는 방법입니다.
대표적으로 유튜브, 페이스북, 인스타그램의 피드목록 조회 방식이 무한스크롤로 구현이 되어있습니다. (스크롤의 지점이 맨아래에 도달하면, 페이지를 1씩 늘려주면서 서버에 요청합니다.)
여러가지 방법을 통한 글 목록을 불러오기
그래서 이번 시간에는 임의의 데이터들을 이용해서 무한스크롤을 구현 해보도록 하겠습니다.
저는 src 디렉토리 안에 components 디렉토리에 컴포넌트를 생성하였습니다.
InfiniteScroll.tsx 파일에 아래와 같이 코드를 추가해주도록 하겠습니다.
아 그리고, 이번 글에서는 styled-components를 이용하여 스타일링을 하도록 하겠습니다.
이번 글에서는 단순 구현을 목적으로 하기 때문에 디자인은 그냥 알아볼 수 있을 정도로만 하겠습니다.
// components/InfiniteScroll.tsx
import React, { useState, useCallback, useEffect } from 'react';
import styled from 'styled-components';
const InfiniteScroll = (): JSX.Element => {
const [page, setPage] = useState<number>(1);
// 요청할 페이지 번호 변수
return (
<Container>
</Container>
);
};
export default InfiniteScroll;
const Container = styled.div`
width: 100%;
max-width: 1000px;
margin: 4rem auto;
`;
const PostItem = styled.div`
width: 100%;
height: 350px;
border: 2px solid black;
`;
그 다음으로, 임의의 데이터 파일을 추가해주도록 하겠습니다. 저는 lib 디렉토리에 postList.ts 파일을 생성해준다음, 아래와 같이 적어주었습니다.
// lib/postList.ts
export type postType = {
page: number;
contents: string;
};
export const getPostList = (page: number): postType[] => {
// 매개변수로 받은 페이지와 동일한 페이지 객체들만 return 해줍니다.
return postList.filter((post: postType) => post.page === page);
};
export const postList: postType[] = [
{
page: 1,
contents: '안녕하세요 1번째 글',
},
{
page: 1,
contents: '안녕하세요 2번째 글',
},
{
page: 1,
contents: '안녕하세요 3번째 글',
},
{
page: 2,
contents: '안녕하세요 4번째 글',
},
{
page: 2,
contents: '안녕하세요 5번째 글',
},
{
page: 2,
contents: '안녕하세요 6번째 글',
},
{
page: 3,
contents: '안녕하세요 7번째 글',
},
{
page: 3,
contents: '안녕하세요 8번째 글',
},
{
page: 3,
contents: '안녕하세요 9번째 글',
},
{
page: 4,
contents: '안녕하세요 10번째 글',
},
];
위와같이 데이터들을 다 쌓아준 다음, InfiniteScroll.tsx에 아래의 state 배열을 선언해주세요!
// InfiniteScroll.tsx
const [posts, setPosts] = useState<postType[]>(getPostList(1));
// posts 배열의 초기값은 페이지가 1인 객체들 입니다.
현재 posts 배열에는 페이지가 1인 객체들로 이루어져 있습니다. 이제 배열을 map을 사용하여 렌더링을 해주도록 하겠습니다. jsx return 부분을 아래와 같이 수정해주세요.
return (
<Container>
{
posts.map((post: postType, idx: number) => (
<PostItem key={idx}>{post.contents}</PostItem>
))
}
</Container>
);
// 컴포넌트 바깥에 아래의 코드를 작성해주세요.
const PostItem = styled.div`
width: 100%;
height: 350px;
border: 2px solid black;
`;
이제 기본적인 스타일링과 데이터 쌓기를 모두 해주었습니다. 프로젝트를 실행하면 아래와 같은 컴포넌트의 모습을 보실 수 있습니다.
현재는 글 3개, 즉 페이지가 1인 객체들만 렌더링이 되었는데요, 이제부터 스크롤이 아래로 닿을때마다 페이지를 증가시켜서 페이지가 2, 3, 4...인 글들을 불러온 뒤, 합치는 작업을 해주겠습니다.
무한스크롤을 구현하기 위해서 가장 먼저 스크롤에 대한 이벤트를 추가해주어야 합니다. InfiniteScroll.tsx 파일에 아래의 useEffect와 함수 코드를 추가시켜주세요!
const handleScroll = useCallback((): void => {
// 스크롤을 하면서 실행할 내용을 이곳에 추가합니다.
}, []);
useEffect(() => {
window.addEventListener('scroll', handleScroll, true);
// 스크롤이 발생할때마다 handleScroll 함수를 호출하도록 추가합니다.
return () => {
window.removeEventListener('scroll', handleScroll, true);
// 해당 컴포넌트가 언마운트 될때, 스크롤 이벤트를 제거합니다.
};
}, [handleScroll]);
위와 같이 코드를 적어주고 나면 스크롤 이벤트가 추가되었습니다. 이제 handleScroll 함수안에 내용을 추가하여 구현해보겠습니다. 아래의 코드를 handleScroll 함수에 추가해주세요!
const handleScroll = useCallback((): void => {
const { innerHeight } = window;
// 브라우저창 내용의 크기 (스크롤을 포함하지 않음)
const { scrollHeight } = document.body;
// 브라우저 총 내용의 크기 (스크롤을 포함한다)
const { scrollTop } = document.documentElement;
// 현재 스크롤바의 위치
if (Math.round(scrollTop + innerHeight) >= scrollHeight) {
// scrollTop과 innerHeight를 더한 값이 scrollHeight보다 크다면, 가장 아래에 도달했다는 의미이다.
setPosts(posts.concat(getPostList(page + 1)));
// 페이지에 따라서 불러온 배열을 posts 배열과 합쳐줍니다.
setPage((prevPage: number) => prevPage + 1);
// 페이지 state 변수의 값도 1씩 늘려줍니다.
}
}, [page, posts]);
위에서 가장 중요한 부분이 있다면, scrollTop과 innerHeight를 더한 값이 scrollHeight보다 크다면, 아래에 도달했다라는 점이 가장 중요한 포인트입니다. 위의 함수를 추가시키고 나서, 프로젝트를 실행하여 결과물을 확인해보세요!
처음에는 3개만 렌더링을 해주었다가 스크롤이 아래쯔음에 도달할때마다 페이지를 1씩 증가시켜 주면서 다른 페이지의 글들을 순차적으로 불러온다음, 배열에 합쳐주는 모습입니다.
기존에 페이스북, 유튜브 등에서 흔히 볼 수 있었던 무한스크롤 방식을 위처럼 구현하게 되었습니다. 나중에 이 작업을 서버 데이터 통신과 연결을 시킨다면 로딩도 쉽게 구현 가능할 것입니다. 이 글을 보고나서 무한스크롤을 쉽게 구현하실 수 있다면 좋겠습니다. 😀
이상으로 글을 마치도록 하겠습니다. 긴 글 읽어주셔서 감사합니다 😀
// InfiniteScroll.tsx
import React, { useState, useCallback, useEffect } from 'react';
import styled from 'styled-components';
import { getPostList, postType } from 'lib/postList';
const InfiniteScroll = (): JSX.Element => {
const [page, setPage] = useState<number>(1);
const [posts, setPosts] = useState<postType[]>(getPostList(1));
const handleScroll = useCallback((): void => {
const { innerHeight } = window;
const { scrollHeight } = document.body;
const { scrollTop } = document.documentElement;
if (Math.round(scrollTop + innerHeight) >= scrollHeight) {
setPosts(posts.concat(getPostList(page + 1)));
setPage((prevPage: number) => prevPage + 1);
}
}, [page, posts]);
useEffect(() => {
window.addEventListener('scroll', handleScroll, true);
return () => {
window.removeEventListener('scroll', handleScroll, true);
}
}, [handleScroll]);
return (
<Container>
{
posts.map((post: postType, idx: number) => (
<PostItem key={idx}>{post.contents}</PostItem>
))
}
</Container>
);
};
export default InfiniteScroll;
const Container = styled.div`
width: 100%;
max-width: 1000px;
margin: 4rem auto;
`;
const PostItem = styled.div`
width: 100%;
height: 350px;
border: 2px solid black;
`;
// lib/postList.ts
export type postType = {
page: number;
contents: string;
};
export const getPostList = (page: number): postType[] => {
return postList.filter((post: postType) => post.page === page);
};
export const postList: postType[] = [
{
page: 1,
contents: '안녕하세요 1번째 글',
},
{
page: 1,
contents: '안녕하세요 2번째 글',
},
{
page: 1,
contents: '안녕하세요 3번째 글',
},
{
page: 2,
contents: '안녕하세요 4번째 글',
},
{
page: 2,
contents: '안녕하세요 5번째 글',
},
{
page: 2,
contents: '안녕하세요 6번째 글',
},
{
page: 3,
contents: '안녕하세요 7번째 글',
},
{
page: 3,
contents: '안녕하세요 8번째 글',
},
{
page: 3,
contents: '안녕하세요 9번째 글',
},
{
page: 4,
contents: '안녕하세요 10번째 글',
},
];
글 잘 봤습니다!