❤벨로퍼트 리액트 - 6장. 리덕스
5) 카운터 구현하기
(1) 프리젠테이셔널 컴포넌트 만들기
프리젠테이셔널 컴포넌트 : 리덕스 스토어에 직접적으로 접근하지 않고 필요한 값 또는 함수를 props로만 받아와서 사용하는 컴포넌트.
src/components/Counter.js
import React from 'react';
function Counter({ number, diff, onIncrease, onDecrease, onSetDiff }) {
const onChange = e => {
// e.target.value 의 타입은 문자열. -> 숫자로 변환
onSetDiff(parseInt(e.target.value, 10));
};
return (
<div>
<h1>{number}</h1>
<div>
<input type="number" value={diff} min="1" onChange={onChange}/>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
</div>
);
}
export default Counter;
위와같이 프리젠테이셔널 컴포넌트에서는 주로 UI를 선언하는 것에 집중하며 필요한 값과 함수는 props로 받아와서 사용하는 형태로 구현함.
(2) 컨테이너 컴포넌트 만들기
컨테이너 컴포넌트 : 리덕스 스토어의 상태 조회나 액션을 디스패치 할 수 있는 컴포넌트. HTML 태그들을 사용하지 않고 다른 프리젠테이셔널 컴포넌트들을 불러와서 사용함.
src/containers/CounterContainer.js
import React from "react";
import { useSelector, useDispatch } from 'react-redux';
import Counter from "../components/Counter";
import { increase, decrease, setDiff } from "../modules/counter";
function CounterContainer() {
// useSelector : 리덕스 스토어의 상태를 조회하는 Hook
// state의 값은 store.getState() 를 호출했을때와 같음
const { number, diff } = useSelector(state => ({
number: state.counter.number,
diff: state.counter.diff
}));
// useDispatch : 리덕스 스토어의 dispatch를 함수에서 사용할 수 있게 해주는 Hook
const dispatch = useDispatch();
// 각 액션들을 디스패치하는 함수
const onIncrease = () => dispatch(increase());
const onDecrease = () => dispatch(decrease());
const onSetDiff = diff => dispatch(setDiff(diff));
return (
<Counter
// 상태와
number={number}
diff={diff}
// 액션을 디스패치 하는 함수들을 props로 넣어줌
onIncrease={onIncrease}
onDecrease={onDecrease}
onSetDiff={onSetDiff}
/>
);
}
export default CounterContainer;
App.js에서 렌더링
import React from 'react';
import CounterContainer from './containers/CounterContainer';
function App() {
return (
<div>
<CounterContainer />
</div>
);
}
export default App;import React from 'react';
function Counter({ number, diff, onIncrease, onDecrease, onSetDiff }) {
const onChange = e => {
// e.target.value 의 타입은 문자열. -> 숫자로 변환
onSetDiff(parseInt(e.target.value, 10));
};
return (
<div>
<h1>{number}</h1>
<div>
<input type="number" value={diff} min="1" onChange={onChange}/>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
</div>
);
}
export default Counter;
(3) 프리젠테이셔널 컴포넌트와 컨테이너 컴포넌트
반드시 이대로 할 필요는 없지만, 컴포넌트를 분리해서 작업하는 것이 정석이기때문에 이 프로젝트에서는 분리해서 작업한다.
6) 리덕스 개발자도구 적용하기
리덕스 개발자도구 사용시, 현재 스토어의 상태를 개발자 도구에서 조회할 수 있고 지금까지 어떤 액션들이 디스패치 되었는지, 그리고 액션에 따라 상태가 어떻게 변화했는지 확인할 수 있다. 추가적으로 액션을 직접 디스패치 할수도 있다.
웹스토어 확장프로그램 설치(https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd)
$ npm add redux-devtools-extension
index.js 수정
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './modules';
import { composeWithDevTools } from 'redux-devtools-extension'; // 리덕스 개발자 도구
const store = createStore(rootReducer, composeWithDevTools()); // 스토어 생성
// composeWithDevTools 를 사용하여 리덕스 개발자도구 활성화
console.log(store.getState()); // 스토어의 상태 확인
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
reportWebVitals();
7) 할 일 목록 구현하기
(1) 프리젠테이셔널 컴포넌트 구현하기
컴포넌트의 리렌더링 성능을 최적화 하기 위해 3개의 컴포넌트 생성
- TodoItem
- TodoList
- Todos
components/Todos.js
import React, { useState } from "react";
// 컴포넌트 최적화를 위해 React.memo 사용
const TodoItem = React.memo(function TodoItem({ todo, onToggle }){
return (
<li
style={{ textDecoration: todo.done ? 'line-through' : 'none'}}
onClick={()=> onToggle(todo.id)}
>{todo.text}</li>
);
});
// 컴포넌트 최적화를 위해 React.memo 사용
const TodoList = React.memo(function TodoList({todos, onToggle}){
return (
<ul>
{todos.map(todo=> (
<TodoItem key={todo.id} todo={todo} onToggle={onToggle} />
))}
</ul>
);
});
function Todos({todos, onCreate, onToggle}) {
// 리덕스를 사용한다고 해서 모든 상태를 리덕스에서 관리해아하는 것은 아니다
const [text, setText] = useState('');
const onChange = e => setText(e.target.value);
const onSubmit = e => {
e.preventDefault(); // submit 이벤트 발생시 새로고침 방지
onCreate(text);
setText(''); // 인풋 초기화
}
return (
<div>
<form onSubmit={onSubmit}>
<input
value={text}
placeholder="할일을 입력해주세요"
onChange={onChange} />
<button type="submit">등록</button>
</form>
<TodoList todos={todos} onToggle={onToggle} />
</div>
);
}
export default Todos;
(2) 컨테이너 컴포넌트 만들기
containers/TodosContainer.js
import React, {useCallback} from "react";
import { useSelector, useDispatch } from "react-redux";
import Todos from "../components/Todos";
import { addTodo, toggleTodo } from "../modules/todos";
function TodosContainer() {
// useSelector에서 반드시 객체를 반환해야할 필요는 없다
// 한 종류의 값만 조회하고싶을 경우 원하는 값만 바로 반환하면 된다
const todos = useSelector(state => state.todos);
const dispatch = useDispatch();
const onCreate = text => dispatch(addTodo(text));
const onToggle = useCallback(id => dispatch(toggleTodo(id)), [dispatch]); // 최적화를 위해 useCallback 사용
return <Todos todos={todos} onCreate={onCreate} onToggle={onToggle} />;
}
export default TodosContainer;