이번 글에서는 React
에서 state management(상태 관리)
를 할 때
널리 사용되어지고 있는 React Redux
에 대해 알아보고자 한다.
⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
이 글은 Redux
의 기초 개념을 어느정도 이해하고 있는 상태로 작성된 글입니다.
Store
, Action
, Reducer
, Dispatch
에 대해 처음 들어보시는 등
Redux
에 대해 전혀 접근해보지 못한 분들은
제 블로그의 Redux 입문을 살펴보시는 것을 추천드립니다.
⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
React Redux is maintained by the Redux team, and kept up-to-date with the latest APIs from Redux and React.
Redux
와 React Redux
모두, state management(상태 관리)
를 위해 쓰이는 Redux
라는 것은 다를 게 없으나
React Redux
는 Redux
팀에서 공식적(Official)으로 관리, 배포하고 있는
React
전용 Redux
패키지이다.
그래서 Redux
와 React Redux
의 공식 문서는 각각 개별적으로 존재하고 있다.
Redux
를 시작하기 전에 바로 React Redux
를 사용해도 무방하나,
React Redux
는 Redux
를 기초로 설계되어있다.
그래서 시간이 좀 소요되더라도, 더 편하게 React Redux
를 다루기 위해서는
Redux
에 대한 기초 개념을 이해한 뒤에 React Redux
를 사용하는 것이 편하다고 생각한다.
(↑ 개인적인 생각입니다. 이 학습 순서가 나에게는 더 적합했다.)
⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
그러므로 이 글은 Redux
의 기초 개념을 어느정도 이해하고 있는 상태로 작성된 글입니다.
Store
, Action
, Reducer
, Dispatch
에 대해 처음 들어보시는 등
Redux
에 대해 전혀 접근해보지 못한 분들은
제 블로그의 Redux 입문을 살펴보시는 것을 추천드립니다.
⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
React Redux
에는 설치 방법이 2가지가 있다.
이 방법은 React
프로젝트 생성 시작부터 React Redux
를 사용할 예정일 때 사용하면 유용하다.
React Redux
별도로 설치하지 않아도 React Redux
가 포함된 채로 프로젝트를 시작할 수 있기 때문이다.
npx create-react-app my-app --template redux
React
프로젝트를 생성할 당시에 React Redux
를 함께 설치하지 못했고
이미 진행 중인 프로젝트에 뒤늦게 추가로만 React Redux
를 추가할 때 사용.
npm i react-redux
또는
yarn add react-redux
이번 글에서는 To-do List를 React Redux
를 이용하여 구현해보며,
React Redux
의 기초 구문을 사용해보고자 한다.
이 코드에 React Redux
를 도입하여 To-do List를 완성시켜나가고자 한다.
바탕이 될 코드는 createStore
를 이용하여 간단하게 Store
를 만들어뒀고,
reducer 함수
도 작성해둔 상태이다.
Redux
를 사용함에 있어서 제일 핵심이 되는 기술은 바로 Store
이다.
React
로 만들어진 애플리케이션은 대체로 다수의 컴포넌트로 이루어져있는데,
이 수많은 컴포넌트들이 Store
에 access(접근)할 수 있게 하기 위해서는 어떻게 해야할까?
이 때 사용되는 것이 바로 Provider
라는 컴포넌트이다.
- The Provider component makes the Redux store available to any nested components that need to access the Redux store.
- 출처: Provider - React Redux 공식 문서
Provider
란 하나의 컴포넌트이며,
Provider 컴포넌트
로 React
컴포넌트를 감싸줌으로써
Provider 컴포넌트
하위 컴포넌트들이 Provider
를 통해 Redux
의 store
에 access(접근)할 수 있게 해준다.
이것은 예시를 보는 것이 더 편하다.
// index.js
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import App from "./components/App";
import store from "./store";
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
React Redux
로부터 Provider 컴포넌트
를 import
한 후,
Provider 컴포넌트
로 store
를 이용할 컴포넌트를 감싸준다.
그 후 Provider 컴포넌트
의 props
로 해당 store
를 설정해주면 된다.
여기서 App 컴포넌트
를 Provider 컴포넌트
로 감싸준 이유는,
App 컴포넌트
가 우리가 만든 프로젝트의 컴포넌트 중 가장 상위에 있는 컴포넌트이기 때문이다.
최상위 컴포넌트인 App 컴포넌트
를 감싸준다면 그 하위의 모든 컴포넌트에서 당연히 store
를 access(접근)할 수 있게 될 것이기 때문이다.
// store.js
const toDosStore = createStore(reducer);
export default toDosStore;
그리고 우리는 템플릿 코드와 ↑ 이 코드를 보다시피,
store.js
라는 별도의 파일 내에서 createStore()
를 이용하여 store
를 생성해줬고,
그 store
를 export
해줬다.
export
한 store
를 index.js
에서 import
했으며
그 store
를 Provider 컴포넌트
의 store props
로 설정해준 것이다.
Provider 컴포넌트
를 이용하여 React 컴포넌트
가 store
에 access(접근)할 권한을 얻게 되었다면,
이제는 store
에 저장되어 있는 state
를 이용할 단계다.
각 컴포넌트들이 state
를 이용하기 앞서서 store
를 이용하기 위해서는
컴포넌트들을 store
에 연결(connect)시켜야 한다. (state
는 store
에 저장되어 있기 때문)
그럼 컴포넌트를 store에 어떻게 연결(connect)시킬 수 있을까?
여기서 connect 함수
를 사용한다.
connect 함수의 기본 구문은 다음과 같다.
connect(mapStateToProps, mapDispatchToProps)(Home);
connect 함수의 인자
- 첫번째 인자 mapStateToProps: 함수이다.
store
로부터state
를 가져와서 컴포넌트의props
로 보내게 해준다. 자세한 용법은 3️⃣ mapStateToProps 참고- 두번째 인자 mapDispatchToProps:
dispatch
를props
로 보낼 수 있다. 자세한 용법은 4️⃣ mapDispatchToProp 참고- Home: 취득한 데이터를
props
로 사용하고 싶은 컴포넌트를 지정한다.
그렇다면 실제로 connect 함수를 써보자.
템플릿 코드에서, Home 컴포넌트(Home.js)가 실질적으로 To-do(state)들을 렌더링하기 때문에
Home 컴포넌트
를 store
와 연결시킨다.
// routes/Home.js
import React, { useState } from "react";
import { connect } from "react-redux";
function Home() {
const [text, setText] = useState("");
function onSubmit(event) {
event.preventDefault();
console.log(text);
setText("");
}
function onChange(event) {
setText(event.target.value);
}
return (
<>
<h1>To-do List</h1>
<form onSubmit={onSubmit}>
<input
onChange={onChange}
type="text"
value={text}
placeholder="✍️Write To-do..."
/>
<button>✚</button>
</form>
<ul></ul>
</>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
이제 connect 함수
의 인수로 들어가는 mapStateToProps
, mapDispatchToProps
에 대해 각각 알아보자.
mapStateToProps
should be defined as a function:function mapStateToProps(state, ownProps?)
출처: React Redux 공식문서
mapStateToProps
는 함수이며, connect 함수
의 첫번째 인수이다.
mapStateToProps
는 store
로부터 state
를 가져와서, 컴포넌트의 props
로 state
를 보내주는 역할을 한다.
즉, mapStateToProps
를 사용한다는 것은, store
로부터 데이터를 가져와서
그 데이터를 컴포넌트의 props
에 넣는다는 뜻이다.
mapStateToProps의 인자
- 첫번째 인자 state:
store
로부터 온state
- 두번째 인자 ownProps: 생략가능. 컴포넌트가 현재 가지고 있는 모든
props
를 보여준다
중요한 것은, mapStateToProps
에서 return된 값이 컴포넌트의 props
에 추가된다는 점이다.
위의 그림을 보면, mapStateToProps 함수
에서 text: "hey"
라는 값을 가진 object(객체)를 return시켰다.
그리고 connect 함수
에서 연결했었던 Home 컴포넌트
의 props
를 확인한 결과
text: "hey"
라는 값을 받고 있는 것이 보인다.
(그 외의 값은 react-router
로부터 받은 props
이다)
여기서 알 수 있는 것은, mapStateToProps 함수
에서 store
로부터 가져온 state
를 return시킨다면
컴포넌트에서 state
를 props
로 받아서 사용할 수 있다는 것이다.
자, 그러면 우리의 To-do List 앱으로 돌아와서
store
에서 받은 state
를 컴포넌트에서 props
로 받을 수 있도록
다음과 같이 코드를 작성한다.
// routes/Home.js
import React, { useState } from "react";
import { connect } from "react-redux";
function Home({ toDos }) {
console.log(toDos);
const [text, setText] = useState("");
function onSubmit(event) {
event.preventDefault();
console.log(text);
setText("");
}
function onChange(event) {
setText(event.target.value);
}
return (
<>
<h1>To-do List</h1>
<form onSubmit={onSubmit}>
<input
onChange={onChange}
type="text"
value={text}
placeholder="✍️Write To-do..."
/>
<button>✚</button>
</form>
<ul></ul>
</>
);
}
function mapStateToProps(state) {
return {
toDos: state
};
}
function mapDispatchToProps() {}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
mapDispatchToProps
는 connect 함수
의 두번째 인자이며,
action
을 reducer
함수에게 보내는 역할을 가진 dispatch
를 props
로 보낼 수 있다.
function mapDispatchToProp(dispatch, ownProps?)
mapDispatchToProp의 인자
- 첫번째 인자 dispatch:
Redux
의store.dispatch()
와 같음- 두번째 인자 ownProps: 생략가능. 컴포넌트가 현재 가지고 있는 모든
props
를 보여준다
위에서 mapDispatchToProps
의 첫번째 인자인 dispatch
는
Redux
의 store.dispatch()
와 같다고 표현했는데,
그 이유는 다음의 그림과 같다.
mapDispatchToProps
의 첫번째 인자의 dispatch
를 그대로 return
해주고,
Home 컴포넌트
의 props
으로 받은 dispatch
를 보면
store.dispatch()
와 같은 메소드가 들어있다는 것을 알 수 있다.
이렇게 mapDispatchToProp
을 이용함으로써 컴포넌트 내에서 dispatch
를 사용할 수 있게 되었다.
이제부터는 위에서 공부한 문법을 바탕으로 활용을 하는 것만 남았다.
To-do
를 추가해서 페이지에 출력하기 위해서는 어떻게 해야할까?
input
에 텍스트를 입력하여 submit
(To-do를 추가하는 행위)하면
store.js
의 addToDo
라는 Action
을 발동시켜야 한다.
addToDo
를 발동시키기 전에, store.js
를 다음과 같이 리팩토링하자.
// store.js
import { createStore } from "redux";
import { v4 as uuidv4 } from "uuid";
const ADD_TODO = "ADD_TODO";
const DELETE_TODO = "DELETE_TODO";
// Action Creator
const addToDo = (text) => {
return {
type: ADD_TODO,
text
};
};
// Action Creator
const deleteToDo = (id) => {
return {
type: DELETE_TODO,
id
};
};
const reducer = (toDos = [], action) => {
switch (action.type) {
case ADD_TODO:
return [{ text: action.text, id: uuidv4() }, ...toDos];
case DELETE_TODO:
return toDos.filter((toDo) => toDo.id !== action.id);
default:
return toDos;
}
};
const toDosStore = createStore(reducer);
// Action Creator의 묶음
export const actionCreators = {
addToDo,
deleteToDo
};
export default toDosStore;
Aciton creator
인 addToDo
와 deleteToDo
를
actionCreators
라는 object(객체)에 하나로 묶어주었다.
그 다음, 다시 Home.js로 돌아와서
본격적으로 To-do를 추가하기 위해 addToDo
라는 Action
을 발동시켜보자.
// routes/Home.js
import React, { useState } from "react";
import { connect } from "react-redux";
import { actionCreators } from "../store";
function Home({ toDos, addToDo }) {
const [text, setText] = useState("");
console.log(toDos);
function onSubmit(event) {
event.preventDefault();
addToDo(text);
setText("");
}
function onChange(event) {
setText(event.target.value);
}
return (
<>
<h1>To-do List</h1>
<form onSubmit={onSubmit}>
<input
onChange={onChange}
type="text"
value={text}
placeholder="✍️Write To-do..."
/>
<button>✚</button>
</form>
<ul>
{toDos.map((toDo) => (
<li key={toDo.id}>{toDo.text}</li>
))}
</ul>
</>
);
}
function mapStateToProps(state) {
return {
toDos: state
};
}
function mapDispatchToProps(dispatch) {
return {
addToDo: (text) => dispatch(actionCreators.addToDo(text))
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
mapDispatchToProps 함수
는 object
를 return하고 있다.
mapDispatchToProps 함수
에서 return되는 object
내의 addToDo
라는 키를 가진 함수는 text
를 받아야 하며,
dispatch
는 actionCreators.addToDo
를 받는다. actionCreators.addToDo
역시 text
를 받는다.
mapDispatchToProps 함수
에서 return
되는 object
는
Home 컴포넌트
에서 props
로 보내진다.
그렇게 props
로 받아진 addToDo
을 onSubmit
에 추가해줌으로써
input
을 submit
할 때마다 addToDo
라는 Action
이 발동되게 했다.
이때 addToDo
는 useState
의 text
를 받는다.
addToDo
라는 Action
이 발동되면 input
의 입력값은 state(toDos)
에 추가되고,
state(toDos)
에 map
을 사용하여 state(toDos)
의 항목을 각각 출력해주었다.
다음은 각 To-do
에 삭제 버튼을 추가하고,
To-do
버튼을 클릭했을 때 deleteToDo
라는 Action
을 발동시켜서
To-do
를 state(toDos)
로부터 삭제시켜야한다.
그 전에 먼저, 윗 단계에서 완성된 Home.js
에서 각 To-do
를 의미하는 <li>
를
별도의 컴포넌트로 분리한 후 각 To-do
에 삭제 버튼을 추가해보자. (코드가 커지기 때문)
// routes/Home.js
import React, { useState } from "react";
import { connect } from "react-redux";
import ToDo from "../components/ToDo";
import { actionCreators } from "../store";
function Home({ toDos, addToDo }) {
const [text, setText] = useState("");
function onSubmit(event) {
event.preventDefault();
addToDo(text);
setText("");
}
function onChange(event) {
setText(event.target.value);
}
return (
<>
<h1>To-do List</h1>
<form onSubmit={onSubmit}>
<input
onChange={onChange}
type="text"
value={text}
placeholder="✍️Write To-do..."
/>
<button>✚</button>
</form>
<ul>
{toDos.map((toDo) => (
<ToDo key={toDo.id} {...toDo} />
))}
</ul>
</>
);
}
function mapStateToProps(state) {
return {
toDos: state
};
}
function mapDispatchToProps(dispatch) {
return {
addToDo: (text) => dispatch(actionCreators.addToDo(text))
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
// components/ToDo.js
import React from "react";
function ToDo({ text, id }) {
return (
<>
<li id={id}>
{text}
<button>
<span role="img" aria-label="delete">
❌
</span>
</button>
</li>
</>
);
}
export default ToDo;
이제 ToDo
의 삭제 버튼에 onClick
이벤트를 걸고
onClick
이벤트가 발동했을 때 (ToDo
의 삭제 버튼을 클릭했을 때)
dispatch
에서 deleteToDo
라는 Action
을 발동되게 해야 한다.
컴포넌트 내에서 dispatch를 이용하기 위해서 필요한 것은?
그렇다. connect
함수와 mapDispatchToProps
이다.
이것을 당장 ToDo
컴포넌트에 사용해주자.
그리고 dispatch
에서 deleteToDo
라는 Action
을 설정하는 것은
위의 ToDo
를 Add
할 때 기재했던 방식과 동일하다.
// components/ToDo.js
import React from "react";
import { connect } from "react-redux";
import { actionCreators } from "../store";
function ToDo({ text, deleteToDo }) {
return (
<>
<li>
{text}
<button onClick={deleteToDo}>
<span role="img" aria-label="delete">
❌
</span>
</button>
</li>
</>
);
}
function mapDispatchToProps(dispatch, ownProps) {
return {
deleteToDo: () => dispatch(actionCreators.deleteToDo(ownProps.id))
};
}
export default connect(null, mapDispatchToProps)(ToDo);
여기서 처음 눈에 띄는 것은 connect 함수
의 첫번째 인자인 mapStateToProps
자리에 null
이 작성 되어 있다.
이 ToDo
컴포넌트에서는 mapStateToProps
를 사용할 필요가 없기 때문에,
(Home
컴포넌트로부터 props
을 전달해받고있기 때문에)
mapStateToProps
자리에 null
을 작성한 것이다.
그리고 눈에 띄는 것은 mapDispatchToProps
의 두번째 인자인 ownProps
이다.
4️⃣ mapDispatchToProp의 인자를 설명해주는 부분에서
ownProps
는 컴포넌트가 현재 가지고 있는 모든 props
를 보여준다고 했다.
ownProps
를 확인해보자.
위의 Home 컴포넌트의 코드에서 ToDo
컴포넌트에게 state(toDos)
의 각 내용을 {...ToDo}
로 통째로 보내줬었다.
그렇게해서 현재 받은 props
를 보여주는 것이 바로 ownProps
이다.
ToDo
를 삭제하는데에 필요한 deleteToDo Action
은
id
를 parameter(매개변수)로 필요로 하는데 (store.js
의 deleteToDo
참고)
이 ownProps
를 활용하면 이미 id
값을 가져올 수 있다.
그래서 deleteToDo
키값의 parameter(매개변수)에 id
를 주지 않았고
dispatch
의 actionCreators.deleteToDo
에 바로 ownProps
의 id
를 주었다.
그 코드가 아래의 코드이다.
function mapDispatchToProps(dispatch, ownProps) {
return {
deleteToDo: () => dispatch(actionCreators.deleteToDo(ownProps.id))
};
}
이렇게 만들어진 deleteToDo
는 ToDo
컴포넌트로 return
되어 props
로 보내지고,
props
로 받은 deleteToDo
를 삭제 버튼의 onClick
이벤트에 등록시키면...
function ToDo({ text, deleteToDo }) {
return (
<>
<li>
{text}
<button onClick={deleteToDo}>
<span role="img" aria-label="delete">
❌
</span>
</button>
</li>
</>
);
}
이렇게 ToDo
를 삭제시키는 것에 성공했다!🙌
지금까지 To-do
를 추가하고 삭제하는 기본적인 동작까지 구현하는 것에 성공했다.
마지막으로, React Router
를 이용해 ToDo
컴포넌트의 각 ToDo
에
Detail
페이지의 링크를 걸어서 To-do
의 페이지에 접속해보며
Detail
페이지에 To-do
와 To-do
의 id
를 출력해보자.
먼저 각 ToDo
를 클릭하면 Detail
페이지로 이동할 수 있도록
React Router
를 이용해 ToDo
컴포넌트의 각 ToDo
에
Detail
페이지의 링크를 걸자.
(Router
설정은 템플릿 코드의 component/App.js
에서 미리 진행해두었다.)
// components/ToDo.js
import React from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { actionCreators } from "../store";
function ToDo({ text, deleteToDo, id }) {
return (
<>
<li>
<Link to={`/${id}`}>{text}</Link>
<button onClick={deleteToDo}>
<span role="img" aria-label="delete">
❌
</span>
</button>
</li>
</>
);
}
function mapDispatchToProps(dispatch, ownProps) {
return {
deleteToDo: () => dispatch(actionCreators.deleteToDo(ownProps.id))
};
}
export default connect(null, mapDispatchToProps)(ToDo);
Detail
페이지의 parameter
는 각 To-do
의 id
로 설정하기 위해
ToDo
컴포넌트의 props로 가져온 id
를 Link
에 설정했다.
이제 Detail
페이지에 To-do
와 To-do
의 id
를 출력할 차례이다.
To-do
를 출력하기 위해서는 state
에 저장된 ToDo
를 불러와야 한다.
컴포넌트 내에서 state
를 가져와서 state
를 props
로 보낼 때 사용하는 것은?
3️⃣ mapStateToProps이다.
Detail
컴포넌트에서 mapStateToProps
를 불러오자.
// routes/Detail.js
import React from "react";
import { connect } from "react-redux";
function Detail(state) {
return (
<>
<h1>Detail</h1>
<p>id</p>
</>
);
}
function mapStateToProps(state, ownProps) {
console.log(ownProps)
return {
state
};
}
export default connect(mapStateToProps)(Detail);
mapStateToProps
의 두번째 인자인 ownProps
를 이용해서 Detail
컴포넌트의 현재 props
를 확인해보자.
Detail
컴포넌트는 components/App.js
에서 React Router
의 Route
로써 사용되고 있다.
Detail
컴포넌트처럼 Route
로 사용된 컴포넌트는 props로 history
, match
, location
라는 3개의 object(객체)를 받을 수 있게 된다.
history
, match
, location
라는 각각의 object(객체)는
URL path, 페이지 경로명, 페이지 탐색 history 등과 같은 정보를 담고 있다.
history
, match
, location
가 담고 있는 정보들에 대해서는
이 블로그에서 자세한 정보를 얻을 수 있다.
우리의 To-do List 앱에서는
다양한 ToDo
가 저장되어있는 state
중에서
현재 접속한 Detail
페이지의 URL parameter(id)
와 같은 id
를 가진 그 ToDo
가 가진 정보를 출력할 것이다.
이때 현재 접속한 Detail
페이지의 URL parameter(id)
를 찾기 위해
match
object(객체)의 정보를 이용하려 한다.
// routes/Detail.js
import React from "react";
import { connect } from "react-redux";
function Detail({ toDo }) {
return (
<>
{/* toDo뒤의 ?는 Optional Chaining이라는 문법임.
null이나 undefined인 값이 반환되면, 코드를 즉시 중단하고 undefined를 반환함 */}
<h1>{toDo?.text}</h1>
<p>{toDo?.id}</p>
</>
);
}
function mapStateToProps(state, ownProps) {
// App.js에서 Detail Route에 설정한 동적 라우팅(path="/:id")으로 전달된
// path 파라미터 정보를 불러옴
const {
match: {
params: { id }
}
} = ownProps;
return {
// path 파라미터의 id와 같은 toDo.id를 찾음
toDo: state.find((toDo) => toDo.id === id)
};
}
export default connect(mapStateToProps)(Detail);
여기서 중요한 포인트는 toDo
를 출력하는데에 Optional Chaining
이라는 문법을 사용했다.
Optional Chaining에 대하여 (MDN 문서)
이번에는 간단한 React
프로젝트에서 React Redux
를 배워보는 시간을 가졌다.
Redux
는 이해하는데에 약간 비용이 들긴 하지만
(무엇이든 배우는 데에는 시간과 노력이 필요하긴 하다!)
Redux
를 사용함으로써 코드가 정리된 듯한 느낌이 든다.
좀 더 연습해서 숙련될 수 있도록 노력해야겠다💪
좋은글 감사합니다 이해하는데 많은 도움되었어요!