게시판, 포럼 등 내용이 많은 페이지에 대해서 스크롤이 길어질 때 페이지를 새로 고치지 않고, 한 페이지에서 모든 내용을 볼 수 있게 해주는 무한 스크롤 기능이 최신의 웹개발 트렌드인 것 같습니다. 웹개발에 많이 사용되는 리액트에서 무한 스크롤 기능을 구현하는 방법은 여러 가지가 있는데, 이 글에서는 세 가지 방법에 대해서 알아보겠습니다.
먼저, 리액트로 무한 스크롤을 구현하는 구체적인 방법은 아래와 같습니다.
리액트에서 무한 스크롤 구현하기
1. 초기 데이터 로딩: 페이지가 처음 로드될 때 초기 데이터를 가져와야 합니다. 이를 위해서는 API 요청 등을 사용하여 초기 데이터를 가져옵니다.
2. 스크롤 이벤트 감지: 무한 스크롤을 구현하려면 스크롤 이벤트를 감지해야 합니다. 이를 위해서는 window.addEventListener 함수를 사용하여 스크롤 이벤트를 등록합니다.
componentDidMount() { window.addEventListener('scroll', this.handleScroll); } componentWillUnmount() { window.removeEventListener('scroll', this.handleScroll); } handleScroll = () => { const { loading, hasMore } = this.props; if (loading || !hasMore) return; if ( window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight ) { this.loadMore(); } };
위의 코드에서 loading은 현재 데이터를 로딩 중인지를 나타내는 상태 값입니다. hasMore는 더 많은 데이터가 있는지 나타내는 상태 값입니다. 스크롤 이벤트를 처리하는 handleScroll 메소드는 스크롤 위치를 계산하여 더 많은 데이터를 로드하는 loadMore 메소드를 호출합니다.
3. 더 많은 데이터 로딩: 스크롤 이벤트가 발생하면 loadMore 메소드를 호출하여 더 많은 데이터를 가져와야 합니다. 이를 위해 API 요청 등을 사용하여 새로운 데이터를 가져옵니다. 가져온 데이터는 이전 데이터와 병합하여 화면에 렌더링합니다.
loadMore = () => { const { loadMore, page } = this.props; loadMore(page + 1); };
위의 코드에서 loadMore는 새로운 데이터를 로드하는 메소드입니다. page는 현재 페이지를 나타내는 상태 값입니다. loadMore 함수는 새로운 페이지를 인수로 받아 API 요청 등을 사용하여 새로운 데이터를 가져옵니다.
4. 데이터 렌더링: 가져온 데이터를 이전 데이터와 병합하여 화면에 렌더링합니다. 이를 위해 map 메소드 등을 사용하여 데이터를 반복하고, 각 데이터를 컴포넌트로 변환하여 렌더링합니다.
render() { const { data } = this.props; return ( <div> {data.map((item) => ( <Item key={item.id} item={item} /> ))} </div> ); }
위 코드에서 data는 현재까지 로드된 모든 데이터를 나타내는 상태 값입니다. Item 컴포넌트는 각 데이터를 나타내는 컴포넌트입니다. map 메소드를 사용하여 data 배열을 반복하면서 Item 컴포넌트를 생성합니다. 각각의 Item 컴포넌트는 item prop으로 현재 데이터를 전달받습니다.
5. 로딩 상태 표시: 새로운 데이터를 로딩하는 동안 사용자에게 로딩 상태를 표시해야 합니다. 이를 위해 로딩 중임을 나타내는 UI를 렌더링합니다.
render() { const { data, loading } = this.props; return ( <div> {data.map((item) => ( <Item key={item.id} item={item} /> ))} {loading && <div>Loading...</div>} </div> ); }
위 코드에서 loading은 현재 데이터를 로딩 중인지 여부를 나타내는 상태 값입니다. loading이 true인 경우, Loading… 메시지를 화면에 렌더링합니다.
6. 더 이상 데이터가 없음을 표시: 추가로 로드할 데이터가 없는 경우 사용자에게 표시해야 합니다. 이를 위해 hasMore 상태 값을 사용하여 더 이상 로드할 데이터가 없는 경우 UI를 렌더링합니다.
render() { const { data, loading, hasMore } = this.props; return ( <div> {data.map((item) => ( <Item key={item.id} item={item} /> ))} {loading && <div>Loading...</div>} {!loading && !hasMore && <div>No more data</div>} </div> ); }
위 코드에서 hasMore는 추가로 로드할 데이터가 있는지 나타내는 상태 값입니다. hasMore가 false인 경우 No more data 메시지를 화면에 렌더링합니다.
7. 추가적인 최적화: 위 코드로 무한 스크롤 작업이 진행되는 동안 불필요한 데이터 요청 등이 발생할 수 있습니다. 이를 해결하기 위해서는 스크롤 이벤트 등을 디바운스 처리하거나, 데이터를 캐싱하는 등 추가적인 최적화 작업이 필요할 수도 있습니다.
다음으로, 리액트로 무한 스크롤을 구현하는 다른 방법에 대해서도 알아보겠습니다. 이 글에서는 IntersectionObserver API를 사용하는 방법, react-infinite-scroll-component 라이브러리를 사용하는 방법, Server-Side Rendering (SSR)을 활용하는 방법에 대해서 알아보겠습니다.
IntersectionObserver API 사용 방법
리액트에서 IntersectionObserver API를 사용하여 무한 스크롤을 구현하는 방법은 아래와 같습니다.
1. IntersectionObserver 객체 생성: IntersectionObserver API는 IntersectionObserver 객체를 생성하여 뷰포트 내의 요소와 상호작용할 때 사용됩니다. IntersectionObserver 객체를 생성할 때는 Callback 함수를 등록해야 합니다. 이 Callback 함수는 뷰포트 내의 요소와 교차할 때마다 호출됩니다.
componentDidMount() { const options = { root: null, rootMargin: '0px', threshold: 0.1 }; this.observer = new IntersectionObserver(this.handleObserver, options); }
위 코드에서 options는 IntersectionObserver 객체를 생성할 때 사용하는 옵션 객체입니다. root는 뷰포트를 기준으로 교차 검사를 할 요소를 지정하는데, null을 지정하면 뷰포트를 기준으로 교차 검사를 합니다. rootMargin은 교차 영역의 마진 값을 설정하고, threshold는 교차 영역의 비율을 나타냅니다.
2. 요소 감시: IntersectionObserver 객체를 사용하여 감시할 요소를 등록합니다. 이를 위해서는 observe 메소드를 사용합니다. 등록된 요소는 뷰포트 내의 요소와 교차할 때마다 콜백 함수가 호출됩니다.
componentDidMount() { const options = { root: null, rootMargin: '0px', threshold: 0.1 }; this.observer = new IntersectionObserver(this.handleObserver, options); if (this.loadingRef.current) { this.observer.observe(this.loadingRef.current); } }
위 코드에서 loadingRef는 무한 스크롤을 위한 로딩 요소의 ref 객체입니다. observe 메소드를 사용하여 loadingRef를 등록하면 loadingRef 요소가 뷰포트 내에 진입할 때마다 콜백 함수가 호출됩니다.
3. 콜백 함수 처리: IntersectionObserver 객체 콜백 함수에서는 뷰포트 내의 요소와 교차한 것을 확인하고, 새로운 데이터를 가져오는 작업을 수행합니다.
handleObserver = (entries, observer) => { const { loading, hasMore } = this.props; if (loading || !hasMore) return; entries.forEach(entry => { if (entry.isIntersecting) { this.loadMore(); observer.unobserve(entry.target); } }); };
위 코드에서 handleObserver 함수는 IntersectionObserver 객체의 콜백 함수입니다. entries는 뷰포트 내의 요소와 교차한 정보를 담고 있는 배열입니다. observer는 IntersectionObserver 객체를 참조하는데, 이 객체를 사용하여 요소 감시를 중지할 수 있습니다.
entries 배열을 반복하면서 isIntersecting 속성이 true인 요소를 찾으면, loadMore 함수를 호출하여 새로운 데이터를 가져옵니다. 가져온 데이터는 이전 데이터와 병합하여 화면에 렌더링합니다. 새로운 데이터를 가져온 후 observer 객체의 unobserve 메서드를 사용하여 감시 대상에서 제외합니다.
4. 컴포넌트 언마운트 시 요소 감시 중지: IntersectionObserver 객체는 컴포넌트가 언마운트될 때 반드시 중지해야 합니다. 이를 위해 componentWillUnmount 메소드에서 disconnect 메소드를 호출하여 감시를 중지합니다.
componentWillUnmount() { if (this.observer) { this.observer.disconnect(); } }
위 코드에서 disconnect 메소드는 IntersectionObserver 객체에서 요소 감시를 중지하는 메소드입니다. 지금까지 알아본 내용으로, IntersectionObserver API를 사용하여 리액트로 무한 스크롤을 구현할 수 있습니다.
react-infinite-scroll-component 사용 방법
리액트에서 react-infinite-scroll-component를 사용하여 무한 스크롤을 구현하는 방법은 아래와 같습니다.
1. 라이브러리 설치: 먼저 react-infinite-scroll-component 라이브러리를 설치해야 합니다. 이를 위해, npm 또는 yarn을 사용하여 라이브러리를 설치합니다.
npm install --save react-infinite-scroll-component
2. InfiniteScroll 컴포넌트 사용: react-infinite-scroll-component 라이브러리에서는 InfiniteScroll 컴포넌트를 제공합니다. 이를 사용하여 무한 스크롤을 구현할 수 있습니다.
import InfiniteScroll from 'react-infinite-scroll-component'; class App extends React.Component { state = { items: Array.from({ length: 20 }) }; fetchMoreData = () => { // 새로운 데이터를 가져와서 state 업데이트 }; render() { return ( <InfiniteScroll dataLength={this.state.items.length} next={this.fetchMoreData} hasMore={true} loader={<h4>Loading...</h4>} > {this.state.items.map((item, index) => ( <div key={index}>{item}</div> ))} </InfiniteScroll> ); } }
위 코드에서 InfiniteScroll 컴포넌트는 next prop을 통해 fetchMoreData 함수를 호출하여 더 많은 데이터를 가져옵니다. hasMore prop은 더 이상 데이터가 없는지 나타냅니다. loader prop은 데이터를 가져오는 동안 표시할 로딩 UI입니다.
3. 데이터 로딩 함수 작성: next prop으로 전달된 fetchMoreData 함수에서는 새로운 데이터를 가져와서 state를 업데이트합니다.
fetchMoreData = () => { // 새로운 데이터를 가져와서 state 업데이트 const newItems = Array.from({ length: 20 }); this.setState({ items: [...this.state.items, ...newItems] }); };
위 코드에서는 Array.from 메소드를 사용하여 임의의 데이터를 생성하고, 기존 데이터와 병합하여 state를 업데이트합니다.
무한 스크롤 최적화: 무한 스크롤을 구현하는 동안 불필요한 데이터 요청 등이 발생할 수 있습니다. 이를 최적화하려면 InfiniteScroll 컴포넌트에서 제공하는 scrollThreshold prop 등을 활용하여 스크롤 이벤트를 디바운스 처리하거나, 데이터를 캐싱하는 등의 추가적인 최적화 작업을 수행할 수 있습니다.
<InfiniteScroll dataLength={this.state.items.length} next={this.fetchMoreData} hasMore={true} loader={<h4>Loading...</h4>} scrollThreshold={0.5} > {this.state.items.map((item, index) => ( <div key={index}>{item}</div> ))} </InfiniteScroll>
위 코드에서 scrollThreshold는 무한 스크롤을 구현할 때 언제 데이터를 로드할지 결정하는 기준값을 나타냅니다. scrollThreshold 값이 0.5인 경우 스크롤이 뷰포트 하단의 50% 지점에 도달했을 때 새로운 데이터를 로드한다는 것을 의미합니다.
Server-Side Rendering (SSR) 활용 방법
리액트에서 Server-Side Rendering (SSR)을 사용하여 무한 스크롤을 구현하는 방법은 다음과 같습니다.
1. 서버사이드 렌더링 모듈 설치: 먼저, 서버사이드 렌더링 모듈을 설치해야 합니다. 여기서는 react-dom/server 모듈을 설치합니다.
npm install --save react react-dom react-dom/server
2. 서버사이드 렌더링 함수 작성: 서버사이드 렌더링 함수에서는 초기 데이터를 로드하고, 초기 렌더링 결과를 반환합니다.
import React from 'react'; import ReactDOMServer from 'react-dom/server'; import App from './App'; const renderApp = (initialData) => { const app = ReactDOMServer.renderToString(<App initialData={initialData} />); return `<!doctype html> <html> <head> <title>My App</title> </head> <body> <div id="root">${app}</div> <script> window.__INITIAL_DATA__ = ${JSON.stringify(initialData)}; </script> <script src="/bundle.js"></script> </body> </html>`; };
위 코드에서 renderApp 함수는 ReactDOMServer.renderToString 메소드를 사용하여 초기 렌더링 결과를 문자열로 반환합니다. 이 결과를 HTML로 렌더링하고, 초기 데이터를 window.__INITIAL_DATA__ 객체에 저장하여 클라이언트에서 사용할 수 있도록 합니다. bundle.js는 클라이언트에서 사용되는 자바스크립트 번들 파일입니다.
3. 초기 데이터 로드: 서버사이드 렌더링 함수에서 초기 데이터를 로드해야 합니다. 이를 위해 fetch API를 사용하여 서버에서 데이터를 가져옵니다.
import React from 'react'; import ReactDOMServer from 'react-dom/server'; import fetch from 'node-fetch'; import App from './App'; const renderApp = async () => { const response = await fetch('https://my-api.com/data'); const initialData = await response.json(); const app = ReactDOMServer.renderToString(<App initialData={initialData} />); return `<!doctype html> <html> <head> <title>My App</title> </head> <body> <div id="root">${app}</div> <script> window.__INITIAL_DATA__ = ${JSON.stringify(initialData)}; </script> <script src="/bundle.js"></script> </body> </html>`; };
위 코드에서 fetch 함수를 사용하여 https://my-api.com/data(예시)에서 데이터를 가져옵니다. 이 데이터는 initialData 객체에 저장되며, 이를 <App> 컴포넌트의 initialData prop으로 전달합니다.
4. 서버사이드 렌더링 실행: 서버사이드 렌더링 함수를 실행하여 초기 렌더링 결과를 반환합니다. 이를 위해서는 서버에서 HTTP 요청을 처리할 때 renderApp 함수를 호출합니다.
import http from 'http'; const server = http.createServer(async (req, res) => { const html = await renderApp(); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(html); }); server.listen(3000);
위 코드에서 http 모듈을 사용하여 서버를 생성하고, 요청이 들어올 때마다 renderApp 함수를 호출하여 HTML을 생성합니다. 이 HTML을 응답으로 반환합니다.
5. 클라이언트 사이드 렌더링: 클라이언트에서는 서버에서 전달받은 초기 데이터를 사용하여 렌더링을 수행합니다. 이때, 무한 스크롤을 구현하기 위해 IntersectionObserver API나 react-infinite-scroll-component 라이브러리를 사용하여 클라이언트에서 무한 스크롤을 구현합니다.
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; const initialData = window.__INITIAL_DATA__; ReactDOM.hydrate(<App initialData={initialData} />, document.getElementById('root'));
위 코드에서 ReactDOM.hydrate 함수를 사용하여 서버에서 전달받은 초기 데이터를 사용하여 클라이언트에서 렌더링을 수행합니다. initialData는 window.__INITIAL_DATA__ 객체에서 가져옵니다.
지금까지 리액트에서 무한 스크롤 기능을 구현하는 방법에 대해서 알아봤습니다.