yarn create react-app react-redux
cd react-redux
yarn add redux react-redux
프로젠테이셔널 컴포넌트
: 상태 관리 X
: props 받아와 화면에 UI 보여주기만 함
컨테이너 컴포넌트
: 리덕스와 연동된 컴포넌트
: 리덕스로부터 상태 받아와 리덕스 스토어에 액션을 디스패치
숫자 더하기/빼기
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" />
<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 패턴 사용
: 액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 몰아 작성
1. 액션 타입 정의
src/modules/counter.js
const INCREASE="counter/INCREASE";
const DECREASE="counter/DECREASE";
2. 액션 생성 함수 만들기/내보내기
src/modules/counter.js
(...)
export const increase=()=>({type:INCREASE});
export const decrease=()=>({type:DECREASE});
3. 초기 상태 및 리듀서 함수 생성
src/modules/counter.js
(...)
//초기 상태 설정
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;
1. 액션 타입 정의
src/modules/todos.js
const CHANGE_INPUT="todos/CHANGE_INPUT"; //Input 값 변경
const INSERT="todos/INSERT"; //새로운 todo 등록
const TOGGLE="todos/TOGGLE"; //todo 체크/체크해제
const REMOVE="todos/REMOVE"; //todo 제거
2. 액션 생성 함수 작성
src/modules/todos.js
(...)
export const changeInput=input=>({
type:CHANGE_INPUT,
input
});
let id=3; //초기 상태 작성 때 todo 객체 2개를 사전에 미리 넣을 예정임
export const insert=text=>({
//호출될 때마다 id값에 1씩 더해줘야함으로 id 값은 todo객체가 들고 있게 될 고유값
type:INSERT,
todo:{
id:id++,
text,
done:false
}
});
export const toggle =id=>({
type:TOGGLE,
id
});
export const remove=id=>({
type:REMOVE,
id
});
3. 초기 상태 설정
src/modules/todos.js
//초기 상태 설정
const initialState={
input:"",
todos:[
{
id:1,
text:"리덕스 기초 배우기",
done:true
},
{
id:2,
text:"리액트와 리덕스 사용하기",
done:true
},
]
}
4. 리듀서 함수 만들기
src/modules/todos.js
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;
5. 루트 리듀서 생성
리덕스에서 제공하는 combineReducers 함수 이용해 기존에 만든 여러 리듀스를 하나로 합쳐서 리듀서 한개만 사용하는 스토어에서 가능하도록 함
src/modules/index.js
import { combineReducer } from "redux";
import counter from "./counter";
import todos from "./todos";
const rootReducer = combineReducer({
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"));
리액트 컴포넌트에서 스토어를 사용할 수 있도록 App 컴포넌트를 react-redux에서 제공하는 Provider 컴포넌트로 감싸기
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";
import { Provider } from "react-redux";
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
크롬 확장 프로그램에서 redux DevTools 설치
//패키지 설치
yarn add redux-devtools-extension
프로젝트에 패키지 적용
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";
import { Provider } from "react-redux";
import { composeWithDevTools } from "redux-devtools-extension";
const store = createStore(rootReducer, composeWithDevTools());
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
개발자 도구 열어 Redux에서 State로 리덕스 스토어 내부 상태 확인
컨테이너 컴포넌트
: 리덕스 스토어와 연동된 컴포넌트
컴포넌트에서 리덕스 스토어에 접근해 원하는 상태 받아오기
액션도 디스패치 해주기
src/containers/CounterContainer.js
import React from "react";
import Counter from "../components/Counter";
const CounterContainer = () => {
return <Counter />;
};
export default CounterContainer;
react-redux의 connect 함수 사용해 컴포넌트를 리덕스와 연동
connect(mapStateToProps,mapDispatchToProps)(연동할 컴포넌트)
src/containers/CounterContainer.js
import React from "react";
import { connect } from "react-redux";
import Counter from "../components/Counter";
const CounterContainer = ({ number, increase, decrease }) => {
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease} />
);
};
const mapStateToProps = (state) => ({
//state를 파라미터로 받아오며 해당 값은 현재 스토어가 지닌 상태
number: state.counter.number,
});
const mapDispatchToProps = (dispatch) => ({
//store의 내장 함수 dispatch를 파라미터로 받아옴
//진행 절차 설명을 위한 임시 함수
increase: () => {
console.log("increase");
},
decrease: () => {
console.log("decrease");
},
});
//connect 함수 호출
export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);
App에서 CounterContainer로 교체
App.js
import React from "react";
import Todos from "./components/ToDos";
import CounterContainer from "./containers/CounterContainer";
const App = () => {
return (
<div>
<CounterContainer />
<hr />
<Todos />
</div>
);
};
export default App;
브라우저에서 +1과 -1 클릭에 따른 콘솔에 increase와 decrease 찍힘
임시 함수 대신 액션 생성 함수 가져와 액션 객체 생성 후 디스패치
src/containers/CounterContainer.js
(...)
import { increase, decrease } from "../modules/counter";
(...)
const mapDispatchToProps = (dispatch) => ({
//store의 내장 함수 dispatch를 파라미터로 받아옴
increase: () => {
dispatch(increase());
},
decrease: () => {
dispatch(decrease());
},
});
export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);
+1, -1 버튼 클릭에 따른 숫자 변화
connect 함수 내부에 익명 함수 형태로 선언 가능
(...)
const CounterContainer = ({ number, increase, decrease }) => {
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease} />
);
};
export default connect(
//connect 함수 내부 익명 함수로 작성
state=>({
number:state.counter.number,
}),
dispatch=>({
increase:()=>dispatch(increase()),
decrease=()=>dispatch(decrease())
})
)(CounterContainer);
리덕스 제공 bindActionCreators 함수 이용해 각 액션 생성 함수 호출하고 dispatch로 감싸는 작업 한 번에 수행 가능
(...)
import { bindActionCreators } from "redux";
(...)
export default connect(
(state) => ({
number: state.counter.number,
}),
(dispatch) =>
bindActionCreators(
{
increase,
decrease,
},
dispatch
)
)(CounterContainer);
mapDispatchToProps의 파라미터를 액션 생성 함수로 이루어진 객체 형태로 넣어 connect 함수가 내부적으로 bindActionCreators 작업 수행
(...)
export default connect(
(state) => ({
number: state.counter.number,
}),
{
increase,
decrease,
}
)(CounterContainer);
todos 모듈에서 작성한 액션 생성 함수와 상태 안의 값을 컴포넌트의 props로 전달
src/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 분리
//state.todos.input 대신 todos.input 사용
({ todos }) => ({
input: todos.input,
todos: todos.todos,
}),
{
changeInput,
insert,
toggle,
remove,
}
)(TodosContainer);
App 컴포넌트에서 TodosContainer 컴포넌트 보이게 작성
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;
Todos 컴포넌트에서 받아 온 props 사용하도록 구현
src/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;
//라이브러리 설치
yarn add redux-actions
counter 모듈에 적용
modules/counter.js
import { createAction, handleActions } from "redux-actions";
//액션 타입 정의
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
//createAction 사용해 액션 생성 함수 선언/내보내기
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
//초기 상태 설정
const initialState = {
number: 0,
};
//handleActions 사용해 리듀서 함수 작성 (가독성 높아짐)
const counter = handleActions(
{
//각 액션에 대한 업데이트 함수
[INCREASE]: (state, action) => ({ number: state.number + 1 }),
[DECREASE]: (state, action) => ({ number: state.number - 1 }),
},
//초기 상태 넣기
initialState
);
//리듀서 함수 내보내기
export default counter;
todos 모듈에 적용
//예시
const MY_ACTION="sample/MY_ACTION";
const myAction=createAction(MY_ACTION,text=>`${text}!`);
const action=myAction('hello world');
/*결과
{type : MY_ACTION, payload : 'hello world!' }*/
import { createAction, handleActions } from "redux-actions";
(...)
export const changeInput = createAction(CHANGE_INPUT, (input) => input);
let id = 3; //초기 상태 작성 때 todo 객체 2개를 사전에 미리 넣을 예정임
export const insert = createAction(INSERT, (text) => ({
//insert가 호출될 때마다 1씩 더해짐
id: id++,
text,
done: false,
}));
export const toggle = createAction(TOGGLE, (id) => id);
export const remove = createAction(REMOVE, (id) => id);
(...)
import { createAction, handleActions } from "redux-actions";
(....)
const todos = handleActions(
{
[CHANGE_INPUT]: (state, action) => ({ ...state, input: action.payload }),
[INSERT]: (state, action) => ({
...state,
todos: state.todos.concat(action.payload),
}),
[TOGGLE]: (state, action) => ({
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload ? { ...todo, done: !todo.done } : todo
),
}),
[REMOVE]: (state, action) => ({
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload),
}),
},
initialState
);
export default todos;
(...)
const todos = handleActions(
{
[CHANGE_INPUT]: (state, { payload: input }) => ({ ...state, input }),
[INSERT]: (state, { payload: todo }) => ({
...state,
todos: state.todos.concat(todo),
}),
[TOGGLE]: (state, { payload: id }) => ({
...state,
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
),
}),
[REMOVE]: (state, { payload: id }) => ({
...state,
todos: state.todos.filter((todo) => todo.id !== id),
}),
},
initialState
);
export default todos;
todos 모듈에 적용
간단한 counter 모듈에 적용시 더 복잡해짐
//라이브러리 설치
yarn add 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;
//사용법
const 결과 = useSelector(상태 선택 함수);
counter.number
조회하고 props로 전달src/container/CounterContainer.js
//useSelector 불러오기
import { useSelector } from "react-redux";
(...)
//useSelector 사용해 counter.number 조회
const CounterContainer = () => {
const number = useSelector((state) => state.counter.number);
return <Counter number={number} />;
};
export default CounterContainer;
//사용법
const dispatch = useDispatch();
dispatch({type:"SAMPLE_ACTION"})
src/container/CounterContainer.js
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import Counter from "../components/Counter";
import { increase, decrease } from "../modules/counter";
const CounterContainer = () => {
const number = useSelector((state) => state.counter.number);
const dispatch = useDispatch();
return (
<Counter
number={number}
onIncrease={() => dispatch(increase())}
onDecrease={() => dispatch(decrease())}
/>
);
};
export default CounterContainer;
import React, { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import Counter from "../components/Counter";
import { increase, decrease } from "../modules/counter";
const CounterContainer = () => {
const number = useSelector((state) => state.counter.number);
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;
//사용법
const store=useStore();
store.dispatch({type: "SAMPLE_ACTION"});
store.getState();
TodosContainer를 useSelector와 useDispatch 사용해 다시 작성해보기
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 = () => {
const { input, todos } = useSelector(({ todos }) => ({
//useSelector 사용해 비구조화 할당 문법
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;
lib/useAction.js
import { bindActionCreators } from "redux";
import { useDispatch } from "react-redux";
import { useMemo } from "react";
export default function useActions(actions, deps) {
const dispatch = useDispatch();
return useMemo(
() => {
if (Array.isArray(actions)) {
return actions.map((a) => bindActionCreators(a, dispatch));
}
return bindActionCreators(actions, dispatch);
},
deps ? [dispatch, ...deps] : deps
);
}
TodoContainer에서 useActions 불러와 사용
import React, { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import { changeInput, insert, toggle, remove } from "../modules/todos";
import Todos from "../components/ToDos";
import useActions from "../lib/useAction";
const TodosContainer = () => {
const { input, todos } = useSelector(({ todos }) => ({
input: todos.input,
todos: todos.todos,
}));
const [onChangeInput, onInsert, onToggle, onRemove] = useActions(
[changeInput, insert, toggle, remove],
[]
);
return (
<Todos
input={input}
todos={todos}
onChangeInput={onChangeInput}
onInsert={onInsert}
onToggle={onToggle}
onRemove={onRemove}
/>
);
};
export default TodosContainer;
connect 사용해 컨테이너 컴포넌트 만드는 경우
해당 컨테이너 컴포넌트의 부모 컴포넌트가 리랜더링될 때 해당 컨테이너 컴포넌트의 props가 바뀌지 않으면 리랜더링 방지되며 최적화됨
useSelector 사용해 리덕스 상태 조회하는 경우
자동으로 최적화 작업 이뤄지지 않음
React.memo를 컨테이너 컴포넌트에 사용해 줘야함
//예시
export default React.memo(CounterContainer);