들어가기전에..
React에 대한 기본 지식은 생략하고 리덕스 위주로 설명했습니다.
리액트 프로젝트에서 리덕스 사용 시 가장 많이 사용하는 패턴은 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 분리하는 것입니다.
프레젠테이셔널 컴포넌트 : 주로 상태 관리가 이루어지지 않고, 그저 props를 받아 와서 화면에 UI를 보여 주기만 하는 컴포넌트
컨테이너 컴포넌트 : 리덕스와 연동되어 있는 컴포넌트로, 리덕스로부터 상태를 받아 오기도 하고 리덕스 스토어에 액션을 디스패치한다.
modules/counter.js
(1) 카운터 액션타입 정의하기
//액션 타입은 대문자로 정의하고 문자열 내용은 '모듈이름/액션 이름'과 같은 형태로 작성한다
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
(2) 카운터 액션 생성 함수 만들기
(...)
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
(3) 카운터 초기 상태 및 리듀서 함수 만들기
(...)
// 초기 상태 및 리듀서 함수 만들기
const iniitalState = {
number: 0,
};
// 초기 상태에 nubmer 값을 설정했으며, 리듀서 함수에는 현재 상태를 참조하여 새로운 객체를 생성해서 반환하는 코드를 작성함
// export default와 export의 차이점은 export는 여러 개를 내보낼 수 있지만 export default는 하나만 내보냄
function counter(state = iniitalState, action) {
switch (action.type) {
case INCREASE:
return {
number: state.number + 1,
};
case DECREASE:
return {
number: state.number - 1,
};
default:
return state;
}
}
export default counter;
modules/todos.js
(1) TODO 액션타입 정의하기
//액션 타입은 대문자로 정의하고 문자열 내용은 '모듈이름/액션 이름'과 같은 형태로 작성한다
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
(2) TODO 액션 생성 함수 만들기
(...)
export const changeInput = (input) => ({
type: CHANGE_INPUT,
input,
});
let id = 3; // insert 호출 시 1씩 더해진다. id 값은 각 todo 객체가 들고 있게 될 고윳값이다.
export const insert = (text) => ({
type: INSERT,
todo: {
id: id++,
text,
done: false,
},
});
export const toggle = (id) => ({
type: TOGGLE,
id,
});
export const remove = (id) => ({
type: REMOVE,
id,
});
(3) TODO 초기 상태 및 리듀서 함수 만들기
(...)
// 객체에 한 개 이상의 값이 들어가므로 불변성을 유지해 주어야 한다. spread 연산자를 활용하여 구현
const initialState = {
input: "",
todos: [
{
id: 1,
text: "리덕스 기초 배우기",
done: true,
},
{
id: 2,
text: "리액트와 리덕스 사용하기",
done: false,
},
],
};
function todos(state = initialState, action) {
switch (action.type) {
case CHANGE_INPUT:
return {
...state,
input: action.input,
};
case TOGGLE:
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
),
};
case INSERT:
return {
...state,
todos: state.todos.concat(action.todo),
};
case REMOVE:
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.id),
};
default:
return state;
}
}
export default todos;
modules/index.js
// 리듀서를 여러 개 만들었고 추 후 createStore 함수를 사용하여 스토어를 만들 때는 리듀서를 하나만 사용해야 한다.
// 그러므로 기존에 만들었던 리듀서를 하나로 합쳐 주어야 하는데 이작업은 리덕스에서 제공하는 combineReducers라는 유틸 함수를 사용하여 처리
import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";
const rootReducer = combineReducers({
counter,
todos,
});
export default rootReducer;
src/index.js
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { composeWithDevTools } from "redux-devtools-extension";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import rootReducer from "./modules";
//스토어 만들기, redux devtools 적용하기
const store = createStore(rootReducer, composeWithDevTools());
// 스토어를 사용할 수 있도록 Provider 컴포넌트로 감싸 주기 이때 store를 props로 전달해주어야 한다.
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
serviceWorker.unregister();
(1) 카운터 컨테이너 만들기
containers/CounterContainer.js
import React, { useCallback } from "react";
import Counter from "../components/Counter";
import { increase, decrease } from "../modules/counter";
import { useSelector, useDispatch } from "react-redux";
// useSelector을 사용하여 counter.number값을 조회 후 Counter에게 props로 넘겨준다.
const CounterContainer = () => {
const number = useSelector((state) => state.counter.number);
// 컨테이너 컴포넌트에서 액션을 디스패치해야 한다면 useDispatch사용
// useCallback으로 액션을 디스패치 하는 함수를 감싸 준다.
const dispatch = useDispatch();
const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
return (
<Counter number={number} onIncrease={onIncrease} onDecrease={onDecrease} />
);
};
export default CounterContainer;
(2) TODO 컨테이너 만들기
containers/TodosContainer.js
import React, { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import { changeInput, insert, toggle, remove } from "../modules/todos";
import Todos from "../components/Todos";
const TodosContainer = () => {
// useSelector 비구조화 할당
const { input, todos } = useSelector(({ todos }) => ({
input: todos.input,
todos: todos.todos,
}));
const dispatch = useDispatch();
const onChangeInput = useCallback((input) => dispatch(changeInput(input)), [
dispatch,
]);
const onInsert = useCallback((text) => dispatch(insert(text)), [dispatch]);
const onToggle = useCallback((id) => dispatch(toggle(id)), [dispatch]);
const onRemove = useCallback((id) => dispatch(remove(id)), [dispatch]);
return (
<Todos
input={input}
todos={todos}
onChangeInput={onChangeInput}
onInsert={onInsert}
onToggle={onToggle}
onRemove={onRemove}
/>
);
};
export default TodosContainer;
components/Counter.js
import React from "react";
const Counter = ({ number, onIncrease, onDecrease }) => {
return (
<div>
<h1>{number}</h1>
<div>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
</div>
);
};
export default Counter;
components/Todos.js
import React from "react";
const TodoItem = ({ todo, onToggle, onRemove }) => {
return (
<div>
<input
type="checkbox"
onClick={() => onToggle(todo.id)}
checked={todo.done}
readOnly={true}
/>
<span style={{ textDecoration: todo.done ? "line-through" : "none" }}>
{todo.text}
</span>
<button onClick={() => onRemove(todo.id)}>삭제</button>
</div>
);
};
const Todos = ({
input, // 인풋에 입력되는 텍스트
todos, // 할 일 목록이 들어있는 객체
onChangeInput,
onInsert,
onToggle,
onRemove,
}) => {
const onSubmit = (e) => {
e.preventDefault();
onInsert(input);
onChangeInput(""); // 등록 후 인풋 초기화
};
const onChange = (e) => onChangeInput(e.target.value);
return (
<div>
<form onSubmit={onSubmit}>
<input value={input} onChange={onChange} />
<button type="submit">등록</button>
</form>
<div>
{todos.map((todo) => (
<TodoItem
todo={todo}
key={todo.id}
onToggle={onToggle}
onRemove={onRemove}
/>
))}
</div>
</div>
);
};
export default Todos;
src/App.js
import React from "react";
import CounterContainer from "./containers/CounterContainer";
import TodosContainer from "./containers/TodosContainer";
const App = () => {
return (
<div>
<CounterContainer />
<hr />
<TodosContainer />
</div>
);
};
export default App;
정리
작은 프로젝트에서 리덕스를 적용하면 오히려 위와 같이 프로젝트의 복잡도가 높아질 수 있다. 하지만 로그인 시스템 혹은 프로젝트의 규모가 커질수록 전역 상태 관리에 대한 필요성은 높아지기 때문에 리덕스 사용시 상태를 체계적으로 관리할 수 있다.
참고 - 리액트를 다루는 기술(김민준)