영화 타이틀, 릴리즈 년도, 장르, 평균 평점을 설정하는 각 컴포넌트들을 작성하였다.
각 컴포넌트들은 각자 movieFilterReducer를 통해 state에 값을 저장하고 있으며,
state에 저장된 값들을 전달인자로 하여 movieFilterActions의 getFilteredMovies 함수를 통해
api get 요청을 하도록 한다.
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
에러 메시지에서 알 수 있듯이, useSelector
Hook은 React function component 내부에서나, custom React Hook function 에서만 불러와 질 수 있다.
내가 시도 했던 방법은 일반 function 에서 React Hook을 사용하고자 하였던 것이고, 결과적으로
이는 React Hook 사용의 기본 룰을 위반하는 행위였다.
일반 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을 실행하는 방식으로 접근해 보았다.
// 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 할 수 있도록 구현할 수 있었다.