- 리액트 애플리케이션에서 리덕스를 사용하면, 상태 업데이트에 관한 로직을 모듈로 따로 분리하여 컴포넌트 파일과 별개로 관리할 수 있으므로 코드를 유지 보수 하는데 도움이 된다.
- 여러 컴포넌트에서 동일한 상태를 공유해야할 때 매우 유용
- 실제 업데이트가 필요한 컴포넌트만 리렌더링되도록 쉽게 최적화해 줄 수 있다.
- 리액트 애플리케이션에서 리덕스를 사용할 때는 store인스턴스를 직접 사용하기보다는 주로 react-redux라는 라이브러리에서 제공하는 유틸 함수(connect)와 컴포넌트(Provider)를 사용하여 리덕스 관련 작업을 처리
1. 작업 환경 설정
프로젝트 생성 미치 라이브러리 설치
$ yarn create react-app react-redux-tutorial
$ cd react-redux-tutorial
$ yarn add redux react-redux
Prerrier를 적용하고 싶다면 디렉터리에 다음과 같이 .prerrierrc 파일을 작성하세요.
{
"singleQuote": true,
"semi": true,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80
}
2. UI 준비하기
- 리액트 프로젝트에서 리덕스르 사용할 때 가장 많이 사용하는 패턴 : 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트의 분리
- 프리젠테이셔널 컴포넌트 : 주로 상태관리가 이루어지지 않고, 그저 props를 받아 와서 화면에 UI를 보여 주기만 하는 것
- 컨테이너 컴포넌트 : 리덕스와 연동되어 있는 컴포넌트로, 리덕스로부터 상태를 받아오기도 하고 리덕스 스토어에 액션을 디스패치하기도 한다.
- 장점 : 코드의 재사용성 높아짐, 관심사의 분리가 이루어져 UI를 작성할 때 좀 더 집중할 수 있다.
2.1. 카운터 컴포넌트 만들기
[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;
[결과]
2.2. 할 일 목록 컴포넌트 만들기
[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 onSunmit = e => {
e.preventDefault();
};
return (
<div>
<form onSubmit={onSunmit}>
<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;
[결과]
3. 리덕스 관련 코드 작성하기
3.1.1. 액션 타입 정의하기
[modules/counter.js]
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
- 액션타입 정의
- 액션타입 : 대문자
- 문자열 내용 : 모듈이름 / 액션이름
3.1.2. 액션 생성 함수 만들기
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
- export라는 키워드 => 추후 이 함수를 다른 파일에 불러와 사용할 수 있다.
3.1.3. 초기 생성 및 리듀서 함수 만들기
[modules/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;
- 초기상태 number 값 설정
- 리듀서 함수에는 현재 상태를 참조하여 새로운 객체를 생성해서 반환하는 코드를 작성
- export 와 export default의 차이점
- export - 여러 개를 내보낼 수 있다.
- export default - 단 한 개만 내보낼 수 있다.
- 불러오는 방식도 다음과 같이 다르다.
import counter from './counter';
import { increase, decrease } from '.counter';
// 한꺼번에 불러오고 싶을 때
import counter, { increase, decrease } from './counter';
3.2.todos 모듈 만들기
3.2.1. 액션 타입 정의하기
- 가장 먼저 해야할 일은 액션 타입 정의
[modules/todos.js]
const CHANGE_INPUT = 'todos/CHANGE_IMPUT'; // 인풋 값을 변경함
const INSERT = 'todos/INSERT'; // 새로운 todo를 등록함
const TOGGLE = 'todos/TOGGLE'; // todo를 체크/체크 해제함
const REMOVE = 'todos/REMOVE'; // todo를 제거함
3.2.2. 액션 생성 함수 만들기
- 액션 생성 함수에서 파라미터가 필요하다.
- 전달받은 파라미터는 액션 객체 안에 추가 필드로 들어간다.
[modules/todos.js]
const CHANGE_INPUT = 'todos/CHANGE_IMPUT'; // 인풋 값을 변경함
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++,
text,
done: false
}
});
export const toggle = id => ({
type: TOGGLE,
id
});
export const remove = id => ({
type: REMOVE,
id
});
- 위 액션 함수 중에서 insert 함수는 액션 객체를 만들 때 파라미터 외에 사전에 이미 선언되어 있는 id라는 값에도 의존한다.
- id는 todo 객체가 돌고 있게 될 고윳값
- 액션 생성 함수는 호출될 때마다 id값에 1씩 더해준다.
3.2.3. 초기 상태 및 리듀서 함수 만들기
- 모듈의 초기 상태와 리듀서 함수를 작성
- 객체에 한 개 이상의 값이 들어가므로 불변성을 유지해야 해서 조금 까다로워짐
- spread(...) 연산자 활용
- 배열에 변화를 줄 때는 배열 내장 함수를 사용하여 구현하면 됨
[modules/todos.js]
const CHANGE_INPUT = 'todos/CHANGE_IMPUT'; // 인풋 값을 변경함
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++,
text,
done: false
}
});
export const toggle = id => ({
type: TOGGLE,
id
});
export const remove = id => ({
type: REMOVE,
id
});
const initialState = {
imput: '',
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;
3.3. 루트 리듀서 만들기
[modules/index.js]
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';
const rootReducer = combineReducers({
counter,
todos,
});
export default rootReducer;
- 이번 프로젝트에서 리듀서를 여러 개 만들었다. 나중에 createStore함수를 사용하여 스토어를 만들 때는 리듀서를 하나만 사용해야한다.
- 그렇기 때문에 기존에 만들었던 리듀서를 하나로 합쳐줘야 한다.
- 이 작업은 리덕스에서 제공하는 combineReducers라는 유틸 함수를 사용하면 쉽게 처리할 수 있다.
- 파일이름을 index.js로 설정해 주면 나중에 불러올 때 디렉터리 이름까지만 입력하여 불러올 수 있다.
- import rootReducers from './modules';
4. 리액트 애플리케이션에 리덕스 적용하기
4.1. 스토어 만들기
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import rootReducer from './modules';
const store = createStore(rootReducer);
ReactDOM.render(<App />, document.getElementById('root'));
reportWebVitals();
4.2. Provider 컴포넌트 사용하여 프로젝트에 리덕스 적용하기
- 리액트 컴포넌트에서 스토어를 사용할 수 있도록 App 컴포넌트를 react-redux에서 제공하는 Provider 컴포넌트로 감싸준다.
- 이 컴포넌트를 사용할 때는 store를 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 reportWebVitals from './reportWebVitals';
import rootReducer from './modules';
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
reportWebVitals();
4.3. Redux DevTools의 설치 및 적용
- Redux DevTools는 리덕스 개발자 도구이며, 크롬 확장 프로그램으로 설치하여 사용할 수 있다.
- 크롬웹스토어(https://chrome.google.com/webstore/)에서 Redux DevTools를 검색하여 설치해준다.
코드를 더 깔끔하게 사용하기 위해 패키지도 설치
$ yarn add redux-devtools-extension
[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 reportWebVitals from './reportWebVitals';
import rootReducer from './modules';
const store = createStore(rootReducer, composeWithDevTools());
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
reportWebVitals();
크롬 개발자 도구 실행 후 Redux 탭 열기
5. 컨테이너 컴포넌트 만들기
- 컴포넌트에서 리덕스 스토어에 접근하여 원하는 상태를 받아오고, 액션도 디스패치해 줄 차례
- 이덕스 스토어와 연동된 컴포넌트를 컨테이너 컴포넌트라고 한다.
5.1. CounterContainer 만들기
[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)(연동할 컴포넌트)
- mapStateProps - 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수
- mapDispatchToProps - 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수
- 이렇게 connect 함수를 호출하고 나면 또 다른 함수를 반환한다.
- 반환된 함수에 컴포넌트를 파라미터로 넣어 주면 리덕스와 연동된 컴포넌트가 만들어진다.
- 위 코드를 더 쉽게 풀면 다음과 같은 형태이다.
const makeContainer = connect(mapStateToProps, mapDispatchToProps)
makeContainer(타깃 컴포넌트)
- 이제 CounterContainer 컴포넌트에서 connect를 사용해보자
[containers/CountContainer.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 => ({
number: state.counter.number,
});
const mapDispatchToProps = dispatch => ({
// 임시 함수
increase: () => {
console.log('increase');
},
decrease: () => {
console.log('decrease');
},
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(CounterContainer);
- mapStateToProps와 mapDispatchProps에서 반환하는 객체 내부의 값들은 컴포넌트의 props로 전달된다.
- mapStateToProps는 state를 파라미터로 받아 오며, 이 값은 현재 스토러가 지니고 있는 상태를 가리킨다.
- mapDispatchToProps에서는 진행 절차를 설명하기 위해 임시로 console.log를 사용한다.
- App에서 Counter를 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;
[결과]
이번에는 console.log대신 액션 생성 함수를 불러와서 액션 객체를 만들고 디스패치 해주겠다.
[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 = ({number, increase, decrease}) => {
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를 미리 선언해 놓고 사용한다.
- 하지만 connect 함수 내부에 익명 함수로 선언해도 문제가 되지 않는다.
- 어떻게 보면 코드가 더 깔끔해지기도 한다.
[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 = ({number, increase, decrease}) => {
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease} />
);
};
export default connect(
state => ({
number: state.count.number,
}),
dispatch => ({
increase: () => dispatch(increase()),
decrease: () => dispatch(decrease()),
}),
)(CounterContainer);
- 위 코드에서는 액션 생성 함수를 호출하여 디스패치하는 코드가 한 줄이기 때문에 불필요한 코드 블록을 생략해 주었다.
- 컴포넌트에서 액션을 디스패치하기 위해 각 액션 생성 함수를 호출하고 dispatch로 감싸는 작업이 조금 번거로울 수도 있다.
- 특히 액션 생성 함수의 개수가 많아진다면 더더욱 그럴 것이다.
- 이와 같은 경우 리덕스에서 제공하는 bindActionCreators 유틸함수를 사용하면 간편하다.
[containers/CounterContainer.js]
import React from 'react';
import { bindActionCreators } from 'redux';
import {connect} from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease } from '../modules/counter';
const CounterContainer = ({number, increase, decrease}) => {
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease} />
);
};
export default connect(
state => ({
number: state.counter.number,
}),
dispatch =>
bindActionCreators(
{
increase,
decrease,
},
dispatch,
),
)(CounterContainer);
- 방금 작성한 방법보다 한 가지 더 편한 방법이 있다.
- mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 액션 생성 함수로 이루어진 객체 형태로 넣어 주는 것
[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 = ({number, increase, decrease}) => {
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease} />
);
};
export default connect(
state => ({
number: state.counter.number,
}),
{
increase,
decrease,
},
)(CounterContainer);
- 위와 같이 두 번째 파라미터를 아예 객체 형태로 넣어 주면 connect 함수가 내부적으로 bindActionCreators 작업을 대신해준다.
5.2. TodosContainer 만들기
- CounterContainer를 만들때 배웠던 connect 함수를 사용하고, mapDispatchToProps를 짧고 간단하게 쓰는 방법을 적용해서 코드를 작성하겠다.
[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);
- 이전에 todos 모듈에서 작성했던 액션 생성 함수와 상태 안에 있던 값을 컴포넌트의 props로 전달해 주었다.
- App 컴포넌트에서 보여주던 Todos컴포넌트를 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;
그 다음 Todos컴포넌트에서 받아 온 props를 사용하도록 구현해보자
[components/Todos.js]
import React from 'react';
const TodoItem = ({todo, onToggle, onRemove}) => {
return (
<div>
<input
type='checkbox'
onClick={() => onToggle(todo.id)}
chacked={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;
[결과]
'Front > React' 카테고리의 다른 글
[React] 코드 스플리팅 (0) | 2021.01.20 |
---|---|
[React] 리덕스 미들웨어를 통한 비동기 작업 관리 (0) | 2021.01.18 |
[React] 리덕스 라이브러리 (0) | 2021.01.14 |
[React] Context API (0) | 2021.01.14 |
[React] 외부 API를 연동하여 뉴스 뷰어 만들기 (0) | 2021.01.12 |