무비앱을 리덕스를 사용하여 데이터 흐름 관리해보기
movie-app
├─ env
│ └─ .env
├─ src
│ ├─ index.css
│ ├─ main.jsx
│ ├─ App.css
│ ├─ App.jsx
│ ├─ routes.jsx
│ ├─ components
│ │ ├─ Gnb.jsx
│ │ ├─ Pagination.jsx
│ │ └─ movies
│ │ ├─ detail.jsx
│ │ └─ item.jsx
│ ├─ hooks
│ │ ├─ useFetchMovies.js
│ │ └─ usePagination.js
│ ├─ pages
│ │ ├─ index.jsx
│ │ ├─ cart
│ │ └─ movies
│ │ ├─ [id].jsx
│ │ └─ index.jsx
│ └─ store
│ ├─ index.js
│ └─ modules
│ └─ favoriteMovie.js
├─ index.html
├─ .eslintrc.cjs
├─ .gitignore
├─ .prettierrc.json
├─ package-lock.json
├─ package.json
└─ vite.config.js
리덕스
와 관련해서 보아야 할 폴더구조는 store
이다
당연히 하나의 앱에는 하나의 스토어만이 존재해야 하지만, 데이터의 특성에 따라 나누고 끌어다가 쓸 분리가 필요하다고 생각하여 modules
폴더를 두었다
(사실 당장에는 즐겨찾기 하는 저장소만이 필요해서 이런 분리가 반드시 필요했냐고 묻는다면 대답은 No
이다_그러나 미래를 위해서)
이 모듈들을 index.js
에서 끌어다가 createStore
를 해주면 된다
// src/store/index.js
import { configureStore } from '@reduxjs/toolkit';
import favoriteMovieReducer from './modules/favoriteMovie';
const store = configureStore({
reducer: {
favoriteMovie: favoriteMovieReducer,
},
});
export default store;
configureStore
에는 reducer
뿐만 아니라 middleware
도 설정 가능하다
// src/store/modules/favoriteMovie.js
import { createSlice } from '@reduxjs/toolkit';
export const favoriteMovie = createSlice({
// action.type에 아래의 name + '/addFavorite' 요런식으로 붙어서 실행 됨
name: 'favoriteMovie',
initialState: {
list: [],
},
reducers: {
addFavorite: (state, action) => {
console.log(action);
state.list.push(action.payload);
},
deleteFavorite: (state, action) => {
state.list = state.list.filter(movie => movie.id !== action.payload);
},
},
});
export const { addFavorite, deleteFavorite } = favoriteMovie.actions;
// 여기서의 state.favoriteMovie의 favoriteMovie index에서 createStore에서 설정
export const selectFavoriteMovie = state => state.favoriteMovie.list;
export default favoriteMovie.reducer;
redux-toolkit
의 미친점(?)은 createSlice
의 내부 코드에 immer
라이브러리를 사용하여서 불변성을 지키면서 코드를 작성하지 않아도 된다
Remember: reducer functions must always create new state values immutably, by making copies! It's safe to call mutating functions like Array.push() or modify object fields like state.someField = someValue inside of createSlice(), because it converts those mutations into safe immutable updates internally using the Immer library, but don't try to mutate any data outside of createSlice!
출처 - 리덕스 공식문서
// src/pages/movies/[id].jsx
import { useParams } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { addFavorite, deleteFavorite, selectFavoriteMovie } from '../../store/modules/favoriteMovie';
// Hook
import useFetchMovies from '../../hooks/useFetchMovies';
// Component
import MovieDetail from '../../components/movies/detail';
const MovieDetailPage = () => {
// routes
const { id } = useParams();
// redux
const favoriteList = useSelector(selectFavoriteMovie);
const dispatch = useDispatch();
// TODO: delete line when deploy
console.log(favoriteList);
const { data, isLoading, error } = useFetchMovies('/movie_details.json', `movie_id=${id}`);
if (isLoading) return <p>Loading...</p>;
if (error) console.error(error);
return (
<>
{data ? (
<>
<MovieDetail
item={data?.movie}
dispatch={dispatch}
addFavorite={addFavorite}
deleteFavorite={deleteFavorite}
/>
</>
) : null}
</>
);
};
export default MovieDetailPage;
selectFavoriteMovie
를 모듈에서 가져와서 react-redux
의 useSelector
에 넣어준다
dispatch
를 발생시키려면 react-redux
의 useDispatch()
를 통해 사용가능하다
// src/components/movies/detail.jsx
const MovieDetail = ({ item, dispatch, addFavorite, deleteFavorite }) => {
return (
<div>
<img src={item.background_image_original} alt={`${item.background_image_original}의배경화면`} />
<h3>{item.title}</h3>
<p>{item.year}</p>
<p>{item.description_full}</p>
<p>{item.rating}</p>
<p>{item.runtime}</p>
<img src={item.large_cover_image} alt={`${item.large_cover_image}의커버이미지`} />
<button
onClick={() => {
dispatch(addFavorite(item));
}}>
Add favorite
</button>
<button
onClick={() => {
dispatch(deleteFavorite(item.id));
}}>
Delete favorite
</button>
</div>
);
};
export default MovieDetail;
본 후기는 유데미-스나이퍼팩토리 10주 완성 프로젝트캠프 학습 일지 후기로 작성 되었습니다.