challenge_1_filtering

JINBOK LEE·2022년 8월 26일
0

react-netflix-project

목록 보기
1/3
post-thumbnail

❗To do

영화 타이틀, 릴리즈 년도, 장르, 평균 평점을 설정하는 각 컴포넌트들을 작성하였다.

각 컴포넌트들은 각자 movieFilterReducer를 통해 state에 값을 저장하고 있으며,

state에 저장된 값들을 전달인자로 하여 movieFilterActions의 getFilteredMovies 함수를 통해
api get 요청을 하도록 한다.


❗Issue_1

movieFilterReducer의 state에 저장된 각 필터링 값들을 movieFilterActions action에서 api get 요청을 하기 전에, react-redux의 useSelector Hook을 통해 전달받고자 시도하였고 아래와 같은 에러가 발생하였다.

import { useSelector } from "react-redux";
import api from "../api";

const API_KEY = process.env.REACT_APP_API_KEY;

const {
  keyword,
  sortBy,
  withGenres,
  includeVideo,
  releaseDateGte,
  releaseDateLte,
  voteAverageGte,
  voteAverageLte,
} = useSelector((state) => state.movieFilterActions); // Error occurred

function getFilteredMovies(
  keyword,
  sortBy,
  withGenres,
  includeVideo,
  releaseDateGte,
  releaseDateLte,
  voteAverageGte,
  voteAverageLte
) {
  return async (dispatch) => {
    try {
      dispatch({ type: "GET_FILTERED_MOVIES_REQUEST" });

      const FilteredMovies = await api.get(
        `/discover/movie?api_key=${API_KEY}&language=en-US&page=1${
          keyword ? `&with_text_query=${keyword}` : ""
        }${sortBy ? `&sort_by=${sortBy}` : ""}${
          includeVideo ? `&include_video=${includeVideo}` : ""
        }${releaseDateGte ? `&release_date.gte=${releaseDateGte}` : ""}${
          releaseDateLte ? `&release_date.lte=${releaseDateLte}` : ""
        }${voteAverageGte ? `&vote_average.gte=${voteAverageGte}` : ""}${
          voteAverageLte ? `&vote_averag.lte=${voteAverageLte}` : ""
        }`
      );

      dispatch({
        type: "GET_FILTERED_MOVIES_SUCCESS",
        payload: {
          FilteredMoviesJson: FilteredMovies,
        },
      });
    } catch (error) {
      dispatch({ type: "GET_FILTERED_MOVIES_FAILURE", payload: { error } });
    }
  };
}

export const movieFilterActions = {
  getFilteredMovies,
};

위 방식으로 실행한 결과, 아래와 같은 에러가 발생하였다.

React Hook "useSelector" cannot be called at the top level
React Hooks must be called in a React function component or a custom React Hook function

❗Issue_1_cause

에러 메시지에서 알 수 있듯이, useSelector Hook은 React function component 내부에서나, custom React Hook function 에서만 불러와 질 수 있다.

내가 시도 했던 방법은 일반 function 에서 React Hook을 사용하고자 하였던 것이고, 결과적으로
이는 React Hook 사용의 기본 룰을 위반하는 행위였다.

❗Issue_1_trying

일반 javascript function으로 작성한 api request action을 React function으로 전환 한 뒤,
다시 useSelector Hook을 사용하면 되겠다는 생각을 했다.
이후 코드를 아래와 같이 재작성 하였다.

import { useSelector } from "react-redux";
import api from "../api";

const API_KEY = process.env.REACT_APP_API_KEY;

export default function GetFilteredMovies() {
  const {
    keyword,
    sortBy,
    withGenres,
    includeVideo,
    releaseDateGte,
    releaseDateLte,
    voteAverageGte,
    voteAverageLte,
  } = useSelector((state) => state.movieFilterActions);

  return async (dispatch) => {
    try {
      dispatch({ type: "GET_FILTERED_MOVIES_REQUEST" });

      const FilteredMovies = await api.get(
        `/discover/movie?api_key=${API_KEY}&language=en-US&page=1${
          keyword ? `&with_text_query=${keyword}` : ""
        }${sortBy ? `&sort_by=${sortBy}` : ""}${
          includeVideo ? `&include_video=${includeVideo}` : ""
        }${releaseDateGte ? `&release_date.gte=${releaseDateGte}` : ""}${
          releaseDateLte ? `&release_date.lte=${releaseDateLte}` : ""
        }${voteAverageGte ? `&vote_average.gte=${voteAverageGte}` : ""}${
          voteAverageLte ? `&vote_averag.lte=${voteAverageLte}` : ""
        }`
      );

      dispatch({
        type: "GET_FILTERED_MOVIES_SUCCESS",
        payload: {
          FilteredMoviesJson: FilteredMovies,
        },
      });
    } catch (error) {
      dispatch({ type: "GET_FILTERED_MOVIES_FAILURE", payload: { error } });
    }
  };
}

그러나 위의 에러처럼 Invalid Hook call 에러가 발생하였다.

Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app

결국, movieFilterActions action에서 useSelector hook을 이용하여 한번에
데이터를 받아오는 방식으로 접근 하는 것은 구조적으로 문제가 있다고 판단하여
필터링을 담당하는 각 컴포넌트에서 useSelector hook을 이용해 데이터를 받아오고,
그것을 movieFilterActions action에 전달인자로 전달하여
api call을 실행하는 방식으로 접근해 보았다.

❗Solve

// MovieFilterInput 컴포넌트 예시

import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { createTheme, ThemeProvider, styled } from "@mui/material/styles";
import TextField from "@mui/material/TextField";
import MuiToggleButton from "@mui/material/ToggleButton";
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
import { movieFilterActions } from "../redux/actions/movieFilterActions";

const MovieFilterInput = ({ show }) => {
  useEffect(() => {
    dispatch({ type: "RESET_FILTERED_MOVIES_STORE_SUCCESS" });
  }, []);

  const [
    keyword,
    sortBy,
    withGenres,
    includeVideo,
    releaseDateGte,
    releaseDateLte,
    voteAverageGte,
    voteAverageLte,
  ] = useSelector((state) => [
    state.movieFilter.keyword,
    state.movieFilter.sortBy,
    state.movieFilter.withGenres,
    state.movieFilter.includeVideo,
    state.movieFilter.releaseDateGte,
    state.movieFilter.releaseDateLte,
    state.movieFilter.voteAverageGte,
    state.movieFilter.voteAverageLte,
  ]);

  const dispatch = useDispatch();

  const theme = createTheme({
    palette: {
      primary: {
        light: "#ff5f52",
        main: "#c62828",
        dark: "#8e0000",
        contrastText: "#ffffff",
      },
      secondary: {
        light: "#83312c",
        main: "#510002",
        dark: "#310000",
        contrastText: "#aaaaaa",
      },
    },
  });

  const ToggleButton = styled(MuiToggleButton)({
    "&.MuiToggleButton-root": {
      fontWeight: "bold",
      color: "white",
      backgroundColor: theme.palette.secondary.dark,
      transition: ".3s",
    },
    "&.MuiToggleButton-root:hover": {
      backgroundColor: theme.palette.primary.dark,
      transition: ".3s",
    },
    "&.Mui-selected,&.Mui-selected:hover": {
      backgroundColor: theme.palette.primary.main,
      transition: ".3s",
    },
  });

  const [select, setSelect] = React.useState("ALL");

  const handleselect = (event, newSelect) => {
    setSelect(newSelect);
  };

  const test = (toggle) => {
    dispatch({
      type: "INCLUDE_MOVIE_VIDEO_TOGGLE_SUCCESS",
      payload: toggle,
    });
  };

  return (
    <>
      <ThemeProvider theme={theme}>
        <div className="searchBar">
          <h2>SEARCH</h2>
          <ToggleButtonGroup
            color="primary"
            value={select}
            exclusive
            onChange={handleselect}
            size="small"
            aria-label="Include Movie Video"
          >
            <ToggleButton
              value="ALL"
              aria-label="ALL"
              onClick={() => test(false)}
            >
              ALL
            </ToggleButton>
            <ToggleButton
              value="Include Movie Video"
              aria-label="Include Movie Video"
              onClick={() => test(true)}
            >
              Include Movie Video
            </ToggleButton>
          </ToggleButtonGroup>
        </div>
        <TextField
          id="search_input"
          variant="filled"
          label="Movie Title"
          color="primary"
          sx={{ width: "310px" }}
          onKeyPress={(e) => {
            if (e.key === "Enter") {
              show(false);
              dispatch({
                type: "SEARCH_KEYWORD_STORE_SUCCESS",
                payload: e.target.value,
              });

              dispatch(
                movieFilterActions.getFilteredMovies(
                  keyword,
                  sortBy,
                  withGenres,
                  includeVideo,
                  releaseDateGte,
                  releaseDateLte,
                  voteAverageGte,
                  voteAverageLte
                )
              );
            }
          }}
        />
      </ThemeProvider>
    </>
  );
};

export default MovieFilterInput;
// movieFilterActions 예시

import api from "../api";

const getFilteredMovies = (
  keyword,
  sortBy,
  withGenres,
  includeVideo,
  releaseDateGte,
  releaseDateLte,
  voteAverageGte,
  voteAverageLte
) => {
  
  const API_KEY = process.env.REACT_APP_API_KEY;

  return async (dispatch) => {
    try {
      dispatch({ type: "GET_FILTERED_MOVIES_REQUEST" });

      const FilteredMovies = await api.get(
        `/discover/movie?api_key=${API_KEY}&language=en-US&page=1${
          keyword ? `&with_text_query=${keyword}` : ""
        }${
          includeVideo ? `&include_video=${includeVideo}` : ""
        }${releaseDateGte ? `&release_date.gte=${releaseDateGte}` : ""}${
          releaseDateLte ? `&release_date.lte=${releaseDateLte}` : ""
        }${voteAverageGte ? `&vote_average.gte=${voteAverageGte}` : ""}${
          voteAverageLte ? `&vote_average.lte=${voteAverageLte}` : ""
        }`
      );

      dispatch({
        type: "GET_FILTERED_MOVIES_SUCCESS",
        payload: {
          FilteredMoviesJson: FilteredMovies,
        },
      });
    } catch (error) {
      dispatch({ type: "GET_FILTERED_MOVIES_FAILURE", payload: { error } });
    }
  };
};

export const movieFilterActions = {
  getFilteredMovies,
};

위와 같은 접근 방식으로 다시 시도해 보니, 원하던대로 다중 필터값들을 만족시키며 해당하는 데이터를 get 할 수 있도록 구현할 수 있었다.

profile
깔끔한 비즈니스 로직 설계를 위해 공부하는 FE 개발자

0개의 댓글