[React] 상태 관리와 Redux & Recoil

planted-ji·2023년 6월 8일
0
post-thumbnail

🔖개요

react를 사용하면 항상 상태 관리가 중요하다는 말이 따라 붙는다. 팀 프로젝트에 본격적으로 돌입하기 전에 상태 관리와 Redux, Recoil의 개념을 다잡고자 한다.

참고 자료
React 상태관리 라이브러리 비교분석-!(Redux, Recoil 편)

🏷️상태 관리?

  • React 애플리케이션에서 상태는 컴포넌트의 데이터, 즉 변수를 나타낸다. 이 데이터는 애플리케이션의 상태 변화에 따라 업데이트된다.

  • React는 컴포넌트 기반 라이브러리로서 컴포넌트 상태가 변경될 때마다 해당 컴포넌트를 다시 렌더링하여 UI를 업데이트한다. 이러한 리렌더링은 성능 문제를 야기할 수 있으며, 컴포넌트 간에 데이터를 공유하거나 상태의 일관성을 유지하는 것에 대한 복잡성을 증가시킬 수 있다.

props drilling

props란?

  • props는 "properties"의 약어로, 부모 컴포넌트로부터 자식 컴포넌트로 전달되는 데이터를 의미한다. 부모 컴포넌트는 자식 컴포넌트에게 props를 통해 정보를 전달하고, 자식 컴포넌트는 해당 정보를 사용하여 동적으로 렌더링한다.

  • props는 부모 컴포넌트에서 자식 컴포넌트로 단방향으로 전달되는 데이터이므로 자식 컴포넌트에서는 props를 읽기만 할 수 있고, 직접 수정할 수는 없다.

  • props는 자식 컴포넌트에서 함수형 컴포넌트의 매개변수로 전달되거나, 클래스 컴포넌트에서는 this.props를 통해 접근할 수 있습니다. 자식 컴포넌트에서 props의 값을 사용하여 화면에 데이터를 표시하거나, 이벤트 처리 등 다양한 작업을 수행할 수 있다.

단방향 데이터에서 발생하는 문제

  • 컴포넌트는 props 형태로 상태를 공유한다. 이때 자식 컴포넌트 간에는 상태 공유가 불가능하고 부모 컴포넌트를 통해서만 상태를 공유할 수 있다. 문제는 컴포넌트 계층이 많아지면 하나의 props를 전달하는데 거쳐야하는 컴포넌트가 많아지고, 중간 단계의 컴포넌트는 사용하지 않는 props를 들고 있어야 하는 문제가 생긴다.
  • 이러한 문제를 props drilling이라고 부르며, 프로젝트가 커질 수록 state 관리가 어려워진다. 이를 해결하기 위해 전역적인 상태 관리가 필요해지는 것이다.

🏷️상태 관리 라이브러리

상태 관리 라이브러리의 순위를 보면 부동의 1위에는 언제나 Redux의 이름이 올라가 있다. 그러나 라이브러리를 사용하는 프로젝트의 사양은 모두 달라 인지도만으로 결정할 수 없고, Redux와 같은 기능을 하면서도 문법이 간단해 보이는 Recoil에 흥미가 생겨 기본 내용을 정리하며 둘의 차이점을 알아가보기로 했다.

Redux

기본 구조

// 1. 액션 타입 정의
// 액션 타입은 액션을 식별하는 데 사용되는 문자열 값입니다.
const ADD_TODO = 'ADD_TODO';

// 2. 액션 생성자 함수 작성
// 액션 객체를 생성하는 함수입니다. 
// 이 함수는 액션 객체를 반환하며, Redux에서 액션을 보내는 주체가 됩니다.
function addTodoAction(todo) {
  return {
    type: ADD_TODO,
    // 데이터를 담을 필요가 있는 경우, 
    // 'todo'처럼 payload에 추가 정보를 포함할 수 있습니다.
    payload: todo 
  };
}

// 3. 리듀서 함수 정의
// 리듀서는 현재 상태와 액션을 받아 새로운 상태를 반환하는 함수입니다.
// 리듀서 함수를 정의해 액션에 따른 상태 변화 처리를 합니다.
function todoReducer(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      // 상태에 대한 변화를 수행하고, 새로운 상태를 반환합니다.
      return [...state, action.payload];
    default:
      // 기본적으로 현재 상태를 반환합니다.
      return state;
  }
}

// 4. 스토어 생성
// 스토어는 Redux 애플리케이션의 상태를 보관하는 객체입니다.
// 스토어는 리듀서와 함께 생성됩니다.
// 애플리케이션의 상태를 유지하고 상태 변화를 처리하며, 
// 액션을 디스패치(dispatch)합니다.
import { createStore } from 'redux';
const store = createStore(todoReducer);

// 액션을 디스패치하여 상태 변경
store.dispatch(addTodoAction('Buy groceries'));

// getState 함수를 사용해 현재 상태에 접근
const currentState = store.getState();
console.log(currentState); // ['Buy groceries']

장점

  • 예측 가능한 상태 관리: Redux는 단일 스토어와 순수한 리듀서 함수를 사용하여 상태 변화를 예측 가능하게 만든다. 이로 인해 디버깅과 테스트가 쉬워진다.

  • 커뮤니티 및 생태계: Redux는 많은 사용자와 활발한 커뮤니티를 가지고 있다. Redux를 지원하는 다양한 라이브러리와 도구가 있어 개발을 보다 쉽게 할 수 있다.

  • 시간 여행 디버깅: Redux는 시간 여행 디버깅을 지원하여 과거 상태로 돌아가거나 특정 시간에 애플리케이션의 상태를 확인할 수 있다.

단점

  • 상태 업데이트 작업의 복잡성: Redux는 상태 업데이트를 위해 액션, 액션 생성자 및 리듀서를 작성해야 한다. 이로 인해 코드량이 증가하고 초기 설정에 시간이 걸릴 수 있다.
  • Boilerplate 코드: Redux는 액션 타입, 액션 생성자 및 리듀서를 작성하기 위한 Boilerplate 코드가 필요하다. 작은 규모의 애플리케이션에서는 과도한 복잡성을 초래할 수 있다.

Recoil

기본 구조

// 1. Recoil 상태 정의
// atom 함수를 사용해 상태를 정의합니다.
// atoms는 Recoil에서 관리되는 단일 값으로, 
// 애플리케이션의 상태 조각을 나타내며 읽고 쓸 수 있습니다.
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';

const todoListState = atom({
  key: 'todoListState',
  default: []
});

// 2. selector 함수를 사용해 파생된 상태 정의
// 상태에 기반하여 계산된 값입니다. 
// 다른 상태 또는 파생된 상태에 의존할 수 있으며 읽을 수만 있습니다.
const todoListCountState = selector({
  key: 'todoListCountState',
  get: ({ get }) => {
    const todoList = get(todoListState);
    return todoList.length;
  }
});

// 3. 상태 사용
// Recoil에서 상태를 사용하기 위해서는 
// useRecoilState 또는 useRecoilValue 훅을 사용합니다.

// useRecoilState는 읽기 및 쓰기를 위해 사용되며,
// useRecoilValue는 읽기만을 위해 사용됩니다.

// useRecoilState 훅을 사용해 상태 및 상태 업데이트 함수에 접근하고,
// useRecoilValue 훅을 사용해 파생된 상태에 접근합니다.
function TodoList() {
  const [todoList, setTodoList] = useRecoilState(todoListState);
  const todoListCount = useRecoilValue(todoListCountState);

  // 4. 상태 업데이트
  // 컴포넌트에서 상태 업데이트 함수를 호출해 상태를 변경합니다.
  const addTodo = (todo) => {
    setTodoList([...todoList, todo]);
  };

  return (
    <div>
      <p>Todo List Count: {todoListCount}</p>
      <ul>
        {todoList.map((todo, index) => (
          <li key={index}>{todo}</li>
        ))}
      </ul>
      <button onClick={() => addTodo('Buy groceries')}>
        Add Todo</button>
    </div>
  );
}
+) RecoilRoot

Recoil 상태를 사용하기 위해 React 애플리케이션의 최상위 컴포넌트로 감싸는 역할을 한다. 애플리케이션의 다른 컴포넌트에서 Recoil 상태를 사용하려면 RecoilRoot로 감싸야 한다.

import { RecoilRoot } from 'recoil';

function App() {
  return (
    <RecoilRoot>
      {/* 애플리케이션의 나머지 컴포넌트 */}
    </RecoilRoot>
  );
}

장점

  • 간단하고 직관적인 API: Recoil은 React 훅을 사용하여 간단하고 직관적인 API를 제공한다. 상태와 파생된 상태를 선언하는 것이 간단하며, 컴포넌트 내에서 상태를 사용하기 쉽다.

  • 선언적인 상태 의존성: Recoil은 상태 의존성을 선언적으로 관리한다. 상태가 업데이트될 때 자동으로 관련된 컴포넌트가 다시 렌더링된다.

    • Recoil에서는 상태를 원자(atom)라고 부르는 단위로 나누고, 각각의 원자는 의존성 그래프(dependency graph)를 통해 상호작용한다. 원자는 의존하는 다른 원자를 선언적으로 정의할 수 있으며, 해당 원자가 의존하는 상태가 변경되면 자동으로 재렌더링되고 업데이트된다.
    • 이와 달리, Redux는 명시적인 의존성 추적을 제공하지 않는다. Redux에서는 상태를 중앙에서 관리하는 단일 스토어에 저장하고, 상태 갱신은 액션을 통해 이루어진다. Redux는 액션을 디스패치하여 상태를 변경하고, 이에 따라 리듀서 함수가 호출되어 상태를 업데이트한다. 따라서 Redux에서는 개발자가 상태 갱신과 재렌더링을 명시적으로 관리해야 한다.
  • 유연한 상태 구조: Recoil은 복잡한 상태 구조를 지원한다. 여러 개의 상태를 조합하거나 공유하는 것이 쉽고 유연하다.

단점

  • 상태 관리의 복잡성: Recoil은 Redux에 비해 초기 설정 및 상태 구조 관리가 상대적으로 단순하지만, 큰 규모의 애플리케이션에서 상태 관리가 복잡해질 수 있다.
  • 상태 저장소의 한계: Recoil은 단일 저장소 개념이 없다. 대신, 상태와 파생된 상태를 직접 사용한다. 따라서 상태 관리 및 상태 공유에 대한 명확한 패턴이 필요할 수 있다.
    • Redux는 애플리케이션 상태를 단일 스토어에 저장하고 관리한다. Recoil은 상태를 여러 개의 원자(atom)로 분리하여 각각을 독립적으로 관리한다.
    • Recoil에서 각 원자는 자체적으로 상태 값을 가지고 있으며, 해당 상태 값에 의존하는 컴포넌트들이 이를 구독(subscribe)한다. 따라서 Recoil은 상태를 분산하여 저장하고 관리하며, 각각의 원자는 자신만의 의존성을 가진다.

🏷️결론

  • Redux는 대규모, 복잡한 상태 관리 및 시간 여행 디버깅이 필요한 경우 유용하다.
  • Recoil은 간단하고 작은 규모의 상태 관리 및 React와의 통합이 필요한 경우 적합하다.

0개의 댓글