기록
0422: redux toolkit 본문
지금부터 리덕스 툴킷 도입해서 데이터를 관리해야겠다.
velog.io/@velopert/using-redux-in-2021
Redux 어떻게 써야 잘 썼다고 소문이 날까?
40% 이상의 개발자들이 리액트 프로젝트에서 사용하고 있는 리덕스(Redux), 더 잘 쓰는 방법을 알려드립니다. 리덕스 말고 다른 선택지, 그리고 리덕스를 사용 할 떄 알고있으면 유용한 팁들을 다
velog.io
이 글을 정독!
여기서 잠깐, 오해할 만한 부분이 있는데 Context 는 리덕스와의 비교 대상이 아닙니다. Context는 수단일 뿐 사실상 상태관리 자체는 리액트 컴포넌트의 useState와 useReducer 로 하게 되는 것 입니다.
리덕스와의 주요 차이는 성능 면에서 나타나게 됩니다.
리덕스에서는 컴포넌트에서 글로벌 상태의 특정 값을 의존하게 될 때 해당 값이 바뀔 때에만 리렌더링이 되도록 최적화가 되어있습니다. 따라서, 글로벌 상태 중 의존하지 않는 값이 바뀌게 될 때에는 컴포넌트에서 낭비 렌더링이 발생하지 않겠지요.
반면 Context에는 이러한 성능 최적화가 이뤄지지 않았습니다. 컴포넌트에서 만약 Context의 특정 값을 의존하는 경우, 해당 값 말고 다른 값이 변경 될 때에도 컴포넌트에서는 리렌더링이 발생하게 됩니다.
따라서, Context 를 사용하게 될 때에는 관심사의 분리가 굉장히 중요합니다. 서로 관련이 없는 상태라면 같은 Context 에 있으면 안됩니다. Context를 따로 따로 만들어주어야하죠.
Redux Toolkit을 사용하면 리듀서, 액션타입, 액션 생성함수, 초기상태를 하나의 함수로 편하게 선언 할 수 있습니다. 이 라이브러리에선 이 4가지를 통틀어서 slice 라고 부릅니다.
이 라이브러리를 사용하면 이렇게 리듀서와 액션 생성 함수를 한방에 만들 수가 있답니다. 그리고, Immer가 내장되어있기 때문에, 불변성을 유지하기 위하여 번거로운 코드들을 작성하지 않고 원하는 값을 직접 변경하면 알아서 불변셩 유지되면서 상태가 업데이트 됩니다.
import { createSlice } from '@reduxjs/toolkit';
const msgboxSlice = createSlice({
name: 'msgbox',
initialState: {
open: false,
message: '',
},
reducers: {
open(state, action) {
state.open = true;
state.message = action.payload
},
close(state) {
state.open = false;
}
}
});
export default msgboxSlice;
reducers 라는 속성 하나로 액션타입, 액션생성함수, 리듀서 3가지를 한 번에 동작시킬 수 있다.
1. 필요한 패키지 설치
npm i react-redux redux
npm i redux-saga
npm i @reduxjs/toolkit
npm i -D redux-loggernpm i -D redux-devtools-extension 툴킷은 디폴트로 데브툴 제공
+) redux 공식 페이지 읽다가 알게 되었는데, CRA를 --template redux로 설치할 수도 있다.
폴더구조를 어떻게 할지..?
최상단 index.js 에서 const store = createStore()로 store 생성하는게 제일 먼저..
였는데 툴킷은 또 다르겠지
reducer, saga 폴더를 각각 만들고 index.js + @ 이렇게?
아니면 그냥 store 폴더에 몰빵?
예전에는 actions, reducers, constants 등등 각 기능 별로 폴더를 나누는 것을 권장했다면,
redux-toolkit에서 actions와 reducer등을 한 방에 만들어주는 redux-toolkit의 createSlice가 나오면서
features/ 폴더에 각 화면 별로 폴더를 나누고 그 폴더 안에
관련 컴포넌트 파일, actions와 reducer가 들어있는 slice.js 파일, 그리고 saga.js 파일을 한꺼번에 묶는 것이 권장되고 있다.
출처: https://im-developer.tistory.com/195
2. src/store/index.js & search.js 생성
늘 그랬듯이 index.js에서 리듀서들을 모아주고
각 리듀서에서 여기에 상태 관리를 위한 모든 것.. 초기상태, 액션, 액셩생성함수, 리듀서
일단 search 리듀서만 만들었는데, 더 추가될 수 있음.
// search.js
import { createSlice } from '@reduxjs/toolkit';
// 초기 상태
export const initialState = {
mainList: [], // 현재 화면에 그려진 영화 목록
searchList: [], // API 요청을 통해 받아온 영화 목록(최대 100개)
searchMovieLoading: false, // 영화 검색 시도 중
searchMovieDone: false,
searchMovieError: null,
loadMoreLoading: false, // 10개 더 불러오기 시도
loadMoreDone: false,
loadMoreError: null,
};
// 액션 타입
export const SEARCH_REQUEST = 'SEARCH_REQUEST';
export const SEARCH_SUCCESS = 'SEARCH_SUCCESS';
export const SEARCH_FAILURE = 'SEARCH_FAILURE';
export const LOADMORE_REQUEST = 'LOADMORE_REQUEST';
export const LOADMORE_SUCCESS = 'LOADMORE_SUCCESS';
export const LOADMORE_FAILURE = 'LOADMORE_FAILURE';
// toolkit
const searchSlice = createSlice({
name: 'search',
initialState,
reducers: {
SEARCH_REQUEST(state) {
state.searchMovieLoading = true;
},
SEARCH_SUCCESS(state, action) {
state.searchMovieLoading = false;
state.searchMovieDone = true;
state.searchList = action;
},
SEARCH_FAILURE(state, action) {
state.searchMovieError = action.error;
},
},
});
export default searchSlice;
+) 액션 타입 적은 것들은 지우기
/store/index.js
import { configureStore } from '@reduxjs/toolkit';
import searchSlice from './search';
export const store = configureStore({
reducer: {
search: searchSlice,
},
});
음..? 헷갈린다 어떻게 배치해야하지
2.1 다시 공부
velog.io/@djaxornwkd12/Redux-Toolkit-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0
Redux Toolkit 알아보기
오늘은 리덕스 툴킷에 대해 알아보자~! Redux Toolkit이란 사용이유 Redux Toolkit은 Redux를 더 사용하기 쉽게 만들기 위해 Redux에서 공식 제공하는 개발도구이다. Redux Toolkit은 아래와 같은 Redux의 문제점
velog.io
툴킷 자체는 이 글이 가장 잘 나와있다.
+)configureStore는 toolkit에서 리덕스 createStore를 베이스로하여 새롭게 만들어준 기능입니다.
createStore를 사용하면 devTools를 이용할때는 composeWithDevTools를 넣어줘야하고,
미들웨어를 사용할때도 applyMiddleware도 넣어줘야 합니다.
configureStore를 이용한다면 간단하게 리듀서와 미들웨어를 쉽게 넣어줄 수 있습니다.
cofigureStore는 Redux DevTools와 리덕스 미들웨어를 포함하고 있어서 devtools는 따로 넣어주지 않아도 알아서 들어감
사가랑 리듀서를 한 곳에 할지,,
노드버드 때처럼 분리할지..
둘다 해보지뭐~!
일단 합친 상태로 해보자.
위에도 썼지만, 아래 글을 따라서 해보자.
예전에는 actions, reducers, constants 등등 각 기능 별로 폴더를 나누는 것을 권장했다면,
redux-toolkit에서 actions와 reducer등을 한 방에 만들어주는 redux-toolkit의 createSlice가 나오면서
features/ 폴더에 각 화면 별로 폴더를 나누고
그 폴더 안에 관련 컴포넌트 파일,
actions와 reducer가 들어있는 slice.js 파일,
그리고 saga.js 파일을 한꺼번에 묶는 것이 권장되고 있다.
(이러한 패턴을 ducks 패턴이라고 한다.)
src/api/
src/components/
src/features/
ImageGrid/
index.js
slice.js
saga.js
src/store/
src/App.js
src/index.js
출처: https://im-developer.tistory.com/195
즉, 사가 리듀서 각각을 한 곳에 적고 index에서 컴바인만 해라?
너무 길어질 것 같다.
노드버드에서 하던 대로 saga 폴더를 따로 만들고,
store/index.js에서 이 둘을 합쳐야겠다?
saga를 노드버드에서 했던 틀을 사용했는데, 문제가 생겼다..
function searchMovieAPI(data) {
return axios.get(``, data);
}
function* searchMovie(action) {
try {
const result = yield call(searchMovieAPI, action.data);
yield put({
type: SEARCH_SUCCESS,
data: result.data,
});
} catch (err) {
이렇게 searchMovie에서 searchMovieAPI로 api 요청을 보내야하는데,
axios
.get(`/v1/search/movie.json?query=${keyword}&display=100`, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-Naver-Client-Id': REACT_APP_CLIENT_ID,
'X-Naver-Client-Secret': REACT_APP_CLIENT_SECRET,
},
})
.then((res) => {
console.log(res);
})
.catch((error) => console.dir(error));
axios 보낼 때 이런 형태로 보내야하니까,, 복잡해지겠는데,,,
keyword를 받아서 url에 전달해야하는데, 어떻게 하지...?
저 axios 코드를 통으로 api함수에 넣으려고 했는데, 이 함수는 data를 받아오잖아? 그 데이터
아아아 그 데이터를 저 keyword에 넣으면 될 것 같은데?
// searchForm
const onSubmit = (e) => {
e.preventDefault();
console.log(keyword);
dispatch({
type: SEARCH_REQUEST,
data: keyword,
});
};
dispatch 할 때 data로 keyword를 담았음.
확실한 건, api함수들을 따로 빼야겠다. api/search 이런 식으로..
일단 이렇게 테스트하기로 하고,
제일 중요한 App, index 세팅
logger까지.
2.2 세팅
일단 지금까지 확실한 내용을 정리하자면,
1. saga 미들웨어를 사용하려면,
const sagaMiddleware = createSagaMiddleware();
2. 그리고 툴킷에서 store를 만들 땐 createStore가 아니라 configureStore
const store = configureStore({
reducer: rootReducer,
middleware: [...getDefaultMiddleware({ thunk: false }), sagaMiddleware, logger],
});
(thunk를 설치없이 디폴트로 제공하니까 false로 하는 듯)
3. 그리고 마지막으로 saga 미들웨어를 실행시켜야 모니터링을 한다고 함.
sagaMiddleware.run(rootSaga);
지금 이거를 최상단 index.js에 넣고 있는데, 그냥 configureStore.js로 만들어서 store를 가져오는게 더 간단할 것 같다.
// index.js
import store from './redux/configureStore';
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root'),
);
// src/redux/configureStore.js
import createSagaMiddleware from 'redux-saga';
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import rootReducer from '../store';
import rootSaga from '../saga';
import logger from 'redux-logger';
const sagaMiddleware = createSagaMiddleware();
// const middleware = [sagaMiddleware];
const store = configureStore({
reducer: rootReducer,
middleware: [...getDefaultMiddleware({ thunk: false }), sagaMiddleware, logger],
});
sagaMiddleware.run(rootSaga);
export default store;
+) 또는 리듀서, 사가 폴더에 index 만들어서 combine 하지 말고,
아예 리덕스+사가 합치고 있는 폴더에서 한 번에 작성하는 방법도 있다.
import {
combineReducers,
configureStore,
getDefaultMiddleware,
} from '@reduxjs/toolkit';
import todosReducer from './todos';
import createSagaMiddleware from 'redux-saga';
import { all } from 'redux-saga/effects';
import { todosSaga } from './todosaga';
const rootReducer = combineReducers({
todos: todoReducer,
})
function* rootSaga() {
yield all([todoSaga()]);
}
const sagaMiddleware = createSagaMiddleware();
const store = configureStore({
reducer: rootReducer,
middleware: [...getDefaultMiddleware(), sagaMiddleware],
})
sagaMiddleware.run(rootSaga);
export type RootState = ReturnType<typeof store.getState>;
export default store;
여기서 rootSaga, rootReducer 를 작성하고, store에서 연결
지금 궁금증
1. 개발/배포 모드는 어떻게 작성할지
2. 그냥 리덕스에서 하던 요청/성공/실패 이렇게 나누던 걸 툴킷에서는 정확히 어떻게 나누는지..
3. 사가에서 put( { type: 어쩌구 이렇게 하려면 액션타입을 선언해야하는데,,
그거를 사가파일 안에서 하면 되나?
더 편하자고 툴킷 쓰는 건데 더 모르겠다ㅜㅜ
와 이것저것 막 누르다보니까 일단 리덕스랑 사가 연결은 성공함..
디테일에 문제가 있는 듯..
const onSubmit = useCallback(
(e) => {
e.preventDefault();
console.log(keyword);
// SEARCH_REQUEST({ keyword });
dispatch({
type: SEARCH_REQUEST,
data: keyword,
});
},
[dispatch, keyword],
);
원래 툴킷은 slice에서 액션타입, 액션생성함수, 리듀서를 한 번에 처리하는데,
나는 지금 그냥 리덕스 방식이랑 혼용해서 사용하고 있다.
reducer에서 액션타입을 선언할 필요도 없고,
저렇게 dispatch 객체 형식으로 보내는게 아니라, 함수 형식으로 보내야하는데,
일단 saga에서 result.data가 아니라 그냥 result로 했더니,
const SearchForm = () => {
const [keyword, setKeyword] = useState('');
const dispatch = useDispatch();
const onChange = useCallback((e) => {
setKeyword(e.target.value);
}, []);
const onSubmit = useCallback(
(e) => {
e.preventDefault();
console.log(keyword);
// SEARCH_REQUEST({ keyword });
dispatch({
type: SEARCH_REQUEST,
data: keyword,
});
},
[dispatch, keyword],
);
return (
<>
<form onSubmit={onSubmit}>
<input value={keyword} onChange={onChange} placeholder="영화 제목을 입력하세요!" />
{/* <Link to={`/search?q=${keyword}`}>
<button>검색</button>
</Link> */}
</form>
</>
);
};
SUCCESS가 뜨긴 했는데, data는 전달되지 않은 것으로 보인다.
초기 상태값에도 변화가 없다..
그래도 네이버 api에서 데이터는 잘 불러와진다.
'TIL*' 카테고리의 다른 글
0503: styled-components 공부 (0) | 2021.05.04 |
---|---|
0423: api 수정 (0) | 2021.04.23 |
0420: 검색한 결과 화면에 그리기 (0) | 2021.04.21 |
0419: 영화검색 (0) | 2021.04.19 |
SSR CSR (0) | 2021.04.16 |