Handling error with recoil and axios
Tags: react, axios, recoil, errors
Published: 2021-03-20 07:05:38


### Axios 

api 호출을 할 때마다 **try** and **catch** 를 사용해서 catch 구문에서 에러 핸들링을 하는 방법은 굉장히 번거로운 방법이다. axios의 `interceptors` 는 이름 그대로 then이나 catch로 응답이나 에러가 오는걸 가로챌 수가 있어서 에러 핸들링을 굉장히 편리하게 할 수가 있다. 


```javascript
  interceptors: {
    request: AxiosInterceptorManager<AxiosRequestConfig>;
    response: AxiosInterceptorManager<AxiosResponse>;
  };
```
axios의 `interceptors는` request 속성과 response 속성을 가지고 있는 객체이다. response의 타입을 보면 `AxiosResponse`를 제너릭 인자로 받는 AxiosInterceptorManager 객체이다.

```javascript
interface AxiosInterceptorManager<V> {
  use(onFulfilled?: (value: V) => V | Promise<V>, onRejected?: (error: any) => any): number;
  eject(id: number): void;
}
```

response의 `use` 메소드는 두 개의 함수를 인자로 받는다. 첫 번째 함수는 요청이 성공적이면 실행이 되고, 두번째 함수는 요청이 실패 했을 때 받는 함수로 에러가 나게 되면 이 함수가 실행된다. 

```javascript
 export const setupInterceptors = (): void => {
  axios.interceptors.response.use(
    (response) => response,
    (error) => {
      --에러처리를 하는구간--
    },
  );
};

```
`setupInterceptors`를 함수를 만들어서, 리엑트의 가상돔을 렌더링 해주는 index.tsx 파일에서 import를 해서 실행을 해준다.

이렇게 되면, 리엑트의 어느 api 호출하는 함수를 실행을 했는데 에러가 났다면, 컴포넌트에 있는 catch 구문에 에러가 전달되기 전에 미리 intercepts를 통해서 에러 핸들링을 할 수가 있다.

```javascript
import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot } from 'recoil';
import { setupInterceptors } from '@service/axios';

import App from './App';

setupInterceptors();

ReactDOM.render(
  <RecoilRoot>
    <App />
   </RecoilRoot>,
   document.getElementById('root'),
);

```


### Change recoil state outside of React


####1. RecoilExternalStatePortal 

axios로 에러 핸들링을 하기 위해서는, 리엑트 바깥에서 recoil 스테이트를 업데이트해 주어야 한다. 이 부분을 해결하기 아래 링크에서 힌트를 얻어서 해결하게 됐다.

https://github.com/facebookexperimental/Recoil/issues/289

recoil은 redux처럼 Provider의 store를 통해서 state를 업데이트 할 수 있는 방법이 없기 때문에,  RecoilExternalStatePortal(`jsx를 리턴하는 함수`)를 생성을 해서 <RecoilRoot> 안에 넣어준다. 이렇게 되면 RecoilExternalStatePortal 함수 안에서 recoil 훅들을 사용할 수 있다.

```javascript
ReactDOM.render(
  <RecoilRoot>
    <RecoilExternalStatePortal />
      <App />
   </RecoilRoot>,
   document.getElementById('root'),
);
```

####2. externalRecoilLoadable


```javascript
export let externalRecoilLoadable: <T>(recoilValue: RecoilValue<T>) => Loadable<T> = null;

export let externalRecoilSet: <T>(recoilVal: RecoilState<T>, valOrUpdater: ((currVal: T) => T) | T) => void = null;

export function RecoilExternalStatePortal() {
  useRecoilTransactionObserver_UNSTABLE(({ snapshot }) => {
    externalRecoilLoadable = snapshot.getLoadable;
  });

  useRecoilCallback(({ set }) => () => {
    externalRecoilSet = set;
  })();

  return <></>;

```

`useRecoilTransactionObserver_UNSTABLE` 훅의 콜백은 **snapshot**과 **previousSnapshot**을 속성으로 가진 객체를 인자로 받는다. 

```javascript
function useRecoilTransactionObserver_UNSTABLE(({
  snapshot: Snapshot,
  previousSnapshot: Snapshot,
}) => void)
```

`Snapshot은` 객체이며 getLoadable 이라는 메소드를 가지고 있다. getLoadable의 타입을 살펴보자.

```javascript
 getLoadable<T>(recoilValue: RecoilValue<T>): Loadable<T>;
```

`useRecoilTransactionObserver_UNSTABLE`의 콜백은 최초 실행이 될때 
externalRecoilLoadable 변수안에 스냅샷의 getLoadable 함수를 넣어준다. 이 콜백함수같은 리코일의 스테이트가 바뀔때마다 실행이 되기 때문에, 바뀐 스테이트에 대한 스냅샷이 계속 업데이트가 된다.

`getLoadable`은 Loadable이라는 객체를 리턴을 해준다. Loadabl 안에는 getValue라는 메소드가 있는데 이 메소드를 통해서 리코일 스테이트값에 접근을 할수가 있다.

useRecoilTransactionObserver_UNSTABLE의 콜백은 `RecoilExternalStatePortal` 실행이 될 때  최초 실행이 된다.
 
externalRecoilLoadable 변수안에 스냅샷의 getLoadable 함수를 넣어준다. 이 콜백함수는 리코일의 스테이트가 바뀔 때마다 실행이 되기 때문에, 바뀐 스테이트에 대한 스냅샷이 계속 업데이트가 된다.

####3. externalRecoilSet

externalRecoilLoadable로 스테이트에 대한 접근을 할 수는 있지만, 아직 Recoil의 스테이트를 업데이트 할 수는 없다. 따라서 스테이트를 업데이트 할 수 있는 함수가 필요하다.

```javascript
type CallbackInterface = {
  snapshot: Snapshot,
  gotoSnapshot: Snapshot => void,
  set: <T>(RecoilState<T>, (T => T) | T) => void,
  reset: <T>(RecoilState<T>) => void,
};

function useRecoilCallback<Args, ReturnValue>(
  callback: CallbackInterface => (...Args) => ReturnValue,
  deps?: $ReadOnlyArray<mixed>,
): (...Args) => ReturnValue
```


위의 있는 useRecoilCallbac은 유저가 넣은 콜백 함수를 실행하는 함수를 반환해준다. 유저의 콜백 함수를 감싸는 wrapper 함수가 CallbackInterface를 전달해줘서. 여기서 set 함수를 인자로 받는다. 

콜백 함수 안에서 인자로 받은 `set` 함수를 externalRecoilSet 변수에 넣어준다. 이 `set` 함수는 첫 번째 인자로 리코일 상태 값을 받고, 두번째 인자로 업데이트할 상태 값을 넣어 줄 수가 있다. 

`useRecoilCallbac` 에서 반환된 함수가 아직 실행되지 않았기 때문에 set 함수를 넣어주기 위해서는`useRecoilCallbac`을 실행해준다. 이렇게 되면 externalRecoilSet 변수에 set 함수를 넣어줄 수 있다. 

### 에러핸들링

```javascript
import { modalState } from '@state/modal';
import { externalRecoilLoadable, externalRecoilSet } from '../RecoilExternalStatePortal';

 export const setupInterceptors = (): void => {
  axios.interceptors.response.use(
    (response) => response,
    (error) => {
      const currentModalList = [...externalRecoilLoadable(modalState).getValue()];
      const newModalList = currentModalList.concat([
        {
          key: 'basicModal',
          props: { text: error.response.data.message }
        },
      ]);
      externalRecoilSet(modalState, newModalList);
    },
  );
};

```
  
- `RecoilExternalStatePortal` 에서 `externalRecoilLoadable` 함수와 `externalRecoilSet` 함수를 임포트 해보자.
- `externalRecoilLoadable`의 getValue를 통해서 recoil의 스테이트 값을 가져온다. 
- `externalRecoilSet`에 첫 번째 인자로 recoil 스테이트 갑을 넣어주고, 두 번째 인자로 업데이트할 valu값을 넣어서 업데이트 했다.

위의 예시 같은 경우는, 에러가 일어나면 `interceptors`에서 에러를 먼저 처리해서 에러로 받은 메시지 값을 보여주기 일괄적으로 보여주기 위해서 위와 같이 사용되었다.