[React] 상태 관리, 이제 Recoil 하세요 (FE CONF)

Recoil 선택 배경

상태관리 라이브러리에 대한 고민

너도 나도 쓰는 Redux

"귀찮고 번거로워요...

 

Recoil의 핵심 컨셉

  • 오직 React만을 위해 React처럼
  • React 내부 상태만 이용
  • 작은 Atom 단위로 관리
  • 순수함수 Selector
  • Re-Render 최소화
  • 데이터 흐름을 따라서
  • 곧 새로운 React 기능과 호환성

 

기본 활용법

예시로 상품 리스트 화면을 살펴보자.

데이터 흐름은 아래와 같다.

Modal 팝업은 간단한 기능이지만 실제 열고 닫는 이벤트가 각각 다른 컴포넌트에서 이루어져야 하기 때문에

상태관리를 해야 하는 대표적인 예이다.

List 역시 여러 곳에서 사용될 수 있기 때문에 상태 관리를 해보도록 하겠다.

해당 atom을 호출하는 방법은 React Hook와 유사하다.

Recoil에서 제공하는 state에  미리 정의해두었던 atom을 넣으면 바로 Hook처럼 사용이 가능하다.

 

동일한 방식으로 atom을 하나 만들고 이 atom을 바로 호출해서 사용할 수 있는데,

Recoil에서는 특이하게 읽기 전용 함수와 쓰기 전용 함수를 추가로 제공한다.

 

List에 추가하는 Register 컴포넌트, 또한 Product를 읽거나 수정을 위해 Product atom도 상태 관리를 해야한다.

data.idx를 받아서 해당 idx인 product 정보를 atom에 저장한다.

이 모달이 등록  팝업인지, 수정 팝업인지 분기 처리를 해주어야 한다.

따라서, 컴포넌트가 사라질 때 이 값을 초기화해주어야 한다.

Recoil은 전역 상태이기 때문에 특정 값으로 변경되지 않는 이상 이전의 값을 유지한다.

 

Recoil에서는 Reset 함수를 추가로 제공을 하고 있으니, useEffect를 통해서 컴포넌트가 unmount 되는 시점에 초기화를 해주어야 한다.

 

다음은 필터링 기능을 구현해보겠다.

Recoil에는 getter, setter를 직접 정해서 쓸 수 있는 selector라는 순수함수를 이용해보겠다.

이를 활용하면 상태를 읽고 쓸 때 데이터를 가공하거나 유효성 검사 같은 부수적인 로직을 추가할 수 있다.

 

그리고 또 다른 특징으로는 selector 내부에서 다른 atom이나 다른 selector를 참조할 수 있다.

이를 구독한다고 표현한다.

 

구독이 되면 다른 데이터들에 대한 의존성을 갖게 되는데, 구독하고 있는 상태가 외부에서 변경이 되면 본 slector는 이를 감지하고 재평가돼서 컴포넌트를 다시 렌더링한다.

 

selector는 getter를 필수로 갖고 있지만, setter는 선택적으로 정의할 수 있다.

앞서 말했던 읽기 전용, 쓰기 전용 함수가 제공되는 이유는 여기에서 찾을 수 있다.

만약 getter만 제공되는 selector 함수를 사용한다고 했을 때 읽기 쓰기 모두 가능한 Hook을 사용하면 오류가 발생하기 때문에, 이러한 오류를 미연에 방지하기 위해 읽기, 쓰기 전용 함수를 사용하는 것을 권고한다.

 

이제 필터링 부분을 보자.

필터 모달용 atom과 이 필터 값을 list에 반영하기 위해 이전에 만들었던 list는 selector로 변경을 해줄 것이다.

선택된 필터를 리스트에 적용해서 컴포넌트에 내려주는 로직을 구현해보자.

 

필터링된 리스트를 출력하는 코드를 살펴보자.

이 리스트는 기존의 filter atom을 적용하기 위해 구독한다.

이때 selector는 list와 filter의 영향을 받아서 필터가 변경되면 해당 selector가 재평가된다.

 

비동기 데이터 다루기

조금은 불편한 Recoil 비동기 데이터 쿼리

실제 서버 데이터를 가져와보자.

getProductList 라는 함수를 추가해주고, 아까 만들어둔 selector의 getter 에서 받아와 출력해주는 로직이다.

단순히 앞에 async만 붙이면 기존의 코드를 사용할 수 있다.

 

다음으로는 상태 데이터도 가져와보자.

초반 예제에서는 내부 상태에서 데이터를 가지고 오는 것이기 때문에 필요 없었던 선택된 Idx를 저장하는 Idx Atom을 추가하고, Product Selector 가 구독하게 한다.

그렇게 되면 idx값이 변경됨에 따라 서버에서 매번 다른 값을 가져올 수 있다.

 

상세 데이터 조회

product Idx를 selector에서 참조하게 되면, 이 idx가 변경될 때마다 재평가된다.

하지만 또 다른 방법도 제공하고 있다.

Recoil에서는 SeletorFamily라는 util을 제공한다.

기존에 구독하고 있던 idx를 매개변수로 받을 수 있다.

 

여기에서 두 가지 방법 모두 동일한 의존성을 갖고 있다.

 

이렇게 짠 코드를 실행해보면 다음과 같은 오류를 볼 수 있다.

비동기 Recoil State를 사용하게 되면 Suspense를 사용하라는 오류를 보게 된다.

 

Error Handling

 

한 번 사용한 API를 다시 호출한 경우

Recoil에서는 한 번 API를 호출하면 데이터를 캐싱한다.

이는 트래픽 비용을 절감할 수도 있지만, 데이터를 갱신할 필요도 있다.

 

동일한 데이터를 여러 API에서 사용하는 경우

데이터를 가져오는 시점과 실제 데이터를 사용하는 시점이 다를 때 이러한 문제가 발생한다.

 

비동기 데이터를 갱신 방법

비동기 데이터 갱신에 영향을 주는 요소

 

비동기 데이터를 가져오는 방식은 아래와 같다.

갱신을 위해 Req ID Atom을 하나 추가한다.

쉽게 설명하자면 API로 통신하는 횟수를 저장하는 atom이다.

 

추가로 이 Req ID를 갱신할 수 있는 함수를 하나 만들었다.

결과적으로 Refresh Function이 실행되면 Req ID 는 갱신되면서 Product Selector가 재평가되면서 새로운 데이터를 가져올 수 있다.

 

Recoil이 제안한 Request ID를 이용한 명시적 갱신

selector 내에서는 사용되지 않지만 Req ID를 구독하는 것을 볼 수 있다.

이 값을 변경할 refresh 함수를 하나 더 만들어주었다.

이 refresh 함수에서는 req ID를 순차적으로 증가시켜주는 로직이 포함되어 있다.

 

따라서,

refresh 함수를 실행➡ req ID 변경 ➡ 이것을 참조하는 Selector는 의존성으로 인해 새로운 값을 불러옴

 

Setter를 활용한 개선 버전

앞서 만들었던 refresh 함수를 만들지 않고 

기존에 selector에서 제공하는 setter 내에 req ID를 갱신시켜줄 수 있는 로직을 추가했다.

함수를 호출하는 것 대신에 이 selector의 setter를 실제 상태 변경이 없는 형태로 실행시켜보면 좀 전과 같은 결과를 받을 수 있다.

여기에 사용된 req ID는 별도의 상태로 관리되기 때문에 다른 여러 Selector에 의해 구독될 수 있고, 동시에 갱신시켜줄 수 있는  장점이 있다.

 

Atom 구독 공유 시 주의점

여기서 req ID 같이 공유 가능한 구독용 atom 을 사용할 경우에 주의점이다.

atom은 별다른 제약이 없어서 원치 않은 다른 Selector에 의해 영향을 받을 수 있다.

 

이러한 종류의 atom은 private로 정의하는 것이 좋다.

의존 관계를 갖는 상태끼리 한 파일로 묶어서 관리를 하거나 

또는 atomFamily를 이용해서 그룹화 할 수 있는 값을 매개변수로 받아서 그룹핑하는 방법도 있다.

 

다른 방법은 없나요...?

이 비동기 데이터를 selecor에 넣지 않고 atom으로 관리하는 방법도 있다.

앞서 강조했던 구독으로 인한 의존성으로 인해서 데이터 자체는 주입하지 않고 

별도의 selector를 이용해서 재평가되는 시점에 이 값을 업데이트 해주는 방식이다.

 

명시적 / 주기적 업데이트

useRefreshRightNowAsyncRecoilState()

아직 이런 기능은 없지만, 강제 refresh가 필요했던 순간들이 많다.

단순히 refresh를 구현하기 위해서 앞에 나온 방법들이 너무나 번거로웠다.

 

물론 외부 상태를 이용한 다른 여러 selector들의 의존성을 주입할 수있다는 장점이 있지만

이런 간단하게 사용할 수 있는 refresh 함수가 제공되면 좋을 것 같다.

 

앞서 설명했던 강제 refresh 같은 경우에는 사용할 수 있는 상황들이 제한적이다.

특정 이벤트로 인해서 어떤 이벤트가 언제 변경이 될지 예측이 되는 경우에만 사용할 수 있다.

(명시적 업데이트)

 

반면에 대부분의 데이터들이 언제 바뀔지 모르거나 매번 새로운 것을 요청해야 할 수 있다.

 

 Version Param을 이용한 주기적 갱신

앞에서 다른 refresh와는 조금 다른 관점에서의 방식이다.

조금 고전적이지만 확실하게 캐시를 컨트롤 할 수 있는 방식이다.

 

날짜와 family를 이용해서 서버로 요청할 때마다 매번 새로운 파라미터를 내려주면서 

매번 새로운 값을 받아올 수 있기 때문에 캐시를 사용하지 않는 것과 동일한 효과를 줄 수 있고 

시간 단위를 컨트롤할 수 있다.

  • 고전적이지만 확실한 Cache Control
  • 1분 / 30초 / 1시간  세부 설정 가능

구현된 화면을 보면 API 요청 시마다 같은 행을 클릭 했음에도 불구하고 새로운 값을 받는다.

여기서 주의할 점은, 버전 파라미터를 내부 state에 담아서 useEffect로 변경하게 되면 실제 데이터를 받아 놓은 시점과 버전이 바뀌는 시점에 차이가 발생해서 중복적으로 데이터를 가져오는 오류가 발생한다.

또한 이 버전 파라미터는 1초 미만으로 설정할 경우, 한 번 요청했을 때 상당히 많은 요청이 동시에 요청되는 걸 볼 수 있으니 주의해야 한다.

 

Recoil은 무조건 caching되는 이슈가 있는데, 캐싱되는 것을 컨트롤 할 수 있는 기능이 추가된다고 한다.

 

마무리

특징

Recoil은 atom을 이용해서 아주 작은 단위로 상태를 관리한다.

selector라는 순수 함수를 활용해서 데이터를 가공하거나 다른 상태를 구독할 수 있다.

 

아쉬운 점

React Query나 SWR 같은 비동기에 특화된 라이브러리들이 제공하는 기능들이 아직은 조금 부족하다.

 

Recoil이 강조하는 핵심

데이터 플로우 그래프를 잘 설계해서 의존성을 갖는 여러 상태들을 체계적으로 관리하라.

 

참고 영상 

https://www.youtube.com/watch?v=0-UaleJZOw8