리덕스
- 리액트 생태계에서 가장 사용률이 높은 상태관리 라이브러리
- 컴포넌트들의 상태 관련 로직들을 다른 파일들로 분리시켜 더욱 효율적으로 관리할 수 있게 도와준다.
- 글로벌 상태 관리도 손쉽게 할 수 있다.
- 미들웨어로 다양한 작업(비동기, 로깅) 가능
🤔 왜 상태관리 라이브러리로 리덕스를 선택했는가?
npmtrends를 통해 살펴보면, Redux는 다른 상태관리 라이브러리보다 월등히 높은 사용률을 보이고 있다.
요즘 채용공고를 보면 recoil, zustand도 제법 보이는데 recoil의 경우 1년 전이 마지막 update라 관리가 안 되고 있는 느낌이었고, zustand는 다른 프로젝트에서 한 번 다루어보려고 계획하고 있다.
Redux는 Context API가 지금 형태로 사용되기 전(`useReducer` 훅이 존재하기도 전)에 만들어진 라이브러리이다. 모든 상태관리 라이브러리의 근본이라는 느낌이라, 프로젝트에서 사용할 상태관리 라이브러리는 큰 고민 없이 Redux로 결정했다.
리덕스 설치
npm i redux react-redux
Action 생성함수 생성하기
🐱🏍 액션Action
- 앱에서 store로 보내는 데이터 묶음
- state에 어떠한 변화가 필요할 때 발생시킴
- 하나의 객체로 표현된다.
- `type` 필드는 필수이며, 그 외의 값들은 개발자 재량
- `store.dispatch()`를 사용해 액션을 보낼 수 있다.
{
type: SET_VISIBILITY_FILTER,
filter: SHOW_COMPLETED
}
- 액션을 만드는 함수.
- 파라미터를 받아와 액션 객체 형태로 만들어준다.
- 컴포넌트에서 쉽게 액션을 발생시키기 위해 사용한다.
// src/redux/transaction/action.js
// actionTypes 정의
export const ADD_TRANSACTION = "ADD_TRANSACTION";
export const EDIT_TRANSACTION = "EDIT_TRANSACTION";
export const REMOVE_TRANSACTION = "REMOVE_TRANSACTION";
// 액션 생성자 함수 정의
export const addTransaction = (transaction) => ({
type: ADD_TRANSACTION,
payload: transaction,
});
export const editTransaction = (transaction) => ({
type: EDIT_TRANSACTION,
payload: transaction,
});
export const removeTransaction = (id) => ({
type: REMOVE_TRANSACTION,
payload: id,
});
화살표 함수로 3개의 액션을 정의했다.
- addTransaction: 가계부 내역 추가 함수
- editTransaction: 가계부 내역 수정 함수
- removeTransaction: 가계부 내역 삭제 함수
각각의 함수에서 사용하고 있는 payload는 상태를 변경시키는 데 필요한 데이터이다.
예를 들어, 값을 추가하기 위한 함수에는 당연히 추가할 가계부 데이터가 필요하다. 리듀서 함수에서 처리할 수 있도록 매개변수로 가져온 가계부 데이터를 payload로 지정해 주었다.
* payload
- 객체 형태
- 해당 액션에 필요한 추가 데이터
- payload의 데이터는 리듀서에서 사용되어 상태를 업데이트하는 데 활용된다.
Reducer 정의하기
- 변화를 일으키는 함수
- 현재 상태와 액션을 받아 새로운 상태를 반환하는 순수 함수
- 반드시 순수함수로 작성되어야 한다.
- 여러 개의 리듀서(서브 리듀서)를 만들고 이를 합쳐 루트 리듀서를 만들 수 있다.
// src/redux/transaction/reducers.js
import { ADD_TRANSACTION, EDIT_TRANSACTION, REMOVE_TRANSACTION } from "./actions";
// 초기 상태 정의
const initialState = {
transactions: [],
};
// 리듀서 함수 정의
const transactionReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TRANSACTION:
return {
...state,
transactions: [...state.transactions, action.payload],
};
case EDIT_TRANSACTION:
return {
...state,
transactions: state.transactions.map((transaction) =>
transaction.id === action.payload.id ? action.payload : transaction,
),
};
case REMOVE_TRANSACTION:
return {
...state,
transactions: state.transactions.filter((transaction) => transaction.id !== action.payload),
};
default: //default는 기존 state를 그대로 반환하도록 해야 한다
return state;
}
};
export default transactionReducer;
전체적인 데이터 형식은 배열 안에 가계부 객체가 들어와 있는 형태이다. 따라서 초기값은 빈 배열로 정했다.
ADD_TRANSACTION은 기존 저장된 값에 payload로 받아온 새로운 가계부 데이터를 붙여 넣는 방식으로 동작한다.
EDIT_TRANSACTION은 기존 저장된 가계부 데이터를 순회하며 수정 요청이 들어온 가계부 데이터의 id와 동일한 id를 가진 데이터를 찾고, 해당 데이터의 내용을 새로운 가계부 데이터로 바꿔주는 원리로 구현했다.
REMOVE_TRANSACTION의 경우는 payload로 들어온 id값이 아닌 데이터만 필터링하는 식으로 삭제를 구현해 주었다.
스토어(Store) 만들기
💁♀️ 스토어(Store)
- 앱의 전체 상태 트리를 가지고 있는 저장소
- 클래스가 아닌 메서드가 들어있는 객체이다.
- 스토어 생성 방법은 루트 리듀싱 함수(루트 리듀서)를 `createStore`에 전달하는 것이다.
- 스토어의 상태는 읽기 전용. 상태를 바꾸는 유일한 방법은 액션을 보내는 것뿐이다.
- 스토어 안에는 현재의 앱 상태, 리듀서, 추가적으로 몇 가지 내장 함수들이 들어간다.
- 리액트 프로젝트에서는 단 하나의 스토어만 존재한다.
// src/redux/transaction/store.js
import { createStore } from "redux";
import transactionReducer from "./reducers";
const store = createStore(transactionReducer);
export default store;
🦆 Ducks 패턴 적용
가계부 어플을 관리하기 위한 리듀서까지 작성하고 실제 데이터를 가져와 써보니(dispatch 부분은 다음 포스팅에...) 원하는 대로 잘 동작하고 있음을 확인할 수 있었다.
다만 리덕스 파일이 중구난방으로 흩어져 있는 게 보기 싫어 파일을 어떻게 관리하는 게 좋을까 고민이 되었다.
그러던 와중 Redux의 덕스Ducks 패턴을 발견했다.
위 두 개의 사이트에서 실제 파일 정리에 도움을 많이 받았다.
Ducks 패턴을 적용한 파일구조는 아래와 같다.
가장 먼저, `action.js`와 `reducer.js`의 action, reducer를 합쳐
새로운 파일 `transactions.js`을 만들었다.
// src/modules/transactions.js
/* ----------------- 액션 타입 ------------------ */
export const ADD = "transactions/ADD";
export const EDIT = "transactions/EDIT";
export const REMOVE = "transactions/REMOVE";
/* ----------------- 액션 생성 함수 ------------------ */
export const addTransaction = (transaction) => ({
type: ADD,
payload: transaction,
});
export const editTransaction = (transaction) => ({
type: EDIT,
payload: transaction,
});
export const removeTransaction = (id) => ({
type: REMOVE,
payload: id,
});
/* ----------------- 모듈의 초기 상태 ------------------ */
const initialState = {
transactions: [],
};
/* ----------------- 리듀서 ------------------ */
const transactionReducer = (state = initialState, action) => {
switch (action.type) {
case ADD:
return {
...state,
transactions: [...state.transactions, action.payload],
};
case EDIT:
return {
...state,
transactions: state.transactions.map((transaction) =>
transaction.id === action.payload.id ? action.payload : transaction,
),
};
case REMOVE:
return {
...state,
transactions: state.transactions.filter((transaction) => transaction.id !== action.payload),
};
default:
return state;
}
};
export default transactionReducer;
// App.js
import React from "react";
import AppRouter from "./routes/Router";
function App() {
return <AppRouter />;
}
export default App;
`App.js`에서 store를 전역 관리해 주던 코드(`<Provider>` 사용한 코드)를 제거하고
store를 생성하던 파일인 `store.js` 파일도 삭제했다.
이후 삭제한 모든 코드를 최상위 코드인 `index.js`로 옮겼다.
👇
// index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { Provider } from "react-redux";
import { createStore } from "redux";
import transactionReducer from "./modules/transactions";
const store = createStore(transactionReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root"),
);
✅덕스패턴 적용까지 완료
정의된 액션 생성자 함수로 리덕스 스토어의 값을 어떻게 가져와 사용했는지는 다음에 포스팅하도록 하겠다.
참고
https://lunit.gitbook.io/redux-in-korean