리액트 환경의 무한 스크롤

무한 스크롤은 페이지 하단 영역까지 스크롤될 경우 다른 컨텐츠를 새롭게 로딩해 페이지에 추가되는 방식이다.

react-intersection-observer 라이브러리를 사용해서 구현해보았다.

 

- 라이브러리 설치

$ npm install react-intersection-observer --save

 

- useInView hook

  const { ref: observerRef, inView } = useInView()

 

=> 관찰하는 객체 (observer) 하나를 ref로 설정한 후 해당하는 객체가 화면에 보이면 특정 코드를 실행시킬 수 있다.

https://github.com/thebuilder/react-intersection-observer

 

GitHub - thebuilder/react-intersection-observer: React implementation of the Intersection Observer API to tell you when an eleme

React implementation of the Intersection Observer API to tell you when an element enters or leaves the viewport. - thebuilder/react-intersection-observer

github.com

 

- 전체 코드

import { useInView } from 'react-intersection-observer'

const Bookmark = () => {
	const { ref: observerRef, inView } = useInView()

	React.useEffect(() => {
        if (inView) {
          const totalPage = queryBookmarks.data.totalPage
          if (totalPage != null && bookmarkPage < totalPage) {
          	// 뷰포트에 들어오면 count + 1 해주기
            setBookmarkPage((prev) => prev + 1)
          }
        }
    }, [inView])


    return (
    	<div>
    		<div>
                {items.map((item) => (
                    <BookmarkItem 
                        title={bookmark.title}
                        ...
                    />
                )}
            </div>
         <div ref={observerRef} />
       </div>
    )
 }

 


문제 상황

useInView의 observerRef가 뷰포트에 들어올 때마다 inView 상태가 갱신되는 방식으로 구현을 했는데, 

페이지가 다시 로딩하는 문제가 일어났다.

이유는 무한 스크롤이나 하단에 위치한 요소가 여러 번 감지되면 이런 문제가 발생하는데, observerRef가 계속해서 inView 상태를 변경하기 때문이다.

 

해결법

useInView의 triggerOnce 옵션을 활용하여 한 번만 실행할 수 있도록 하자.

triggerOnce: true를 설정하면, 한 번 감지된 이후에는 inView 상태가 유지되어, 반복적으로 호출되지 않도록 제어할 수 있다.

const { ref: observerRef, inView } = useInView({
  triggerOnce: true, // 추가
});

return (
	<div>
    	{inView && ( // 추가
        	<div>
                <BookmarkItem
                 ...
                 />
                <div ref={observerRef} />
            </div>
         }
    </div>
)

 

 

추가로 inView가 True 일 때만 해당 아이템이 보이도록 구현한다.

React-Native에서는 비율대로 잡아주고, style 속성에서 width, height을 지정해줬다.

하지만, Reactstrap에서는 xs, sm, md, lg 속성으로 이해가 안되는 부분들이 있어 하나씩 정리해보았다. 😀

 

 

Reactstrap의 가장 큰 특징은 "모바일 우선"이라는 것이다.

 

 

1. Grid Class

매우 작은 기기 (ex. 모바일) - 너비가 768px 미만 - xs

작은 기기 (ex. 태블릿) - 너비가 768px 이상 - sm

중간 기기 (ex. 노트북) - 너비가 992px 이상 - md

큰 기기 (ex. 노트북/데스크탑) - 너비가 1200px 이상 - lg

 

 

2. Layout

Container, Row, Col 을 이용하여 콘텐츠를 레이아웃하고 정렬한다.

import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';

function ContainerExample() {
  return (
    <Container>
      <Row>
        <Col>1 of 1</Col>
      </Row>
    </Container>
  );
}

export default ContainerExample;

 

여기서 Container에 fluid 속성을 주면, 디바이스 크기에 따라 100% 비율로 맞춰진다.

import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';

function ContainerFluidExample() {
  return (
    <Container fluid>
      <Row>
        <Col>1 of 1</Col>
      </Row>
    </Container>
  );
}

export default ContainerFluidExample;

 

fluid 속성에는 sm, md, lg, xl, xxl로 크기를 다르게 지정할 수 있다.

return (
      <Container>
        <Row>
          <Col className="item" md="6">
            col
          </Col>
          <Col className="item" md="3">
            col
          </Col>
          <Col className="item" md="3">
            col
          </Col>
        </Row>
      </Container>
    );

 

md 크기부터는 6:3:3 비율로 만들어진다.

여기서 중요한 것은, Reactstrap은 모바일 우선이기 때문에, xs 즉, 작은 기기에서는 각 칸의 12개로 나타나다가 지정한 크기(md) 부터는 해당 비율로 공간을 차지하게 된다. 

 

 


Reactstrap에서는 비율을 이렇게 지정하는구나!

상태 관리의 필요성

뭐가 중헌디

 

React에서 가장 중요한 상태값은

state가 많은 것은 문제가 되지 않지만, state가 많아지고 그것을 사용하는 컴포넌트의 갯수가 늘어날 수록 props drilling이 심해진다. 

state를 관리하지 않으면 여러 상태 값에 의해 컴포넌트가 재렌더링이 된다는 뜻이다.

 

state가 어디서 어떻게 변하는 지 한 눈에 파악하기 힘들어지고, 어떤 컴포넌트에 의해 상태 값이 변화했는지  손쉽게 파악하기 힘들다.

 

그래서 우리는 전역 상태를 관리해야 하고, React 컴포넌트 트리 안에서 데이터를 전역적으로 공유할 수 있는 상태 관리 라이브러리를 사용한다.

 

 

상태 관리 라이브러리

상태 관리 라이브러리는 다양하다. Context API, Redux, MobX, Recoil 등의 많은 도구가 있고, 각각의 장/단점이 분명하게 존재한다.

 

 

Context API

  • 가장 많이 쓰이는 상태 관리 라이브러리로 간단한 상태 관리만 필요하다면 쓰기 좋은 라이브러리이다.
  • 함수형 Hooks에서는 기능이 제한되어 있다.
  • 성능과 기능이 적다.

=> 간단한 프로젝트에 사용하기 좋은 상태 관리 라이브러리이다.

참고 자료 https://ko.legacy.reactjs.org/docs/context.html

 

Redux

  • 1개의 root & 1개의 store 만이 존재하고, 순수 함수(pure function)에 의존한다.
  • MVC 패턴의 문제점을 극복하고자 페이스북에서 고안한 상태 관리 도구이다.
  • 상태가 읽기 전용이다. 🥶
    • 지속적으로 불변성을 유지해야 하기 때문에 immutable.js와 같은 라이브러리를 사용해서 상태 값을 바꿔야 한다.
  • 상태 값의 변경 사항을 Redux Devtools를 이용해 직관적으로 볼 수 있는 방법이 있다.
    • 상태 값이 많아질 경우 디버깅 할 때 굉장히 유용할 것이라고 생각한다.
  • 작은 상태 하나를 변경하려고 해도, actions, reducer, type 등 여러번 작성해야하는 번거로움이 있다.
    • 그렇기 때문에 상대적으로 더 많은 양의 코드를 작성해야 한다.

Recoil

Recoil은 페이스북에서 만든 React를 위한 상태 관리 라이브러리이다. 

  • React의 useState와 비슷하게 동작하여, 러닝 커브가 적고 직관적인 구조로 구성되어 있다.
    • useState -> useRecoilState
    • state -> useRecoilValue
    • setState -> useSetRecoilState
  • 비동기 처리를 기반으로 작성되어 동시성 모드(concurrent mode)를 제공하기 때문에, Redux와 다르게 비동기 처리 라이브러리에 의존할 필요가 없다.
  • 단점으로는 Redux처럼 안정적인 Devtool이 없다.
    • Snapshot을 사용하면 RecoilRoot 컴포넌트 안에서 사용하는 모든 Recoil 상태에 접근이 가능하지만, 직관적으로 볼 수 있는 것은 아니고 콘솔을 이용하는 형태로 볼 수 있다.

=> 확실히 React를 만든 Facebook에서 직접 만든 것이라 다른 상태 관리 라이브러리보다 호환성이 좋다고 생각한다. 
뿐만 아니라, 러닝 커브가 적어 세팅하기도 편하다.

 

참고자료

https://recoiljs.org/ko/docs/introduction/getting-started/

+ Recent posts