Redux Toolkit
- Redux Toolkit은 Redux를 더 쉽게 사용하기 위해 만들어졌습니다.
- Redux에서 공식으로 제공하는 개발도구입니다.
Redux Toolkit 사용 이유
- 리덕스를 사용하면 보일러 플레이트 코드가 많다는 단점이 있습니다.
- 리덕스 툴킷을 사용하면 보일러 플레이트 코드를 줄이고 액션 타입, 액션 생성함수, 리듀서를 하나의 함수로 선언할 수 있습니다.
- 그리고 패키지 의존성을 줄여줍니다. 리덕스 툴킷에는 많은 라이브러리들이 내장되어 있기 때문에 redux-thunk 등을 따로 설치하지 않고 사용할 수 있습니다.
- 참고) 보일러 플레이트 코드: 최소한의 변경으로 여러곳에서 재사용되며, 반복적으로 비슷한 형태를 띄는 코드.
// JS (CRA + redux-toolkit)
$ npx create-react-app 프로젝트명 --template redux
// TS (CRA + redux-toolkit)
$ npx create-react-app 프로젝트명 --template redux-typescript
// 기존 파일에 설치 (JS, TS 동일)
npm install @reduxjs/toolkit react-redux
// + logger 미들웨어 설치 (선택사항)
npm i -D redux-logger @types/redux-logger
// src/store/counter.ts ---> reducer 파일 1 (action + reducer)
import { createSlice } from '@reduxjs/toolkit';
const counter = createSlice({
name: 'counter',
initialState: { // 초기값 설정
value: 0,
arr: [] as number[],
bool: false,
str: ''
},
reducers: { // 개별 리듀서 (reducers는 액션 생성자를 자동으로 만들어준다)
// 1번) action.type === up 이면 -> 이 함수를 실행시켜줘
up: (state, action) => { // 2번) state = initialState 를 안써도 처음에 initialState가 들어간다.
const step = action.payload.step; // 3번) payload가 여러개 일 경우에는 다음과 같이 사용한다.
state.value = state.value + step;
state.arr.push(action.payload.step); // 4번) redux-toolkit에서는, 맨 처음에 ...state 로 불변성 전후비교 했던거 안해도된다.
state.bool = !state.bool; // true -> false -> true -> ...
},
// 5번) action.type === down 이면 -> 이 함수를 실행시켜줘
down: (state, action) => {
state.value = state.value - action.payload.step;
state.arr.pop(); // 6번) 이외에 splice 등 원본훼손도 가능해졌다.
},
write: (state, action) => {
state.str = action.payload; // 7번) payload가 1개일 경우에는 다음과 같이 사용한다.
}
}
});
export default counter;
export const { up, down, write } = counter.actions; // 8번) dispatch에서 간편히 사용하기 위해 구조분해할당으로 내보내어 사용할 수도 있다
// src/store/index.ts ---> store 파일
import { configureStore } from '@reduxjs/toolkit'; // 1번) 반드시 필요
import logger from 'redux-logger'; // 6번) logger를 사용하고 싶다면,
import counter from './counter'; // 2번) 개별 리듀서 가져오기
export type RootState = ReturnType<typeof store.getState>; // 5번) 타입스크립트에서 useSelector를 사용할 때 반드시 필요
const store = configureStore({ // 3번) createStore + combineReducers
reducer: {
counter: counter.reducer, // 4번) 개별 리듀서 넣기
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger) // 7번) 다음과 같이 사용
});
export default store;
// src/App.tsx
import { useSelector, useDispatch } from "react-redux"; // 1번) 반드시 필요
import { RootState } from './store'; // 2번) 타입스크립트에서 useSelector를 사용할 때 반드시 필요
import counter, { up, down, write } from './store/counter'; // 3번) dispatch를 사용하기 위한 방법1 & 2
function App() {
const dispatch = useDispatch();
const { value, str } = useSelector((state: RootState) => state.counter); // 4번) state.개별리듀서명.state값
const upButton = () => {
// 5번) createSlice상수명.acionts.액션타입(payload) // 방법1) 액션 생성자 사용
dispatch(counter.actions.up({step: 2})); // === dispatch(up({step: 2}));
dispatch(write('up'));
};
const downButton = () => {
// 6번) 액션타입(payload) // 방법2) 액션 생성자를 더욱 간단하게 사용
dispatch(down({step: 2})); // === dispatch(counter.actions.down({step: 2}));
dispatch(write('down'));
};
return (
<div>
<div>
<button onClick={downButton}>-</button>
<span>{ value }</span>
<button onClick={upButton}>+</button>
</div>
<div>{ str }</div>
</div>
);
}
export default App;
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux'; // 1번) store를 사용하기 위해서 필요한 단계 1
import store from './store'; // 2번) store를 사용하기 위해서 필요한 단계 2
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<Provider store={ store }>
<App />
</Provider>
</React.StrictMode>
);
// src/store/goods.ts ---> reducer 파일 1 (action + reducer) + thunk
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; // 1번) createAsyncThunk
import { productsApi, singleApi } from '../apis/goodsApi';
export interface ProductGuard {
id: number,
title: string,
price: number,
description: string,
category: string,
image: string,
rating: { rate: number, count: number}
};
const getAll = createAsyncThunk( // 2번) 비동기 처리 액션 생성자 (thunk 함수)
'goods/getAll', // 3번) 액션 타입
async () => {
const data = await productsApi();
return data;
}
);
const getSingle = createAsyncThunk( // 비동기 처리 액션 생성자 (thunk 함수)
'goods/getSingle', // 액션 타입
async (productId: string) => {
const data = await singleApi(productId);
return data;
}
);
const goods = createSlice({
name: 'goods',
initialState: { // 초기값 설정
all: {
status: null as string | null,
data: [] as ProductGuard[]
},
single: {
status: null as string | null,
data: {} as ProductGuard
},
},
reducers: {}, // 4번) 동기적인 reducers를 사용하지 않더라도, 빈 객체라도 넣어줘야 오류가 발생하지 않는다.
// 5번) creatAsyncThunk는 액션 생성자를 자동으로 생성 X, createSlice의 extraReducers에 직접 액션 생성자를 정의해야한다.
extraReducers: (builder) => {
builder.addCase(getAll.pending, (state) => { // pending(시작, 로딩중)일때 동작할 reducer를, 2번째 인자에 함수로 전달.
state.all.status = 'loading';
})
builder.addCase(getAll.fulfilled, (state, action) => { // fulfilled(성공)일때 동작할 reducer를, 2번째 인자에 함수로 전달.
state.all.data = action.payload; // getAll thunk함수의 return 값 === action.payload
state.all.status = 'complete';
})
builder.addCase(getAll.rejected, (state) => { // rejected(실패)일때 동작할 reducer를, 2번째 인자에 함수로 전달.
state.all.status = 'fail';
})
builder.addCase(getSingle.pending, (state) => {
state.single.status = 'loading';
})
builder.addCase(getSingle.fulfilled, (state, action) => {
state.single.data = action.payload;
state.single.status = 'complete';
})
builder.addCase(getSingle.rejected, (state) => {
state.single.status = 'fail';
})
}
});
export default goods;
export { getAll, getSingle }; // 6번) thunk함수 내보내기
// src/store/index.ts ---> store 파일
import { configureStore } from '@reduxjs/toolkit'; // 1번) 반드시 필요
import { useDispatch } from "react-redux";
import logger from 'redux-logger'; // 7번) logger를 사용하고 싶다면,
import counter from './counter'; // 2번) 개별 리듀서 가져오기
import goods from './goods';
export type RootState = ReturnType<typeof store.getState>; // 6번) 타입스크립트에서 useSelector를 사용할 때 반드시 필요
export const useAppDispatch = () => useDispatch<typeof store.dispatch>(); // 9번) thunk를 사용할 때는 여기서 useDispatch를 내보낸다!
const store = configureStore({ // 3번) createStore + combineReducers
reducer: {
counter: counter.reducer, // 4번) 개별 리듀서 넣기
goods: goods.reducer, // 5번) thunk를 사용할 때도 같은 방식으로 넣어준다
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger) // 8번) 다음과 같이 사용
});
export default store;
// src/App.tsx
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux"; // 1번) thunk를 사용할 때는, 여기서 useDispatch를 가져와서
import { RootState, useAppDispatch } from './store'; // 3번) store에서 정의한 useAppDispatch를 가져와서
import { up, down } from './store/counter';
import { getAll, getSingle } from './store/goods';
function App() {
// const dispatch = useDispatch(); // 2번) 사용하는 것이 아니라,
const dispatch = useAppDispatch(); // 4번) useDispatch처럼 사용한다! (thunk 뿐만 아니라, 동기적인 reducers도 당연히 사용가능하다!)
const { value, arr } = useSelector((state: RootState) => state.counter);
const { all, single } = useSelector((state: RootState) => state.goods);
const upButton = () => dispatch(up({step: 2}));
const downButton = () => dispatch(down({step: 2}));
useEffect(() => { // 5번) Redux를 쓸 때와 다르게, redux-toolkit에서는 로딩중에 tag렌더링 error가 발생하지 않는다!
dispatch(getAll());
}, []);
const arrLists = arr.map((num, index) => <span key={index}>{num}</span>);
const allLists = all.data.map((item, index) => {
return (
<div key={index}>
<span>{item.id}</span> /
<span>{item.title}</span>
</div>
)
});
const getSingleBtn = (productId: string) => dispatch(getSingle(productId));
const singleLists = () => {
return (
<div>
<span>{single.data.id}</span> /
<span>{single.data.title}</span>
</div>
)
};
return (
<div>
<div>
<button onClick={downButton}>-</button>
<span>{ value }</span>
<button onClick={upButton}>+</button>
<div>{arrLists}</div>
</div>
<br/>
<div>
<button onClick={() => getSingleBtn('1')}>싱글</button>
<div>{allLists}</div>
<div>{singleLists()}</div>
</div>
</div>
);
}
export default App;
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux'; // 1번) store를 사용하기 위해서 필요한 단계 1
import store from './store'; // 2번) store를 사용하기 위해서 필요한 단계 2
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<Provider store={ store }>
<App />
</Provider>
</React.StrictMode>
);
useEffect mounted 에서 데이터를 요청하면, state가 undefined일때 tag가 렌더링되어 error가 발생하는 문제를 해결하기 위한 방법 2가지
function Game() {
const dispatch = useDispatch();
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
dispatch(resetBoard({height: 16, width: 16})); // 1번) dispatch를 실행하고
setIsLoaded(true); // 2번) useState로 로딩이 완료되었다는 것을 알린다
}, []); // 4번) 단, 초기에 리렌더링이 발생한다는 단점이 있다.
return (
<div>
{isLoaded ? cleanBoard(height, width): <div>Loading...</div>} // 3번) useState의 업데이트에 따라 tag를 보여주면 문제 해결!
</div>
)
}
function Game() {
const dispatch = useDispatch();
const isLoaded = useRef(false);
useEffect(() => {
dispatch(resetBoard({height: 16, width: 16})); // 1번) dispatch를 실행하고
isLoaded.current = true; // 2번) useRef로 로딩이 완료되었다는 것을 알린다
}, []); // 4번) useState와 다르게, 초기에 리렌더링이 발생하지 않는다는 장점이 있다. (리렌더링 개선 가능)
return (
<div>
{isLoaded.current ? cleanBoard(height, width): <div>Loading...</div>} // 3번) useRef의 업데이트에 따라 tag를 보여주면 문제 해결!
</div>
)
}