웹개발

리액트 훅과 리덕스를 사용한 리액트 상태 관리

별을 보고 걷는 사람 2020. 7. 20. 23:39
 

React State Management using React hooks and Redux

React components has a built-in state object. The state is encapsulated data where you store assets that is persistent between component renderings. The state is just a fancy term for a JavaScript data structure.

www.loginradius.com

 

리액트 컴포넌트들은 상태(state) 객체(object)를 내장하고 있다. '상태'는 컴포넌트가 구현될 때마다 계속 존재하는 자산들을 저장하는 캡슐화된 데이터이다. 상태란 그냥 자바스크립트 데이터 구조를 가리키는 고급 용어일뿐이다.

 

Versha Gupta, 2020년 7월 17일

 

 

프론트엔드 개발자들에게 리액트 애플리케이션에서 가장 큰 도전은 상태의 관리이다. 리액트 하나로 그 복잡함을 다루기는 충분치 않기 때문에 어떤 개발자들은 리액트 훅이나 리덕스와 같은 여타 상태 관리 라이브러리들을 사용하는 것이다. 이 게시글에서 우리는 상태 관리를 위해 리액트 훅과 리덕스 둘 다를 자세히 살펴 볼 것이다.

 

상태란 무엇인가?

리액트 컴포넌트들은 내장된 상태 객체를 가지고 있다. 상태는 컴포넌트 렌더링 마다 지속되는 재산들을 저장하는 캡슐화된 데이터이다. 상태란 그냥 자바스크립트 데이터 구조를 가리키는 고급 용어일뿐이다. 사용자가 당신의 애플리케이션과 상호작용하면서 상태를 변화시키면, 그 이후 UI는 완전히 달라 보일 수 있다. 왜냐하면 기존의 상태가 아닌 새로운 상태가 구현된 것이기 때문이다.

 

효율적 사용을 위해 하나의 상태 변수는 한 가지 일만 책임지도록 하라.

 

리액트 애플리케이션들은 컴포넌트들을 사용하여 만들어지고 컴포넌트들은 자신의 상태를 내부적으로 관리한다. 이것이 컴포넌트가 몇 개 안 될 때는 잘 돌아가지만 애플리케이션이 커지면서 컴포넌트들 간 공유하는 상태들 관리의 복잡성 때문에 일이 어려워진다.

 

여기 한 전자상거래 애플리케이션의 간단한 예시가 있다. 여기서 상품을 하나 구매하면 다수의 컴포넌트들의 상태는 변화할 것이다.

 

  • 상품을 구매 목록에 더한다
  • 고객 구매 내역에 상품을 더한다.
  • 구매된 상품의 수량 조절을 작동시킨다.

만약 개발자들이 확장성을 염두해두지 않았다면 뭔가 잘못되었을 때 무슨 일이 벌어지고 있는지 찾기가 정말 어려울 것이다. 그렇기 때문에 당신의 애플리케이션에서 상태 관리가 필요한 것이다.

 

리덕스란 무엇인가

리덕스는 이 특정한 문제를 해결하기 위해 만들어졌다. 리덕스는 애플리케이션의 모든 상태들을 묶어두는 중앙 저장소를 제공한다. 각 컴포넌트는 한 컴포넌트에서 다른 컴포넌트로 상태를 보내지 않고, 각자가 저장된 상태에 접근할 수 있다. 여기 리덕스가 어떻게 작동하는지 간단한 그림이 있다.

 

상태는 UI를 정의, UI는 동작을 촉발, 동작은 리듀서에 보내짐, 리듀서는 창고를 업데이트, 창고는 상태를 보관

세 가지 건설 부품들이있다: 동작, 창고, 그리고 리듀서. 각각 무슨 일을 하는지 짧게 논의해 보자.

 

리덕스에서의 동작들

동작이란 애플리케이션에서 창고로 데이터를 보내는 정보들의 적재량(payload)이다. 동작들은 store.dispatch()를 사용하여 보내진다. 동작들은 동작 생성기를 통해 생성된다. 새로운 할 일 아이템을 표현하는 예시 동작이 여기 있다:

 

{ 
  type: "ADD_TODO", 
  payload: {text:"Hello Foo"}
}

 

동작 생성기의 예시는 여기에 있다: 

const addTodo = (text) => {
  return {
     type: "ADD_TODO",
     text
  };
}

 

리덕스에서의 리듀서들

리듀서는 창고에 보내진 동작들에 상응하여 어떻게 애플리케이션의 상태를 변화시킬지를 구체화한다. 리덕스에서 리듀서들이 어떻게 작동하는지의 예시는 다음과 같다:

 const TODOReducer= (state = {}, action) => {
  switch (action.type) {
    case "ADD_TODO":
      return {
        ...state,
        ...action.payload
      };
    default:
      return state;
  }
};

 

리덕스에서의 창고

창고는 애플리케이션의 상태를 보관한다. 도우미 메소드들을 통해 저장된 상태에 접근하거나 상태를 업데이트 하거나 청자(listener)를 등록 또는 탈퇴시킬 수 있다.

 

우리의 할 일 앱을 위한 창고를 만들자:

const store = createStore(TODOReducer);

다시 말해 리덕스는 코드 관리와 디버깅 수퍼파워를 준다. 이것은 더욱 유지 보수가 가능한 코드를 만들고, 또 뭔가 잘못 되었을 때 근본 원인을 추적하여 찾아내는 것을 훨씬 쉽게 한다.

 

 

리액트 훅이란 무엇인가?

이것들은 당신을 함수 컴포넌트들로부터 리액트 상태 및 기능들로 끌어들이는 함수들이다. 훅들은 클래스들 안에서는 작동하지 않고 클래스를 쓰지 않아도 리액트의 기능들을 사용할 수 있게 해준다. 훅은 이전 기종과 호환 가능한데 그 말은 이것이 아무런 브레이킹 체인지*를 보유하지 않는다는 것이다. 리액트는 useState, UseEffect 및 useReducer 등과 같은 내장된 훅을 제공한다. 맞춤형 훅을 직접 만들어도 된다.

 

훅의 규칙들

  • 최고위 층에서 훅을 부른다는 것은 룹, 내포된 함수 또는 조건들 안에서 불러야 한다는 뜻일 뿐이다.
  • 리액트 함수 컴포턴트들은 훅이라고만 불려진다.

다음의 몇 가지 리액트 훅들의 예시를 보길 바란다:

 

 

useState와 그 사용법은 무엇인가

useState는 함수 컴포넌트들에 리액트의 상태를 더하게 해주는 훅이다. 예시: 상태 변수를 클래스 안에서 선언하고 this.state 를 {count:0} 으로 설정함으로써 카운트 상태를 0으로 초기화하기.

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

함수 컴포넌트에서는 이것이 없기 때문에 this.state를 배정하거나 읽을 수 없다. 그 대신 우리는 컴포넌트 안에서 useState 훅을 직접 부른다.

function Example() {
    const [count, setCount] = useState(0);
}

count 라 불리는 상태 변수를 선언하고 0으로 설정한다. 리액트는 재구현에도 그 현재의 값을 기억하고 우리의 함수에 가장 최근의 값을 제공할 것이다. 현재 카운트를 업데이트 하고 싶으면 setCount를 부르면 된다.

 

 

useReducer와 그 사용법은 무엇인가

useReducer는 애플리케이션의 상태를 관리하기 위해 내가 때때로 사용하는 훅이다. 이것은 useState 훅과 매우 유사한데, 단지 더 복잡하다. useReducer 훅은 리덕스의 리듀서와 같은 개념을 사용한다. 이것은 기본적으로 부작용 없는 순수 함수이다.

 

useReducer의 예시가 여기 있다:

 

useReducer는 컴포넌트 내부에 상태 컨테이너와 병합 설립된 독립적인 컴포넌트를 생성한다. 반면 리덕스는 애플리케이션 전체의 어딘가 위에서 머무르는 전반적인 상태를 생성한다.

          +----------------+              +----------------+
          |  Component A   |              |                |
          |                |              |                |
          |                |              |      Redux     |
          +----------------+              |                |
          | connect Redux  |<-------------|                |
          +--------+-------+              +--------+-------+
                   |                               |
         +---------+-----------+                   |
         |                     |                   |
         |                     |                   |
+--------+-------+    +--------+-------+           |
|  Component B   |    |  Component C   |           |
|                |    |                |           |
|                |    |                |           |
+----------------+    +----------------+           |
|    useReducer  |    | connect Redux  |<----------+
+----------------+    +--------+-------+
                               |
                      +--------+-------+
                      |  Component D   |
                      |                |
                      |                |
                      +----------------+

아래에 useReducer 리액트 훅을 사용하지 않고 완성된 할 일 아이템 예시가 있다.

 

아이템 목록을 위한 상태 변환을 관리하는 리듀서 함수인 다음 함수를 보라:

const todoReducer = (state, action) => {
      switch (action.type) {
        case "ADD_TODO":
          return state.map(todo => {
            if (todo.id === action.id) {
              return { ...todo, complete: true };
            } else {
              return todo;
            }
          });
        case "REMOVE_TODO":
          return state.map(todo => {
            if (todo.id === action.id) {
              return { ...todo, complete: false };
            } else {
              return todo;
            }
          });
        default:
          return state;
      }
    };

두 가지 상태들에 상응하는 두 동작 타입들이 있다. 이것들은 complete 불리언 필드를 왔다갔다 하고 입력되는 동작을 구분해내기 위한 추가적인 데이터 적재를 위해 사용된다.

 

이 리듀서에서 관리되는 상태는 아이템들의 배열이다:

const initialTodos = [
      {
        id: "t1",
        task: "Add Task 1",
        complete: false
      },
      {
        id: "t2",
        task: "Add Task 2",
        complete: false
      }
    ];

코드에서, useReducer 훅은 복잡한 상태와 복잡한 변환에 사용된다. 리듀서 함수와 초기 상태를 입력으로 받아들여 현재 상태와 디스패치 함수를 출력으로 반환한다.

 const [todos, dispatch] = React.useReducer(
    todoReducer,
    initialTodos
  );

완성된 파일:

import React from "react";
const initialTodos = [
  {
    id: "t1",
    task: "Add Task 1",
    complete: false
  },
  {
    id: "t2",
    task: "Add Task 2",
    complete: false
  }
];
const todoReducer = (state, action) => {
  switch (action.type) {
    case "ADD_TODO":
      return state.map(todo => {
        if (todo.id === action.id) {
          return { ...todo, complete: true };
        } else {
          return todo;
        }
      });
    case "REMOVE_TODO":
      return state.map(todo => {
        if (todo.id === action.id) {
          return { ...todo, complete: false };
        } else {
          return todo;
        }
      });
    default:
      return state;
  }
};
const App = () => {
  const [todos, dispatch] = React.useReducer(todoReducer, initialTodos);
  const handleChange = todo => {
    dispatch({
      type: todo.complete ? "REMOVE_TODO" : "ADD_TODO",
      id: todo.id
    });
  };
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <label>
            <input
              type="checkbox"
              checked={todo.complete}
              onChange={() => handleChange(todo)}
            />
            {todo.task}
          </label>
        </li>
      ))}
    </ul>
  );
};
export default App;

리덕스로 비슷한 예제를 해보자.

 

App.js 의 창고.

import React from "react";
import { Provider } from "react-redux";
import { createStore } from "redux";
import rootReducer from "./reducers";
import Todo from "./Components/TODO";
const store = createStore(rootReducer);
function App() {
  return (
    <div className="App">
      <Provider store={store}>
        <Todo />
      </Provider>
    </div>
  );
}
export default App;

actions/index.js 의 동작들.

export const addTodo = id => ({
  type: "ADD_TODO",
  id
});
export const removeTodo = id => ({
  type: "REMOVE_TODO",
  id
});

reducers/index.js 의 리듀서들

const initialTodos = [
  {
    id: "t1",
    task: "Add Task 1",
    complete: false
  },
  {
    id: "t2",
    task: "Add Task 2",
    complete: false
  }
];
const todos = (state = initialTodos, action) => {
  switch (action.type) {
    case "ADD_TODO":
      return state.map(todo => {
        if (todo.id === action.id) {
          return { ...todo, complete: true };
        } else {
          return todo;
        }
      });
    case "REMOVE_TODO":
      return state.map(todo => {
        if (todo.id === action.id) {
          return { ...todo, complete: false };
        } else {
          return todo;
        }
      });
    default:
      return state;
  }
};
export default todos;

components/Todo.js 파일

import React from "react";
import { connect } from "react-redux";
import { addTodo, removeTodo } from "../../redux/actions/authActions";
const Todo = ({ todos, addTodo, removeTodo }) => {
  const handleChange = todo => {
    if (todo.complete) {
      removeTodo(todo.id);
    } else {
      addTodo(todo.id);
    }
  };
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <label>
            <input
              type="checkbox"
              checked={todo.complete}
              onChange={() => handleChange(todo)}
            />
            {todo.task}
          </label>
        </li>
      ))}
    </ul>
  );
};
const mapStateToProps = state => ({ todos: state.auth.todos });
const mapDispatchToProps = dispatch => {
  return {
    addTodo: id => dispatch(addTodo(id)),
    removeTodo: id => dispatch(removeTodo(id))
  };
};
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Todo);

 

리액트는 connect() 의 대체용으로 사용될 수 있는 리액트 훅들을 제공한다. useState, UseReducer, 그리고 useContext와 같은 내장된 훅들을 사용할 수 있고 이 것들이 있으니 리덕스를 필요로 하지 않을 때가 많을 것이다. 그러나 규모가 큰 애플리케이션들에는 리덕스와 리액트 훅들 둘 다 사용할 수 있다. 아주 잘 된다! 리액트 훅은 유용한 새 기능이고 리액트-리덕스에 더해 리덕스 전용 훅들은 리덕스 개발을 단순화하기 위한 위대한 발걸음이다.

 

역주: breaking change: 브레이킹 체인지 - 소프트웨어 시스템에서 한 부분의 변화가 다른 부분을 작동 못하게 하는 것. 주로 다수의 애플리케이션에서 사용되는 공유 코드 라이브러리들에서 발생한다.