Redux 알아보기

박요진·2024년 1월 26일
0

1. Redux란 무엇인가?

  • Redux란 여러 컴포넌트가 공유하는 상태를 관리하기 위한 라이브러리로, 메타(페이스북)가 설계한 flux 규격에 맞추어져 있습니다.
  • 리엑트 컨텍스트에 기반을 둔 라이브러리 이므로 Provider 컴포넌트가 항상 최상위로 동작해야 합니다.

1-1. Redux의 기본 개념 : 세가지 원칙

1) Single source of truth

  • 동일한 데이터는 항상 같은 곳에서 가지고 온다.
  • 스토어라는 하나 뿐인 데이터 공간이 있다.

2) State is read-only

  • React에서는 setState 메소드를 활용해야만 상태를 변경할 수 있다.
  • Redux에서도 Action이라는 객체를 활용해야만 상태를 변경할 수 있다.

3) Changes are made with pure functions

  • 변경은 순수 함수로만 가능하다.
  • Reducer와 연관되는 개념이다.
  • Store(스토어) - Action(액션) - Reducer(리듀서)

1-2. Store, Action, Reducer의 의미와 특징

![[Pasted image 20240124130312.png]]

1) Store (스토어)

  • 컴포넌트와는 별개로 스토어라는 공간이 있어서 그 스토어 안에 앱에서 필요한 상태를 담는다.
  • 컴포넌트에서 상태 정보가 필요할 때 스토어에 접근한다.

2) Action (액션)

  • Action은 앱에서 스토어에 운반할 데이터를 말합니다. (주문서)
  • Action은 자바스크립트 객체 형식으로 되어 있습니다.
{
	type: 'ACTION_CHANGE_USER', // 필수
	payload:  // 옵션
	{ 
		name: '하피몬',
		age: 130
	}
}

3) Reducer (리듀서)

  • Action을 Store에 바로 전달 하는 것이 아닌 Reducer에 전달해야 합니다.
  • Reducer가 주문을 보고 Store의 상태를 업데이트 합니다.
  • Action을 Reducer에 전달하기 위해서는 dispatch() 메소드를 사용해야 합니다.

4) Dispatch (디스패치)

  • Dispatch는 store의 내장 함수 중 하나로, Action을 발생시킵니다.
  • Action을 파라미터로 전달하고, Reducer를 호출합니다.

Action -> dispatch() -> Reducer -> Store

Redux가 이런 공식을 따르는 이유는 데이터가 한 방향으로만 흘러야 하기 때문이다.

![[Pasted image 20240124131046.png]]

1-3. Redux에서 위의 개념을 구현하기 위해서는 두 가지 방법을 사용할 수 있다.

1) mapStateToProps
2) Redux hooks - 최근에 나온 기술

  • useSelector
  • useDispatch

1-4. Redux의 장점

  • 상태를 예측할 수 있다. (순수 함수를 사용하기 때문에)
  • 유지보수가 쉽다.
  • 디버깅에 유리하다.
  • 테스트를 붙이기가 용이하다.

2. Redux 사용하기

2-1. Redux 라이브러리 설치하기

npm install redux react-redux

2-2. 세부 reducer 정의하기

// reducers/cartReducer.js

/** 1. Redux 사용을 위해 빈배열을 초기값으로 변수를 생성합니다. */
let initialValue = [];

function cartReducer(state = initialValue, action) {
	let copyValue = [...state]; // 2. state는 불변성을 가져야 하므로 state를 복사해줍니다.

 /** 장바구니에 아이템을 추가하는 예시 입니다. */

// *3. action.type 은 `ADD_CART` 입니다.*

/** 4. findIndex를 이용하여 state에 담긴 데이터 중 id가 일치하는 데이터를 찾습니다. state에 담긴 값은 Store에 저장되어 있는 값입니다.*/

  if (action.type === 'ADD_CART') {
     const findIndexDataByIdData = copyValue.findIndex(
     item => item.id === id,
    );

/** 5. findIndexDataByIdData가 -1이면 state에 담긴 데이터 중 id가 일치하는 데이터가 없기 때문에 받아온 데이터인 action.payload 데이터를 state에 push합니다. */

/** 6. findIndexDataByIdData가 -1이 아니면 state에 담긴 데이터 중 id가 일치하는 데이터가 있기 때문에 해당 데이터의 count를 증가시킵니다. */

// *action.payload에는 dispatch로 받아온 업데이트 될 데이터가 저장되어 있습니다.*

  if (findIndexDataByIdData === -1) {
     copyValue.push(action.payload);
   } else {
	 copyValue[findIndexDataByIdData].count += action.payload.count;
	}
	return copyValue; // 7. 값을 업데이트 한 copyValue를 return 해줍니다.

/** 7. 추가적인 예시를 위해 전체 삭제 기능의 예시를 추가하였습니다. **/
  } else if (action.type === 'DELETE_ALL_CART') { 
  return (copyValue = []); // 해당 action이 호출되면 copyValue의 값을 빈배열로 반환합니다.
  
 /** 8. redux의 마지막은 state를 return 해주어야 에러가 발생하지 않습니다. */
  } else {
	  return state;
  } 
}

2-3. Reducer 정의하기

  • RootReducer는 여러 reducer를 사용하는 경우에 reducer를 하나로 묶어주는 메소드 입니다.

만약 하나의 reducer 만 사용한다면 rootReducer를 정의할 필요가 없습니다. 바로 store에 넣으면 됨

Reducer를 한 개만 넣을 경우

function reducer(state = initialValue, action) {
... 내용 생략
}
export default reducer;

넣어야 할 Reducer가 여러 개 일 경우

import { combineReducers } from "redux";
function cartReducer(state = initialValue, action) {
... 내용 생략
}
function deliveryReducer(state = initialValue, action) {
... 내용 생략
}
const rootReducer = combineReducers({
	cartReducer,
    deliveryReducer,
})
export default rootReducer;

2-4. Store 만들기

전역 상태이므로 최상위 Provider에 주입합니다.

2-4-1) 하나의 reducer를 사용할 때

const store = createStore(reducer);

  return (
    <Provider store={store}>
        <Router />
    </Provider>
  );

2-4-2) 두 개 이상의 reducer를 사용할 때

const store = createStore(rootReducer);

  return (
    <Provider store={store}>
        <Router />
    </Provider>
  );

2-5. 현재 store에 있는 데이터 가져오기 (useSelector)

import { useSelector } from 'react-redux';

  /** order 페이지에 데이터를 넘겨주기 위해 useSelector를 이용하여 state 값을 cartData 변수에 저장합니다. */

  const cartData = useSelector(state => {
    return state.cart; // state에서 cart라는 name을 가지고 있는 데이터를 반환합니다.
  });

2-6. store에 저장되어 있는 데이터 값 변경하기 (useDispatch)

import { useState } from 'react';
import { useDispatch } from 'react-redux';

const [detailData, setDetailData] = useState({});
const dispatch = useDispatch(); // redux의 dispatch를 편하게 사용하기 위해 변수화 해줍니다.

/** 이벤트가 발생할 때마다 상태값을 변경해주기 위해 함수를 만들어 줍니다. **/
const putInCartData = () => {
	dispatch({ // 1. dispatch 함수를 이용해 상태를 변화시킬 action.type을 설정해줍니다.
    type: 'ADD_CART',

    payload: { // 2. payload로 변화된 상태값을 객체형태로 데이터를 넣어줍니다.
	    id: detailData?.id,
    },
  });
}

3. MiddleWare

Reducer에서 반환하는 상태 변경 전에 어떠한 동작을 처리하고 싶을 때 사용하는 MiddleWare

  • 순서
    middleware 미사용 시 : action → reducer → store
    middleware 사용 시 : action → middleware → reducer → store

3-1. middleware를 왜 사용할까?

  • setTimeout을 이용하여 1초 뒤 dispatch가 되도록 만드는 경우
  • dispatch 전 들어온 액션 타입을 콘솔에 표시하는 경우
  • dispatch 전 비동기 데이터 요청을 한 뒤 받아온 데이터를 전역 상태에 담는 경우 등등..

3-2. Redux Middleware 사용방법

  • Redux 미들웨어는 Redux store를 생성하는 부분의 두번 째 인자에 넣어 적용시킬 수 있습니다. 보통 미들웨어로 적용할 파일을 직접 만들거나, redux-logger, redux-thunk와 같은 패키지들을 선언해주며 콤마로 구분해서 여러개 적용할 수 있습니다.
import { applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
import logger from 'redux-logger';
import ReduxThunk from 'redux-thunk';

const store = createStore(reducer, applyMiddleware(ReduxThunk, logger));

3-3. redux-logger

redux-logger는 dispatch될 때 action의 타입과 변경 전 상태와 변경 후 상태를 콘솔에 출력해주는 패키지 입니다.

3-3-1. redux-logger 설치하기

npm i redux-logger

3-3-2. logger 사용하기

redux-logger 의 경우에는 middleware의 인자로 넣어주면 되지만 순서는 항상 맨 마지막에 위치해야 한다!

const store = createStore(reducer, applyMiddleware(ReduxThunk, logger));

3-3-3. 사용 시 화면 예시

3-4. redux-thunk

redux-thunk는 가장 많이 사용되는 미들웨어로써 비동기 작접을 처리할 때 용이합니다. 이 패키지를 사용하면 dispatch 할 때, 액션 타입을 객체가 아닌 함수를 dispatch 할 수 있습니다.

3-4-1. redux-thunk 설치하기

npm i redux-thunk

3-4-2. redux-thunk 사용 예제

const getComments = () => (dispatch, getState) => {
  // 이 안에서는 액션을 dispatch 할 수도 있고
  // getState를 사용하여 현재 상태도 조회 할 수 있습니다.
  const id = getState().post.activeId;

  // 요청이 시작했음을 알리는 액션
  dispatch({ type: 'GET_COMMENTS' });

  // 댓글을 조회하는 프로미스를 반환하는 getComments 가 있다고 가정해봅시다.
  api
    .getComments(id) // 요청을 하고
    .then(comments => dispatch({ type: 'GET_COMMENTS_SUCCESS', id, comments })) // 성공시
    .catch(e => dispatch({ type: 'GET_COMMENTS_ERROR', error: e })); // 실패시
};
  • setTimeout을 사용해서 Action이 dispatch 되는 걸 1초 딜레이 시키기
// 액션 타입
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';

// 액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

// getState를 쓰지 않는다면 굳이 파라미터로 받아올 필요 없습니다.
export const increaseAsync = () => dispatch => {
  setTimeout(() => dispatch(increase()), 1000);
};
export const decreaseAsync = () => dispatch => {
  setTimeout(() => dispatch(decrease()), 1000);
};

// 초깃값 (상태가 객체가 아니라 그냥 숫자여도 상관 없습니다.)
const initialState = 0;

export default function counter(state = initialState, action) {
  switch (action.type) {
    case INCREASE:
      return state + 1;
    case DECREASE:
      return state - 1;
    default:
      return state;
  }
}
profile
프론트엔드 개발자 지망생입니다.

0개의 댓글