https://velog.io/@cks3066/ReactRedux
Redux Toolkit 공식 문서
리덕스 툴킷은 Redux 팀이 제공하는 라이브러리로써 간편하고 효율적인 리덕스 개발을 할 수 있게끔 도와준다.
프로젝트를 진행하며 실제 리덕스를 적용하여 사용하게 됨에 따라 필요한 모든 코드를 작성해야하는 naïve한 리덕스는 프로젝트에 리덕스 도입을 꺼리게 만들었으며 그에 따라 좀 더 쉬운 리덕스의 사용을 위해 RTK가 개발되었다.
CRA 로 리액트 프로젝트를 생성하는 과정에서 RTK 템플릿 코드를 가져올 수 있다.
npx create-react-app my-app --template redux
npx create-react-app my-app --template redux-typescript
이미 개발이 진행된 프로젝트라면 다음 파일들을 설치하면 된다.
redux, react-redux, @reduxjs/toolkit(툴킷), redux-devtools(개발 툴)
yarn add -D redux react-redux @reduxjs/toolkit
yarn add -D redux-devtools
리덕스의 createStore
을 사용하기위한 툴킷이다.기존의 리덕스의 경우 logger
, saga
, thunk
등 미들웨어를 적용하려면 꽤나 복잡한 코드를 작성했어야했다.하지만 thunk
는 기본적으로 configureStore
에 적용되어있으며 다른 미들웨어도 비교적 간단하게 선언만으로 적용가능하다.
// 루트리듀서
const rootReducer = {
test,
};
// 스토어 생성
const store = configureStore({
reducer: rootReducer,
middleware: [logger],
});
// (기존) const store = createStore(reducer);
리덕스에서 action을 만들기 위해서는 액션
을 선언하고 액션 생성자 함수
를 분리하여 선언해야한다. 이때 createAction를 사용하여 인자로 액션을 넣어주기만 하면 간편하게 액션 생성자 함수를 생성할 수 있다.
// 액션
const ADD = "ADD";
// 액션 크리에이터
const addToDo = createAction(ADD);
// (기존)
// const addToDo = (text) => {
// return {
// type: ADD,
// text,
// };
// };
상태변화를 일으키는 리듀서 함수를 생성하는 함수이다. 기존에 불변성유지를 위해 하나하나의 불변성을 위한 코드작성에 주의를 기울이든, immer 라이브러리를 사용하든 불변성을 위한 코드작성이 불편했지만 createReducer는 자동적으로 불변성 관리를 해주기 때문에 좀 더 쉽게 Reducer 함수를 작성할 수 있게 해준다.
// 리듀서
export default createReducer(initialState, {
[PLUS]: (state, action) => {
state.value++;
},
[MINUS]: (state, action) => {
state.value--;
},
[ADD]: (state, action) => {
state.array.push(action.payload.value);
},
[REMOVE]: (state, action) => {
state.array.filter((item) => item.key !== action.payload.key);
},
});
//(기존) 리듀서 함수의 작성법이 다양하기때문에 생략
앞서 설명한 createAction, createReducer 가 내부적으로 모두 포함되어 있는 createSlice
는 가장 쉽게 액션과 리듀서 함수를 만들 수 있는 함수이다.
// 슬라이스
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo(state, action) {
const { id, text } = action.payload
state.push({ id, text, completed: false })
},
toggleTodo(state, action) {
const todo = state.find(todo => todo.id === action.payload)
if (todo) {
todo.completed = !todo.completed
}
}
}
// 디스패치용 액션크리에이터
export const { addTodo, toggleTodo } = todosSlice.actions
// 리듀서 내보내기
export default todosSlice.reducer
createSlice
가 반환하는 객체
{
name: "todos",
reducer: (state, action) => newState,
actions: {
addTodo: (payload) => ({type: "todos/addTodo", payload}),
toggleTodo: (payload) => ({type: "todos/toggleTodo", payload})
},
caseReducers: {
addTodo: (state, action) => newState,
toggleTodo: (state, action) => newState,
}
}
cra-template-redux/template at master · reduxjs/cra-template-redux
// counterSlice.js
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
// createAsyncThunk API 비동기 샘플 함수
function fetchCount(amount = 1) {
return new Promise((resolve) =>
setTimeout(() => resolve({ data: amount }), 500)
);
}
// state 초기 값
const initialState = {
value: 0,
status: 'idle',
};
// 아래 함수를 thunk라고 하며 비동기 로직을 수행할 수 있다.
// 일반적인 동작처럼 dispatch(incrementAsync(10))할 수 있다.
// 이는 첫 번째 인수로 디스패치 기능을 가진 thunk를 부를 것이다.
// 그런 다음 비동기 코드를 실행하고 다른 액션을 디스패치할 수 있다.
// 씽크는 일반적으로 비동기 요청을 만드는 데 사용된다.
// createAsyncThunk(액션, 비동기 콜백);
export const incrementAsync = createAsyncThunk(
'counter/fetchCount',
async (amount) => {
const response = await fetchCount(amount);
return response.data;
}
);
// slice 객체 생성
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
// 직접 state를 변경하는 것처럼 보이지만,
// 함수 내부에서 Immer 라이브러리를 사용하기때문에 불변성이 유지된다.
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
// 사용처: dispatch(incrementByAmount(payload));
incrementByAmount: (state, action) => {
state.value += action.payload;
// state.value += action.payload.value;
// (개인적으로는 payload에 직접적으로 값을 넣기보다는 객체로 넣어주는 것이 더 가독성이 좋아보임)
},
},
// 'extraReducers' 필드를 사용하여 slice는 다른 곳에서 정의된 action들을 처리할 수 있다.
// (createAsyncThunk 또는 다른 slice에서 생성된 action)
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = 'loading';
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle';
state.value += action.payload;
});
},
});
// dispatch의 인자로 들어갈 actionCreator
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// useSelector를 통해 값을 가져올 때 value만 가져오도록 하는 편의성을 위한 함수
export const selectCount = (state) => state.counter.value;
// 직접 thunk를 쓸 수 있는데, 이때 동기 로직과 비동기 로직을 모두 포함할 수 있다.
// 다음은 현재 state를 기준으로 action을 조건부로 디스패치하는 예시이다. (비동기 아님)
export const incrementIfOdd = (amount) => (dispatch, getState) => {
// store.getState() === store.state // 현재의 state를 반환함
// const currentValue = getState().counter.value;
const currentValue = selectCount(getState());
if (currentValue % 2 === 1) {
dispatch(incrementByAmount(amount));
}
};
export default counterSlice.reducer;
// Counter.js
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
incrementIfOdd,
selectCount,
} from './counterSlice';
export function Counter() {
// const count = useSelector((state) => state.couter.value);
const count = useSelector(selectCount);
const dispatch = useDispatch();
dispatch(increment()) // store.state.counter.value 1 증가
dispatch(decrement()) // store.state.counter.value 1 감소
// payload를 객체로 넣고싶다면 : dispatch(incrementByAmount({ value: 2 }));
dispatch(incrementByAmount(2)) // store.state.counter.value 2 증가
dispatch(incrementAsync(2)) // 5초 뒤 스토어의 state.counter.value 증가
dispatch(incrementIfOdd(2)) // 아무일도 일어나지 않음
dispatch(incrementIfOdd(3)) // store.state.counter.value 3 증가
return <></>;
}
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});