[React] 노마드 코더 [ReactJS로 영화 웹 서비스 만들기] 리덕스와 인피니티 스크롤 적용하여 무한으로 즐기기

이준희·2021년 4월 10일
1

REACT 정복기

목록 보기
1/9

👊🏼 들어가기 앞서

위 글은 기존 노마드 코더님의 강의인 'ReactJS로 영화 웹서비스 만들기'를 바탕으로 공부한 부분을 적용하여 리뉴얼 하였습니다.

기존 클래스형 컴포넌트에서 함수형 컴포넌트로 변경한 후에, 리덕스 및 리듀서를 통해 데이터를 관리할 수 있도록 만들었습니다. 구글 크롬 확장 프로그램인 리덕스-데브툴즈를 통해 콘솔창을 넘어서, 데이터의 흐름을 파악할 수 있습니다.

리듀서와 리덕스-사가에 대한 설정부터 차례대로 나열하기에는 너무 길어져서 기술의 흐름을 우선적으로 다뤄봤습니다.

결과 먼저 확인하기!

gh-pages로 바로가기

redux-devTools 크롬 확장앱을 통해 dispatch와 state의 흐름을 파악할 수 있습니다.

추가된 기술

  • redux-saga를 통한 비동기 데이터 불러오기
  • reducer로 관리하기
  • 인피니티 스크롤(무한스크롤)로 데이터 무한으로 즐기기

폴더 구조

폴더 구조는 기존 구조를 유지하되, reducer와 saga에 대한 폴더를 추가하였고 HomeChanged 파일에 이를 정리하였습니다.

👊🏼 1. 외부 API를 이용하여 리액트로 데이터 표현하기

외부 URL : https://yts.mx/api/v2/list_movies.json

첫 번째로 외부 API를 axios 라이브러리로 불러올 떄 데이터의 구조를 파악하고, 우리가 원하는 데이터를 알맞게 불러올 수 있어야 합니다.

위와 같은 형식으로 외부 URL에 어떤 구조로 데이터가 들어있는 지 확인합니다.

const getMovies = async () => {
    const response = await axios.get('https://yts-proxy.now.sh/list_movies.json?limit=30&&sort_by=download_count');
    console.log(response);
  };

출력한 response는 다음과 같습니다.

우리가 원하는 데이터인 movies 안에 들어 있는 Array(30)에 접근하기 위해서 구조분해할당을 통해 해당 데이터를 변수에 담아 줍니다.

위에서 response 변수에 axios로 불러온 데이터를 담았던 것을 구조분해할당을 통해 movies 변수에 담아줄 것입니다.

const {data: {data: { movies }}} = await axios.get('https://yts-proxy.now.sh/list_movies.json');

👊🏼 2. 리덕스-사가, 리듀서 적용하기

2.1) useEffect 훅 함수로 액션 디스패치하기

  1. 첫 번째로, 화면을 보여주는 view 단에서 useEffect()를 통해 상황을 감지합니다. 아무것도 없는 상태일 때, dispatch를 통해 (데이터를 불러올 액션)LOAD_MOVIES_REQUEST 액션을 실행하게 됩니다.
useEffect(() => {
    dispatch({
      type: LOAD_MOVIES_REQUEST,
    });
  }, [dispatch]);

2.2) saga에서 이를 감지하여 함수 호출하기

  1. 📁 sagas/movies 에서 해당 액션 타입을 감지하고, 영화를 불러오는 함수(loadMovies)를 실행시킵니다.
function* watchLoadMovies() {
  yield takeLatest(LOAD_MOVIES_REQUEST, loadMovies);
}

export default function* movieSaga() {
  yield all([fork(watchLoadMovies)]);
}

2.3) axios로 데이터 fetching 후, 변수에 저장하기

  1. 영화를 불러오는 함수(loadMovies)를 실행하고 결과값을 movies 변수에 저장합니다.
async function loadMoviesAPI() {
  try {
    const {
      data: {
        data: { movies },
      },
    } = await axios.get('https://yts-proxy.now.sh/list_movies.json?limit=10&&sort_by=download_count');
    // 여기서 확인
    console.log(`movies 가져와서 구조분해할당으로 담기`);
    console.log(movies);
    return movies;
  } catch (err) {
    console.error(err);
    return;
  }
}

function* loadMovies() {
  const result = yield call(loadMoviesAPI); // loadMoviesAPI 함수 호출에 의해 return 된 movies 객체
  // 여기서 잘 불러왔는지 확인
  console.log('리턴된 result 출력 :');
  console.log(result);

2.4) try catch로 감싸주기

  1. open api를 불러오기 때문에 실패할 일은 없지만, try catch로 감싸고, 해당 코드를 작성해줍니다.
function* loadMovies() {
  const result = yield call(loadMoviesAPI); // loadMoviesAPI 함수 호출에 의해 return 된 movies 객체
  // 여기서 잘 불러왔는지 확인
  console.log('리턴된 result 출력 :');
  console.log(result);
  try {
    console.log('saga loadMovies start!');
    yield put({
      type: LOAD_MOVIES_SUCCESS,
      data: result,
    });
  } catch (err) {
    console.error(err);
    yield put({
      type: LOAD_MOVIES_FAILURE,
      error: err.response.data,
    });
  }
}

2.5) 리덕스에서 넘긴 값을 리듀서에서 처리하기

  1. 위 작업을 통해 LOAD_MOVIES_SUCCESS 또는, LOAD_MOVIES_FAILURE를 실행시키고 이를 리듀서에서 전달 받습니다.
const movies = (state = initialState, action) =>
  produce(state, (draft) => {
    switch (action.type) {
      case LOAD_MOVIES_REQUEST: {
        ...
      }
      case LOAD_MOVIES_SUCCESS: {
        draft.movies = draft.movies.concat(action.data);  
        // action.data 에는 사가에서 넘겨준 result 값이 담겨있다.
        draft.isLoading = false;  
        // 데이터를 성공적으로 넘겨받을 시, initialStae에서 관리하는 isLoading을 false 로 바꿔준다. 이 조건을 통해 movies를 매핑할 수 있게 된다.
        draft.loadMovieDone = true;
        break;
      }
      case LOAD_MOVIES_FAILURE: {
        draft.loadMovieError = action.error;
        break;
      }
      default:
        return state;
    }
  });

2.6) 렌더링하기

  1. virtual Dom에서 바뀐 state 값을 감지하고 리렌더링 합니다.
return (
    <section className="container">
      {isLoading ? (
        <div>로딩중..</div>
      ) : (
        <div className="movies">
          {movies.map((movie) => (
            <Movie
              key={movie.id}
              id={movie.id}
              year={movie.year}
              title={movie.title}
              summary={movie.summary}
              poster={movie.medium_cover_image}
              genres={movie.genres}
            />
          ))}
        </div>
        // <div>로딩 완료!</div>
      )}
    </section>
  );

👊🏼 3. 인피니티 스크롤 적용하여 데이터 무한으로 즐기기

3.1) 쿼리문 처리하기

오픈 API 또는 백엔드에서 받아오는 데이터를 사용자의 요청에 맞춰 데이터 갯수를 나누어 보내준다면, 초기 렌더링 속도도 높일 수 있고 더 많은 데이터를 처리하는데 서버의 과부하가 적어질 것입니다.

하지만, 오픈 데이터를 사용자의 요청에 따라 dispatch 액션으로 추가적인 요청을 보내기 위해서는 오픈 API에서 제공하는 쿼리에 적용 가능한 속성을 유심히 봐야 합니다.


'

그래서 저는 데이터(limit)를 10개씩 끊어서, 다운로드 순(sort_by=download_count)으로 페이지(page)로 나눠서 받는 쿼리문을 작성하였습니다.

axios.get(`https://yts-proxy.now.sh/list_movies.json?limit=10&&sort_by=download_count&page=${data}`);

3.2) view단 코드에 적용하기

이 페이지 속성(${data})를 useState로 관리하여, LOAD_MOVIES_REQUEST 를 호출 시에, 이 페이지 속성을 한 개씩 더하면서 불러올 수 있도록 구성해 보았습니다.

인피니티 스크롤을 적용한 페이지단의 코드는 다음과 같습니다.

...

const [pageNumber, setPageNumber] = useState(1);

const dispatch = useDispatch();

useEffect(() => {
  dispatch({
    type: LOAD_MOVIES_REQUEST,
    data: pageNumber,
  });
  setPageNumber((pageNumber) => pageNumber + 1);
}, [dispatch]);

const handleScroll = () => {
  const scrollHeight = document.documentElement.scrollHeight;
  const scrollTop = document.documentElement.scrollTop;
  const clientHeight = document.documentElement.clientHeight;
  if (scrollTop + clientHeight >= scrollHeight) {
    console.log(`pageNumber 업데이트  ${pageNumber}`);
    // 페이지 끝에 도달하면 추가 데이터를 받아온다
    dispatch({
      type: LOAD_MOVIES_REQUEST,
      data: pageNumber,
    });
    setPageNumber((pageNumber) => pageNumber + 1);
  }
};

useEffect(() => {
  // scroll event listener 등록
  window.addEventListener('scroll', handleScroll);
  return () => {
    // scroll event listener 해제
    window.removeEventListener('scroll', handleScroll);
  };
}, [pageNumber, dispatch]);

pageNumber의 초기값을 1로 주었고, 처음 아무것도 없는 상태에서 LOAD_MOVIES_REQUESt를 실행한 후, pageNumber를 올려줍니다. 그러면 axios 단에서 다음 페이지에 limit=10 속성과 함께 영화 데이터를 추가적으로 가져올 것입니다.

3.3) 리덕스에 적용하기

추가적으로 액션 타입에 데이터를 붙여줬기 때문에 사가에서도 위 데이터를 받아 쿼리문에 적용해줍니다.

📁/sagas/movies
...

async function loadMoviesAPI(data) {
  // 3. data 안에는 action.data 즉 useState로 관리하는 pageNumber 가 들어있습니다.
  // 4. 추가적으로 요청이 생길 때마다 1, 2, 3, ... 증가하며 데이터를 페이지 별로 나눠 불러올 수 있습니다.
  try {
    const {
      data: {
        data: { movies },
      },
    } = await axios.get(`https://yts-proxy.now.sh/list_movies.json?limit=10&&sort_by=download_count&page=${data}`);
    return movies;
  } catch (err) {
    console.error(err);
    return;
  }
}

function* loadMovies(action) {
  const result = yield call(loadMoviesAPI, action.data); 
  // 1. action.data에는 dispatch 시에 액션 타입과 같이 보낸 data, 즉 pageNumber 가 들어있습니다.
  // 2. 이를 call loadMoviesAPI를 호출하면서 결과값을 result에 담습니다.
  try {
    yield put({
      type: LOAD_MOVIES_SUCCESS,
      data: result,
    });
  } catch (err) {
    console.error(err);
    yield put({
      type: LOAD_MOVIES_FAILURE,
      error: err.response.data,
    });
  }
}

👊🏼 4. 느낀점

기존 노마드 코더님의 강의로 짜여진 코드가 깃허브에 방치되어 있는 것을 보고, '지금 공부하고 있는 것을 적용해보면 어떨까'라고 생각이 들었습니다. 하지만, 백엔드 서버에서 받아오는 데이터 처리는 많이 해봤는데, 리덕스와 리듀서를 적용하면서 거기에 open api를 바탕으로 데이터를 불러오려니 많은 시행착오를 겪었던 것 같습니다.

많은 분들이 입문으로 들었던 강의라고 생각해서, 이를 바탕으로 어떤 걸 적용해보면 좋을까? 라고 생각했는데 추가적으로 배우는 게 있다면 계속 적용해볼 예정입니다.

결과적으로 출력은 되었는데, 아직은 코드가 많이 지저분한 것 같아서 추가적으로 공부하는 부분에 있어서 적용해볼 예정입니다!!

깃허브로만 관리하다가 velog에 글을 처음 써보는데 정말 쉽지 않음을 느끼네요. 제가 다른 개발자분들에게 얻었던 정보들 처럼 저 또한 입문하시는 분들에게, 새로운 정보와 자극이 될 수 있도록 열심히 써보겠습니다.

profile
https://junheedot.tistory.com/ 이후 글 작성은 티스토리에서 보실 수 있습니다.

0개의 댓글