본문 바로가기

Web Frontend Developer

[Recoil] 리액트 상태관리 라이브러리 리코일

리액트의 상태관리 라이브러리인 리코일(recoil.js)에 대해서 공식 문서를 정리한 내용을 포스팅 해 보려고 한다. 리코일은 기존의 리덕스와 MobX 같은 상태관리 도구들에 비해 가볍고 유연하게 사용할 수 있도록 페이스북에서 오픈소스로 공개한 라이브러리이다. 기존에 전역 상태를 관리할 수 있는 훌륭한 여러가지 방법들이 있는데 왜 페이스북은 새로운 라이브러리를 만들게 된 것일까?

기존의 리액트 상태관리 라이브러리는 Store 라는 곳에 상태를 저장한다. 여기서 Store는 외부 요인이기 때문에 리액트의 내부 스케줄러에 접근할 수가 없게 된다. 그리고 리액트에서도 동시성 모드(concurrent mode)가 등장하면서 리액트와 동시성 모드를 사용할 수 있는 방법을 고민하게 되었다. 덧붙여서, 리덕스와 같은 라이브러리는 Store를 구성하기 위해 많은 양의 코드를 작성해야 하고 비동기 데이터 처리를 하기 위해서는 Redux-saga와 같은 별도의 라이브러리를 추가로 사용해 주어야 한다. 

그렇다면 리액트 자체적으로 가지고 있는 Context API는 어떤가? 반복되고 복잡한 업데이트를 하게 될 때 비효율적이다. 정적인 데이터 위주로 처리하거나 적은 업데이트가 일어나는 어플리케이션에는 적합하지만 '결국 Context는 Flux와 같은 상태 관리 시스템을 대체할 수 없다'라고 페이스북 엔지니어 Sebastian Markbage는 이야기 한다. Context의 경우 Provider-Consumer 구조로 상태를 주고 받는데 Provider 하위의 모든 Consumer는 Provider 속성이 변경 될 때 마다 다시 렌더링이 된다. 여기에서 해당 Context를 구독하고 있는 모든 요소들이 렌더링이 되기 때문에 상당히 비효율적이다.

 

리코일은 방향이 있는 그래프가 직교한 형태로 우리의 리액트 트리 구조에 붙어있게 만들어 준다. 이 그래프의 루트에서부터 상태 변화가 일어나는데 이 루트를 우리는 아톰(atom)이라고 한다. 그리고 이 과정은 순수함수를 통해서 일어나며 이를 선택자(selector)라고 한다. 이러한 접근을 통해 리코일은 다음과 같은 특징을 가질 수 있다.

  • 보일러플레이트가 없는 API 이며 리액트 지역 상태로서 단순한 get/set 인터페이스로 상태를 공유할 수 있다.
  • 동시성 모드와 양립할 수 있는 가능성이 있으며 새로운 리액트의 특성이 가능하게 한다.
  • 상테 정의는 증가하면서 분산되어서 코드 스플리팅이 가능하다.
  • 상태는 컴포넌트의 변경 없이 파생된 상태로 대체가 될 수 있다.

핵심적인 개념을 한 번 살펴보도록 하자.

Recoil lets you create a data-flow graph that flows from atoms (shared state) through selectors (pure functions) and down into your React components. Atoms are units of state that components can subscribe to. Selectors transform this state either synchronously or asynchronously. - Recoil official docs

리코일은 데이터 흐름 그래프를 만들게 해주는데, 그 그래프는 아톰(공유된 상태)에서 선택자(순수 함수)를 거쳐서 리액트 컴포넌트로 내려간다. 아톰은 컴포넌트가 구독할 수 있는 상태의 단위이다. 선택자는 상태를 동기적 또는 비동기적으로 바꿔준다. -리코일 공식 문서

 

아톰 (Atoms)

아톰은 상태의 단위이다. 업데이트가 가능하고 구독이 가능하다. 아톰이 업데이트 되면 각각을 구독하고 있는 컴포넌트들은 새로운 값으로 리렌더링이 일어난다. 런타임 환경에서 생성될 수가 있다. 아톰은 리액트의 지역 컴포넌트 상태에서 사용된다. 만약 같은 아톰이 여러 컴포넌트에 사용되면, 그 모든 컴포넌트들은 그 상태를 공유한다.

아톰은 유니크 키 값을 필요로 하는데, 이 키는 디버깅, 지속, 모든 아톰의 지도를 보기 위한 고급 API를 위해서 사용된다. 같은 키 값을 두 개의 아톰이 같이 들고 있으면 에러가 발생하므로 유의해야 한다. 컴포넌트에서 아톰을 읽고 쓰기 위해서는 useRecoilState라는 훅을 사용해야 한다. 리액트의 useState와 비슷하지만, 차이가 있다면 이제 상태는 컴포넌트 간에 공유가 가능하다.

 

선택자 (Selectors)

선택자는 순수 함수이고 아톰이나 다른 선택자를 입력값으로 받는다. 이러한 상위 아톰이나 선택자가 업데이트 되면 선택자 함수는 다시 계산 될 것이. 컴포넌트들은 아톰처럼 선택자를 구독할 수 있으며, 선택자가 변화하면 리렌더링이 일어난다. 선택자는 상태에 기반하여 파생된 데이터를 계산할 때 사용된다. 최소한의 상태가 아톰에 저장되어 있고 효율적으로 함수에 의해 계산되기 때문에 취약한 상태를 가지는 것을 피할 수 있다. 선택자가 계속하여 어떤 컴포넌트가 필요로 하고 어떠한 상태에 그들이 의존하는지를 추적하기 때문에 이러한 함수적 접근은 매우 효율적이다. 선택자는 selector 함수를 사용하여 정의한다.

get 프로퍼티는 계산이 되어야 하는 함수이다. 아톰과 다른 선택자에 접근할 수 있는데 get의 매개변수(argument)로 넘겨서 할 수 있다. 다른 아톰이나 선택자에 접근할 때 마다 의존성 관계가 만들어져서 다른 아톰을 업데이트하거나 선택자가 다시 계산이 되기도 한다. 선택자는 useRecoilValue()를 가지고 읽혀질 수 있다. 이 메서드는 아톰과 선택자를 매개변수로 받아서 상응하는 값을 반환한다.

 

그러면 지금부터는 리코일의 비동기 데이터 쿼리에 대해서 알아보자.

비동기 데이터 쿼리

리코일은 데이터 흐름 그래프를 통해 상태(혹은 파생된 상태)와 리액트 컴포넌트를 연결하여 준다. 리코일의 강력한 점은 이 그래프에서 함수가 비동기적이라는 점이다. 이는 동기적인 리액트 렌더링 함수에서 비동기적인 함수를 잘 사용할 수 있게 도와준다. 리코일은 보이지 않게 동기적인 함수들과 비동기적인 함수들을 선택자 데이터 흐름 그래프에서 섞을 수 있게 허용한다. 간단하게 Promise 형태의 리턴값을 선택자로부터 가지고, 인터페이스는 동일하게 유지된다. 선택자들은 서로서로 의존하면서 데이터를 변형한다.

선택자는 비동기적인 데이터를 리코일 데이터 흐름 그래프로 통합하는데 사용될 수 있다. 선택자는 멱등성(idempotent, 연산을 여러번 적용하여도 달라지지 않는 성질)을 가진 함수임을 반드시 명심해야 한다. 어떤 입력값의 집합이 주어졌을 때 항상 같은 결과를 만들어야 한다. 선택자 평가에서 이는 캐시되고, 재시작되고, 또는 실행되는 과정이 여러번 반복된다는 측면에서 중요하다. 이 때문에 선택자는 일반적으로 일기 전용 DB 쿼리 모델에 적합하다. 변하는 데이터를 위해서는 query refresh를 사용하여 변하는 상태, 지속되는 상태 또는 다른 부작용에 대한 동기화를 진행한다.

아래는 동기적인(synchronous) 아톰과 선택자에 대한 예제 코드이다.

 

만약 유저 이름이 어떤 DB에 저장되어 있고 쿼리가 되어야 한다면, Promise를 반환하거나 async 함수를 사용해야 한다. 만약 어떠한 의존성이 변하면, 선택자는 다시 평가되고 새로운 쿼리를 실행한다. 결과는 캐시되며, 쿼리는 하나의 고유한 입력값에 대해 오직 한 번만 실행될 것이다. 아래는 비동기적인(asynchronous) 아톰과 선택자에 대한 예제 코드이다. 

리액트 렌더링 함수가 동기적이기 때문에 프로미스가 리졸브 되게 전에 무엇을 렌더링 할 것인지 고민이 있을 수 있다. 리코일은 대기중인 데이터를 처리하기 위해 React Suspense와 함게 동작하도록 디자인 되었다. 컴포넌트를 다음과 같이 감싸면 대기중인 경우 fallback UI를 렌더링 해 줄 것이다.

 

에러 핸들링 (Error Handling)

만약 요청이 에러가 있다면? 리코일 선택자는 그 값을 사용하려 할 때 에러를 반환한다. 그리고 이는 <ErrorBoundary>로 감싼다.

 

데이터 흐름 그래프(Data-Flow Graph) 

기억해야 할 것은 선택자로 쿼리를 모델링함으로써, 우리는 혼성 상태, 파생된 상태 그리고 쿼리에 대한 데이터 흐름 그래프를 만들 수 있다. 이 그래프는 상태가 업데이트 될 때 마다 자동적으로 리액트 컴포넌트를 업데이트 하고 리렌더링 할 것이다. 아래 예제는 현재 유저 이름과 그들의 친구들 리스트를 렌더링할 것이다. 친구 이름을 클릭하면 그들이 현재 유저가 되고 자동으로 리스트는 업데이트가 될 것이다. 

 

동시적인 요청 (Concurrent Request)

위의 예제에서 friendsInfoQuery는 각각의 친구에 대한 정보를 얻기 위한 쿼리이다. 하지만, 반복문에서 이렇게 하면 그들은 본질적으로 직렬화가 이루어진다. 빠르게 보이면 문제가 없지만, 만약 무거운 것들이면 병렬적인 처리를 위해 waitForAll 과 같은 동시성 헬퍼 함수를 사용할 수 있다. 이 헬퍼는 배열과 의존성 이름 객체를 둘 다 수용한다.

 

이번 포스팅은 여기에서 마무리를 하고 다음 포스팅에서 추가적인 비동기 데이터 쿼리 관련 부분과 아톰 효과 등에 대해 더 알아보려고 한다.

 

참고자료