Redux로 상태관리하기 - Redux Basic 5

lbr·2022년 8월 16일
0

Redux를 React에 연결(react-redux 쓰고 연결하기)

이번에는 react-redux 라이브러리를 사용하여
react의 store와 react의 컴포넌트를 연결해보겠습니다.

react-redux 개요

  • Provider 컴포넌트를 제공해줍니다.
  • connect 함수를 통해 "컨테이너"를 만들어줍니다.
    - 컨테이너는 스토어의 state 와 dispatch(액션) 를 연결한 컴포넌트에 props 로 넣어주는 역할을 합니다.
    - connect 함수를 사용하기 위해서 필요한 것은 ?
    - 어떤 state 를 어떤 props 에 연결할 것인지에 대한 정의
    - 어떤 dispatch(액션) 을 어떤 props 에 연결할 것인지에 대한 정의
    - 그 props 를 보낼 컴포넌트를 정의

react-redux 설치

npm i react-redux

Context 대신 라이브러리에서 제공하는 Provider 사용하기

// index.js
import { Provider } from "react-redux";
// ...
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

react-redux 에서는 Provider에 value 속성이 아닌 좀 더 정확한 명칭인 store를 사용합니다.

이제 주입된 store를 사용할 곳에서 연결해주어야 합니다.
연결해주는 HOC 함수가 connect 함수 입니다.

connect함수로 store와 컴포넌트를 연결해주기

// TodoList.jsx
import { connect } from "react-redux";
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => {
        return <li>{todo.text}</li>;
      })}
    </ul>
  );
}

// 함수의 인자로 들어오는 것이 state 입니다.
// props 객체를 return하면 됩니다.
// return된 내용이 뒤의 함수 인자로 들어간 컴포넌트에게 todos라는 props로 들어가고,
// todos에는 값으로 store의 state의 todos가 들어가게 됩니다.
const mapStateToProps = (state) => {
  return {
    todos: state.todos,
  };
};

// return 에는 props 객체가 들어갑니다.
// todos를 변경할 때 사용하는 함수를 만들어서 그 함수를 넣어줍니다.
const mapDispatchToProps = (dispatch) => {
  return {};
};

const TodoListContainer = connect(
  // config
  // config에는 크게 보면 2가지가 들어갑니다.
  // 1. store의 state를 받아서 어떤 props를 넣어줄 것인가
  //    이 함수의 이름을 mapStateToProps 이라고 합니다.
  // 2. 두번째 인자로는 state에 dispatch를 할 수 있는 함수를 props로 넣어주는 함수입니다.

  mapStateToProps,
  mapDispatchToProps
)(TodoList);
// connect함수를 실행한 결과물이 HOC함수가 되고,
// HOC함수를 실행한 결과가 컨테이너입니다.
// connect의 앞쪽 함수를 실행할 때는 config가 들어가고,
// 뒤의 함수 인자로는 컴포넌트(TodoList)가 들어간다고 생각하면 됩니다.

export default TodoListContainer;
// TodoForm.jsx

import { useRef } from "react";
import { connect } from "react-redux";
import { addTodo } from "../redux/actions";

function TodoForm({ add }) {
  const inputRef = useRef();

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={click}>추가</button>
    </div>
  );

  function click() {
    add(inputRef.current.value);
  }
}

export default connect(
  (state) => ({}),
  (dispatch) => ({
    // todo를 추가할 수 있는 dispatch를 실행하는 로직을 함수로 만들어서 props로 넣도록 하겠습니다.
    add: (text) => {
      dispatch(addTodo(text));
    },
  })
)(TodoForm);

여기까지 진행하고서 Form 컴포넌트의 코드를 보면 달라진 점이 있습니다.
TodoForm컴포넌트 로직과 store 관련 로직이 이전에는 한곳에 모여있었다면 이제는 정확히 분리되어 TodoForm 컴포넌트는 이제 그냥 하나의 비주얼한 컴포넌트이고, 이 컴포넌트에서는 함수를 사용하거나 그냥 데이터를 받아서 보여주는 역할만 하고 있습니다.
이러한 컴포넌트를 프리젠테이셔널 컴포넌트 혹은 그냥 컴포넌트라고 부릅니다.

connect()()한 부분은 컨테이너 혹은 스마트한 컴포넌트 라고 부릅니다.

이렇게 역할을 완벽하게 분리하여
connectstore와 프리젠테이셔널 컴포넌트를 이어주는 역할을 하는 컴포넌트이고,
프리젠테이셔널 컴포넌트는 컨테이너가 주는 데이터나 함수를 받아서 그냥 보여주거나 함수를 실행하는 역할만 하는 컴포넌트가 되었습니다.

역할이 완벽하게 분리되어 있으니 컨테이너만 따로 폴더와 파일로 나눠서 관리할 수 있습니다.

컨테이너(connect)를 따로 파일로 분리하여 관리하기

containers 폴더를 생성하여 이 안에 컨테이너만 따로 분리하여 관리합니다.

// TodoList.jsx
export default function TodoList({ todos }) {
  // const state = useReduxState();
  return (
    <ul>
      {todos.map((todo) => {
        return <li>{todo.text}</li>;
      })}
    </ul>
  );
}

// TodoListContainer.jsx
import { connect } from "react-redux";
import TodoList from "../components/TodoList";

const mapStateToProps = (state) => {
  return {
    todos: state.todos,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {};
};

const TodoListContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList);

export default TodoListContainer;
// Todoform.jsx
// 이 컴포넌트에는 이제 redux의 흔적이 전혀 없습니다.
// 그냥 props를 받아서 실행하는 컴포넌트가 되었습니다.
import { useRef } from "react";

export default function TodoForm({ add }) {
  const inputRef = useRef();

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={click}>추가</button>
    </div>
  );

  function click() {
    add(inputRef.current.value);
  }
}

// TodoFormContainer.jsx
import { connect } from "react-redux";
import TodoForm from "../components/TodoForm";
import { addTodo } from "../redux/actions";

const TodoFormContainer = connect(
  (state) => ({}),
  (dispatch) => ({
    add: (text) => {
      dispatch(addTodo(text));
    },
  })
)(TodoForm);

export default TodoFormContainer;
// App.js
import TodoListContainer from "./containers/TodoListContainer";
import TodoFormContainer from "./containers/TodoFormContainer";

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <TodoListContainer />
        <TodoFormContainer />
      </header>
    </div>
  );
}

App.js 에서도 <TodoList /><TodoForm /><TodoListContainer /><TodoFormContainer /> 로 바꿔줍니다. (컨테이너가 HOC 함수 였다는 사실을 잊지 말자)

지금까지는 connect라는 HOC함수를 이용해서 연결해봤습니다.

이번에는 컨테이너를 다른방식으로 바꿔보겠습니다.

HOC 방식으로 연결했던 것을 Hook 방식으로 바꿔보기

위에는 connect로 연결을 했습니다. 생각해보면,
HOC로 공통로직을 제공하던 부분이 Hook으로 많이 바뀌었다는 것을 알 수 있습니다.
그래서 TodoListContainer를 Hook으로 한 번 작성해보도록 하겠습니다.

// TodoListContainer.jsx
import { useSelecter } from "react-redux";
import TodoList from "../components/TodoList";

//jsx를 리턴하는 컴포넌트입니다.
function TodoListContainer() {
  // <하는일>
  // store를 연결한 다음에 store에 있는 state를 꺼내서 그 중에서 필요한 것을 props로 넣어주는 일입니다.
  // 그렇기 때문에 react-redux에서 제공하는 hook을 사용하게 되면
  // 조금 더 편하게 이런 작업을 명시적으로 보이게 할 수 있습니다.

  // useSelecter 는 react-redux에서 제공하는 Hook입니다.
  const todos = useSelecter((state) => state.todos);
  // 인자로 함수가 들어오고, 함수의 인자로는 state가 들어오고, 리턴값으로 어느 것을 가지고 올 것인지를 state로부터 고르면 됩니다.

  return <TodoList todos={todos}/>;
}
export default TodoListContainer;

useSelecter() 는 react-redux에서 제공하는 Hook입니다.
이전에는 HOC를 이용했던 것이 이번에는 Hook을 이용해서 그냥 하위 컴포넌트한테 props로 찔러넣어주는 방식으로 바뀌었습니다.

// TodoFormContainer.jsx
import { useCallback } from "react";
import { useDispatch } from "react-redux";
import TodoForm from "../components/TodoForm";
import { addTodo } from "../redux/actions";

export default function TodoFormContainer() {
  // dispatch함수를 주는 react-redux의 Hook인 useDispatch 이용하면됩니다.
  const dispatch = useDispatch();

  const add = useCallback(
    (text) => {
      dispatch(addTodo(text));
    },
    [dispatch]
  );
  // 디펜던시로 dispatch 함수를 넣으면 이 dispatch 함수가 바뀌었을 때, useCallback의 콜백함수도 새로 만들어 질 것입니다.
  // 하지만 dispatch라는 함수는 달라질 일이 거의 없습니다. 그렇기 때문에 우리가 처음에 HOC로 만들었던 형식과 거의 같은 형식이 됩니다.
  // 이렇게 하면 이제 불필요하게 함수가 새로 만들어지는 걱정은 하지 않아도 됩니다.

  // 아래 함수처럼 작성해도 동작은하지만 TodoFormContainer 가 실행될 때마다 add에 새로운 함수를 계속
  // 주입하고 있기 때문에 useCallback Hook으로 한번만 전달하게 합니다.
  // function add(text) {
  //   dispatch(addTodo(text));
  // }

  return <TodoForm add={add} />;
}

정리

connect라는 HOC와 useDispatch, useSeletor를 이용해서 컨테이너를 만들고, 컴포넌트한테 props로 전달하는 방식에 대해서 알아봤습니다.
이제부터 redux를 사용할 경우에 컨테이너와 컴포넌트를 명확하게 구분하고 그 역할에 맞는 로직을 작성하는 것이 좋을 것 같습니다.

0개의 댓글