ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 페이스북에서 만든 React 상태 관리 라이브러리, Recoil.js
    Frontend 2020. 12. 13. 19:48

     

     

    리액트 상태 관리

    리액트로 개발을 할 때 리액트에서 기본으로 제공하는 상태 관리 메소드들을 사용하는 것이 가장 간단하고 편리하겠지만, 약간의 한계는 분명히 있다. 리액트 컴포넌트의 상태는 상위에서 하위로 단방향으로 흘러갈 수밖에 없는데, 이 과정에서 거대한 트리 구조가 만들어지고, 무수히 많은 re-render가 일어난다.

     

    자식 컴포넌트에서 부모 컴포넌트의 상태를 제어하기 위해서는 부모 컴포넌트가 본인이 가지고 있는 상태를 제어할 수 있는 메소드들을 자식 컴포넌트에 전달을 해주어야만 가능하다. 그러니까 자식 컴포넌트에서 상위에서 전달된 메소드들로 무언가 상태를 변경하면 부모 컴포넌트들부터 하위에 연결된 컴포넌트들 전부가 re-render 된다.

     

    리액트에 Context API라는 것이 생기면서 리액트에서도 상위의 상태값을 중간에 여러 컴포넌트들을 거치지 않고 다이렉트로 접근해서 조작할 수 있게 되었는데, 이 것 또한 약간의 한계는 있다. Context는 오로지 하나의 단일 value만 저장할 수 있고, 특정 상태 구독자만을 위해 저장하는 것도 어렵다.

     

    이러한 점들 때문에 리액트에서는 상태값을 가지는 주체인 트리의 중심 코드와 해당 상태들을 끌어다 쓰는 잎사귀 코드를 분리하는 것이 매우 어려웠다. 이러한 점들 때문에 리액트에서 조금 더 쉽게 상태를 관리할 수 있도록 여러 외부 라이브러리들이 등장하기 시작했는데, 그 중에서 가장 핫했던 라이브러리는 단연코 Redux일 것이다. 

     

     

    Redux의 등장

    Flux design pattern

     

    Redux는 약간 변형되긴 했지만 Flux 디자인 패턴을 바탕으로 만들어졌는데, 사용자의 액션이 일어나면 그 이벤트가 dispatch되고, Reducer에게 전달된다. 그리고 이 Reducer에서 immutable한 새로운 state 객체를 만들어서 store에 update하면 view에 반영이 되는 식이다. 

     

    Redux를 사용하면서 가장 편리했던 것은, 액션이 뷰에 반영되기 까지의 그 흐름이 너무나 일관되고, 명확해서 디버깅이 쉽다는 점이었다. 그리고 Redux와 같이 사용하면 편리한 여러 비동기 제어 미들웨어들이 많다. 회사에서도 지금 Redux와 함께 Redux Saga로 비즈니스 로직을 관리하고 있는데 일단 한 번 이 패턴에 익숙해지면 비즈니스 로직을 구현하는 것이 매우 쉬워진다. Generator 함수를 통해 비동기 로직을 마치 동기 로직과 같이 보이도록 코드를 짤 수 있다.

     

    그러나 불편한 점도 많은데, 예를 들면 Redux를 사용하면 이 라이브러리의 패턴에 맞게 무수히 많은 행사 코드가 필요한다는 점이다. 액션 객체를 생성하는 함수, 액션 객체를 받아서 각 상황에 맞는 새로운 상태 객체를 만들어주는 reducer, 스토어에 저장된 상태를 가져다 쓰기 위한 selector 등등, 정말 많은 코드들이 필요하다. 초기 보일러 플레이트를 위해 액션, 리듀서 등등을 잔뜩 생성해주고, Store를 만들어서 루트에 연결을 해주어야 하는데, 정말 번거롭기 그지없다. 이런 문제를 해결하기 위해 조금이라도 편하게 생성할 수 있도록 도와주는 redux-toolkit이라는 라이브러리가 나왔지만 그럼에도 불구하고 Redux 라이브러리를 처음에 세팅하는 것이 정말 불편하다는 것은 써본 사람들이라면 다들 공감할 것이다.

     

     

    Recoil.js의 탄생

    그래서 페이스북에서 새로운 라이브러리를 만들었는데, 바로 Recoil.js!

    Recoil 공식 홈페이지에서 이 라이브러리가 어떤 목적으로 만들어졌는지, 컨셉은 무엇인지 등을 읽어봤는데 간단하게 요약해보면 이렇다.

     

    Recoil은 보일러 플레이트 필요 없이 간단하게 상태 관리를 도와주는 라이브러리이다.

     

    Recoil은 atom(공유할 state)에서부터 selector(순수 함수)를 통해 리액트 컴포넌트까지 아래로 흐르는 데이터 플로우 그래프를 만들 수 있도록 해준다. 여기서 Atom은 컴포넌트들이 구독할 수 있는 상태의 단위를 말하고, Selector는 그 상태를 동기적으로나 비동기적으로 변경할 수 있는 순수 함수들을 말한다.

     

    나는 Recoil을 써보려고 심플한 To do App을 만들어봤는데, 써보면서 느낀 점은 Recoil 라이브러리는 정말 쉽게 시작할 수 있다는 점이다. 그리고 hook 형태로 되어 있어서 이해가 쉽다.

     

     

    예전에 Redux 라이브러리는 초기에 보일러 코드를 써야하는 것도 너무 많고, 그 흐름을 이해하는 데 오랜 시간이 걸렸는데, Recoil은 Redux에 비해서 정말 단순하고 쉽다. 근본적으로 Atom이라는 것과 Selector, 이 2가지만 이해해도 일단 단순한 것들은 금방 만들 수 있다.

     

     

    Atom

    Atom은 상태의 단위를 말한다. Atom 하나가 상태 하나이다. 물론 새로운 값으로 업데이트가 가능하고 구독도 할 수 있다. 만약에 atom이 업데이트되면, 이 atom을 구독하고 있는 모든 컴포넌트들이 새로운 값과 함께 re-render가 된다. Atom은 런타임 환경에서도 생성될 수 있다. Atom을 리액트의 로컬 컴포넌트 상태값을 대체해서 사용할 수도 있다. 만약에 같은 atom을 여러 컴포넌트에서 사용한다면, 해당 컴포넌트들은 상태를 공유하게 된다. 

     

    const numberState = atom({
      key: 'numberState',
      default: 0,
    });

     

    Atom은 atom 함수를 통해 생성하는데, 위와 같이 Key와 default value를 지정하면 된다. 물론 이 key는 unique해야 한다. 이 key는 상태를 지속하는데도 사용되고, 디버깅에서도 사용되고 그리고 다른 고급 API들을 사용할 때도 사용된다. 

     

    function Example() {
      const [number, setNumber] = useRecoilState(numberState);
      return (
        <button onClick={() => setNumber((prevNum) => prevNum + 1)}>
          { number }
        </button>
      );
    }

     

    하나의 상태를 업데이트하는 여러 hook이 존재하는데, 그 중에서 가장 기본이 되는 hook은 useRecoilState다. 그냥 리액트에서 기본으로 제공하는 useState와 동일한데, 다른 점은 이 상태값이 다른 컴포넌트들에게 공유가 된다는 점이다. 

     

    function Example2() {
      const number = useRecoilValue(numberState);
      return <p>{ number }</p>;
    }

     

    Example 컴포넌트 내에서 사용된 이 number 값은 다른 컴포넌트 내에서도 얼마든지 가져다 쓸 수 있다.

     

     

    Selector

    Selector는 순수 함수로 atom이나 다른 selector를 받을 수 있다. 만약에 어떤 selector, A가 구독하는 atom이나 selector들이 업데이트가 된다면, 이 A selector 역시 다시 계산이 된다. 컴포넌트는 atom을 직접 구독할 수도 있지만 selector를 구독할 수도 있다. 그렇기 때문에 만약에 어떤 selector가 다시 계산되면, 그 selector를 구독하는 컴포넌트 역시 re-render된다.

     

    Selector는 state로 저장된 값을 베이스로 해서 값을 계산하는 순수 함수이다. 이러한 역할을 함으로써 수많은 불필요한 상태 값들이 생겨나는 것을 막고 진짜 꼭 필요한 상태만을 가질 수 있도록 한다. 예를 들자면 이렇다. 만약에 To do list를 만든다고 했을때, 유저가 입력하는 할 일 들이 배열의 형태로 저장되는 상태값은 꼭 필요한 값일 것이다.

     

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

     

    그런데 만약에 그 할 일 리스트의 총 개수와 유저가 완료한 일의 개수를 화면에 표시한다고 해보자. 이 때, todoList 배열의 총 길이와 완료된 일만 모은 List 배열의 길이를 각각 따로 state로 저장할 필요는 없을 것이다. 바로 이 때 selector를 유용하게 사용할 수 있다.

     

    const todoListStateSelector = selector({
      key: 'todoListStateSelector',
      get: ({get}) => {
        const todoList = get(todoListState);
    
        return {
          totalCount: todoList.length,
          completeCount: todoList.filter(item => item.is_complete).length,
        };
      },
    });
    

     

    즉, 이렇게 selector 함수를 사용해서, todoListState 값을 가져온 후, 해당 list 데이터를 기반으로 원하는 전체 개수와 완료된 일의 개수를 계산해서 컴포넌트에 전달하면 된다. 

     

    위 selector는 계산하는데 필요한 todoListState를 구독하고 있기 때문에, todoListState가 업데이트될 때마다 다시 계산된다. 그렇기 때문에 컴포넌트의 입장에서는 selector와 atom은 같은 기능을 하는 인터페이스라고 할 수 있다. 

     

    function TodoList() {
      const todoList = useRecoilValue(todoListStateSelector);
      return (
        <ul>
          { todoList.map(todo => <li key={ todo.id }>{ todo.text }</li>) }
        </ul>
      );
    }

     

    컴포넌트 내에서 Selector를 구독하려면 useRecoilValue와 같은 hook을 사용할 수 있다. useRecoilState는 사용할 수 없는데, 그 이유는 selector는 writable하지 않기 때문이다. 즉, 오로지 값을 읽을 수만 있지 값을 업데이트할 수는 없다.

     


     

    여기까지 Recoil의 아주 베이직한 컨셉을 살짝 봤는데, 간단하게 요약해보자면 이렇다.

     

    • Recoil에서 하나의 상태는 하나의 atom 단위로 관리된다. atom으로 만들어진 상태는 여러 컴포넌트들끼리 공유할 수 있다.
    • useRecoilState라는 hook은 리액트의 useState와 거의 똑같이 동작하고, atom 값을 구독할 수도 있고, 값을 업데이트할 수도 있다.
    • 값을 단순 구독만 하기 위해서는 useRecoilValue라는 hook을 사용한다.
    • Selector는 atom으로 저장된 상태 값을 내가 원하는 대로 가공해서 사용하기 위해 사용하는 순수 함수이다.
    • 컴포넌트에서 Selector를 atom과 똑같은 방식으로 구독할 수 있지만, selector로 값을 update할 수는 없다.

     

    사실, 이 2개만 알아도 엄청 간단한 앱은 만들 수 있다.

    다만 이제 비동기 로직을 처리하거나 복잡한 일들을 하려면 더 깊이 공부해야하는데 그건 기회되면 다음 포스팅에 정리해보려고 한다.

     

     

    Reference

     

    Recoil

    A state management library for React.

    recoiljs.org

     

    jy7123943/practice-recoil

    Contribute to jy7123943/practice-recoil development by creating an account on GitHub.

    github.com

     

    반응형

    COMMENT