React Redux :: React Redux 입문 (Provider, Connect, mapStateToProps, mapDispatchToProps)

Hayoung·2021년 5월 12일
12

Redux

목록 보기
2/3
post-thumbnail

이번 글에서는 React에서 state management(상태 관리)를 할 때
널리 사용되어지고 있는 React Redux에 대해 알아보고자 한다.

⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
이 글은 Redux의 기초 개념을 어느정도 이해하고 있는 상태로 작성된 글입니다.
Store, Action, Reducer, Dispatch에 대해 처음 들어보시는 등
Redux에 대해 전혀 접근해보지 못한 분들은
제 블로그의 Redux 입문을 살펴보시는 것을 추천드립니다.
⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️

React Redux란?

React Redux is maintained by the Redux team, and kept up-to-date with the latest APIs from Redux and React.

ReduxReact Redux 모두, state management(상태 관리)를 위해 쓰이는 Redux라는 것은 다를 게 없으나

React ReduxRedux 팀에서 공식적(Official)으로 관리, 배포하고 있는
React 전용 Redux 패키지이다.

그래서 ReduxReact Redux의 공식 문서는 각각 개별적으로 존재하고 있다.

Redux 공식 문서
React Redux 공식 문서

Redux를 시작하기 전에 바로 React Redux를 사용해도 무방하나,
React ReduxRedux를 기초로 설계되어있다.

그래서 시간이 좀 소요되더라도, 더 편하게 React Redux를 다루기 위해서는
Redux에 대한 기초 개념을 이해한 뒤에 React Redux를 사용하는 것이 편하다고 생각한다.
(↑ 개인적인 생각입니다. 이 학습 순서가 나에게는 더 적합했다.)

⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
그러므로 이 글은 Redux의 기초 개념을 어느정도 이해하고 있는 상태로 작성된 글입니다.
Store, Action, Reducer, Dispatch에 대해 처음 들어보시는 등
Redux에 대해 전혀 접근해보지 못한 분들은
제 블로그의 Redux 입문을 살펴보시는 것을 추천드립니다.
⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️


React Redux 설치하기⤴️

React Redux에는 설치 방법이 2가지가 있다.

1️⃣ Create React App으로 React 프로젝트와 React Redux 설치를 동시에 진행하는 방법

이 방법은 React 프로젝트 생성 시작부터 React Redux를 사용할 예정일 때 사용하면 유용하다.
React Redux별도로 설치하지 않아도 React Redux가 포함된 채로 프로젝트를 시작할 수 있기 때문이다.

npx create-react-app my-app --template redux

2️⃣ React Redux만 추가로 설치하는 방법

React 프로젝트를 생성할 당시에 React Redux를 함께 설치하지 못했고
이미 진행 중인 프로젝트에 뒤늦게 추가로만 React Redux를 추가할 때 사용.

npm i react-redux

또는

yarn add react-redux

React Redux 기초 구문📝

이번 글에서는 To-do List를 React Redux를 이용하여 구현해보며,
React Redux의 기초 구문을 사용해보고자 한다.

템플릿 코드

이 코드에 React Redux를 도입하여 To-do List를 완성시켜나가고자 한다.
바탕이 될 코드는 createStore를 이용하여 간단하게 Store를 만들어뒀고,
reducer 함수도 작성해둔 상태이다.

1️⃣ Provider 컴포넌트

Redux를 사용함에 있어서 제일 핵심이 되는 기술은 바로 Store이다.
React로 만들어진 애플리케이션은 대체로 다수의 컴포넌트로 이루어져있는데,
이 수많은 컴포넌트들이 Storeaccess(접근)할 수 있게 하기 위해서는 어떻게 해야할까?

이 때 사용되는 것이 바로 Provider라는 컴포넌트이다.

Provider란 하나의 컴포넌트이며,
Provider 컴포넌트React 컴포넌트를 감싸줌으로써
Provider 컴포넌트 하위 컴포넌트들이 Provider를 통해 Reduxstore에 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 컴포넌트를 감싸준다면 그 하위의 모든 컴포넌트에서 당연히 storeaccess(접근)할 수 있게 될 것이기 때문이다.

// store.js
const toDosStore = createStore(reducer);

export default toDosStore;

그리고 우리는 템플릿 코드와 ↑ 이 코드를 보다시피,
store.js라는 별도의 파일 내에서 createStore()를 이용하여 store를 생성해줬고,
storeexport해줬다.

exportstoreindex.js에서 import했으며
storeProvider 컴포넌트store props로 설정해준 것이다.

2️⃣ connect 함수

Provider 컴포넌트를 이용하여 React 컴포넌트store에 access(접근)할 권한을 얻게 되었다면,
이제는 store에 저장되어 있는 state를 이용할 단계다.

각 컴포넌트들이 state를 이용하기 앞서서 store를 이용하기 위해서는
컴포넌트들을 store연결(connect)시켜야 한다. (statestore에 저장되어 있기 때문)

그럼 컴포넌트를 store에 어떻게 연결(connect)시킬 수 있을까?
여기서 connect 함수를 사용한다.

connect 함수의 기본 구문은 다음과 같다.

connect(mapStateToProps, mapDispatchToProps)(Home);

connect 함수의 인자

  • 첫번째 인자 mapStateToProps: 함수이다. store로부터 state를 가져와서 컴포넌트의 props로 보내게 해준다. 자세한 용법은 3️⃣ mapStateToProps 참고
  • 두번째 인자 mapDispatchToProps: dispatchprops로 보낼 수 있다. 자세한 용법은 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에 대해 각각 알아보자.

3️⃣ mapStateToProps

mapStateToProps should be defined as a function:

function mapStateToProps(state, ownProps?)

출처: React Redux 공식문서

mapStateToProps는 함수이며, connect 함수의 첫번째 인수이다.
mapStateToPropsstore로부터 state를 가져와서, 컴포넌트의 propsstate를 보내주는 역할을 한다.

즉, 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시킨다면
컴포넌트에서 stateprops로 받아서 사용할 수 있다는 것이다.

자, 그러면 우리의 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);

4️⃣ mapDispatchToProp

mapDispatchToPropsconnect 함수의 두번째 인자이며,
actionreducer 함수에게 보내는 역할을 가진 dispatchprops로 보낼 수 있다.

function mapDispatchToProp(dispatch, ownProps?)

mapDispatchToProp의 인자

  • 첫번째 인자 dispatch: Reduxstore.dispatch()와 같음
  • 두번째 인자 ownProps: 생략가능. 컴포넌트가 현재 가지고 있는 모든 props를 보여준다

위에서 mapDispatchToProps의 첫번째 인자인 dispatch
Reduxstore.dispatch()와 같다고 표현했는데,
그 이유는 다음의 그림과 같다.

mapDispatchToProps의 첫번째 인자의 dispatch를 그대로 return해주고,
Home 컴포넌트props으로 받은 dispatch를 보면
store.dispatch()와 같은 메소드가 들어있다는 것을 알 수 있다.

이렇게 mapDispatchToProp을 이용함으로써 컴포넌트 내에서 dispatch를 사용할 수 있게 되었다.


React Redux를 사용하여 To-do List 나머지 기능 구현하기

이제부터는 위에서 공부한 문법을 바탕으로 활용을 하는 것만 남았다.

input을 submit하면 To-do를 Add하는 Action 발동시키기

To-do를 추가해서 페이지에 출력하기 위해서는 어떻게 해야할까?
input에 텍스트를 입력하여 submit(To-do를 추가하는 행위)하면
store.jsaddToDo라는 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 creatoraddToDodeleteToDo
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를 받아야 하며,
dispatchactionCreators.addToDo를 받는다. actionCreators.addToDo 역시 text를 받는다.

mapDispatchToProps 함수에서 return되는 object
Home 컴포넌트에서 props로 보내진다.
그렇게 props로 받아진 addToDoonSubmit에 추가해줌으로써
inputsubmit할 때마다 addToDo라는 Action이 발동되게 했다.
이때 addToDouseStatetext를 받는다.

addToDo라는 Action이 발동되면 input의 입력값은 state(toDos)에 추가되고,
state(toDos)map을 사용하여 state(toDos)의 항목을 각각 출력해주었다.

To-do 삭제 버튼을 클릭하면 To-do를 삭제시키기

다음은 각 To-do에 삭제 버튼을 추가하고,
To-do 버튼을 클릭했을 때 deleteToDo라는 Action을 발동시켜서
To-dostate(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을 설정하는 것은
위의 ToDoAdd할 때 기재했던 방식과 동일하다.

// 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.jsdeleteToDo 참고)
ownProps를 활용하면 이미 id 값을 가져올 수 있다.
그래서 deleteToDo 키값의 parameter(매개변수)에 id를 주지 않았고
dispatchactionCreators.deleteToDo에 바로 ownPropsid를 주었다.
그 코드가 아래의 코드이다.

function mapDispatchToProps(dispatch, ownProps) {
  return {
    deleteToDo: () => dispatch(actionCreators.deleteToDo(ownProps.id))
  };
}

이렇게 만들어진 deleteToDoToDo 컴포넌트로 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의 Detail 페이지 작성하기

지금까지 To-do를 추가하고 삭제하는 기본적인 동작까지 구현하는 것에 성공했다.
마지막으로, React Router를 이용해 ToDo 컴포넌트의 각 ToDo
Detail 페이지의 링크를 걸어서 To-do의 페이지에 접속해보며
Detail 페이지에 To-doTo-doid를 출력해보자.

먼저 각 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-doid로 설정하기 위해
ToDo 컴포넌트의 props로 가져온 idLink에 설정했다.

이제 Detail 페이지에 To-doTo-doid를 출력할 차례이다.
To-do를 출력하기 위해서는 state에 저장된 ToDo를 불러와야 한다.
컴포넌트 내에서 state를 가져와서 stateprops로 보낼 때 사용하는 것은?
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 RouterRoute로써 사용되고 있다.
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 문서)


Demo 🔍


마치며

이번에는 간단한 React 프로젝트에서 React Redux를 배워보는 시간을 가졌다.
Redux는 이해하는데에 약간 비용이 들긴 하지만
(무엇이든 배우는 데에는 시간과 노력이 필요하긴 하다!)
Redux를 사용함으로써 코드가 정리된 듯한 느낌이 든다.

좀 더 연습해서 숙련될 수 있도록 노력해야겠다💪

profile
Frontend Developer. 블로그 이사했어요 🚚 → https://iamhayoung.dev

3개의 댓글

comment-user-thumbnail
2021년 9월 12일

좋은글 감사합니다 이해하는데 많은 도움되었어요!

2개의 답글