All Articles

실 서비스에 Recoil 활용하기

💡 이 글은 예전팀에서 서비스를 개발하는 시점에 Recoil을 활용했던 내용을 정리한 글입니다. 원래는 21년 초에 작성하려던 글이 계속 밀리면서 이제서야 글을 올립니다.

새로운 서비스를 개발하면서 상태관리에 Redux가 아닌 Recoil을 적용하였다. Recoil을 사용한 이유는 낮은 러닝커브로 React를 처음 접하는 사람들에게 유리하고, 여러가지 모듈(redux-xxxx 등)을 설치할 필요없이 단 하나로 사용이 가능하다는 점이다. 비동기도 지원한다.

🚨 다만, Recoil의 경우 아직 실험적인 부분이 많기에 사용하기에 있어서 선행적인 Test 및 적용 범위 검토가 필요하다고 생각한다.

Recoil !! Atom? Selector?

일단 Recoil을 사용하기 위해서는 react의 component가 함수형(Hook)임을 전제로 한다.

그리고 atomselector의 개념에 대해서도 알아야하는데, atom은 redux의 상태를 나타내는 것과 같은 개념으로 생각하면 쉽다, selector는 순수 function으로 atom 또는 다른 selector의 값을 받아서 사용이 가능하고, 해당 값이 update 될 경우 해당 selector의 function을 수행한다. selector에서 atom의 값을 가져오거나 수정하는 작업도 가능하다.

Recoil 기본 설정

설치방법은 심플하게..

$ yarn add recoil

설치를 했으면, Recoil이 작동할 수 있도록 기본 셋팅을 해주자. redux처럼 복잡한것 하나없이 RecoilRoot만 추가해주면 Recoil을 사용하기 위한 기본 설정이 완료 된다.

// src/App.tsx
import React from "react";
import { RecoilRoot } from "recoil";

const App: React.FC = () => {
  return (
    <RecoilRoot>
      <div className="App"></div>
    </RecoilRoot>
  );
};

Atom 사용

atom은 상태를 나타내는 기본 단위로 아래와 같은 구조를 가진다.

atom:

// src/recoil/atom.ts
import { atom } from 'recoil';

export const stateAtom = atom({
  key: "@common/stateAtom"
  default: ''
})

// 타입을 명시적으로 선언하는 경우 형식
export const numArrayAtom = atom<number[]>({
  key: "@component/numArrayAtom",
  default: [10, 20],
});

위와 같은 형태로 atom을 선언한다. 각 상태의 값을 관리하기 위해서 유일성을 가지는 key를 선언하고, default는 이름 그대로 처음 atom 상태값이 가질 기본 값을 선언한다. default는 str, num, array 등 타입 선언이 가능하다. Atom의 자세한 사용을 위해서는 Atom 구조를 참조하길 바란다

선언한 atom을 사용하기 위해서는 useRecoilState 를 이용한다. 방식은 useState를 사용하는 것과 유사하다.

// src/App.tsx
// 일반적인 형태
import { useRecoilState } from 'recoil';
import { stateAtom } from 'src/atom'

const App: React.FC = () => {
  const [state, setState] = useRecoilState(stateAtom);
  ...
}

위와 같은 일반적인 형태 외에도 recoil은 useSetRecoilState, useRecoilValue 와 같은 다양한 hook을 지원한다.

import { useSetRecoilState, useRecoilValue } from 'recoil';
import { stateAtom } from 'src/atom'

/**
상태값을 사용할 필요는 없고 Update만 하는 경우
*/
const AComponent = () => {
  const setState = useSetRecoilState(stateAtom);
  ...
}

/**
상태값을 이용하기만 하는 경우
*/
const BComponent = () => {
  const state = useRecoilValue(stateAtom);
  ...
}

서비스를 개발하다 보면 다양한 경우가 발생한다. 필요치 않는 변수, 함수를 코드상에 두는 경우에는 Warn이 발생하고, 불필요한 자원의 사용이므로 필요에 맞는 Hook을 이용하면 된다. 위에서 사용한 hook 말고도 다양한 hook을 지원한다.

atom-Family:

상태관리를 할때 각 화면별 기능/서비스 등에서 동일한 상태값을 다르게 사용해야 하는 경우가 있다. 유사한 상태정보를 자원별로 관리해야 하는 경우 Atom에서는 아래와 같이 선언했다.

// src/recoil/atom.ts
import { atom } from 'recoil';

// BAD
export const atomOfA = atom({
  key: "@common/atomOfA"
  default: ''
})

export const atomOfB = atom({
  key: "@common/atomOfB"
  default: ''
})

export const atomOfC = atom({
  key: "@common/atomOfC"
  default: ''
})

이름만 바꿔서 복사/붙여넣기다.. 다행이도 recoil에서는 AtomFamily를 이용 해서 불필요함을 해결한다. 매우 깔끔해진다.

// src/recoil/atom.ts
import { atomFamily } from 'recoil';

// Good
export const resAtom = atomFamily({
  key: "@common/resAtom"
  default: ''
})

// src/App.tsx
import { useRecoilState } from 'recoil';
import { resAtom } from 'src/recoil/atom'

const App: React.FC = () => {
  const [state, setState] = useRecoilState(resAtom('{{resource}}'));

  ...
}

구분하려는 자원명칭을 위와 같이 {{resource}}로 선언하여 atom을 multiple 하게 만들 수 있다.

Selector 사용

selector는 순수함수 또는 특정한 상태를 전달한다. atom이 데이터의 상태를 가진다면, selector는 함수를 통해서 가공된 결과/상태를 얻을 수 있다.

selector는 get/set 내부 함수를 지원하는데 이 내부 함수를 이용해서 Read/Update가 가능하다. 또한 추가 모듈없이도 비동기를 지원한다.(selector도 atom과 마찬가지로 selectorFamily가 존재한다.)

selector:

// src/recoil/selector.ts
import { selector } from 'recoil';
import { stateAtom } from './atom';

export const stateSelector = selector({
  key: '@common/stateSelector',
  get: ({ get }) => {
    const state: string = get(stateAtom);
    return `Selector ${state}`
  },
});

// src/App.tsx
import { useRecoilValue } from 'recoil';
import { stateSelector } from 'src/recoil/selector'

const App: React.FC = () => {
  const state = useRecoilValue(stateSelector);

  ...
}

위의 코드를 보면 get 메소드 안에서 param으로 get 함수를 받는것을 볼 수 있는데, 이것을 이용해서 다른 atom의 상태 값을 가져올 수 있다. read 형태로 selector에서 값을 가져온다면 useRecoilValue 만을 사용해도 된다.

selector: set()

// src/recoil/selector.ts
import { selector } from 'recoil';
import { stateAtom } from './atom';

export const stateSelector = selector({
  key: '@common/stateSelector',
  get: ({ get }) => {
    // stateAtom의 값을 가져온다.
    const state: string = get(stateAtom);
    return `Selector ${state}`
  },
  set: ({ set }, params) => {
    const state: string = params.state;
    // stateAtom에 param으로 전달받은 state 값을 등록한다.
    set(stateAtom, state);
  },
});

// src/App.tsx
import { useRecoilState } from 'recoil';
import { stateSelector } from 'src/recoil/selector'

const App: React.FC = () => {
  const [state, setState] = useRecoilState(stateSelector);

  ...
}

selector에서 set을 사용할 경우 useRecoilState를 이용해서 처리 가능하다. setState를 호출 할때 stateSelector내부의 set함수를 호출한다.

selector: Async/Await

기본 방식과 크게 다르지 않다. 그저 async/await를 선언함으로써 비동기 처리가 가능하다.

// src/recoil/selector.ts
import { selector } from "recoil";

export const itemSelector = selector({
  key: "@common/itemSelector",
  get: async ({ get }) => {
    const res: object = await fetch("url");
    const data: any = res.json(); // any는 자제합시다..
    return data;
  },
});

위와 같이 비동기로 처리할 경우 수행과정 동안의 딜레이 처리방식은 React.Suspense를 이용하는 방식과 useRecoilValueLoadable()를 이용하는 방식이 있다. Loadable을 이용해서 아래와 같이 사용이 가능하다. (React.Suspense 방식은 recoil이 아니어도 다른곳에서 사용하는 방식과 유사하다.)

// src/App.tsx
import React from 'react';
import { useRecoilValueLoadable } from 'recoil';
import { itemSelector } from 'src/selector'

const App: React.FC = () => {
  const itemLoader = useRecoilValueLoadable(itemSelector);
  let state: string = itemLoader.state;

  const renderData = () => {
    switch (state) {
      case 'hasValue':
        return (
          <div className="itemList">
            {itemLoader.contents.map((item) => {
              return <div className="item">{item}</div>})}
          </div>
        )
      case 'loading':
        return <div>...is Loading</div>;
      case 'hasError':
        return <div>Error</div>;
      }
    }
    ...
}

async로 구현된 selector를 useRecoilValueLoadable()를 이용하여 호출하면 해당 객체는 state와 contents 두개의 상태를 가진다. state는 hasValue, loading, hasError 3가지의 상태를 가지는데 contents의 값은 state가hasValue` 일때만 가진다.

selectorFamily:

selector도 atom과 같이 family가 존재한다. 코드상의 resource는 selectorFamily 선언시 넣어주는 resource 값이다.

// src/recoil/selector.ts
import { selectorFamily } from "recoil";
import { selectedItemAtomF } from "src/atom";

export const itemSelectorF = selectorFamily({
  key: "@common/itemSelectorF",
  get: resource => async ({ get }) => {
    const item = get(selectedItemAtomF(resource));
    return { resource, item };
  },
});

ETC: atom.effect

시험적인 기능 중 atom에 effect라는 method가 있다. useEffect와 같이 상태가 초기화/변경 되었을때 선언한 function을 순차적으로 수행하는데 다른점은 상태가 atom 본인이라는 점이다. 해당 기능은 아직 unstable하기에 사용에 있어서 주의를 요한다. 다만, 기능적인 부분에 있어서는 활용도가 높다고 생각하여 같이 설명한다.

// src/recoil/atom.ts
import { atom } from 'recoil';

export const stateAtom = atom({
  key: "@common/stateAtom"
  default: '',
  effects_UNSTABLE: [
    ({ setSelf, onSet }) => {
      onSet((newValue, oldValue) => {
        // 수행할 코드 입력
        ...
        // atom 자신을 수정할 경우
        setSelf(...)
      }
    },
    ({ setSelf, onSet }) => {
      onSet((newValue, oldValue) => {
        // 수행할 코드 입력
        ...
        // atom 자신을 수정할 경우
        setSelf(...)
      }
    }
  ]
})

effect의 경우 사용을 위해서는 effects_UNSTABLE 이라고 선언하여야 한다. 공식 문서를 보면 해당 기능을 이용해서 atom의 log, history 관리 등 다양한 역할을 할 수 있다. 선언하는 function의 경우도 async/await를 이용한 비동기를 지원한다. 더 자세한 내용 및 사용법은 공식문서를 참조 바란다.

글을 쓰는 사이에 버전업이 많이 되었다… 새롭게 추가된 기능들에 대해서는 많은 분들이 글을 읽어주시면 추가로 정리하겠습니다.