React Query의 편리함을 직접 느껴보기 위해 오랜만에 다시 Redux를 꺼내서 사용해보자.
// Reducer 함수 생성
const reducer = (state, action) => {};
// Redux 데이터 저장소 생성
const store = createStore(reducer);
// Redux 데이터 엑션 요청 (action은 반드시 객체 형태 + type을 갖고 있어야 함.)
store.dispatch({type: "타입"});
// Redux 데이터 요청
store.getState();
- reducer 함수가 반환하는 값이 데이터가 된다.
- 오직 reducer 함수 내에서만 데이터를 수정할 수 있다.
- reducer 함수 내에서 action을 사용해 Reducer 동일한 방법으로 동작을 수행한다.
const reducer = (state = 0, action) => {
if (action.type === "PLUS") {
return state + 1;
} else if (action.type === "MINUS") {
return state - 1;
}
return state;
}
// Refactoring (if-else -> switch)
const reducer = (state = 0, action) => {
switch (action.type) {
case "PLUS":
return state + 1;
case "MINUS":
return state - 1;
default:
return state;
}
}
dispatch()
: 데이터의 변경을 요청할 때 사용subscribe()
: 데이터의 변화를 감지하고 변화되었을 때 콜백함수를 실행시킨다.getState()
: 데이터 가져올 때 사용replaceReducer()
:
※ 절대 객체나 배열 형태의 State를 직접 변경하려 하지 말자. 새 객체나 배열로 만들어서 그걸 넘겨주어야 한다. (React 공식 문서에서 언급하는 내용과 같다. 객체나 배열은 그 데이터 자체가 아니라 데이터가 존재하는 위치에 대한 주소를 참조하고 있는 것이기 때문에 객체나 배열 자체를 새로 넣어주는 것이 아니라면 변화를 감지하지 못한다.)
return [...state, newState]
return {
ob: ject,
...obj
}
$ npm i react-redux react-router-dom
Provider 로 감싸주어야 한다
<Provider store={store}>
<App/>
</Provider>
connect를 사용했지만 현재는 useSelector 사용을 권장한다.
- connect는 state(=mapStateToProps)와 dispatch(=mapDispatchToProps) 두 개의 매개변수를 갖는다.
- 여기서 주의할 점은 해당 컴포넌트를 미리 export 하지 말고 connect 부분에서만 export 해줘야 한다는 것이다.
- mapStateToProps와 mapDispatchToProps 는 반드시 객체 형태를 반환해야 한다.
- 연결된 컴포넌트의 props에서 state와 dispatch를 사용할 수 있다.
import { connect } from "react-redux"
function App({state, dispatch})
function mapStateToProps(state, ownProps) {
return { toDo: state };
}
function mapStateToProps(dispatch, ownProps) {
return { dispatch };
}
export default connect(mapStateToProps, mapStateToDispatch)(Home);
이렇게 connect로 연결하게 되면 store 의 state를 받아올 수 있다.
컴포넌트와 Redux Store state를 연결시켜주는 역할을 한다.
- 매개변수로 state와 ownProps를 받는다.
- state : 현재 Redux store의 state 데이터를 말한다.
- ownProps : history, location, match, staticContext 등 다양한 것들을 담고 있다.
Redux Store state 데이터를 조작하는 역할을 한다.
- 매개변수로 dispatch와 ownProps를 받는다.
mapStateToProps 대신 사용해 Redux store state의 데이터를 가져온다.
const userData = useSelector((state) => state)
위 코드를 사용해 바로 state를 가져올 수 있다.
mapStateToDispatch 대신 사용하는 것
const dispatch = useDispatch();
dispatch(addUser(id))
$ npm i @reduxjs/toolkit
createAction(타입)
: 액션을 생성하고 액션의 타입까지 정의한다.createAction
으로 생성한 변수는 함수 형태이고, 함수를 실행하면 타입과 payload를 가진 객체를 반환한다.
import { createAction } from "@reduxjs/toolkit";
const addUser = createAction("ADD");
const deleteUser = createAction("DELETE");
const reducer = (state = [], action) => {
switch (action.type) {
case addToDo.type:
return [{ text: action.payload, id: Date.now(), ...state }];
case deleteToDo.type:
return state.filter((toDo) => toDo !== action.id);
default:
return state;
}
};
위에서 선언한 리듀서 함수에서 switch-case 문을 제거해 더 간단하게 만든 것
- Redux Toolkit 에서는 state를 직접 변경해도 문제가 없다. 즉, 매번 새 객체나 배열을 생성할 필요가 없다. (Immer을 사용하고 있어서 변경 사항을 체크할 수 있기 때문이다.)
- builder 콜백을 사용하지 않으면 에러가 발생하니 주의하자.
import { createReducer } from "@reduxjs/toolkit";
const reducer = createReducer(initialState, (builder) => {
builder
.addCase(addToDo, (state, action) => {})
.addCase(deleteToDo,(state, action) => {})
})
Redux Store 를 생성
- createSlice로 만든 reducer 들을 하나의 reducer로 묶어준다.
- useSelector을 사용해서 state를 가져오게 되면, state 내부에는 configureStore로 모아둔 리듀서가 포함되어있다. state.리듀서이름.value 를 입력하면 해당 리듀서의 상태 값이 나온다.
export const store = configureStore({
reducer: {
리듀서이름:createSlice이름.reducer
}
})
Reducer, action 을 생성
- 리듀서와 엑션을 생성하기 때문에 createAction과 createReducer가 필요없어진다.
- createSlice는 name, initialState, reducers 를 가진 객체를 받아야 한다.
- configureStore의 인자로 reducer 를 가진 객체를 받아야 한다.
- reducers 에서 state의 불변성을 지킬 필요가 없다. (spread 연산자로 구현할 필요가 없다.)
const toDo = createSlice({
name: "toDosReducer",
initialState: [],
reducers: {
add: (state, action) => {
state.push({ text: action.payload, id: Date.now() });
},
remove: (state, action) => {
state.filter((toDo) => toDo.id !== action.payload);
},
},
});
export const store = configureStore({ reducer: toDos.reducer });
생성
const toDoSlice = createSlice({
name: "toDosReducer",
initialState: [],
reducers: {
add: (state, action) => {
state.push({ text: action.payload, id: Date.now() });
},
remove: (state, action) => {
state.filter((toDo) => toDo.id !== action.payload);
},
},
});
export const store = configureStore({
reducer: {
toDo:toDosReducer.reducer
}
})
상태 호출
useSelector((state) => {
return state.toDo.value
})
상태 변경
dispatch({type:createSlice내부의name/reducers이름, 값})
// 예시
dispatch({type:toDosReducer/add, text:1}) // payload 사용 X
dispaych(toDo.actions.add(1)) // 값이 자동으로 payload로 저장
configureStore는 따로 store 파일에 만들어서 분리해둔다.
createSlice는 따로 slice 파일에 만들어서 분리해둔다.
- createSlice 에서 reducers 에는 동기적인 액션을, extraReducers 에는 비동기적인 액션을 사용한다.
비동기 작업을 처리하는 액션을 만들어준다.
const 이름 = createAsyncThunk(타입, 액션 실행 시 처리되어야 하는 작업 코드)
createSlice의 extraReducers 에서는 async의 진행 상태인 pending, fulfilled, rejected 를 제공해주는데, 이때 해당 진행 상태에 따라 액션을 수행한다.
const asyncTest = createAsyncThunk(
'testSlice/asyncTest',
async () => {
// 비동기 Fetch 요청
}
)
const testSlice = createSlice({
name: `testSlice`,
initialState:{
value:0,
status:"test"
},
extraReducers: (builder) => {
builder.addCase(asyncTest.pending, (state, action) => {
state.status = "로딩중";
})
builder.addCase(asyncTest.fulfilled, (state, action) => {
state.status = "성공";
state.value = action.payload;
})
builder.addCase(asyncTest.rejected, (state, action) => {
state.status = "실패";
})
}
})
// 사용
const status = useSelector((state) => {
return state.test.status;
})
const result = useSelector((state) => {
return state.test.value;
})
<button onClick={() => {
dispatch(asyncTest());
}}> Async Test : {status} {result}</button>
직접 구현하는 것은 다른 상태 관리 기술들을 사용하기 전에 그 기술들의 작동 방식이나 내부 코드를 이해하기에 아주 좋은 방법이지만, Context 기반의 Redux가 굳이 비동기 처리까지 직접 구현해서 사용하는 것은 비효율적이고 번거로운 작업이 아닌가 하는 생각이 든다. 따라서 Redux Toolkit 을 통해 기본기를 학습한 후, 실제 비동기 처리까지 구현하는 경우에는 Redux Toolkit 말고 React Query 를 사용하는 것이 좀 더 효율적일 것 같다. React Query는 좀 더 간단하게 비동기 요청의 진행 상태와 결과를 isLoding 과 data로 반환해주기 때문이다.