heyday2024 님의 블로그
[React 숙련 1주차(5)] Redux: todolist, RTK 본문
<TODOLIST 코드>
// Action value
const ADD_TODO = "ADD_TODO";
const DELETE_TODO = "DELETE_TODO";
const TOGGLE_TODO = "TOGGLE_TODO";
// Action Creator
export const addTodo = (payload) => {
return { type: ADD_TODO, payload };
};
export const deleteTodo = (payload) => {
return { type: DELETE_TODO, payload };
};
export const toggleTodo = (payload) => {
return { type: TOGGLE_TODO, payload };
};
// initial State
const initialState = {
todos: [
{
id: 1,
title: "react를 배워봅시다.",
},
{
id: 2,
title: "redux를 배워봅시다.",
},
],
};
// Reducer
const todos = (state = initialState, action) => {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, action.payload],
};
case DELETE_TODO:
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload.id),
};
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload.id
? { ...todo, isDone: !todo.isDone }
: todo
),
};
default:
return state;
}
};
export default todos;
- src >>redux >> modules >> todos.js
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { addTodo } from "../redux/modules/todos";
import styled from "styled-components";
const AddForm = () => {
const [title, setTitle] = useState("");
const dispatch = useDispatch();
const onSubmitHandler = (e) => {
e.preventDefault();
if (title === "") return; // 아무것도 입력하지 않았을 때 dispatch 하지 않음
dispatch(
addTodo({
id: new Date().getTime(),
title,
})
);
};
return (
<StFormContainer>
<form onSubmit={onSubmitHandler}>
<label>Todos의 제목을 입력하세요</label>
<br />
<StInput
type="text"
value={title}
onChange={(e) => {
setTitle(e.target.value);
}}
/>
<StButton>추가하기</StButton>
</form>
</StFormContainer>
);
};
export default AddForm;
const StFormContainer = styled.div`
display: flex;
gap: 24px;
padding: 30px;
text-indent: 2rem;
line-height: 40px;
`;
const StButton = styled.button`
border: none;
background-color: #eee;
height: 25px;
cursor: pointer;
width: 120px;
border-radius: 12px;
&:hover {
background-color: orange;
color: white;
border: 2px solid black;
}
`;
const StInput = styled.input`
border: 1px solid #eee;
margin: 0 24px;
height: 25px;
width: 300px;
border-radius: 12px;
outline: none;
padding: 0 10px;
&:focus {
border: 2px solid black;
}
`;
- AddForm.jsx
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import styled from "styled-components";
import { deleteTodo, toggleTodo } from "../redux/modules/todos";
const TodoList = () => {
const { todos } = useSelector((state) => state.todos);
const dispatch = useDispatch();
// handlers
const onDeleteHandler = (id) => {
dispatch(deleteTodo({ id }));
};
const onToggleHandler = (id) => {
dispatch(toggleTodo({ id }));
};
// todoList, doneList
const todoList = [];
const doneList = [];
todos.forEach((todo) => {
if (todo.isDone) {
doneList.push(todo);
} else {
todoList.push(todo);
}
});
return (
<>
<h2>할 일</h2>
<StTodos>
{todoList.map((todo) => (
<StTodo key={todo.id}>
{todo.title}
<StButtonContainer>
<StButton color="green" onClick={() => onToggleHandler(todo.id)}>
완료
</StButton>
<StButton color="red" onClick={() => onDeleteHandler(todo.id)}>
삭제
</StButton>
</StButtonContainer>
</StTodo>
))}
</StTodos>
<h2>완료한 일</h2>
<StTodos>
{doneList.map((todo) => (
<StTodo key={todo.id}>
{todo.title}
<StButtonContainer>
<StButton color="green" onClick={() => onToggleHandler(todo.id)}>
취소
</StButton>
<StButton color="red" onClick={() => onDeleteHandler(todo.id)}>
삭제
</StButton>
</StButtonContainer>
</StTodo>
))}
</StTodos>
</>
);
};
export default TodoList;
const StTodos = styled.div`
display: flex;
gap: 12px;
flex-wrap: wrap;
`;
const StTodo = styled.div`
border: 1px solid #ddd;
width: 20%;
height: 100px;
display: flex;
gap: 12px;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0 24px;
border-radius: 12px;
`;
const StButtonContainer = styled.section`
display: flex;
gap: 6px;
`;
const StButton = styled.button`
border: none;
cursor: pointer;
border-radius: 12px;
padding: 6px 12px;
${(props) =>
props.color === "green"
? "background-color: lightgreen;"
: props.color === "red"
? "background-color: lightcoral;"
: "background-color: #eee;"}
&:hover {
background-color: #f7ed63;
border: 2px solid black;
}
`;
- TodoList.jsx
RTK(Redux ToolKit)이란 무엇일까요?
리덕스 툴킷은 리덕스를 개량한 것!
리덕스를 사용하기 위해 작성했던 ducks 패턴의 요소들이 전체적인 코드의 양을 늘린다는 개발자들의 불만이 발생하기 시작했고, 리덕스 팀에서는 이것을 수용하여 코드는 더 적게, 그리고 리덕스를 더 편하게 쓰기 위한 기능들을 흡수해서 만든 것이 리덕스툴킷(RTK)
리덕스 툴킷은 우리가 배웠던 리덕스와 구조나 패러다임과 같고, 그저 일일히 손으로 다 적어줘야했던 ducks 패턴을 위한 코드들이 새로운 API의 추가에 의해 어느정도 자동화됨.
===> 컴포넌트에서 useSelector를 통해서 사용하는 것은 모두 똑같음. 바뀐부분은 그저 모듈 파일 뿐!!
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
number: 0,
};
// action과 reducer를 합치는 느낌
const counterSlice = createSlice({
name: "counter",
initialState,
// 객체 형태로: 여러개의 reducer가 들어갈 수 있음.
reducers: {
addNumber: (state, action) => {
state.number = state.number + action.payload;
},
minusNumber: (state, action) => {
state.number = state.number - action.payload;
},
},
});
// 액션크리에이터는 컴포넌트에서 사용하기 위해 export 하고
export const { addNumber, minusNumber } = counterSlice.actions;
// reducer 는 configStore에 등록하기 위해 export default 합니다.
export default counterSlice.reducer;
- Slice API를 사용해서 Modules 디렉토리가 아닌 Slice 디렉토리에 저장한 위 파일을 보면,
- 확실히 코드가 짧아졌다!!!!
==> 큰 차이점은 Action Value와 Action Creator를 이제 직접 생성해주지 않고, Action Value, Action Creator, Reducer가 하나로 합쳐졌다는 점
//createSlice API 뼈대
const counterSlice = createSlice({
name: '', // 이 모듈의 이름
initialState : {}, // 이 모듈의 초기상태 값
reducers : {}, // 이 모듈의 Reducer 로직
})
counterSlice 리듀서 객체 안에서 만들어주는 함수가 리듀서의 로직이 되면서도 동시에 Action Creator가 된다는 점.
그리고 Action Value 까지 함수의 이름을 따서 자동으로 만들어짐
==>결국 우리는 Reducer만 만들어주면 됨.
// counterSlice.js의 Slice 구조
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
// 리듀서 안에서 만든 함수 자체가 리듀서의 로직이자, 액션크리에이터가 된다.
addNumber: (state, action) => {
state.number = state.number + action.payload;
},
minusNumber: (state, action) => {
state.number = state.number - action.payload;
},
},
});
- 그 후 일반 리덕스에서 export를 통해서 각각의 Action Creator를 내보내주었던 것처럼 똑같이 내보내주고, counterSlice.reducer를 default로 내보내주면됨.
==>나중에 리듀서에 로직을 추가할 때마다 함수를 추가해서 내보내주면 됨.
// 액션크리에이터는 컴포넌트에서 사용하기 위해 export 하고
export const { addNumber, minusNumber } = counterSlice.actions;
// reducer 는 configStore에 등록하기 위해 export default 합니다.
export default counterSlice.reducer;
// src/redux/slices/config/configStore.js
import { configureStore } from "@reduxjs/toolkit";
/**
* import 해온 것은 slice.reducer 입니다.
*/
import counter from "../slices/counterSlice";
import todos from "../slices/todosSlice";
/**
* 모듈(Slice)이 여러개인 경우
* 추가할때마다 reducer 안에 각 모듈의 slice.reducer를 추가해줘야 합니다.
*
* 아래 예시는 하나의 프로젝트 안에서 counter 기능과 todos 기능이 모두 있고,
* 이것을 각각 모듈로 구현한 다음에 아래 코드로 2개의 모듈을 스토어에 연결해준 것 입니다.
*/
const store = configureStore({
reducer: { counter: counter, todos: todos },
});
export default store;
- configStore.js
===> reducer: { counter: counter, todos: todos }, 이런 식으로 모듈을 스토어에 연결해주기....
Redux Devtools
redux로 개발할 때 사용할 수 있는 강력한 개발툴
현재 프로젝트의 state 상태라던가, 어떤 액션이 일어났을 때 그 액션이 무엇이고, 그것으로 인해 state가 어떻게 변경되었는지 등 리덕스를 사용하여 개발할 때 아주 편리하게 사용
Flux 패턴
Facebook에 의해 개발된 애플리케이션 아키텍처(방법론임!!!)로, 주로 React와 함께 사용되며, 데이터의 단방향 흐름을 강조
Q : 단방향 흐름이 무엇을 말하는 걸까요?
A : 액션 → 디스패처 → 스토어 → 뷰. 이렇게 관리되는 순환적인 흐름은 애플리케이션의 데이터 흐름을 예측 가능하게 만들어, 복잡한 상호작용이 많은 대규모 애플리케이션을 쉽게 관리할 수 있게 돕습니다.
(그리고 단방향이다보니까 어느 단계에서 문제가 생겼는지 빠르게 파악하고 해결가능함.)
<구성요소>
Dispatcher: 애플리케이션 내 모든 데이터 흐름을 관리하는 중앙 허브 역할을 합니다. 액션들이 발생하면 디스패처를 통해 스토어로 전달됩니다.
Stores: 애플리케이션의 상태(데이터)와 로직을 보유합니다. 스토어는 디스패처를 통해 전달된 액션에 반응하여 상태를 변경하고, 변경 사항을 뷰에 알립니다.
Actions: 상태 변화를 일으킬 때 사용하는 간단한 객체입니다. 사용자 인터페이스에서 발생한 사용자의 행동을 액션으로 표현하고, 이를 디스패처를 통해 스토어로 전달합니다.
Views (React Components): 사용자 인터페이스를 구성하는 React 컴포넌트들입니다. 스토어에서 상태가 변하면, 뷰는 이를 반영하여 사용자 인터페이스를 업데이트합니다.
Ducks 패턴과 Flux 패턴
Ducks 패턴과 Flux 패턴은 둘 다 Redux 같은 상태 관리 라이브러리에서 코드 구조를 정리하기 위한 패턴임.
==> 애플리케이션의 상태 관리와 데이터 흐름의 체계를 잡기 위한 방법론들!
각각 코드의 구조화와 데이터 관리 방식에서 서로 다른, 차별화된 접근을 제공
==> Ducks는 주로 Redux 코드의 구조를 단순화하는 데 초점을 맞추고, Flux는 애플리케이션의 데이터 흐름을 체계화하는 데 중점을 둠
1. Ducks 패턴
Ducks 패턴은 Redux의 파일 및 코드 관리 문제를 해결하기 위해 고안된 패턴임. Redux를 처음 사용할 때는 파일을 actions, reducers, types 등으로 분리하는 방식으로 코드를 관리하는 경우가 많은데, 이 방식은 규모가 커지면 관리가 어렵고 파일이 분산되어 코드의 가독성이 떨어질 수 있음. Ducks 패턴은 이를 해결하기 위해 기능별로 모든 관련 코드를 하나의 파일에 모아 관리하는 방식임.
Ducks 패턴의 특징
- 하나의 기능(예: todos)에 관련된 action type, action creator, reducer를 한 파일에 묶음.
- 코드 구조가 간결해지며, 파일 개수가 줄어들어 유지보수와 파일 관리가 쉬워짐.
- Redux의 구조적인 복잡성을 줄이기 위해 나온 패턴임.
2. Flux 패턴
Flux 패턴은 Facebook에서 애플리케이션의 데이터 흐름을 단방향으로 관리하기 위해 제안한 아키텍처임. Redux도 Flux 아키텍처에서 영향을 받은 라이브러리이며, Flux 패턴을 따른다고 볼 수 있음.
Flux 패턴의 핵심은 단방향 데이터 흐름과 중앙 집중식 스토어 관리임. 이 패턴을 사용하면 데이터 흐름이 예측 가능해지고, 버그 추적이 쉬워짐.
Flux 패턴의 구성 요소
- Action: 사용자나 시스템이 일으킨 이벤트로, 데이터 변경을 요청하는 역할을 함.
- Dispatcher: 액션을 받아서 스토어로 전달하는 역할을 하는 중앙 허브임. 모든 액션은 반드시 디스패처를 통해 전달됨.
- Store: 상태와 비즈니스 로직을 포함하며, 애플리케이션의 중앙 상태를 관리함.
- View: 사용자 인터페이스를 담당하며, 스토어의 상태를 읽고 렌더링함. 사용자가 상호작용을 하면 액션을 생성하여 디스패처에 전달함.
Flux 패턴의 데이터 흐름
- 사용자가 View와 상호작용을 하여 Action을 생성함.
- Action이 Dispatcher를 통해 Store로 전달됨.
- Store가 상태를 업데이트하고, View에 변화를 반영하도록 알림.
- View는 새로운 상태를 받아 UI를 다시 렌더링함.
Redux는 이러한 Flux 패턴의 단방향 데이터 흐름을 따르지만, Dispatcher 대신 dispatch 함수를 통해 액션을 스토어에 전달함. 또한 Flux와 달리 여러 스토어 대신 단일 스토어를 사용하는 것이 Redux의 특징임.
Ducks 패턴과 Flux 패턴 비교
Ducks | Flux | |
목적 | Redux 파일 구조 간소화 및 유지보수 용이 | 단방향 데이터 흐름을 위한 구조 |
구조 | 기능별로 액션, 리듀서, 타입 등을 한 파일로 통합 | 액션, 디스패처, 스토어, 뷰로 구성 |
사용되는 위치 | 주로 Redux 기반 프로젝트 | 단방향 데이터 흐름을 필요로 하는 구조 |
장점 | 기능별 코드 관리를 통해 유지보수가 용이 | 데이터 흐름이 예측 가능 |
단점 | 대규모 프로젝트에서 파일이 커질 수 있음 | 다소 복잡한 구조 |
===> Ducks 패턴을 사용하여 기능별로 코드를 나누는 것은 유지보수를 쉽게 하고 Redux의 초기 설정 복잡성을 줄일 수 있으며, Flux 패턴은 단방향 데이터 흐름을 강조하여 상태 관리의 예측 가능성을 높이는 데 도움을 줌.
https://bestalign.github.io/translation/cartoon-guide-to-flux/
https://taegon.kim/archives/5288