[Frontend] Redux Toolkit
store
data 저장소, reducer를 받아야함, subscribe로 변화 감지
action
어떻게 수정할 건지 Object (ex. { type: "ADD" }) 넘겨줌, store.dispatch로 실행
reducer
state랑 action 받아서 state 수정하는 함수 -> action.type 별로 스위치 해서 state변경 수행, state 직접 수정 (mutate) 불가
const reducer = () => {};
const store = createStore(reducer);
store.dispatch(action); // dispatch를 써서 action을 reducer로 전달
store.getState()
store.subscribe(()=>{~~~});
createSlice
reducers와 actions을 한번에 만듦
// oneSlice.ts
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
value: 0,
name: 'one',
};
export const oneSlice = createSlice({
name: 'one',
initialState,
reducers: {
onChangeOneValue: (state, action) => {
state.value = action.payload;
},
onChangeOneName: (state, action) => {
state.name = action.payload;
},
},
});
export const { onChangeOneValue, onChangeOneName } = oneSlice.actions;
export type OneState = typeof initialState;
export default oneSlice.reducer;
rootReducer, configureStore
export interface IRootState {
one: oneState; // oneSlice.ts에서 정의함
two: twoState;
ex: exState;
}
interface HydrateAction extends Action<typeof HYDRATE> {
payload: RootState;
}
// 여러 slice reducer들을 하나로 합침
const combinedReducer = combineReducers({
one: oneReducer,
two: twoReducer,
ex: exReducer,
});
export const rootReducer = (
state: IRootStates,
action: HydrateAction | ReturnType<typeof combinedReducer> // AnyAction Deprecated
) => {
switch (action.type) {
case HYDRATE:
return action.payload;
default: {
return combinedReducer(state, action);
}
}
};
// store
export const store = configureStore({
reducer: rootReducer as Reducer<IRootStates, AnyAction>,
});
/** wrapper store - App에서 redux를 결합한 라이프사이클 사용 가능하도록함 */
export const makeStore = () => {
const store = configureStore({
reducer: rootReducer as Reducer<IRootStates, AnyAction>,
});
return store;
};
export type AppDispatch = typeof store.dispatch
// Export a hook that can be reused to resolve types
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof rootReducer>;
//useSelect시 매번 rootState 타입 지정 안하도록 TypedUseSelector에서 지정한 후 사용
export const useAppSelect: TypedUseSelectorHook<RootState> = useSelector;
const wrapper = createWrapper(makeStore);
export default wrapper;
//_app.tsx
export default wrapper.withRedux(App);
next-redux-wrapper
1절 getInitialProps / getStaticProps / getServersideProps
• wrapper는 makeStore를 이용해 빈 initial state를 가진 서버사이드스토어를 만든다. Request, Response 객체를 옵션으로 makeStore에 제공
• app mode : wrapper는 _app의 getInitialProps를 호출하고 이전에 생성한 store를 전달함, next.js가 _app의 getInitialProps 함수에서 받은 props랑 store state를 가져옴.
• pages mode : wrapper는 page에 getXXXProps 함수를 호출하고, 이전에 만들어놓은 store를 전달함, next는 page의 getXXXProps 함수에서 받은 props랑 스토어의 state를 가져옴
--> server, client 양 쪽에서 리덕스 스토어 생성
--> 서버 사이드 렌더링 시 서버에서 생성된 Redux state를 클라이언트로 전달하고, 클라이언트에서 해당 state를 받아 사용
2절 ssr
• wrapper는 makeStore를 이용해서 새 스토어를 만듦
• 래퍼는 하이드레이트 페이로드에 이전 스토어 스테이트를 갖고 있는 액션을 디스패치함.
• 이 스토어는 프로퍼티로 앱이나 페이지 컴포넌트에 전달됨
• 저 컴포넌트들은 스토의 스테이트들을 변경할 수는 있지만, 변경된 스테이트들은 클라이어트한테 전달되진 않음.. --> 그럼 리딩용으로 부름??
3절 클라이언트
• 래퍼가 새 스토어를 만든다
• 래퍼가 리듀서가 하이드레이트인 경우에, 1절의 스테이트를 페이로드로 가진 액션을 디스패치한다.
• 이 스토어는 앱이나 페이지의 프로퍼티로 전달됨
• 래퍼는 클라이언트의 창(window) 객체에 저장소를 유지하므로 HMR의 경우 복원할 수 있습니다.
--> 하이드레이트 : 초기화, 처음 켰을 때 하이드레이트 타면서 서버에서 넘어온 스테이트 (액션 페이로드에 담겨있음) 이거를 클라이언트 스토어로 설정
정리 !!!
next에서 getXXXProps, SSR 쓸려면 next-redux-wrapper를 써야하는데 그 이유는
서버, 클라이언트 두 군데에서 store가 필요함 --> 하지만 redux는 하나의 store만을 갖도록 되어있음
두군데서 모두 가질 수 있도록 next-redux-wrapper가 도와줌
그럼 어떻게 동작하느냐
먼저 getXXXProps하면 makeStore 이용해서 빈 값의 스토어를 만든다
그리고 여기서 스토어 값 막 설정해줌, 이제 그걸 클라이언트로 전달함
클라이언트에서는 리듀서 탈 때
HYDRATE (단순 HTML -> React Application으로 초기화) 면 action.payload 를 리턴하는데
이 action.payload는 저 위 서버에서 만든 스토어 값임
이제 클라이언트의 스토어가 서버에서 만든 스토어랑 같아짐
Next 14에서는 next-redux-wrapper 대신 Provider 사용
Redux Toolkit Setup with Next.js | Redux
redux.js.org
//StoreProvider.tsx
'use client'
import { useRef } from 'react'
import { makeStore, AppStore } from '@/store/configStore'
import { Provider } from 'react-redux'
export default function StoreProvider({
children
}: {
children: React.ReactNode
}) {
const storeRef = useRef<AppStore>()
if (!storeRef.current) {
// Create the store instance the first time this renders
storeRef.current = makeStore()
}
return <Provider store={storeRef.current}>{children}</Provider>
}
// layout.tsx
import StoreProvider from "@/app/StoreProvider";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
// ...
<StoreProvider> {children}</StoreProvider>
// ...
);
}