-
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