이번에는 react-redux 라이브러리를 사용하여
react의 store와 react의 컴포넌트를 연결해보겠습니다.
npm i react-redux
// index.js
import { Provider } from "react-redux";
// ...
root.render(
<Provider store={store}>
<App />
</Provider>
);
react-redux 에서는 Provider에 value
속성이 아닌 좀 더 정확한 명칭인 store
를 사용합니다.
이제 주입된 store를 사용할 곳에서 연결해주어야 합니다.
연결해주는 HOC 함수가 connect
함수 입니다.
// 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()()
한 부분은 컨테이너
혹은 스마트한 컴포넌트
라고 부릅니다.
이렇게 역할을 완벽하게 분리하여
connect
는 store와 프리젠테이셔널 컴포넌트를 이어주는 역할
을 하는 컴포넌트이고,
프리젠테이셔널 컴포넌트
는 컨테이너가 주는 데이터나 함수를 받아서 그냥 보여주거나 함수를 실행하는 역할만 하는 컴포넌트가 되었습니다.
역할이 완벽하게 분리되어 있으니 컨테이너만 따로 폴더와 파일로 나눠서 관리할 수 있습니다.
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함수를 이용해서 연결해봤습니다.
이번에는 컨테이너를 다른방식으로 바꿔보겠습니다.
위에는 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를 사용할 경우에 컨테이너와 컴포넌트를 명확하게 구분하고 그 역할에 맞는 로직을 작성하는 것이 좋을 것 같습니다.