Redux vs Recoil

김명주·2023년 7월 10일
0

Redux

Redux는 데이터의 흐름이 단방향으로 흐르는 Flux 아키텍처 구조이다.
Action, Selector, Store, Reducer로 이루어진 보일러 플레이트로 구성되어 있어서 유지보수와 디버깅에 강점이 있다.
하지만 하나의 앱에는 하나의 store만 존재해야 하는게 권장되기에, store가 추가될수록 보일러 플레이트가 많아진다는 단점이 있다.

Redux와 같은 Flux 구조의 라이브러리는 store에 모든 상태를 저장하는 중앙 집중 방식이다.
store는 외부 요소이기 때문에 리액트 내부 스케줄러에 접근할 수가 없고, Redux경우 비동기 데이터 처리를 하기 위해서 redux-thunk, redux-saga와 같은 별도의 미들웨어 라이브러리를 추가적으로 사용해야 한다는 단점이 있다.

구성 요소

  1. Action
    Action은 간단한 javascript 객체다.
    Redux에서는 상태를 업데이트하기 위해 Action이라는 객체를 사용한다. Action은 액션 타입(type)과 선택적인 데이터(payload)를 가지며, 앱에서 어떤 변화가 일어났는지 기술한다

  2. Dispatch
    dispatch는 store의 내장 함수중 하나이다. 디스패치는 액션을 발생 시키는 것 이라고 이해하면 된다.
    dispatch 라는 함수는 액션을 파라미터로 전달한다. ex) dispatch(actions) 등
    Dispatch 함수는 액션을 전달하고, 이에 대한 상태 업데이트를 Reducer에 요청한다.

  3. Store
    액션과 리듀서를 하나로 모으는 객체 저장소인 store는 애플리케이션의 전체 상태 트리를 보유한다. 내부 상태를 변경하는 유일한 방법은 해당 상태에 대한 action을 store에 전달하는 방법 뿐이다.
    앱 내부의 어느 곳에서든 접근할 수 있다. 프로젝트에 리덕스를 적용하기 위해 필요한 것으로 프로젝트에는 단 한 개의 Store만 가지며 상태의 중앙 저장소라고 할 수 있다.

  4. Reducer
    Redux에서 상태를 변경하는 로직은 Reducer 함수 안에 작성된다. 변화를 일으키는 함수로 Action의 결과로 state를 어떤 식으로 바꿀지 구체적으로 정의하는 부분이다.
    reducer는 인수로 조치를 취하고 store 내부의 상태를 업데이트한다.
    리듀서는 이전 state와 액션 객체를 받아서 새 state를 리턴한다.
    만약 여러개의 reducer가 필요하다면 combineReducers()를 통해 여러개의 reducer를 하나로 묶어 사용할 수 있다.

import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";

const rootReducer = combineReducers({
  counter,
  todos
})

export default rootReducer


const store = createStore(rootReducer);
store.dispatch({
  type: "ADD_TODO",
  text: ["Use Redux And combineReducers"],
});

리듀서는 순수함수이기에 파라미터 외의 값을 의존하면 안되고, 이전 상태는 건드리지 않은 상태로 새로운 상태 객체를 만들어 반환해야 한다. 그리고 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과값을 반환해야만 한다.

  1. Provider
    <Provider /> 는 Redux Store 저장소에 액세스해야 하는 모든 중첩 구성 요소에서 Store 저장소를 사용할 수 있게 해준다.
    React Redux 앱 내의 모든 React 구성 요소는 저장소에 연결할 수 있으므로 최상위 수준에서 렌더링한다.

// 이렇게 앱의 최상위 단계에서 작성해주어야 한다.
const render = () =>
  root.render(
    <React.StrictMode>
      <Provider store={store}>
        <App
          value={store.getState()}
          onIncrement={() => store.dispatch({ type: "INCREMENT" })}
          onDecrement={() => store.dispatch({ type: "DECREMENT" })}
        />
      </Provider>
    </React.StrictMode>
  );

이렇게 Provider로 감싸여진 컴포넌트에서 store에 접근하려면 useSelector와 useDispatch를 이용하면 된다.

useSelector hooks를 이용하여 store의 값을 가져올 수 있다.
useDispatch는 store에 있는 dispatch 함수에 접근할 수 있다.
즉 useSelector는 값을 가져오는 것, useDispatch는 action을 보내는 것이라고 볼 수 있다.

// /reducers/index.tsx
import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";

const rootReducer = combineReducers({
  counter,
  todos
})

export default rootReducer

export type RootState = ReturnType<typeof rootReducer>

// app.tsx

// useSelector를 이용해 store에 있는 특정 값에 접근할 수 있다.
const todos: string[] = useSelector((state:RootState) => state.todos)
const counter = useSelector((state:RootState) => state.counter)


// useDispatch를 이용해 store에 있는 원하는 dispatch에 접근할 수 있다.
const dispatch = useDispatch()
 const addTodo = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    dispatch({type:"ADD_TODO", text: todoValue})
    setTodoValue("")
  };

특징

  1. 데이터 흐름이 단방향으로 이루어진 Flux 구조를 따른다.
  2. 상태를 전역적으로 관리하기 때문에 어느 컴포넌트에 상태를 둬야할지 고민할 필요가 없게 한다.
  3. state를 읽기 전용으로 관리한다. -> 불변성을 유지하기 위해
  4. 액션을 하나 추가하는데, 작성 필요한 부분이 많고, 컴포넌트와 스토어를 연결하는 필수적인 부분들이 있어 코드량이 많아질 수 있다.

Recoil

Recoil은 Context API 기반으로 구현된 함수형 컴포넌트에서만 사용 가능한 라이브러리이다.

Hooks나 Context API와 같은 React에 내장된 상태 관리 기능을 사용하여 상태관리를 할 수 있지만, 아래와 같은 한계가 존재한다.

  • 컴포넌트 상태를 공통된 상위 컴포넌트까지 끌어올려 공유할 수 있지만, 이 과정에서 거대한 트리가 리렌더링 되기도 한다.
  • Context는 단일 값만 저장 가능하고, 자체 Consumer를 가지는 여러 값들의 집합을 담는 것은 불가하다.
  • 위 특성으로 인해 state가 존재하는 곳 부터 state가 사용되는 곳 까지 코드 분할이 어렵게된다.

Recoil은 React스러움을 유지하며 개선하는 방식의 라이브러리다.

Recoil은 atom과 selector로 이루어져 있다.

atom

atom은 상태(state)의 일부를 의미한다. Atoms는 어떤 컴포넌트에서든 읽고 쓸 수 있으며 atom의 값을 읽는 컴포넌트들은 암묵적으로 atom을 구독한다.
그래서 atom에 무슨 변화가 생기면 그 변화가 생긴 atom을 구독하는 컴포넌트들은 리렌더링이 일어난다.
Atom의 상태변화는 순수함수를 통해 일어나는데 이를 Selector라고 한다.
컴포넌트가 atom을 읽고 쓰게 하기 위해서는 useRecoilState()를 아래와 같이 사용해야 한다.

// app.js

import { atom } from "recoil";
import "./App.css";
import TextInput from "./components/TextInput";
import CharacterCount from "./components/CharacterCount";

// 이 atom이 업데이트되면 이 atom을 구독중인 컴포넌트들은 리렌더링이 일어난다.
export const textState = atom({
  key: "textState", 
  default: "",
});


function App() {
  return(
    <div>
      <TextInput />
      <CharacterCount />
    </div>
  )
}

export default App;

// TextInput.js
import React from "react";
import { useRecoilState } from "recoil";
import { textState } from "../App";

const TextInput = () => {
  const [text, setText] = useRecoilState(textState);
  const onChange = (event) => {
    setText(event.target.value)
  }
  return(
    <div>
        <input type="text" value={text} onChange={onChange}/>
        <br />
        Echo : {text}
    </div>
  )
};

export default TextInput;

selector

selector는 atom 혹은 다른 selector 상태를 입력받아 동적인 데이터를 반환하는 순수함수다. selector가 참조하던 다른 상태가 변경되면 이도 같이 업데이트되며, 이때 selector를 바라보던 컴포넌트들이 리렌더링 된다.
useRecoilValue() hooks를 이용하여 값을 읽어낼 수 있다.
Selector는 비동기 처리 뿐만 아니라 데이터 캐싱 기능도 제공하기 때문에 비동기 데이터를 다루기에 용이하다.

// index.js
import { atom, selector } from "recoil";
import "./App.css";
import TextInput from "./components/TextInput";
import CharacterCount from "./components/CharacterCount";

export const textState = atom({
  key: "textState",
  default: "",
});

export const charCountState = selector({
  key:'charCountState',

  // selector 순수함수인 get
  // 사용한 값을 반환하며 매개변수인 콜백 객체 내 get() 메소드로
  // 다른 atom 혹은 selector를 참조
  get : ({get}) => {
    const test = get(textState)

    return test.length;
  }
})

function App() {
  return(
    <div>
      <TextInput />
      <CharacterCount />
    </div>
  )
}

export default App;

// CharacterCount.js
import React from 'react'
import { useRecoilValue } from 'recoil'
import { charCountState } from '../App'

const CharacterCount = () => {
    const count = useRecoilValue(charCountState)
  return (
    <div>
        Character Count : {count}
    </div>
  )
}

export default CharacterCount

구조

  • atom :
    Atoms는 Recoil에서 상태의 단위를 의미하고, 업데이트와 구독이 가능하다.
    atom이 업데이트되면 각각의 구독된 컴포넌트는 새로운 값을 반영해서 리렌더링된다.
    Atoms는 리액트의 로컬 state 대신 사용할 수 있다. 동일한 atom이 여러 컴포넌트에서 사용되는 경우 모든 컴포넌트는 상태를 공유한다. Atoms에는 고유한 키가 필요하고 이 키는 전역적으로 고유해야 한다. 그리고 react state처럼 디폴트 값도 가진다. 컴포넌트에서 atom을 읽고 쓸 때는 useRecoilState라는 훅을 사용해야한다. 이건 리액트의 useState와 비슷하나, 상태가 컴포넌트간에 공유될 수 있다는 점에서 차이가 있다.

  • selector :
    Selector는 atoms나 다른 selectors를 입력으로 받는 순수 함수(pure function) 이다.
    상위 atoms이나 selectors가 업데이트 될 경우 하위 selectors도 재 실행된다. 컴포넌트는 atom 뿐만 아니라 selectors를 구독할 수 있고, 구독하고 있는 selectors가 변경되면 구독한 컴포넌트도 리렌더링된다. Selectors는 상태를 기반으로 데이터를 계산하고 최소한의 상태 집합만 atoms에 저장하고, 파생 데이터는 selector에서 계산하면서 불필요한 상태를 만들어내지 않는다. 컴포넌트 관점에서 atoms와 selectors는 동일한 인터페이스이므로 대체 가능하다. 여기서 get속성은 계산될 함수를 의미하고 전달되는 get인자를 통해 atoms와 다른 selectors에 접근 가능하다. 여기서 접근하면 자동으로 종속 관계가 생성되어 참조했던 atoms나 selectors가 업데이트되면 이 함수도 재실행된다. Selectors는 useRecoilValue()를 통해 조회 가능하다.

특징

  1. 비동기 처리를 기반으로 작성되어 동시성 모드를 제공하기 때문에, Redux와 같이 다른 비동기 처리 라이브러리에 의존할 필요가 없다.
  2. 흐름이 여러 개가 존재하는 경우에 리액트에서 렌더링의 동작 우선순위를 정하여 적절한 때에 렌더링해준다.
  3. atom -> selector를 거쳐 컴포넌트로 전달되는 하나의 data-flow를 가지고 있어, 복잡하지 않은 상태 구조를 가지고 있다. (단방향 데이터 흐름 구조)
  4. store와 같은 외부 요인이 아닌 React 내부의 상태를 활용하고 context API를 통해 구현되어있기 때문에 더 리액트에 가까운 라이브러리라고 할 수 있다.

Redux와 Recoil의 차이점

사실 두 라이브러리는 서로 비슷하다.
Redux와 Recoil 모두 단방향 데이터 흐름 구조이고, Redux에서 action을 통해 데이터 변화를 감지하듯이, recoil에선 atom을 통해 데이터 변화를 감지한다.
하지만 redux는 비동기 처리를 하기 위해서 redux-saga나 redux-thunk같은 미들웨어를 추가로 이용해야 한다. 이와 반대로 recoil은 비동기 처리를 위한 별도의 라이브러리가 필요하지 않다는 장점이 있다.
또한 redux는 개념이 간결하고 직관적이지만 초기 학습이 어렵고, recoil은 개념이 간결하고 초기 학습이 상대적으로 용이하다.

profile
개발자를 향해 달리는 사람

0개의 댓글