본문 바로가기
react

react 무한스크롤 구현하기(react-intersection-observer, createAsyncThunk 사용)

by HomieKim 2022. 4. 7.

리액트를 사용해 infinite scroll을 구현해 보았습니다.

그냥 구현하면 재미없으니까 얼마전에 공부한 Redux-toolkit을 활용해 비동기 요청을 보내 tmdb(https://www.themoviedb.org/?language=ko)에서 제공하는 openAPI를 사용해 영화 정보를 20개씩 불러오는 프로젝트를 만들어 보았습니다! 

 

✔ 전체코드

https://github.com/HomieKim/React_PlayGround/tree/main/simple-infinite-scroll

 

GitHub - HomieKim/React_PlayGround: 리액트 Toy Project

리액트 Toy Project. Contribute to HomieKim/React_PlayGround development by creating an account on GitHub.

github.com


✔ Infinite Scroll

페이지 안에 불러와야할 데이터가 매우 많다면 로딩 시간이 매우 길어질 것입니다. 

특정 갯수의 데이터만 불러오고 스크롤 이벤트를 감지하여 데이터가 화면에 전부 보여줬다면 다음 데이터를 불러오는 기법입니다.

(사실 고려 해줘야 할 사항이 조금 더 있지만 Redux-Toolkit을 이용한 비동기 요청 과 간단하게 무한 스크롤을 구현하는 것에 초점을 맞췄습니다.)

주로 사용 되는 방법은 크게 두가지가 있습니다.

  • 스크롤 이벤트를 사용하는 방법
  • intersection observer api를 사용하는 방법

특히 스크롤 이벤트를 사용하여 구현한다면 스크롤 이벤트는 연속적으로 발생하기 때문에 디바운스나 스로틀을 이용해서 이벤트가 과하게 발생하지 않게 구현해 주어야 합니다. 

이를 조금 편하게 구현하기 위해 interscetion observer 라는 webAPI를 사용할 수 있습니다.

interscetion observer는 특정 target을 지정해 내가 보고 있는 화면에 target 엘리먼트가 보이고 있는지 관찰하는 API 입니다. 자세한 사용법은 MDN에서 확인할 수 있습니다.

https://developer.mozilla.org/ko/docs/Web/API/IntersectionObserver

 

IntersectionObserver - Web API | MDN

Intersection Observer API의 IntersectionObserver 인터페이스는 대상 요소와 상위 요소, 또는 대상 요소와 최상위 문서의 뷰포트가 서로 교차하는 영역이 달라지는 경우 이를 비동기적으로 감지할 수 있는

developer.mozilla.org

intersection observer API는 리액트방식으로 더욱 간편하게 편하게 사용할 수 있는 라이브러리가 존재 합니다.

react-intersection-observer 라이브러리를 사용하여 간단한 예제를 구현해 보겠습니다.

- 설치

npm i react-intersection-observer

✔ Redux-toolkit 비동기 요청

reducer 코드

import {createSlice} from '@reduxjs/toolkit';
import { loadMovie } from '../actions/movie';

const initialState ={
  loadMovieLoading : false,
  loadMovieDone: false,
  loadMovieError: false,
  movieList: []
};

export const movieSlice = createSlice({
  name: 'movie',
  initialState,
  reducers: {},
  extraReducers: (builder) => builder
    .addCase(loadMovie.pending, (state) =>{
      state.loadMovieLoading = true;
      state.loadMovieDone = false;
      state.loadMovieError = null;
    })
    .addCase(loadMovie.fulfilled, (state, action) =>{
      state.loadMovieLoading = false;
      state.loadMovieDone = true;
      state.movieList = [...state.movieList].concat(action.payload.results)
    })
    .addCase(loadMovie.rejected, (state, action)=>{
      state.loadMovieLoading = false;
      state.loadMovieError = action.error;
    })

})
  • 리덕스 툴킷은 createSlice 사용하여 액션을 생성하고 액션에 따른 reducer 함수를 작성할 수 있습니다.
  • slice내부에서 정의되지 action이나 다른 slice에서 정의된 action을 사용하고 싶을 때 extraReducers에 넣어서 사용할 수 있습니다.
  • 비동기 요청 함수를 createAsyncThunk() 를 사용해 리덕스에서 관리할 수 있습니다.

createAsyncThunk

  • 액션 타입 문자열, 프로미스를 반환하는 비동기 함수, 추가 옵션 순서대로 인자를 받는 함수다.
  • 입력받은 액션 타입 문자열을 기반으로 프로미스 라이프사이클 액션 타입을 생성하고, thunk action creator를 반환한다.
  • thunk action creator: 프로미스 콜백을 실행하고 프로미스를 기반으로 라이프사이클 액션을 디스패치한다.

프로미스 기반의 액션 생성이란 프로미스가 가지는 상태를 기반으로 액션을 생성하고 디스패치 한다는 뜻 입니다.

프로미스의 상태는 간단하게

  • pending : 대기
  • fulfilled :  요청 성공
  • rejected : 요청 실패

이렇게 이해할 수 있으며 프로미스에 대한 자세한 설명은 프로미스에 대한 게시물을 보면 이해할 수 있습니다.

2022.04.02 - [javascript/📖 study] - [10주차 스터디]45장-프로미스

나머지는 코드를 보면서 추가 설명을 하겠습니다.

(1~50개의 페이지 중 랜덤으로 한 페이지의 영화 정보 20개를 받아오는 비동기 함수)

import axios from "axios";
import { createAsyncThunk } from "@reduxjs/toolkit";

function getRandNumber() {
  const ranNum = Math.floor(Math.random() * 50 + 1);
  return ranNum;
}

const url = "https://api.themoviedb.org/3/movie/popular";
const my_api_key = process.env.REACT_APP_MOVIE_API_KEY;

export const loadMovie = createAsyncThunk(
  "get/loadMovie",
  async (data, { rejectWithValue }) => {
    try {
      const response = await axios.get(url, {
        params: {
          api_key: my_api_key,
          page: getRandNumber(),
          language: "en-US",
        },
      });
      return response.data;
    } catch (err) {
      return rejectWithValue(err);
    }
  }
);

위 코드 기준으로 전달 받은 액션 타입 문자열 즉, "get/loadMovie"를 기준으로 프로미스 상태에 따른 thunk action creator를 반환합니다. 구체적으로

  • get/loadMovie/pending
  • get/loadMovie/fulfilled
  • get/loadMovie/rejected

세가지 thunk action createor 가 반환된다고 생각할 수 있습니다. 여기서 디스패치 되면 pending 상태에서 비동기 요청 성공, 실패 에 따라 fulfilled 또는 rejected 액션이 디스 패치 됩니다.

컴포넌트에서 사용할 때는 loadMovie라는 이름으로 액션을 export했으므로

useEffect(() => {
    if(movieList.length === 0){
      console.log('첫 포스트 로딩');
      dispatch(loadMovie());
      return;
    }
  }, []);

이런식으로 dispatch 해주면 됩니다.

 

✔ react-intersection-observer 사용

이 라이브러리 사용법은 정말 간단합니다.

먼저 해당 라이브러리에서 useInView 함수를 가져 옵니다.

이 함수에서 ref와 inView를 가져옵니다.

처음 가져오는 inView 값은 false 이고 사용자의 view에 ref가 보이게 되면 inView가 true로 바뀝니다.

이를 이용해서 inView가 true가 될 때마다 비동기 요청을 보내는 방식으로 구현했습니다.

import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { loadMovie } from "../actions/movie";
import { useInView } from 'react-intersection-observer';

/*
스타일 컴포넌트 생략..
*/

const MovieContainer = () => {
  const { movieList } = useSelector((state) => state.movie); // store에서 movieList를 가져옴
  const dispatch = useDispatch();
  const [ref, inView] = useInView();

  useEffect(() => {
    if(movieList.length === 0){
      console.log('첫 포스트 로딩');
      dispatch(loadMovie());
      return;
    }
  }, []);

  useEffect(()=>{
    if(movieList.length !==0 && inView) {
        console.log('첫 로딩 이후 무한 스크롤');
        dispatch(loadMovie());
      }
  },[inView]);
  
  console.log(movieList);
  return (
    <StyleMovieContainer>
      {movieList.map((item) => (
        <MoiveContent key={item.id} movie={item} />
      ))}
      <div ref={ref} />
    </StyleMovieContainer>
  );
};

export default MovieContainer;

✔ 회고

  • 간단한 프로젝트지만 infinite scroll을 구현 해보는데 의의를 두었고 createAsyncThunk함수를 통한 비동기 요청을 써보는게 재밌었음
  • 프로미스도 복습할 수 있었고 saga를 쓰는 것 보다 확실히 toolkit으로 createAsyncThunk 함수를 쓰는 것이 간편한 것 같다 saga로 했으면 watch 함수 쓰고.. 미들웨어 등록해주고.. 조금 더 복잡해진다. 그래도 결국 동작하는 원리는 크게 다르지 않은 것 같다 saga는 generator 문법을 쓰는데 비교적 익숙한 async / await을 쓰는게 나는 더 편한 듯
  • 사실 무한 스크롤도 직접 디바운스나 쓰로틀 구현해야 의미가 있겠지만 공부하면서 알게된거는 data-fetch를 위해 toolkit에서 RTKQuery라는 함수를 제공한다는 점, 비슷한 라이브러리로 server-state를 관리하는 react-qeury라는 라이브러리가 있다는 점, 지금 토이프로젝트 같은 경우 비동기 요청을 보내고 받아온 결과 상태를 관리하는 것이기 때문에 react-query 같은 라이브러리가 성능에 더 좋은 것 같기도 하다. 이 부분을 좀 더 깊게 공부할 예정!
  • 또 알게 된 것인데 이런 data-list를 최적화 하기 위해 virtual-list를 사용하는 것 같다 이 부분도 시간이 된다면 react-query를 공부하고 나서 구현해 보아야 겠음

 

✔ 참조

https://velog.io/@raejoonee/createAsyncThunk

댓글