ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Infinite Scroll 구현
    위코드x원티드 2021. 7. 28. 08:56

    Scroll Event 를 이용해서 구현하려면, 리스너가 자주 호출되고, 메인 스레드를 사용하기 때문에 성능에 좋지 않다

     

    따라서 Web API 중 1인 Intersection Observer API 를 사용한다. 이는 타겟 요소와 상위 요소/최상단 document 의 viewport 와의 교차 관련 변화를 관찰하는 비동기적인 방법이다

     

    Intersection Observer API 는 다음의 상황에 콜백함수를 호출한다

    - 타겟 요소가 기기의 뷰포트/특정 요소와 교차할때. 여기서 특정 요소는 root 요소 혹은 root

    - Observer 가 최초에 타겟 요소를 관찰하라고 요청받았을 때

     

    1. Intersection Observer 의 생성

    옵션과 콜백함수를 생성자에 넘겨준다.

    let options = {
      root: document.querySelector('#scrollArea'),
      rootMargin: '0px',
      threshold: 1.0
    }
    
    let observer = new IntersectionObserver(callback, options);

    옵션의 3개요소, root, rootMargin, threshold 를 설정해 observer 의 콜백이 호출될 환경을 설정한다. 

    - root 는 타겟 요소가 교차하는 지를 확인할 뷰포트로 사용될 요소. 디폴트는 브라우저의 뷰포트로, null 값 주면 됨

    - rootMargin 은 root 를 둘러쌀 마진. 디폴트는 0.

    - threshold 는 root 요소에 타겟 요소가 100% 보일 때 콜백이 호출되려면 1, 조금이라도 보일 때 호출되려면 0.

     

    타겟요소가 Intersection Observer 에 정한 threshold 를 만족할 때마다 콜백이 호출된다. 콜백은 Intersection Observer Entry 객체 리스트와, 그 observer 를 인자로 받는다. 인자로 받는 entries 리스트는 각 타겟 요소 당 1개의 entry 를 포함한다(교차상태 해당하는 거). entry 가 root 와 교차됐는지를 확인하려면 isIntersecting 프로퍼티를 체크해주면 된다

    let callback = (entries, observer) => {
      entries.forEach(entry => {
        // Each entry describes an intersection change for one observed
        if (entry.isIntersecting) {
        }
      });
    };

     

    state 를 페이지, 리스트, 타겟요소로 설정하고, useEffect 훅을 이용해 호출한다.

     

    🙌 코드 재구현

    ytx 영화 목록 API (https://yts.mx/api) 를 사용해 무한 스크롤을 다시 한 번 구현하였다

    Redux 와 custom Hook, typescript 를 사용하여, 이전에 수행한 과제보다 더 많은 기술과 접목해 확장했다. 이전 과제에서 피드백 받은 polyfill 도 추가가 필요하다

     

    - Redux 를 이용해, movie State 에 movies (영화 데이터), page (페이지), status (로딩/성공 등) 을 관리한다

    - 비주얼 컴포넌트로 MovieContainer, MovieList, MovieItem 컴포넌트를 만든다. 각 컴포넌트에 필요한 메소드는 커스텀 훅 useMovie 에 작성 (최초에 movies 데이터를 호출하는 로직, Intersection Observer 관련 로직) 해 넘긴다

     

    ✔ 최초에 영화 데이터 호출하기

    - useMovie Hook 에서는 Redux state 로 작성한 movies, status, error, page state 를 가져온다

    const { movies, status, error, page } = useSelector(selectMovies);
    // const selectMovies = (state: RootState) => state.movies;

    - useMovie Hook 이 mount 되는 순간, api 호출 메소드를 실행한다. 메소드 호출 시, page state 를 넘겨준다

    ( Redux movie 관련 모듈을 만들 때, page state 는 initialState 1 로 설정하였다 )

    useEffect(() => {
      getMovies(dispatch, page);
    }, []);

    - api 호출 관련 메소드는 MovieService.ts 파일에 작성하였다. Redux movie 모듈 하위에 비동기 로직을 처리하는 메소드 getMovies 를 만들고, MovieService.getMovies 를 호출해 Redux movie state 에 영화 데이터를 받아온다

    // redux movie 모듈 movies.ts 파일
    export const getMovies = async (dispatch: Dispatch, page: number) => {
      try {
        dispatch(getMoviesStart(null));
        const data = await MovieService.getMovies(page);
        const moviesOrigin = data?.data.movies;
        const movies = moviesOrigin.map((movie: any) => ({
          id: movie.id,
          ....
        }));
        dispatch(getMoviesSuccess(movies));
      } catch {}
    };
    // MovieService 파일
    const MOVIE_API_URL = 'https://yts.mx/api/v2/list_movies.json';
    
    export default class MovieService {
      public static async getMovies(page: number) {
        const limit = 15;
        try {
          const response = await fetch(MOVIE_API_URL + `?limit=${limit}&page=${page}`);
          const data = response.json();
          return data;
        } catch (error) {
          return null;
        }
      }
    }

     

    ✔ page state 관리하기

    - page state 변화는 따로 관리하기 보다는, getMoviesSuccess 리듀서 메소드가 동작하는 로직 안에, page state +1 하는 로직을 넣어두었다

    // movies.ts 파일
    const movieSlice = createSlice({
      name: 'movie',
      initialState,
      reducers: {
        getMoviesSuccess(state, action) {
          const movies = action.payload;
          state.movies = state.movies ? state.movies?.concat(movies) : movies;
          state.status = Status.Success;
          state.page = state.page + 1;
        },
        ....
      },
    });

     

    ✔ 교차를 감지할 ref 설정하기

    - useMovie Hook 에서 교차를 감지할 ref 를 설정하고, 이를 export 해 MovieList 컴포넌트로 넘긴다. MovieList 컴포넌트에서는 MovieItem 이 모두 로드되고 난 가장 아래부분에 빈 div 를 두고 여기에 넘겨받은 target ref 를 설정한다

    // useMovie.ts (Custom hook 파일)
    const target = useRef<HTMLDivElement>(null);
    // MovieList.tsx 파일
      return (
        <ul>
          {movies?.map((movie) => (
            <Item title={movie.title} img={movie.img} year={movie.year} ... />
          ))}
          <div ref={target} />
        </ul>
      );

     

    ✔ Intersection Observer 설정하기

    - useMovie Hook 에서 dependency array [page] 인 useEffect 에 Intersection Observer 를 설정해준다. observer 가 동작하고 난 다음에는 연결을 해제해줘야 한다

    - 최초에 여러 번 호출되기 때문에 movies length 가 있을 때라는 예외처리 조건을 건다 (...)

      useEffect(() => {
        const options = {
          root: null,
          rootMargin: '20px',
          threshold: 1,
        };
    
        let observer: IntersectionObserver;
        if (target && movies?.length) {
          observer = new IntersectionObserver(onInterSecting, options);
          observer.observe(target.current as Element);
        }
        return () => observer?.disconnect();
      }, [page]);

    - root 와 target 이 교차 시 호출될 메소드에는, 다시 다음 page 의 movies 목록을 가져오는 메소드를 넣는다

      const onInterSecting = (entries: any) => {
        entries.forEach((entry: any) => {
          if (!entry.isIntersecting) {
            return;
          }
          getMovies(dispatch, page);
        });
      };

    재구현 코드: https://github.com/salybu/movie-web-app

    '위코드x원티드' 카테고리의 다른 글

    React Router  (0) 2021.09.06
    쉅정리 실행 컨텍스트, Closure, Hoisting, Scope, this  (0) 2021.08.18
    브라우저 동작 원리  (0) 2021.08.14
    Event Loop  (0) 2021.08.14
    수업 정리  (0) 2021.08.08

    댓글