$ yarn add redux react-redux
//.prettierrc
{
"singleQuote":true,
"semi":true,
"useTabs":false,
"tabWidth":2,
"trailingComma":"all",
"printWidth":80
}
리덕스에서 가장 많이 사용하는 패턴 : 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트 분리
⇒ 코드의 재사용도↑, UI 작성 시 집중 가능
프레젠테이셔널 컴포넌트 - src/components 경로
컨테이너 컴포넌트 - src/containers 경로
// 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;
// App.js
import React from 'react';
import Counter from './components/Counter';
const App = () => {
return (
<div>
<Counter number={0} />
</div>
);
};
export default App;
//components/Todos.js
import React from 'react';
const TodoItem = ({ todo, onToggle, onRemove }) => {
return (
<div>
<input type="checkbox" />
<span>예제 텍스트</span>
<button>삭제</button>
</div>
);
};
const Todos = ({
input,
todos,
onChangeInput,
onInsert,
onToggle,
onRemove,
}) => {
const onSubmit = e => {
e.preventDefault();
};
return (
<div>
<form onSubmit={onSubmit}>
<input />
<button type="submit">등록</button>
</form>
<div>
<TodoItem />
<TodoItem />
<TodoItem />
<TodoItem />
<TodoItem />
</div>
</div>
);
};
export default Todos;
// App.js
import React from 'react';
import Counter from './components/Counter';
**import Todos from './components/Todos';**
const App = () => {
return (
<div>
<Counter number={0} />
**<hr />
<Todos />**
</div>
);
};
export default App;
리덕스 사용 시, 액션 타입, 액션 생성 함수, 리듀서 코드 작성해야 함
코드들을 각각 다른 파일에 작성하는 방법
기능별로 묶어서 파일 하나에 작성하는 방법
모듈 : Ducks 패턴을 사용하여 액션 타입, 액션 생성 함수, 리듀서를 작성한 코드
// modules/counter.js
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
액션 타입은 대문자로 정의
문자열 내용은 ‘모듈 이름/액션 이름’ 형태
// modules/counter.js
// (1) 액션 타입 정의
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
// (2) 액션 생성 함수 만들기
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export 키워드가 들어감 → 이 함수를 다른 파일에서 불러와 사용 가능
// 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 initialState = {
number: 0
};
function counter(state = initialState, action) {
switch (action.type) {
case INCREASE:
return {
number: state.number + 1
};
case DECREASE:
return {
number: state.number - 1
};
default:
return state;
}
}
export default counter;
import counter, {increase,decrease} from './counter';
// modules/todos.js
// (1) 액션 타입 정의하기
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';
// modules/todos.js
// (1) 액션 타입 정의하기
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';
// (2) 액션 생성 함수 만들기
export const changeInput = input => ({
type: CHANGE_INPUT,
input
});
let id = 3;
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
});
불변성 유지 ⇒ spread 연산자 or 배열 내장 함수 사용
// modules/todos.js
(...)
// (3) 초기 상태 및 리듀서 함수 만들기
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 INSERT:
return {
...state,
todos: state.todos.concat(action.todo)
};
case TOGGLE:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo)
};
case REMOVE:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.id)
};
default:
return state;
}
}
export default todos;
createStore 함수를 사용하여 스토어를 만들 때는 리듀서를 ❗하나만❗ 사용해야 함
⇒ combineReducers 유틸 함수를 사용해서 리듀서를 하나로 합쳐주어야 함
// modules/index.js
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 './index.css';
import App from './App';
import rootReducer from './modules';
const store = createStore(rootReducer);
ReactDOM.render(<App />, document.getElementById('root'));
리액트 컴포넌트에서 스토어를 사용할 수 있도록 Provider 컴포넌트로 감싸 줌
→ state를 props로 전달해 주어야 함
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import rootReducer from './modules';
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'));
// 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 rootReducer from './modules';
const store = createStore(rootReducer, composeWithDevTools());
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'));
// containers/CounterContainer.js
import React from 'react';
import Counter from '../components/Counter';
const CounterContainer = () => {
return <Counter />;
};
export default CounterContainer;
리덕스 연동 위해 connect 함수 사용해야 함
connect(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트)
connect 함수 호출 후 또 다른 함수 반환 → 반환된 함수에 컴포넌트를 파라미터로 넣어 줌
// App.js
import React from 'react';
import CounterContainer from './containers/CounterContainer';
import Todos from './components/Todos';
const App = () => {
return (
<div>
<CounterContainer />
<hr />
<Todos />
</div>
);
};
export default App;
// containers/CounterContainer.js
import React from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease } from '../modules/counter';
const CounterContainer = () => {
return (<Counter number={number} onIncrease={increase} onDecrease={decrease} />);
};
const mapStateToProps = state => ({
number: state.counter.number,
});
const mapDispatchToProps = dispatch => ({
increase: () => {
dispatch(increase());
},
decrease: () => {
dispatch(decrease());
},
});
export default connect(
mapStateToProps,
mapDispatchToProps)
(CounterContainer);
connect 함수 사용 시, 보통 mapStateToProps와 mapDispatchToProps 미리 선언하고 사용함
⇒ 익명 함수 형태로 선언해도 됨
만약, 액션을 디스패치 하기 위해 dispatch로 감싸는 작업이 귀찮다면,
// containers/TodosContainer.js
import React from 'react';
import { connect } from 'react-redux';
import { changeInput, insert, toggle, remove } from '../modules/todos';
import Todos from '../components/Todos';
const TodosContainer = ({
input,
todos,
changeInput,
insert,
toggle,
remove,
}) => {
return (
<Todos
input={input}
todos={todos}
onChangeInput={changeInput}
onInsert={insert}
onToggle={toggle}
onRemove={remove}
/>
);
};
export default connect(
({ todos }) => ({
input: todos.input,
todos: todos.todos,
}),
{
changeInput,
insert,
toggle,
remove,
},
)(TodosContainer);
// 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;
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;
$ yarn add redux-actions
// modules/counter.js
import { createAction, handleActions } from 'redux-actions';
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
const initialState = {
number: 0,
};
const counter = handleActions(
{
[INCREASE]: (state, action) => ({ number: state.number + 1 }),
[DECREASE]: (state, action) => ({ number: state.number - 1 }),
},
initialState,
);
export default counter;
각 액션 생성 함수에 파라미터를 필요로 할 경우, 액션에 필요한 추가 데이터는 모두 payload라는 이름을 사용함
파라미터로 받아온 값을 객체에 넣을 때도, action.payload라는 이름으로 넣어야 함
조회할 때도 공통적으로 action.payload 값을 조회하도록 리듀서를 구현해야 함
⇒ 모든 값을 action.payload로 사용하기 때문에 헷갈릴 수 있으므로
객체 비구조화 할당 문법으로 action 값의 payload 이름을 새로 설정해주면 됨
객체의 구조가 복잡해지거나 객체로 이루어진 배열을 다룰 경우, immer를 사용하면 훨씬 편리하게 상태 관리 가능
모든 업데이트 함수에 immer를 적용할 필요는 없음
import { createAction, handleActions } from 'redux-actions';
import produce from 'immer';
(...)
const todos = handleActions(
{
[CHANGE_INPUT]: (state, { payload: input }) =>
produce(state, draft => {
draft.input = input;
}),
[INSERT]: (state, { payload: todo }) =>
produce(state, draft => {
draft.todos.push(todo);
}),
[TOGGLE]: (state, { payload: id }) =>
produce(state, draft => {
const todo = draft.todos.find(todo => todo.id = = = id);
todo.done = !todo.done;
}),
[REMOVE]: (state, { payload: id }) =>
produce(state, draft => {
const index = draft.todos.findIndex(todo => todo.id = = = id);
draft.todos.splice(index, 1);
}),
},
initialState,
);
export default todos;
connect 함수를 사용하는 대신 react-redux에서 제공하는 Hooks 사용 가능
해당 컨테이너 컴포넌트의 props가 바뀌지 않았다면 리렌더링 자동 방지되어 성능 최적화됨
최적화 작업 자동으로 이루어지지 X. 성능 최적화를 위해 React.memo 사용해 주어야 함
답 : 컨테이너 컴포넌트
답 : Ducks 패턴
18장 리덕스 미들웨어를 통한 비동기 작업 관리 (0) | 2022.01.24 |
---|---|
[리액트를 다루는 기술] 16장 리덕스 라이브러리 이해하기 (0) | 2022.01.17 |
[리액트를 다루는 기술] 15장 Context API (0) | 2022.01.10 |
[리액트를 다루는 기술] 14장 외부 API를 연동하여 뉴스 뷰어 만들기 (0) | 2022.01.03 |
[리액트를 다루는 기술]13장 리액트 라우터로 SPA 개발하기 (0) | 2021.12.27 |