업데이트:

카테고리:

/

태그: ,

Redux란?

상태관리 라이브러리

  • component 안에서 데이터를 전달하려면 state를 바꾼다.
  • State 는 바꿀 수 있고, 바꿀 때 마다 리랜더링이 된다.

Data Flow

Untitled

이벤트 발생 → dispatch로 보냄 → Store에서 State를 변경 → UI 변경

  1. Action
    • Js 객체
    • type : 수행하는 작업의 유형을 지정
    • payload : 선택적으로 데이터를 보내는데 사용
  2. Reducer
    • 애플리케이션의 상태의 변경 사항을 결정하고 업데이트의 상태를 반환
    • action object, return 으로next state를 반환
    • store의 내부의 상태를 업데이트 한다.
  3. Redux Store
    • 앱의 전체 상태 트리를 보유한다.
    • 변경이 발생하면 action으로 객체를 보낸ㄴ다.
    • Method가 있는 객체

설치 : npm install redux --save

앱 생성 : npx create-react-app ./ --template typescript → 타입스크립트 기반으로 앱 생성

Reducer

store에 들어갈 state와 state를 변경할 함수를 정의하는 곳

불변성을 지켜야되는 이유 → state가 변경되면 redux가 인식하여 해당 state를 사용하는 컴포넌트에 리랜더링을 요구

reducers 폴더 생성

리듀서 코드

// 초기값은 0, action의 타입은 문자형으로 들어온다.
const couter = (state = 0, action: {type: string}) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      break;
  }
}

export default couter;
// 액션이름앞에 파일 이름 넣기
// 액션 타입 설정
export const INCRESE = "COUNT/INCRESE";

// 타입과 사용할 reducers를 정의
// INCRESE를 부르면, count 변수가 불러나온다.
export const increaseCount = count => ({ type: INCRESE, count});

// 초기값 설정
const initalState = {
	count: 0
};

// Reducer 설정
const counter = (state = initalState, action) => {
	switch (action.type) {
		case INCRESE:
			return {
				...state,
				count: action.count + 1
			};

		default:
			return state;
	}
}

앱에 리덕스 적용하기

import { createStore } from 'redux';
import couter from './reducers'

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

// reducer를 불러온다.
const store = createStore(couter);

const render = () => root.render(
  <React.StrictMode>
		// app 컴포넌트에 적용
    <App 
      value={store.getState()}
      onIncrement={() => store.dispatch({type: "INCREMENT"})}
      onDecrement={() => store.dispatch({type: "DECREMENT"})}
      />
  </React.StrictMode>
);

// 실시간으로 반영
render();
store.subscribe(render)

Combine Reuducer

여러가지 reducers를 파일로 만들어주고 index 파일에서 combine 시켜준다.

import { combineReducers } from "redux";
// import todos from './todos';
import counter from './counter'

// 컴바인 시킬 리듀스 함수
const rootReducer = combineReducers({
  counter
})

export default rootReducer;

store.dispatch 를 통해서 해당하는 타입에 원하는 데이터를 보낸다.

Provider

모든 요소에 대해서 Store에 있는 State의 접근을 할 수 있도록 하는 것

해당 모듈은 react-redux에 존재

설치 : npm install react-redux --save

최상위 컴포넌트에 <Provider> 로 감싸준다.

<React.StrictMode>
		// store 속성에 store를 넣어준다.
    <Provider store={store}>
      <App 
        value={store.getState()}
        onIncrement={() => store.dispatch({type: "INCREMENT"})}
        onDecrement={() => store.dispatch({type: "DECREMENT"})}
        />
    </Provider>
  </React.StrictMode>

접근

useSelector : 스토어의 값을 가져온다.

// reducers/index.tsx 의 하단
// state의 타입을 정해주기 위해서 사용
export type RootState = ReturnType<typeof rootReducer>;

// todos의 데이터를 가져옴
const todos: string[] = useSelector((state: RootState) => state.todos);

useDispath : 스토어에 데이터를 보낸다.

const dispatch = useDispatch();

const addTodo = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
		// dispatch의 원하는 함수와 데이터를 보내어 store에 저장한다.
    dispatch({type: "ADD_TODO", text: todoValue});
    setTodoValue("");
  }

Redux 미들웨어

액션을 dispatch에 전달하고 리듀서에 도달하는 순간 지정된 작업을 실행한다.

로깅, 충돌보고, 비동기 API, 통신, 라우팅등을 위해 사용

import { createStore, applyMiddleware } from 'redux';

// 실행할 미들웨어 설정
const loggerMiddleware = (store: any) => (next: any) => (action: any) => {
  console.log("store", store)
  console.log("action", action)
  next(action)
}

// 미들웨어 적용 및 store에 추가
const middleware = applyMiddleware(loggerMiddleware);
const store = createStore(rootReducer, middleware);

Redux thunk

설치: npm install redux-thunk --save

웹에서 비동기 작업을 실시할 때 많이 사용

useEffect(() => {
		// 해당 부분에 에러가 발생한다.
    dispatch(fetchPosts());  
  }, [dispatch]);
  
  const fetchPosts = (): any => {
    return async function fetchPostsThunk(dispatch: any, getState: any) {
      const response = await axios.get("https://jsonplaceholder.typicode.com/posts");
      dispatch({type: "FETCH_POSTS", payload: response.data})
    }
  }

⇒ Actions 는 객체여야 하는데 함수를 dispatch하고 있어서 그렇다! 그래서 thuck미들 웨어를 통해 함수를 dispatch 할 수 있도록 하는 것

thunk 적용후

// index.tsx에 적용
const middleware = applyMiddleware(thunk, loggerMiddleware);

react-toolkit

  1. toolkit에서 제공되는 configureStore 을 통해서 먼저 store를 제작한다.
     export const store = configureStore({
       reducer: {
            
       },
     })
    
  2. 스토어를 react에 제공

     import { Provider } from 'react-redux';
     import { store } from './app/store';
    
     root.render(
       <React.StrictMode>
         <Provider store={store}>
           <App />
         </Provider>
       </React.StrictMode>
     );
    
  3. state Slice 제공

     export interface CounterState {
       value: number;
       status: 'idle' | 'loading' | 'failed';
     }
    
     const initialState: CounterState = {
       value: 0,
       status: 'idle',
     };
    
    
     export const counterSlice = createSlice({
       name: 'counter',
       initialState,
       // The `reducers` field lets us define reducers and generate associated actions
       reducers: {
         increment: (state) => {
           // Redux Toolkit allows us to write "mutating" logic in reducers. It
           // doesn't actually mutate the state because it uses the Immer library,
           // which detects changes to a "draft state" and produces a brand new
           // immutable state based off those changes
           state.value += 1;
         },
         decrement: (state) => {
           state.value -= 1;
         },
         // Use the PayloadAction type to declare the contents of `action.payload`
         incrementByAmount: (state, action: PayloadAction<number>) => {
           state.value += action.payload;
         },
       },
       // The `extraReducers` field lets the slice handle actions defined elsewhere,
       // including actions generated by createAsyncThunk or in other slices.
       extraReducers: (builder) => {
         builder
           .addCase(incrementAsync.pending, (state) => {
             state.status = 'loading';
           })
           .addCase(incrementAsync.fulfilled, (state, action) => {
             state.status = 'idle';
             state.value += action.payload;
           })
           .addCase(incrementAsync.rejected, (state) => {
             state.status = 'failed';
           });
       },
    

redux-persist

리덕스의 store는 페이지를 새로고침할 경우 state가 전부 날아간다. 그래서 이것에 대응방안으로 localStorage / session 에 저장한다. 이 작동을 대신하기 위해서 redux-persist 를 사용한다.

설치: npm install redux-persist

사용

// reducers/index.tsx

import { combineReducers } from "redux";
 import { persistReducer } from "redux-persist";
// 로컬 스토리지 저장
 import storage from "redux-persist/lib/storage";
// import storageSession from "redux-persist/lib/storage/session"

const persistConfig = {
  key: "root",
  // localStorage에 저장합니다.
  storage,
  // auth, board, studio 3개의 reducer 중에 auth reducer만 localstorage에 저장합니다.
  whitelist: ["auth"]
  // blacklist -> 그것만 제외합니다
};

// reducer 
const rootReducer = combineReducers({
  auth,
  board,
  studio
});

 export default persistReducer(persistConfig, rootReducer);
// src/index.js

import { createStore, applyMiddleware, compose } from "redux";
 import { persistStore } from "redux-persist";
 import { PersistGate } from "redux-persist/integration/react";

const store = createStore(rootReducer);
 const persistor = persistStore(store);

const Root = () => (
  <Provider store={store}><PersistGate loading={null} persistor={persistor}>
      <App />
    </PersistGate>
  </Provider>
);

redux-saga

redux는 무조건 동기적으로 dispatch가 이루어진다. dispatch를 여러번 할 경우도 로직은 2번 작성해야되니 불편하다.

이것을 대신하기 위한 미들웨어로 비동기적으로 disapatch를 사용할 수 있고, 내부 메소드를 활용하여, 사용자의 부주의로 인하여 동일한 api를 여러번 요청할 경우 마지막 req만 받아온다.

generator 문법

// 함수앞에 * 을 붙여준다.
// next로 다음 yield를 호출한다.
const gen = function* () {
  console.log(1);
  yield;
  console.log(2);
  yield;
  console.log(3);
  yield;
  console.log(4)
}
const gener = gen()
// gener() - gener{<suspended>}
gener().next() -> 1
gener().next() -> 2
gener().next() -> 3
gener().next() -> 4
gener().next() -> undifined