Recoil도입 후기

11t518s·2022년 9월 7일
2
post-thumbnail

앞서서 Recoil을 선택한 이유에 대해서는 이 글을 참고해주시면 감사하겠습니다!

그럼 바로 본론으로가서 Recoil에 대한 사용 경험을 나누려 합니다!

실제로 써본 Recoil은 어땠나?

상태관리는 역시 힘든 것 같습니다!

만족하지 못하는 부분도 있었고, 그렇지 않은 부분들도 있고, 스스로 내린 결론도 있어서 그 경험들을 공유하려 합니다.

단점을 먼저 말해보겠습니다.

단점

1. 아.. 이래서 React-Query를 쓰는구나,,

많은 Recoil관련 결국 캐싱이 너무 잘돼서 문제라고들 많이 이야기 합니다.

특히 여기서 서버 데이터를 사용할때 문제가 많이 생기는데 대게 특정 스크린이나 컴포넌트가 마운트 될 때 항상 서버에서 받아오는 신규 데이터를 확인해야할 떄가 있습니다.

그런데 이 캐싱이 너무 잘되다보니 신규 데이터를 불러오지 않고 기존 데이터를 유지해버렸고, 때문에 이를 강제로 refreshTrigger를 사용했던 것입니다.

사실 이 방식이 결코 좋다고 생각하진 않았습니다.

무엇보다 recoil은 아직 초기 값이 생성되지 않았는데 다시 호출되면 아래처럼 에러를 발생시킵니다.
그런데 가끔 타이밍이 잘못 될 때(서버에서 응답이 늦어지거나 할 때) 해당 에러가 실제 환경에서 종종 있었고, 부정적 경험이 됐습니다.

에러도 치명적인데 기본적으로 refresh를 위해 setRecoilValue의 의미를 잘못 사용하는 코드를 작성하는 듯 했습니다. 그래서 더 부정적으로 느낀 듯 합니다.

그래서 서버 데이터는 아예 React-query에서 맡겨둔다는 최근의 흐름이 또 여기서 나오는구나를 좀 더 느꼈습니다.


2. atom? selector?

어떤 상태를 사용해야할 지 모호한 것이 있었습니다.

물론 이 부분은 제가 명확히 Recoil을 알지 못해서 그런 것일 수 있습니다!
혹시 좋은 해결을 알고계신 분 있으시면 공유해주세요!

위에서도 잠깐 언급했던 내용인데

일단 아직도 이해가 안되는 부분은 과연 이 상태는 atom이냐? selector냐 하는 것에 있습니다.

export const exampleSelector3 = selector<any>({
    key: 'exampleSelector3',
    get: async ({}) => {
      // 뭔가.. 로직...
	  return // 뭔가.. 결과값
    },
});

const exampleAtom atom<any>({
  key: 'exampleAtom',
  default: selector({
    key: 'exampleAtom/default',
    get: async ({}) => {
      // 뭔가.. 로직...
	  return // 뭔가.. 결과값
    },
  })
})

위의 코드를 보면 아시겠지만 selector를 사용할 수도있고, 동시에 atom에도 selector를 넣어서 사용할 수 있습니다.

물론 set, reset할 때 차이가 생기긴 하지만 사용하는데 엄청나게 큰 차이를 내진 않습니다.

그래서 내가 만들 상태가 정말 raw한 type의 요소들 예를들어 number, boolean, 등 명확한 것들은 atom으로 서버에서 받아오는데 단순한 구조의 object구조라면 어떻게해야하나 부터 고민이 시작됩니다.

그리고
atom를 모아서 selector를 만든다는 것과
selector를 나누서 atom을 받아온다는 것을 이야기 해보겠습니다!

가령

const exampleAtom1 atom<any>({
  key: 'exampleAtom1',
  default: selector({
    key: 'exampleAtom1/default',
    get: async ({}) => {
      const data1 = await axios.get('...')
      return data1
    },
  })
})

const exampleAtom2 atom<any>({
  key: 'exampleAtom2',
  default: selector({
    key: 'exampleAtom2/default',
    get: async ({}) => {
      const data2 = await axios.get('...')
      return data2
    },
  })
})

export const exampleSelector = selector<any>({
    key: 'exampleSelector',
    get: ({get}) => {
      const data1 = get(exampleAtom1)
      const data2 = get(exampleAtom2)
	  return {data1, data2}
    },
});

이렇게 atom으로 각각의 데이터를 받아와서 exampleSelecotr로 만들어주는 시나리오도 가능하고

ex_ SNS 서비스를 사용하는데

  1. userInfo(게시글을 올린 유저의 정보) => exampeAtom1
  2. postInfo(게시글 정보) => exampleAtom2
    서버에서 각 정보를 따로 보내주고
  3. 뷰에서는 userInfo + postInfo => exampleSelector
    두 정보를 함께 보여줘야할 때

반대로

export const exampleSelector = selector<any>({
    key: 'exampleSelector',
    get: () => {
   	  const data = await axios.get('...')
		
      // 콘솔을 찍어보면 data = { data1: {...}, data2: {...} }
	  return data
    },
});

const exampleAtom1 atom<any>({
  key: 'exampleAtom1',
  default: selector({
    key: 'exampleAtom1/default',
    get: ({get}) => {
      const data = get(exampleSelector)
      return data.data1
    },
  })
})

const exampleAtom2 atom<any>({
  key: 'exampleAtom2',
  default: selector({
    key: 'exampleAtom2/default',
    get: ({get}) => {
      const data = get(exampleSelector)
      return data.data2
    },
  })
})

이렇게 서버에서 크게 가져온다면 그걸 atom으로 나눠줄 수도 있습니다.

ex_ SNS 서비스를 사용하는데

  1. 서버에서 한 게시물의 정보를 한번에 보내줌 userInfo + postInfo => exampleSelector
    그런데 우리 컴포넌트는 유저 정보 보여주는 뷰와 게시글을 보여주는 뷰가 달라서 굳이 exampleSelector를 사용하지 않고 나눠주는게 더 좋음
  2. 유저 정보 컴포넌트에서는 userInfo => exampleAtom1
  3. 게시글 컴포넌트에서는 postInfo(게시글 정보) => exampleAtom2
  • 물론 이 부분에서 exmapleAtom1, exampleAtom2를 selector로 만들어줄 수도 있습니다.

즉 서버에서 어떻게 보내주느냐에 따라 selector, atom을 어떻게 활용할지 달라지는 것이 있다고 느꼈습니다.

그렇지만 프론트의 상태를 서버에 의존적으로 둘 순 없다고 생각했습니다.
그래서 서버가 어떻게 보내주든 신경쓰지 말고, atom과 selector를 어떤 원칙에 의하여 구분하고 사용하느냐가 되는데 더 많이 사용해보면서 분리해야 할 필요를 느꼈습니다.


3. Post는.. 어떻게 해결하지..?

Redux의 경우 action을 dispatch하면 reducer에서 로직을 수행하고 store에 관리됩니다.

그래서 제가 알기로는 post요청, get요청이 중요하기 보단 어떠한 action을 만드는지가 더 중요한 것으로 알고 있었습니다. (Redux를 쓴지 시간이 좀 지나서 가물가물하네요..)

그런데 Recoil은 이러한 요청과 답변이 크게 없습니다. useState처럼 데이터터 초기값을 만들어주고 변경하는 일들만 하게 됩니다.

그리고 한가지 더 아쉬운 점은 selector를 구독하는 컴포넌트가 마운트되는 즉시 get: 내부에 있는 함수를 동작시킵니다.

그렇기 때문에 특정한 액션이 있고 난 다음에 동일 컴포넌트에서 다른 동작을 할 때 컨트롤 하기 어려워 집니다.

이때 제가 생각한 방법은 set을 강제 해줘서 나타나게 하면 되는거 아니야? 였습니다.

https://github.com/facebookexperimental/Recoil/issues/462
recoil github에서도 저와 같은 궁금증을 이슈로 올려준 분이 있었고 위의 해결책이 있어서 시도해봤습니다.

보시는 것처럼 버전이 달라졌는지 모르겠지만 set에 Async를 사용하는 것을 지금 지원하고 있지 않습니다.
공식문서를 다시 보니 set할 때 return 값으로는 Promise< T >가 없었 던게 이제는 잘 보이네요..

하여튼 그래서 특정 요청을 했을 때 상태를 변화시켜주는 것에 대해 Recoil자체적으로 해결하기 어렵습니다.

그래서 useRecoilCallback을 사용하기로 했습니다.


용도는 useCallback과 동일하게 사용하려 했고, 굳이 useRecoilCallback을 사용한 이유는 아래의 set을 상용해서 굳이 불필요한 변수를 만들지 않아도 된다는 장점을 가져가고 싶었습니다.

그런데 이렇게 해결은 할 수 있지만 아쉬움이 남습니다. 바로 useRecoilCallback역시 hooks이기 때문에 Function컴포넌트 내부에서 사용돼야 하고, 비즈니스 로직을 스크린 바깥으로 분리하는 것에 실패했습니다.


4. Suspense

Suspense를 사용하니 좋았지만, 가끔 Susepense가 에러를 발생시킬 때가 있었습니다.

(물론 18버전에서는 안정화 됐을 수도 있겠습니다..)

해당 문제는 다시 발생시키기도 어렵고 해결방법도 안보이는데,
Suspense를 더 잘 사용할 수 있다는 Recoil의 장점을 유지하려면 이 에러를 어쩔 수 없이 사용해야해서 아쉬웠습니다.

장점

굉장히 안좋은점만 썼는데 장점도 많았습니다.

1. 짧은 시간에 익혔다.

이게 사실 위의 저러한 단점들을 모두 커버할 수 있는 엄청난 강점입니다.

누구나 쉽고 빠르게 상태관리를 할 수 있고, 러닝 커브도 작고 성능도 우수했습니다.

신입이 새로운 아키텍쳐를 그것도 1주년 이벤트에 도입한다고 들이밀었는데 이게 가능했던게 Recoil이 그 만큼 쉽고 강력했기 때문입니다.

2. Suspense지원

물론 단점에서 언급했지만 이게 또 어마무시 합니다.

Suspense덕분에 스크린의 관심사는

데이터를 불러와서 뷰를 보여주는 것
=> 데이터가 바르게 불러와졌을 때의 뷰를 보여주는 것

으로 한 단계 더 좁혀졌습니다.

비동기 시간에 보여줄 컴포넌트를 따로 계획하고 그려주는 것의 강점을 잘 알았습니다.

3. atomFamily, selectorFamily

가령 리스트를 불러올 때 리스트 안의 엘리먼트가 하나만 바껴도 리스트 전체가 리랜더링 되는 이슈가 있었습니다.

그런데 family를 사용하면 각각 엘리먼트들에도 특정한 key값을 제공해주는 것인지 그러한 문제가 해결됐습니다.

이 역시 높은 만족감을 줬습니다.

4. 짧아진 코드

1번의 내용과도 연결되는데 굉장히 코드가 간편해집니다.

이건 제가 Redux라는 고정관념이 있어서 그런 것 같기도 하지만, 보일러 플레이트 코드가 굉장히 짧아집니다. 짱좋습니다.

그리고 Suspense와 또 한번 결합하여 useState, useEffect가 엄청 많이 사라지게 됩니다.

아마 추후에 React-query까지 사용하게 된다면 더 많은 코드들이 간편해질 것 같습니다!


개인적 결론

1. 상태관리에 대한 고민을 다시 하게 됐다.

Redux의 과한 보일러플레이트로 상태관리에 대해 부정적이다가, 오히려 상태관리가 없어서 방대해지는 코드를 보며 긍정적으로 도입을 했습니다.

다시 사용해봤을 때는 장점도 있지만 고민되는 지점도 많은 듯 합니다.

그리고 상태관리의 핵심은 서버에서 받아온 데이터를 유저에게 보여주는 부분에서
마치 슈뢰딩거의 고양이처럼 '서버에서 방금 받아온 역시 신뢰할 수 있을까?'라는 질문부터 시작한다면, 그 때 그 때 서버에서 받아온 정보를 유저에게 보여주는 것이 가장 좋은 코드라고 생각했습니다.

그래서 Recoil, Redux, Mobx같은 상태관리 서비스만 단독으로 사용하기보단,
서버 사이드 데이터를 신뢰할 수 있게 관리해주는 React-query, Swr같은 친구들을 같이 사용해주는게 좋을 것 같습니다!

2. Recoil은 그러면 언제 쓰는게 좋냐?

제가 생각한 두가지 부분이 있습니다!

  1. Recoil은 Meta에서 contextApi에 대한 보안제로 나왔다.
  2. 상태관리의 최신 트렌드는 서버 데이터를 신뢰할 수 있게 하는 상태관리이다.

이 둘을 조합해서 정보가 서버에서 들어오고 거의 변화하지 않는 친구들을 관리해줄 때 Recoil이 적당한 것 같습니다!

저희 서비스를 예로는 이모티콘, 콜북, 유저정보 등이 있을 듯 합니다.

이러한 데이터만 Recoil로 관리하고 변화가 생길 가능성이 있는 것들은 상태관리를 사용하지 않는 것이 좋아보인다고 느꼈습니다.


Recoil을 쓰고 정말 좋은데 단점은 고민을 길게 하고 난 이후에 작성하는 것이라 길게 쓰고 장점은 진짜 좋아서 짧게 써지니까 뭔가 단점만 많아진 것 같네요..

혹시 Recoil쓰는 여러분이 더 있다면 혹시 단점에 대한 좋은 해결책이나, 더 활용하면 좋을 기술들에 대해 공유해주시면 감사하겠습니다 :)

profile
사람들에게 행복을 주는 서비스를 만들고 싶은 개발자입니다!

1개의 댓글

comment-user-thumbnail
2024년 1월 5일

좋은 관점 제시인 것 같습니다! 글 잘봤어요!

답글 달기