Redux
연습 프로젝트를 통해, Redux
를 적용시켜보고, ducks 패턴을 적용시켜 본 뒤, Redux Toolkit(RTK)
으로 전환하는 것까지 진행을 해보았다.Redux
를 적용해보고자 한다.Redux
와 RTK
라이브러리의 차이점은 코어기능만 설치하느냐, 아니면 유용한 라이브러리를 함께 설치하느냐의 차이이다.createStore + combineReducers + thunk + devTool→
configureStore
action + action type + reducer + immer →createSlice
RTK
를 함께 설치하는 것을 권장한다.Redux
를 React
환경과 연결하기 위해서는 react-redux
를 함께 설치해주어야 하는데, 이는 그 둘을 연결 시켜주는 역할을 한다.npm i redux react-redux @reduxjs/toolkit
src
폴더에 modules
라는 폴더를 생성해주었고, 여기에 todos
라는 파일을 만들어 상태를 관리하기로 했다.// modules/todos.ts
import { createSlice } from '@reduxjs/toolkit';
interface ITodo {
id: string;
done: boolean;
content: string;
}
const initialState = {
todos: [],
};
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
getLocalStorage(state: { todos: ITodo[] }) {
...
},
addTodo(state: { todos: ITodo[] }, action: { payload: ITodo }) {
...
},
toggleTodoStatus(
state: { todos: ITodo[] }, action: { payload: { id: string } }
) {
...
},
editTodo(state: { todos: ITodo[] }, action: {
payload: { id : string, content: string } }
) {
...
},
deleteTodo(state: { todos: ITodo[] }, action: { payload: { id: string } }) {
...
},
clearDoneList(state: { todos: ITodo[] }) {
...
},
},
});
export const todosActions = todosSlice.actions;
export default todosSlice.reducer;
createSlice
메소드를 불러오고, 이름과 초기 상태, 필요한 reducer
를 객체로 전달해주었다.reducers
에 작성된 함수들은 todosSlice.actions
에 저장되어 기존 action
처럼 사용가능하다.
name
속성이 유니크한action 타입
을 자동으로 만들어주고,action
또한reducers
에 작성된 함수의 이름으로 자동생성 되므로, 따로action 타입
을 만들 필요도,action 생성 함수
를 만들 필요도 없다.
store
에 전달하기 위한 reducer
는 todosSlice.reducer
에 저장된다.// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import App from './App';
import todosSlice from './modules/todos';
const store = configureStore({
reducer: {
todosSlice,
...
// 다수의 리듀서 작성가능.
},
});
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>
);
store
를 전달할 컴포넌트를 Provider
로 감싼다.configureStore
에 객체를 이용하여 reducer
를 전달하여 store
를 만들고, 해당 값을 store
변수에 저장한다.combineReducers
의 기능이 내장되어 있으므로, reducer
속성에 다수의 reducer
를 작성 할 수 있다.Provider
의 store
props 에 저장한 store
를 전달한다.// App.tsx
import { useSelector } from 'react-redux';
...
const todoSlice = useSelector(
(state: { todosSlice: { todos: ITodo[] } }) => state.todosSlice.todos
);
useSelector
를 이용하여 store
에 저장된 상태를 가져 올 수 있다. 위 처럼 작성하여 todoSlice
에 해당 상태가 담길 수 있도록 했다.store
에서 전달한 상태가 담긴다.// components/Input.tsx
import { useDispatch } from 'react-redux';
import {todoActions} from 'modules/todos';
...
const dispatch = useDispatch()
...
const addTodo = (
e:
| React.FormEvent<HTMLFormElement>
| React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
e.preventDefault();
if (!inputValue) return;
dispatch(
todosActions.addTodo({
id: `${uuidv4()}`,
done: false,
content: inputValue,
})
);
setInputValue('');
};
useDispatch
를 이용하여 전달받은 action
을 실행하고, 상태를 변경한다.useDispatch
를 변수에 저장하고, 해당 변수에 인자를 담아 실행하는 것으로 인자 내부의 내용을 payload
로 전달하여 상태를 변경 할 수 있다.const [todoList, setTodoList] = useState<ITodo[]>([]);
useEffect(() => {
const data: string | null = window.localStorage.getItem('data');
if (data) setTodoList(JSON.parse(data));
}, []);
// 페이지를 불러 올 때 데이터가 있는 경우,
// todoList 의 값을 해당 JSON 을 객체로 파싱한 값으로 갱신한다.
useEffect(() => {
window.localStorage.setItem('data', JSON.stringify(todoList));
}, [todoList]);
// 상태가 변경될 때 해당 내용을 로컬 스토리지에 저장한다.
useState
를 사용하는 경우 위의 코드만으로 가능했다.redux
를 사용할 땐 reducer
내부에서 useEffect
를 사용 할 수 없기 때문에 reducer
에서 로컬 스토리지의 값을 가져와 갱신하는 action 을 만들고, 렌더링시에 호출하는 방식을 사용하였다.addTodo
action 을 사용하기 위해선 반복문으로 일일히 넣어줘야하는데, 이 보단 전체 객체를 가져와 업데이트 하는 방식이 더 빠를 것.// module/todos.ts
const initialState = {
todos: [],
};
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// 로컬 스토리지 가져오기
getLocalStorage(state: { todos: ITodo[] }) {
const localData = window.localStorage.getItem('todosData') as string;
state.todos = JSON.parse(localData);
},
},
});
export const todosActions = todosSlice.actions;
export default todosSlice.reducer;
// App.tsx
const dispatch = useDispatch();
useEffect(() => {
const data: string | null = window.localStorage.getItem('todosData');
if (data) dispatch(todosActions.getLocalStorage());
}, []);
useEffect(() => {
window.localStorage.setItem('todosData', JSON.stringify(todoSlice));
}, [todoSlice]);