[TIL][React] 상태관리 고민 (Context, Recoil)

Wendy·2021년 5월 9일
11

학습기록

목록 보기
11/20
post-thumbnail

개발중인 시스템에서 react-query를 도입하며 redux를 제거하기로 했다

(최소화 하고싶지만) 여전히 글로벌 상태값이 필요한 케이스(서버에서 받아오지 않음, 구독기능이 필요함)들을 어떻게 처리할까 고민을 하면서 여러가지 대안을 생각해보았다.

  1. redux를 최소한으로
    최소한으로 사용해도 보일러 플레이트가 여전히 많다

  2. context API
    절대 안되는건 아닌데 더 좋은 대안이 없을까

  3. react-query (꼭 api통신으로만 사용하라는 법은 없으니까...)
    이것도 안되는건 아닌데, 원래 사용목적이 아니기도 하고 깔끔한 사용법을 많이 고민해야 할것같다

  4. recoil
    오래되지 않은 라이브러리지만 심플하고 좋은데?!?!?!?

해서 recoil을 사용하기로 하였다.

그래서 recoil을 조금씩 다뤄보다가
주말이니까 궁금했던것들을 몰아서 파헤쳐보기로 했다.

Recoil의 장점 (+react context의 부족한점)

context의 단점, recoil의 장점

  1. recoil Motivation
    https://recoiljs.org/docs/introduction/motivation

Component state can only be shared by pushing it up to the common ancestor, but this might include a huge tree that then needs to re-render.

같이 쓰는 속성들을 공통 부모한테 들고있으려고 하다보면 트리가 너무 커지지. (그렇지 그래서 리액트는 다양한 상태관리 라이브러리들이 있지)

Context can only store a single value, not an indefinite set of values each with its own consumers.

컨텍스트가 하나의 벨류만 저장이 가능하다고???? context 부터 다시 다시 공부해보자...

Both of these make it difficult to code-split the top of the tree (where the state has to live) from the leaves of the tree (where the state is used).

Context

공식 문서 읽기

https://ko.reactjs.org/docs/context.html

context는 React 컴포넌트 트리 안에서 전역적(global)이라고 볼 수 있는 데이터를 공유할 수 있도록 고안된 방법입니다.

context를 사용하면 컴포넌트를 재사용하기가 어려워지므로 꼭 필요할 때만 쓰세요.
여러 레벨에 걸쳐 props 넘기는 걸 대체하는 데에 context보다 컴포넌트 합성이 더 간단한 해결책일 수도 있습니다.

Provider 하위에서 context를 구독하는 모든 컴포넌트는 Provider의 value prop가 바뀔 때마다 다시 렌더링 됩니다. Provider로부터 하위 consumer(.contextType와 useContext을 포함한)로의 전파는 shouldComponentUpdate 메서드가 적용되지 않으므로, 상위 컴포넌트가 업데이트를 건너 뛰더라도 consumer가 업데이트됩니다.

Q. context같은 구독기능을 js로 구현하려면?
참고1 : https://rinae.dev/posts/why-every-beginner-front-end-developer-should-know-publish-subscribe-pattern-kr
참고2 : https://css-tricks.com/build-a-state-management-system-with-vanilla-javascript/

TEST: context를 쓰면 정말 모든 구독 컴포넌트가 다시 렌더링될까?

YES!

export default function App() {
  const [color, setColor] = useState('white');
  const [todos, setTodos] = useState([]);

  return (
    <AppWrapper>
      <Title />
      <DataContext.Provider value={{ color, setColor, todos, setTodos }} >
        <MainContent>
          <Left />
          <Right />
        </MainContent>
      </DataContext.Provider>
    </AppWrapper>
  );
};

오른쪽에 할일을 적고 Do!를 눌렀더니
사용하지 않는 왼쪽까지 로그가 찍혔다

Github: todo-react-context

다시 Recoil

TEST: recoil로 하면 위의 문제가 발생하지 않을까?

YES!

//App.jsx

export default function App() {
  return (
    <AppWrapper>
      <Title />
      <RecoilRoot>
        <MainContent>
          <Left />
          <Right />
        </MainContent>
        </RecoilRoot>
    </AppWrapper>
  );
};

//atoms.js

export const colorState = atom({
    key: 'colorState', 
    default: 'white',
  });

export const todosState = atom({
    key: 'todosState', 
    default: [],
  });

이제는 할일을 업데이트 할 때
왼쪽박스는 다시 렌더링 되지 않는다

Github: todo-react-recoil

TEST: 추가 궁금한점

Q1. recoilState에 기존과 동일한 값을 set한다면, 구독중인 화면/selector는 재렌더링?

No, 다시 렌더링 되지 않는다.
object의 경우 주소비교이기 때문에 다시 렌더링 된다.

위의 케이스에서 같은색의 버튼을 계속 누르면, 최초 1회만 로그가 찍힌다.
color를 string이 아닌 {name: string;} 으로 변경하여 실험할 경우, 다시 렌더링 된다.

Q2. recoilState에 기존과 다른 값을 set했는데, 구독하고있는 selector의 결과값은 변하지 않았다면, selector를 구독중인 화면들은 재렌더링?

Yes, atom이 변했다면 selector의 계산값이 변하지 않아도 재렌더링 된다.

//state
const colorState = atom<ColorAtomState>({
    key: 'colorState', 
    default: {name:'white'},
  });

const colorSelectorState = selector<Color>({
  key: 'colorSelectorState',
  get: ({get}) => get(colorState).name,
})

//color버튼 클릭이벤트
const handleClick = () => setColor({name:color});

위와 같이 atom을 object로 갖고, selector에서 string을 뽑아오도록 변경하고
동일한 컬러버튼을 계속 클릭하면
로그가 계속 찍힌다...

이와 관련된 정확히 일치하는 논의가 recoil githup에서 진행중이네??
https://github.com/facebookexperimental/Recoil/issues/314

아직 해결이 되지 않았다고 하니(2021/05/09)
atom에 객체로 여러값 모아서 저장한다음에
selector로 나눠서 가져오는 방식을 지양해야겠다...

소스코드 구경하기 : Recoil은 context로 만들어졌을까?

(아마도) 데이터 저장은 context에, 구독기능은 별도의 로직에!

//Recoil_RecoilRoot.react.js
//context 가 들어감

const AppContext = React.createContext<StoreRef>({current: defaultStore});
const useStoreRef = (): StoreRef => useContext(AppContext);

function RecoilRoot(props: Props): ReactElement {
  ...
  return (
    <AppContext.Provider value={storeRef}>
      <MutableSourceContext.Provider value={mutableSource}>
        <Batcher setNotifyBatcherOfChange={setNotifyBatcherOfChange} />
        {children}
      </MutableSourceContext.Provider>
    </AppContext.Provider>
  );
}

//batch가 끝나면 수정된 atom을 구독중인 화면에만 알람을 주는듯?
//즉 context를 사용은 하지만, 별도로 구독알람 로직을 갖고있다

function sendEndOfBatchNotifications(store: Store) {
  const treeState = storeState.currentTree;
  const dirtyAtoms = treeState.dirtyAtoms;
  
  1. storeState.nodeTransactionSubscriptions 중에서 dirtyAtoms에 key가 있다면 subscription(store);
  2. storeState.transactionSubscriptions 의 전부에 subscription(store);
  3. storeState.nodeToComponentSubscriptions에서 key가 있으면 callback
}
profile
개발 공부중!

1개의 댓글

comment-user-thumbnail
2021년 12월 3일

좋아요.

답글 달기