Recoil 사용 방법

Jin·2022년 3월 18일
0

React

목록 보기
13/18
post-custom-banner

리코일은 페이스북에서 만든, 리액트를 위한 상태 관리 라이브러리입니다.

리덕스처럼 보일러 플레이트가 많지 않고, 정말 필요한 코드만을 여러 store로 구성할 수 있어서 개발 생산성과 효율성을 기대하며 프로젝트에 도입하였습니다.

RecoilRoot

RecoilRoot는 리덕스로 따지면 Provider와 같습니다.

<RecoilRoot>
  <App />
</RecoilRoot>

둘의 가장 큰 차이는 리덕스의 Provider는 프로젝트에 오직 1개만 선언할 수 있지만 RecoilRoot는 갯수에 제한이 없습니다.
그래서, 연동되는 컴포넌트끼리 그들만의 store를 만들어서 그 안에서 상태 변경시 모든 store가 변경되지 않아도 되므로 더 효율적으로 상태를 감지하고 변경할 수 있습니다.

atom

리코일에서 상태는 atom으로 정의합니다.

export const tabState = atom<keyof IState['sector']>({
  key: 'tabState',
  default: 'opinion',
})

key에는 고유한 값이 들어가야 합니다.
default에는 초기값이 들어갑니다.

atom의 기본적인 정의 방법은 이게 전부입니다.
정말 간단합니다.
이렇게 정의하고 사용할 때는 useState를 사용하듯이

const [tab, setTab] = useRecoilState(tabState)

이런 식으로 사용하면 tab에는 상태가, setTab은 setter가 됩니다.

만약, 비동기로 불러온 데이터를 초기값으로 넣고 싶으시다면

const stateWithAsyncDefault = selector({
  key: 'AsyncDefault',
  get: async () => {
    const data = await get()

    const newSector = data.sector.reduce(
      (prev: IState['sector'], sec: ISector, index: number) => ({
        ...prev,
        [TYPE[index]]: sec,
      }),
      {}
    )

    const newContent: IState['content'] = {
      opinion: [],
      youtube: [],
      insight: [],
    }

    data.content.forEach((content: IContent) => {
      newContent[TYPE[content.sector_id - 1]].push(content)
    })

    return {
      sector: newSector,
      content: newContent,
    }
  },
})

export const appState = atom<IState>({
  key: 'appState',
  default: stateWithAsyncDefault,
})

위의 selector의 get 로직은 비동기적으로 데이터를 불러와서 그것을 인터페이스에 맞게 정제하여 반환해주는 부분입니다.

이런 식으로 selector로 정의하여서 그것을 atom의 default 값으로 넣어주면 비동기 데이터가 초기값으로 들어가게 됩니다.

selector

selector는 atom의 일부분을 가져오거나 추가적인 연산을 거쳐서 반환하여야 할 때 주로 사용됩니다.
또한, 위와 같이 비동기적으로 데이터를 불러올 수도 있습니다.
selector에는 set과 get 프로퍼티가 존재하고 get만 사용할 때에는 read only 메서드인 useRecoilValue만을 사용할 수 있습니다.

set 프로퍼티 정의시에는 useRecoilState를 사용할 수 있습니다.

비동기로 불러올 때 주의점은 아무래도 시간이 소요되는 작업이기 때문에 로딩 처리를 해주어야 합니다.

<RecoilRoot>
  <Suspense fallback={<LoadingIndicator />}>
    <App />
  </Suspense>
</RecoilRoot>

React 18이 도입되면서 정식으로 Suspense 기능을 지원하므로 로딩 처리를 간단하게 Suspense를 활용하여 처리할 수 있게 되었습니다.

이제 비동기가 아닌, 보편적으로 selector를 사용하는 방법을 알아보겠습니다.

export const contentSelector = selector<IContent[]>({
  key: 'contentSelector',
  get: ({ get }) => get(appState).content[get(tabState)],
  set: ({ set, get }, newContent) => {
    set(appState, (prev) => ({
      ...prev,
      content: {
        ...prev.content,
        [get(tabState)]: newContent,
      },
    }))
  },
})

이렇게 key, get, set 프로퍼티를 선언하여서 get에는 상태의 일부분을 반환하도록 합니다.
위에서 appState라는 atom을 정의하였는데 이 selector는 그 appState의 일부분을 가져오고 있습니다.

set 프로퍼티가 중요합니다.
selector는 결국 atom으로 정의된 상태의 일부분을 가져오는 것입니다.

따라서 selector의 setter 또한 결국 atom에 정의된 상태의 일부분을 변경하여야 변경이 유지됩니다.

set: (
  opts: {
    set: SetRecoilState;
    get: GetRecoilValue;
    reset: ResetRecoilState;
  },
  newValue: T | DefaultValue,
) => void;

이것은 set의 인터페이스입니다. opts 객체 안에 set, get, reset으로 우리는 상태를 변경하고, 가져오고, 초기화할 수 있습니다.
또한, newValue로 atom으로 정의된 상태의 일부분을 갈아 끼울 수 있습니다.

export interface IState {
  sector: {
    opinion: ISector
    youtube: ISector
    insight: ISector
  }
  content: {
    opinion: IContent[]
    youtube: IContent[]
    insight: IContent[]
  }
}

저의 경우에, contentSelector는 결국 IState 인터페이스가 적용된 appState에서 content 객체의 일부분을 갈아 끼우는 역할을 수행하고 있습니다.

set(appState, (prev) => ({
  ...prev,
  content: {
    ...prev.content,
    [get(tabState)]: newContent,
  },
}))

useState의 setter처럼 상태 변경 함수를 정의하여 prev를 통해 immutable하게 appState의 일부분만을 변경하게 하고 있습니다.

mutable하게 변경하고 싶다면 리덕스의 리덕스 툴킷처럼 방법은 있었지만 이것은 추천하지 않습니다.
리코일의 컨셉은 atomic하게 상태를 선언하는 것입니다.
mutable하게 바꾼다는 것 자체가 구조가 복잡한 상태를 간단하게 바꾸려고 하는 것이기 때문에 리코일의 핵심 개념과 맞지 않습니다.

주의사항

Recoil에서 가져온 것들은 기본적으로 frozen 상태입니다. 변경 자체가 되지 않습니다.

하려면 완전히 깊은 복사를 해서 그 복사본을 바꾼 후 setter에 넣어야 합니다.

그래서 저는 lodash로 깊은 복사를 하고 필요한 값을 변경한 뒤 setter에 넣어주었습니다.

const [contentSelect, setContentSelect] = useRecoilState(contentSelector)
const newContents = _.cloneDeep(contentSelect)
const likedContent = newContents.find(
  (content: IContent) => content.id === id
) as IContent

if (likedContent.liked) {
  likedContent.liked = false
  likedContent.like_cnt -= 1
} else {
  likedContent.liked = true
  likedContent.like_cnt += 1

setContentSelect(newContents)

이렇게 깊은 복사를 하지 않으면 Object is not extensible 에러가 뜨게 됩니다.

느낀 점

Recoil은 장단점이 분명한 것 같습니다.

보일러 플레이트가 없고 atomic하게 상태를 구성하여 빠르게 전역 상태를 관리할 수 있게 해주는 것은 분명 큰 강점입니다.

하지만 프로젝트 규모가 커지고 전역으로 관리할 상태의 구조가 복잡해진다면 오히려 리덕스와 리덕스 툴킷의 조합이 나을 수도 있겠다는 생각도 하게 되었습니다.
atomic하게 코드를 짠다는 것은 상태의 구조가 복잡해질수록 추가되는 코드의 양이 점점 늘어나는 것을 의미하기 때문입니다.

결론적으로, recoil은 프로젝트의 상태 관리 컨셉에 따라서 정말 좋은 상태 관리 도구가 될 수 있겠다고 생각합니다.

profile
배워서 공유하기
post-custom-banner

0개의 댓글