1. < create-react-app > 으로 리액트 프로젝트 생성하기
2. 리덕스와 react-redux 라이브러리를 설치
react-redux는 리덕스의 상태를 리액트 컴포넌트에 연결하는 라이브러리다.
3. Prettier를 적용
코드 포맷팅 도구인 Prettier를 설정하여 일관된 코드 스타일을 유지한다.
// .prettierrc
{
"singleQuote": true,
"semi": true,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80
}
리액트 프로젝트에서 리덕스를 사용할 때, 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 분리하는 패턴을 가장 많이 사용한다. 이러한 패턴은 리덕스를 사용하는 데 필수 사항은 아니다. 다만 이 패턴을 사용하면 코드의 재사용성도 높아지고, 관심사의 분리가 이루어져 UI를 작성할 때 좀 더 집중할 수 있다.
17.2.1 카운터 컴포넌트 만들기
숫자를 더하거나 빼는 버튼을 가진 카운터 컴포넌트를 만들고, 이를 App 컴포넌트에서 사용한다.
// 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;
17.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 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;
리덕스를 사용할 때는 액션 타입, 액션 생성 함수, 리듀서 코드를 작성해야 하는데요. 이 코드들을 각각 다른 파일에 작성하는 방법도 있고, 기능별로 묶어서 파일 하나에 작성하는 방법도 있다.
17.3.1 counter 모듈 작성하기
Ducks 패턴을 사용하여 액션 타입, 액션 생성 함수, 리듀서를 작성한 코드를 ‘모듈’이라고 한다.
17.3.1.1 액션 타입 정의하기
// modules/counter.js
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
액션타입은 대문자로 정의하고, 문자열 내용은 '모듈 이름/ 액션이름' 과 같은 형태로 작성한다.
17.3.1.2 액션 생성 함수 만들기
// modules/counter.js
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export 키워드를 사용하여 추후 다시 이 함수를 다른 파일에서 불러와 사용할 수 있도록 한다.
17.3.1.3 초기 상태 및 리듀서 함수 만들기
// 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;
counter모듈의 초기 상태와 리듀서 함수를 만들어 준다.
초기상태에 number값을 설정해 주었으며, 리듀서 함수에는 현재상태를 참조하여 새로운 객체를 생성하여 반환하는 코드를 작서하였습니다.마지막으로 export default 키워드를 사용하여 함수를 내낸다.
( export는 여러 개를 내보낼 수 있지만 export default는 단 한 개만 내보낼 수 있다. )
17.3.2 todos 모듈 만들기
modules 디렉터리에 todos.js 파일을 생성
17.3.2.1 액션 타입 정의하기
// modules/todos.js
const CHANGE_INPUT = 'todos/CHANGE_INPUT'; // 인풋 값을 변경함
const INSERT = 'todos/INSERT'; // 새로운 todo를 등록함
const TOGGLE = 'todos/TOGGLE'; // todo를 체크/체크 해제함
const REMOVE = 'todos/REMOVE'; // todo를 제거함
17.3.2.2 액션 생성 함수 만들기
액션 생성 함수에서 파라미터가 필요하다. 전달받은 파라미터는 액션 객체 안에 추가 필드로 들어가게 된다.
// modules/todos.js
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
});
17.3.2.3 초기 상태 및 리듀서 함수 만들기
객체에 한 개 이상의 값이 들어가므로 불변성을 유지해 주어야 하기 때문에 spread 연산자(…)를 활용하여 작성한다.
// modules/todos.js
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;
17.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;
이런 식으로 작성하면 나중에 불러올 때 디렉터리 이름만 장성하여 불러올 수 있다.
< 리액트 애플리케이션에 리덕스를 적용 >
17.4.1 스토어 만들기
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import rootReducer from './modules';
const store = createStore(rootReducer);
ReactDOM.render(<App />, document.getElementById('root'));
serviceWorker.unregister();
17.4.2 Provider 컴포넌트를 사용하여 프로젝트에 리덕스 적용하기
리액트 컴포넌트에서 스토어를 사용할 수 있도록 App 컴포넌트를 react-redux에서 제공하는 Provider 컴포넌트로 감싸 준다.
// 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 * as serviceWorker from './serviceWorker';
import rootReducer from './modules';
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
serviceWorker.unregister();
17.4.3 Redux DevTools의 설치 및 적용
const store = createStore(
rootReducer, /* preloadedState, */
window._ _REDUX_DEVTOOLS_EXTENSION_ _ && window._ _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 * as serviceWorker from './serviceWorker';
import rootReducer from './modules';
const store = createStore(rootReducer, composeWithDevTools());
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
serviceWorker.unregister();
< 컴포넌트에서 리덕스 스토어에 접근하여 원하는 상태를 받아 오고, 또 액션도 디스패치해 줄 차례 >
리덕스 스토어와 연동된 컴포넌트를 컨테이너 컴포넌트라고 부른다.
17.5.1 CounterContainer 만들기
// src/containers/CounterContainer.js
import React from 'react';
import Counter from '../components/Counter';
const CounterContainer = () => {
return <Counter />;
};
export default CounterContainer;
리덕스와 연동하려면 react-redux에서 제공하는 connect 함수를 사용
mapStateToProps: 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수, state를 파라미터로 받아 오며, 이 값은 현재 스토어가 지니고 있는 상태를 가리킨다.
mapDispatchToProps: 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수, store의 내장 함수 dispatch를 파라미터로 받아 온다.
connect 함수를 호출하고 나면 또 다른 함수를 반환한다. 반환된 함수에 컴포넌트를 파라미터로 넣어 주면 리덕스와 연동된 컴포넌트가 만들어진다.
// 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 => ({
number: state.counter.number,
});
const mapDispatchToProps = dispatch => ({
// 임시 함수
increase: () => {
console.log('increase');
},
decrease: () => {
console.log('decrease');
},
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(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. 액션 생성 함수를 불러와서 액션 객체를 만들고 디스패치
connect 함수를 사용할 때는 일반적으로 위 코드와 같이 mapStateToProps와 mapDispatchToProps를 미리 선언해 놓고 사용.
// components/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);
2. connect 함수 내부에 익명 함수 형태로 선언해도 문제가 되지 않는다.
액션함수의 개수가 많다면 리덕스에서 제공하는 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);
3. 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 작업을 대신해 준다.
17.5.2 TodosContainer 만들기
< CounterContainer를 만들 때 배웠던 connect 함수를 사용하고, mapDispatchToProps를 짧고 간단하게 쓰는 방법을 적용해서 코드 >
// containers/TodosContainer
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.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;
// containers/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;
17.6.1 redux-actions
redux-actions를 사용하면 액션 생성 함수를 더 짧은 코드로 작성할 수 있다.
17.6.1.1 counter 모듈에 적용하기
// 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;
17.6.1.2 todos 모듈에 적용하기
션 생성 함수를 교체를 위해서는 각 액션 생성 함수에서 파라미터를 필요로 한다.
createAction으로 액션을 만들면 액션에 필요한 추가 데이터는 payload라는 이름을 사용한다.
const MY_ACTION = 'sample/MY_ACTION';
const myAction = createAction(MY_ACTION, text => `${text}!`);
const action = myAction('hello world');
17.6.2 immer
immer를 사용하면 상태를 불변성 유지하면서 더 쉽게 관리할 수 있다.
immer를 사용한다고 해서 모든 업데이트 함수에 immer를 적용할 필요는 없습니다. 일반 자바스크립트로 처리하는 것이 더 편할 때는 immer를 적용하지 않아도 된다.
connect 함수를 사용하는 대신 react-redux에서 제공하는 Hooks를 사용할 수도 있다.
17.7.1 useSelector로 상태 조회하기
// containers/CounterContainer.js
import React from 'react';
import { useSelector } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease } from '../modules/counter';
const CounterContainer = () => {
const number = useSelector(state => state.counter.number);
return <Counter number={number} />;
};
export default CounterContainer;
17.7.2 useDispatch를 사용하여 액션 디스패치하기
// containers/CounterContainer.js
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;
17.7.3 useStore를 사용하여 리덕스 스토어 사용하기
useStore는 리덕스 스토어에 직접 접근할 때 사용한다. 하지만 실제로 이런 상황은 드물기 때문에 일반적으로 useSelector와 useDispatch를 사용하는 것이 좋다.
17.7.4 TodosContainer를 Hooks로 전환하기
// containers/TodosContainer.js
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 }) => ({
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;
17.7.5 useActions 유틸 Hook을 만들어서 사용하기
• 참고 링크: https://react-redux.js.org/next/api/hooks#recipe-useactions
여러 액션을 한 번에 관리할 수 있도록 useActions라는 커스텀 Hook을 만들어서, 액션 생성 함수들을 디스패치 함수로 변환하여 코드의 중복을 줄인다.
17.7.6 connect 함수와의 주요 차이점
// containers/CounterContainer.js
import React from 'react';
import { useSelector } from 'react-redux';
import { changeInput, insert, toggle, remove } from '../modules/todos';
import Todos from '../components/Todos';
import useActions from '../lib/useActions';
const TodosContainer = () => {
(...)
};
export default React.memo(TodosContainer);
출처 : 김민준, 『리액트를 다루는 기술』, 길벗(2019).
Corner React.js 1
Editor: MARIN
[React.js 1팀] 16장. 리덕스 라이브러리 이해하기 (1) | 2025.01.31 |
---|---|
[React.js 1팀] project 3 [감정 일기장] 만들기 (0) | 2025.01.24 |
[React.js 1팀] 9장 컴포넌트 트리에 데이터 공급하기 (0) | 2025.01.17 |
[React.js 1팀] 8장. 최적화 (0) | 2025.01.10 |
[React.js 1팀] 7장. useReducer와 상태 관리 (0) | 2025.01.10 |