그럼에도 불구하고

👨‍💻

[Redux with TS] connect 고차 함수와 useSelector, useDispatch 본문

React/Redux

[Redux with TS] connect 고차 함수와 useSelector, useDispatch

zenghyun 2023. 7. 13. 14:29

 

connect 고차 함수와 useSelector, useDispatch에 대해 알아보겠습니다. 

 

 

[ 리덕스 컨테이너 컴포넌트 ]

 

Redux 플로우 전체도 

Connect Component =  Container Component

https://medium.com/@ca3rot/%EC%95%84%EB%A7%88-%EC%9D%B4%EA%B2%8C-%EC%A0%9C%EC%9D%BC-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-%EC%89%AC%EC%9A%B8%EA%B1%B8%EC%9A%94-react-redux-%ED%94%8C%EB%A1%9C%EC%9A%B0%EC%9D%98-%EC%9D%B4%ED%95%B4-1585e911a0a6

 

리덕스 스토어와 연결되는 컴포넌트들은 모두 표현 컴포넌트(presentational component)로 작성할 수 있습니다. react-redux 같은 라이브러리가 리덕스 스토어의 상태를 표현 컴포넌트의 속성으로 전달해 주는 컨테이너 컴포넌트를 손쉽게 작성할 수 있는 리덕스 훅들과 connect()와 같은 고차 함수를 지원하기 때문입니다. 이렇게 작성하면 컴포넌트 트리를 거치면서 반복적, 계층적으로 속성을 전달하지 않아도 됩니다. 

 

 

그렇다고 모든 표현 컴포넌트에 대해 컨테이너를 만드는 것이 좋은 것은 아닙니다.

 

컨테이너 컴포넌트를 만들면 간단하긴 하지만 컨테이너 컴포넌트를 작성해야 하며, 컨테이너 컴포넌트는 리덕스에 의존적이므로 리덕스를 사용하지 않는 환경에서 컴포넌트를 재사용하기 힘듭니다. 

 

그래서 주요 거점 컴포넌트만 컨테이너 컴포넌트로 속성을 주입시키고, 짧은 구간만을 속성으로 전달하도록 구성하는 것이 바람직합니다. 

주요 거점 컴포넌트는 일반적으로 소규모 메뉴나 화면 또는 화면 레이아웃의 취상 위 컴포넌트 단위라고 생각하면 됩니다. 

 

 

다음 그림은 컨테이너 컴포넌트를 만들기 위해 react-redux 라이브러리가 제공하는 connect() 고차 함수를 적용하는 방법입니다. 

 

https://medium.com/@ca3rot/%EC%95%84%EB%A7%88-%EC%9D%B4%EA%B2%8C-%EC%A0%9C%EC%9D%BC-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-%EC%89%AC%EC%9A%B8%EA%B1%B8%EC%9A%94-react-redux-%ED%94%8C%EB%A1%9C%EC%9A%B0%EC%9D%98-%EC%9D%B4%ED%95%B4-1585e911a0a6

 

리덕스 스토어의 상태 이름과 표현 컴포넌트의 이름이 같을 수는 없습니다. 같은 맥락으로 표현 컴포넌트가 속성으로 전달받는 함수의 이름도 마찬가지입니다. 그래서 리덕스 스토어의 상태와 표현 컴포넌트의 속성명을 매핑한 정보를 제공하는 mapStateToProps() 함수를 만들어야 합니다. 

 

mapdispatchToProps() 함수는 표현 컴포넌트의 속성에 dispatch(action) 할 수 있는 함수를 매핑한 정보를 리턴합니다.

 

컨테이너 컴포넌트는 connect 고차 함수에 mapStateToProps, mapDispatchToProps를 인자로 전달하여 호출한 뒤 리턴 받은 함수에 다시 표현 컴포넌트를 인자로 전달하여 호출하면 컨테이너 컴포넌트가 리턴됩니다. 이 컨테이너 컴포넌트는 표현 컴포넌트에 상태와 dispatch(action) 기능의 함수를 속성으로 주입합니다.

 

표현 컴포넌트가 함수 컴포넌트라면 useSelector, useStore, useDispatch와 같은 훅을 사용하면 편리하지만 클래스 컴포넌트에서는 훅을 사용할 수 없으므로 connect() 고차 함수를 사용하는 방법을 알고 있어야만 합니다. 

 

 

[ 예제 ] 

아래의 예제는 이해를 돕기 위한 코드입니다. 

📌 TodoActionCreator.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createAction } from "@reduxjs/toolkit";
 
export const TODO_ACTION = {
  ADD_TODO: "addTodo" as const,
  DELETE_TODO: "deleteTodo" as const,
  TOGGLE_DONE: "toggleDone" as const,
  UPDATE_TODO: "updateTodo" as const,
};
 
const TodoActionCreator = {
  addTodo: createAction<{ todo: string; desc: string; }>("addTodo"),
  deleteTodo: createAction<{ id: number; }>("deleteTodo"),
  toggleDone: createAction<{ id: number; }>("toggleDone"),
  updateTodo: createAction<{ id: number; todo: string; desc: string; done: boolean; }>("updateTodo"),
};
 
export default TodoActionCreator;
 
cs

 

📌 TodoReducer.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { createReducer } from "@reduxjs/toolkit";
import TodoActionCreator from "./TodoActionCreator";
 
export type TodoItemType = {
  id: number;
  todo: string;
  desc: string;
  done: boolean;
};
 
export type TodoStatesType = { todoList: Array<TodoItemType> };
 
const initialState: TodoStatesType = {
  todoList: [
    { id: 1, todo: "ES6 학습", desc: "설명1", done: false },
    { id: 2, todo: "React 학습", desc: "설명2", done: false },
    { id: 3, todo: "ContextAPI 학습", desc: "설명3", done: true },
    { id: 4, todo: "야구 경기 관람", desc: "설명4", done: false },
  ],
};
 
const TodoReducer = createReducer(initialState, (builder) => {
    builder
    .addCase(TodoActionCreator.addTodo, (state, action) => {
        state.todoList.push({
            id: new Date().getTime(),
            todo: action.payload.todo,
            desc: action.payload.desc,
            done: false,
        });
    })
    .addCase(TodoActionCreator.deleteTodo, (state, action) => {
        const index = state.todoList.findIndex(item => item.id === action.payload.id);
        state.todoList.splice(index, 1);
    })
    .addCase(TodoActionCreator.toggleDone, (state, action) => {
        const index = state.todoList.findIndex(item => item.id === action.payload.id);
        state.todoList[index].done = !state.todoList[index].done;
    })
    .addCase(TodoActionCreator.updateTodo, (state, action) => {
        const index = state.todoList.findIndex(item => item.id === action.payload.id);        
        state.todoList[index] = {...action.payload};
    })
    .addDefaultCase((state, action) => state);
})
 
export default TodoReducer;
 
cs

 

📌 TodoActionCreator.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { configureStore } from "@reduxjs/toolkit";
import { combineReducers } from "@reduxjs/toolkit";
import TodoReducer, { TodoStatesType } from "./TodoReducer";
 
export type RootStatesType = {
  todos: TodoStatesType;
};
 
const RootReducer = combineReducers({
  todos: TodoReducer,
});
 
const AppStore = configureStore({ reducer: RootReducer });
export default AppStore;
 
cs

 

📌 TodoList.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import { Link } from "react-router-dom";
import TodoItem from "./TodoItem";
import TodoActionCreator from "../redux/TodoActionCreator";
import { AnyAction, Dispatch } from "redux";
import { connect } from "react-redux";
import { TodoItemType } from "../redux/TodoReducer";
import { RootStatesType } from "../redux/AppStore";
 
type PropsType = {
  todoList: Array<TodoItemType>;
  deleteTodo: (id:number) => void;
  toggleDone: (id: number) => void;
};
 
const TodoList = ({ todoList, deleteTodo, toggleDone }: PropsType) => {
  const todoItems = todoList.map((item) => {
    return <TodoItem key={item.id} todoItem={item} deleteTodo={deleteTodo} toggleDone={toggleDone} />;
  });
  return (
    <>
      <div className="row">
        <div className="col p-3">
          <Link className="btn btn-primary" to="/todos/add">
            할 일 추가
          </Link>
        </div>
      </div>
      <div className="row">
        <div className="col">
          <ul className="list-group">{todoItems}</ul>
        </div>
      </div>
    </>
  );
};
 
const mapStateToProps = (state: RootStatesType) => ({
  todoList: state.todos.todoList,
});
 
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>=> ({
  deleteTodo: (id: number) => dispatch(TodoActionCreator.deleteTodo({id})),
  toggleDone: (id: number) => dispatch(TodoActionCreator.toggleDone({id})),
});
 
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
cs

 

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

const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) => ({
  deleteTodo: (id: number) => dispatch(TodoActionCreator.deleteTodo({id})),
  toggleDone: (id: number) => dispatch(TodoActionCreator.toggleDone({id})),
});

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

 

위의 코드에서 'connect' 함수는 redux 상태와 액션을 컴포넌트에 연결하기 위해 사용되었습니다.

connect 함수는 React 컴포넌트를 Redux 스토어에 연결해주는 역할을 합니다. 이를 통해 컴포넌트가 redux 상태를 읽고, 액션을 디스패치 할 수 있게 됩니다. 

 

connect 함수는 mapStateToProps와 mapDispatchToProps라는 두 개의 매개변수를 받습니다. 

 

📌 mapStateToProps 

 

이 함수는 redux 상태를 컴포넌트의 props로 매핑하는 역할을 합니다. 

state 인자를 받아서 컴포넌트에서 사용할 상태를 반환하며, RootStatesType의 state.todos.todoList 값을 todoList props로 매핑하고 있습니다. 

 

📌 mapDispatchToProps 

 

이 함수는 액션 생성자 함수를 컴포넌트의 props로 매핑하는 역할을 합니다. 

dispatch 인자를 받아서 컴포넌트에서 사용할 액션 생성자 함수를 반환하며, TodoActionCreator의 deleteTodo와 toggleDone 액션 생성자 함수를 deleteTodo와 toggleDone props로 매핑하고 있습니다. 

 

 

connect 함수는 이렇게 매핑된 상태와 액션을 가진 새로운 컴포넌트를 반환합니다. 이 컴포넌트는 Redux 스토어와 연결되어 상태를 구독하고 액션을 디스패치 할 수 있습니다. 

 

따라서 export default connect(mapStateToProps, mapDispatchToProps)(TodoList);는 TodoList 컴포넌트를 Redux 스토어에 연결한 새로운 컴포넌트를 생성하고, 그것을 내보내는 역할을 합니다. 이렇게 생성된 redux 상태와 액션을 사용할 수 있게 됩니다.

 

 

[ react-redux 제공하는 ]

위에서는 리덕스의 상태, 액션 생성자를 표현 컴포넌트로 주입시키기 위해 connect() 고차 함수를 사용했습니다. 하지만 고차 함수가 익숙하지 않으면 불편하고 직관적이지 않은 코드라고 생각할 수 있습니다. 만약 함수 컴포넌트 기반으로 애플리케이션을 개발한다면 좀 더 직관적인 react-redux가 제공하는 훅들을 이용할 수 있습니다.

 

react-redux가 제공하는 훅은 다음과 같습니다. 

 

📌 useStore()

리덕스 스토어 객체를 리턴합니다. 스토어의 상태를 읽어내려면 이 객체의 getState() 함수를 이용합니다.

 

📌 useDispatch() 

스토어의 dispatch() 함수를 리턴합니다. 리턴받은 함수를 이용해 액션을 스토어로 전달할 수 있습니다. 

 

📌 useSelector() 

리덕스 스토어의 특정 상태를 선택하여 리턴합니다

 

 

이 훅들을 TodoList 컴포넌트에 적용해보겠습니다. 

 

📌 TodoList.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import { Link } from "react-router-dom";
import TodoItem from "./TodoItem";
import TodoActionCreator from "../redux/TodoActionCreator";
import { useSelector, useDispatch } from "react-redux";
import { RootStatesType } from "../redux/AppStore";
import { TodoItemType } from "../AppContainer";
 
type PropsType = {
  todoList: Array<TodoItemType>;
  deleteTodo: (id:number) => void;
  toggleDone: (id: number) => void;
};
 
 
const TodoList = ({ todoList, deleteTodo, toggleDone }: PropsType ) => {
  const todoItems = todoList.map((item) => {
    return <TodoItem key={item.id} todoItem={item} deleteTodo={deleteTodo} toggleDone={toggleDone} />;
  });
 
  return (
    <>
      <div className="row">
        <div className="col p-3">
          <Link className="btn btn-primary" to="/todos/add">
            할 일 추가
          </Link>
        </div>
      </div>
      <div className="row">
        <div className="col">
          <ul className="list-group">{todoItems}</ul>
        </div>
      </div>
    </>
  );
};
 
// 훅을 이용한 컨테이너 컴포넌트 생성
const TodoListContainer = () => {
  const todoList = useSelector((state: RootStatesType) => state.todos.todoList);
  const dispatch = useDispatch();
 
  const deleteTodo = (id:number) => {
    dispatch(TodoActionCreator.deleteTodo({ id }));
  };
 
  const toggleDone = (id:number) => {
    dispatch(TodoActionCreator.toggleDone({ id }));
  };
  
  return <TodoList todoList={todoList} deleteTodo={deleteTodo} toggleDone={toggleDone} />;
};
 
export default TodoListContainer;
 
cs

 

  const dispatch = useDispatch();
  
  const deleteTodo = (id:number) => {
    dispatch(TodoActionCreator.deleteTodo({ id }));
  };

  const toggleDone = (id:number) => {
    dispatch(TodoActionCreator.toggleDone({ id }));
  };

 

useDispatch() 훅을 이용해 dispatch 함수를 리턴 받아 표현 컴포넌트의 속성에 전달해 주는 기능을 수행할 함수를 생성합니다.

 

 

const todoList = useSelector((state: RootStatesType) => state.todos.todoList);

 

useSelector() 훅을 이용해 전체 상태 중 todoList를 속성으로 전달하도록 작성합니다. useSelector()에 전달하는 함수의 인자가 state이고, 상태 중 일부 객체를 리턴하도록 하여 필요한 상태만을 추출합니다. 

 

 

connect() 고차 함수 대신에 훅이 좀 더 직관적이고 쉬워 보이지 않나요?

 

둘 중에 자신에게 편리하고 쉽게 접근할 수 있는 방법을 사용하면 됩니다. 하지만 클래스 컴포넌트 기반으로 애플리케이션을 작성하고 있다면 선택의 여지가 없이 connect() 고차 함수를 써야 합니다.

 

Comments