Web

[Frontend] Redux Toolkit

ㅋ. ㅋ 2024. 4. 1. 13:33

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>
    // ...
  );
}