리액트 애플리케이션: 컴포넌트 간의 데이터 props로 전달
컴포넌트 여기저기에서 필요한 데이터 있을 시 주로 최상위 컴포넌트인 App의 state에 넣어서 관리
리덕스(Redux): 전역 상태 관리 작업을 처리하는 상태 관리 라이브러리
프레젠테이셔널 컴포넌트 / 컨테이너 컴포넌트 분리 형식이 가장 많이 사용되는 패턴
프레젠테이셔널 컴포넌트: 상태 관리가 이루어지지 않고 그저 props를 받아와서 화면에 UI를 보여주기만 함
컨테이너 컴포넌트: 리덕스와 연동되어 있음, 리덕스로부터 상태를 받아 오기도 하고 리덕스 스토어에 액션을 디스패치 하기도 함.
리덕스 사용할 때: 액션 타입, 액션 생성 함수, 리듀서 코드 작성 필요
가장 일반적인 구조: actions / constants / reducers 디렉터리 => 그 안에 기능별로 파일 하나씩 만듦
but! 위 세가지를 기능별로 파일 하나에 몰아서 작성하는 방식: Ducks 패턴
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;
}
}
counter.js
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE'
export const increase = () => ({type: INCREASE});
export const decrease = () => ({type:DECREASE});
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;
todos.js - 액션 타입 정의, 액션 생성 함수
*id 값은 todo 개체가 들고있게 될 고유값이므로 잊지말자.
const CHANGE_INPUT = 'todos/CHANGE_INPUT'; //인풋값 변경
const INSERT = 'todos/INSERT'; //새로운 todo 등록
const TOGGLE = 'todos/TOGGLE'; //todo 체크 및 체크 해제
const REMOVE = 'todos/REMOVE'; //todo 제거
export const changeInput = input => ({
type: CHANGE_INPUT,
input
});
let id = 3; //insert 호출될 때마다 1씩 증가
export const insert = text => ({
type: INSERT,
todo:{
id: id++, //todo 항목마다 id 붙여줘야 toggle이나 remove와 같은 작업 처리 가능
//즉 id 값은 각 todo 개체가 들고 있게 될 고유값.
text,
done: false
}
});
export const toggle = id => ({
type: TOGGLE,
id
})
export const remove = id => ({
type: REMOVE,
id
})
리듀서 함수
function todos(state = initialize, 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)
};
}
}
루트리듀서 - 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/client';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import App from './App';
import rootReducer from '../modules';
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
redux-devtools-extension을 사용한 redux store 생성
src/index.js
// import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { composeWithDevTools} from "redux-devtools-extension";
import App from './App';
import rootReducer from '../modules';
// const store = createStore(rootReducer);
// const store = createStore(
// rootReducer, /*preloadedState*/
// window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
// )
const store = createStore(rootReducer, composeWithDevTools());
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
* The slice reducer for key "todos" returned undefined during initialization.
해당 에러 발생
해결방안
todos.js
default:
return {
...state
}
default 상태에서도 state 관련 값 넘겨줘야 함.
Container Component: 컴포넌트에서 리덕스 스토어에 접근하여 원하는 상태 받아오고, 액션도 디스패치.
containers/CounterContainer.js
import Counter from '../components/Counter';
const CounterContainer = () => {
return <Counter/>;
};
export default CounterContainer;
위 컴포넌트를 리덕스와 연동하는 방법: connect 함수 사용
how? connect(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트)
mapStateToProps: 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨줌
mapDispatchToProps: 액션 생성 함수를 컴포넌트의 props로 넘겨줌
same as,
const makeContainer = connect(mapStateToProps, mapDispatchToProps)
makeContainer(target component)
containers/CounterContainer.js
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 => ({
number: state.counter.number, //상태의 카운터의 number
});
const mapDispatchToProps = dispatch => ({
increase: () => {
console.log('increase');
},
decrease: () => {
console.log('decrease');
},
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(CounterContainer);
mapStateToProps와 mapDispatchToProps에서 반환하는 객체 내부의 값 -> 컴포넌트의 props로 전달
mapStateToProps: state를 파라미터로 받아옴 -> 현재 스토어가 지니고 있는 상태 가리킴
mapDispatchToProps: store의 내장 함수 dispatch를 파라미터로 받아옴 -> console.log로 진행상태 확인
then, console.log 대신 액션 생성함수 불러오기
액션 객체 생성 후 dispatch
import {increase, decrease} from '../modules/counter'; //counter함수에서 increase, decrease 함수 불러옴.
const mapDispatchToProps = dispatch => ({
increase: () => {
// console.log('increase');
dispatch(increase());
},
decrease: () => {
// console.log('decrease');
dispatch(decrease());
},
});
숫자가 바뀌는 것 확인O
+만약 액션 생성 함수의 개수가 많아진다면?
컴포넌트에서 액션을 디스패치하기 위해 액션 생성 함수를 호출하고 dispatch로 감싸는 작업이 더욱 번거로울 것.
how to solve?: bindActionCreators
import {bindActionCreators} from "redux";
export default connect(
state => ({
number: state.counter.number,
}),
bindActionCreators(
{increase, decrease}, dispatch,
),
)(CounterContainer);
also,
export default connect(
state => ({
number: state.counter.number,
}),
{increase, decrease},
)(CounterContainer);
connect 함수가 내부적으로 bindActionCreators 작업 대신해줌.
CounterContainer.js
import {bindActionCreators} from "redux";
import {connect} from 'react-redux';
import Counter from '../components/Counter';
import {increase, decrease} from '../modules/counter'; //counter함수에서 increase, decrease 함수 불러옴.
const CounterContainer = ({number, increase, decrease}) => {
return <Counter number={number} onIncrease={increase} onDecrease={decrease}/>;
};
export default connect(
state => ({
number: state.counter.number,
}),
{increase, decrease},
)(CounterContainer);
TodosContainer for Todos component
Todos 컴포넌트에서 받아온 props를 사용
TodosContainer.js
import {bindActionCreators} from "redux";
import {connect} from 'react-redux';
import todos from '../components/Todo';
import {increase, decrease} from '../modules/counter'; //counter 함수에서 increase, decrease 함수 불러옴.
const TodosContainer = ({input, todo, 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);
'FE > React & TS' 카테고리의 다른 글
[Vite+React] 깃허브 페이지 배포 이미지 경로 에러 (0) | 2024.03.16 |
---|---|
redux-actions, immer, hooks(useSelector, useDispatch)를 사용한 container component 생성 (1) | 2023.10.10 |
Todo app (0) | 2023.07.21 |
Sass / styled-components (0) | 2023.05.16 |
[REACT Movie App part-two] React-router / react-router-dom (0) | 2023.05.11 |