본문 바로가기

FrontEnd

Recoil, Redux. 상태관리 라이브러리의 Selector 개념

반응형

Redux의 셀렉터

 

Deriving Data with Selectors | Redux

Usage > Redux Logic > Selectors: deriving data from the Redux state

redux.js.org

모든 앱의 상태를 싱글턴 기반으로 추적하는 개념인 Redux는 탑 다운 스타일로 상태 그래프의 해당 상태를 컴포넌트와 연결한다.

즉, 기본적으로 상태가 변경되면 전부 변경되지만, 그렇게 되면 최적화가 어려울 것이기에, 일부 데이터만 구독하도록 최적화한다.

https://redux.js.org/usage/deriving-data-selectors

 

반대로 contextAPI는 컴포넌트 하단으로 데이터를 전달하기 위한 도구이다.

즉 props drilling을 피하기 위한 도구지,

기본적으로는 상태관리 도구가 아니다.

useState, useReducer를 사용하여 상태처럼 사용할 수 있다만,

selector를 사용 불가능한 리덕스나 마찬가지다.

 

또한, provider에 의해 전달된 context value가 변경되면 하위 컴포넌트는 전부 리렌더링된다.

selector처럼 최적화하려면, Redux처럼 컴포넌트 생명주기와 분리된 스토어에 존재해야 한다.

상단 state가 바뀌면 기본적으로 아래가 다 바뀌어버리는 context의 특성은 최적화에 Memo를 반드시 필요로 한다.

https://kentcdodds.com/blog/how-to-optimize-your-context-value

 

context의 원리가 아직도 헷갈린다면 :

context가 변경되면 useContext 훅에 의해 해당 훅을 사용하는 컴포넌트 들은 memo인 상태여도 변경된다.

 

https://dmitripavlutin.com/react-context-and-usecontext/#32-when-context-changes

 

A Guide to React Context and useContext() Hook

The React context provides data to components no matter how deep they are in the components hierarchy.

dmitripavlutin.com

 

 

Recoil의 셀렉터

https://recoiljs.org/docs/basic-tutorial/selectors

 

Selectors | Recoil

A selector represents a piece of derived state. You can think of derived state as the output of passing state to a pure function that derives a new value from the said state.

recoiljs.org

지식의 축적에 따른 진화는 학문, 기술분야의 특징이다.

그리고 해당 도메인은 방언을 형성한다.

이것이 DDD의 bounded context, ubiquotous language 개념이다.

js의 lexical excution context 개념과도 유사한 면이 있다.

프로그래밍 분야의 용어들은 철학적 개념들을 차용하여 만들어졌기 때문이다.

 

Recoil의 Selector는 반대로 bottom up 식으로 만들어진 Recoil 상태 그래프 트리의 상태 그래프와 컴포넌트를 연결한다.

즉, Selector가 이전과 반대로, atom 또는 다른 리소스를 통해 확장한 데이터가 된다.

 

Redux의 셀렉터가 축소라면, Recoil의 Selector는 확장이다.

Recoil의 특이한 활용법 : 아톰을 props로 전달하여 최적화

해당 패턴을 소개한 글

리코일은 위와 같이 확장 상태가 작은 상태들의 조합이다.

보통 확장 상태는 컴포넌트 상단에서 주로 사용하게 되며,

그렇게 되면 어차피 상단 데이터 종속성에 의한 하단 컴포넌트의 리렌더링 문제는 피할수 없게 된다.

즉, selector의 get을 상단 컴포넌트에서 사용하면 안되는 것이다.

export const DetailsRaw = ({ className }: IWithClassName) => {
  const router = useFlowRouter();
  const { id, journey } = useRoutingInfo();
  const key: IRecoilId = {
    id,
    journey,
  };
  // best practice (encapsulate state within component will result with less rendering)
  const colorState = stateColor(key);
  const sizeState = stateSize(key);
  // 모든 하단 컴포넌트가 리렌더링됨. bad practice
  const [count, setCount] = useRecoilState(stateCount(key));

  return (
    <div className={className}>
      <h1 className="title">Details</h1>
      <ColorPicker className="color" state={colorState} />
      <SizePicker className="size" state={sizeState} />
      <div className="count">
        <h1>Count:</h1>
        <input
          className="count-input"
          type="number"
          value={count}
          onChange={(e) => setCount(e.target.valueAsNumber)}
        />
      </div>
      <div className="next" onClick={() => router.pushStage('continue')}>
        Next
      </div>
    </div>
  );
};

 

상단 컴포넌트에서 하단 컴포넌트로 해당 컴포넌트가 사용하는 특정 state를 atom으로 내려주는 특이한 코드를 봐서 소개한다.

key는 페이지 / 라우터 단위로 받아오는 상위 종속 데이터다.

props로 atom을 전달하여, 하위 컴포넌트에서 훅으로 사용한다.

  // best practice (encapsulate state within component will result with less rendering)
  const colorState = stateColor(key);
  const sizeState = stateSize(key);
  // 파라미터로 아톰 전달. 아톰은 반드시 불변
  return (<SizePicker className="size" state={sizeState} />)

// ColorPickerRaw
// 불변 아톰을 파라미터로 전달하여 훅으로 사용
export const ColorPickerRaw = ({ className, state }: IColorPickerProps) => {
  const [value, setValue] = useRecoilState(state);
...

그럼 위 컴포넌트를 아래와 같이 최적화 해보자.

const Input = ({ state }: { state: RecoilState<number> }) => {
  const [count, setCount] = useRecoilState(state);

  return (
    <input
      className="count-input"
      type="number"
      value={count}
      onChange={(e) => setCount(e.target.valueAsNumber)}
    />
  );
};
export const DetailsRaw = ({ className }: IWithClassName) => {
  const router = useFlowRouter();
  const { id, journey } = useRoutingInfo();
  const key: IRecoilId = {
    id,
    journey,
  };
  // 아톰을 props로 내려준다. 굳이 get으로 조회하지 않는다.
  const colorState = stateColor(key);
  const sizeState = stateSize(key);
  const countState = stateCount(key);
 

  return (
    <div className={className}>
      <h1 className="title">Details</h1>
      <ColorPicker className="color" state={colorState} />
      <SizePicker className="size" state={sizeState} />
      <div className="count">
        <h1>Count:</h1>
        <Input state={countState} />
      </div>
      <div className="next" onClick={() => router.pushStage('continue')}>
        Next
      </div>
    </div>
  );
};

order > product > 선택 후 next > Count 수정 후 devtool로 확인해보자.

 

 

다시 생각해보니 어차피 list 목록이 변경되는 경우는 전체가 리렌더링 되기 때문에 어차피 메모는 필요하긴 하겠다.

상단 컴포넌트가 리렌더링 될 경우에는 유용한 패턴인지 잘 모르겠다

전달하는 props를 간소화 할수 있다는 점? atom은 불변일 테니 하단 컴포넌트를 메모할 필요가 없다는 점?

https://jotai.org/docs/basics/showcase

 

결론 :

jotai 공식 문서의 설명부분을 발췌하였다

atom을 결합하여 상태를 구축할 수 있으며 원자 종속성을 기반으로 렌더링이 최적화됩니다.
이것은 React 컨텍스트의 추가 재렌더링 문제를 해결하고 메모이제이션 기술의 필요성을 제거합니다.

위를 실현하기 위해선, get 컴포넌트와 set 컴포넌트의 적절한 격리 및 관심사의 분리가 필요하다.

이는 당연히 컴포넌트와 분리된 전역 상태에 기반(atom, selector)한다.

(getter, setter가 한 컴포넌트에 있으면 당연히 강결합이 된다.)

특히 주의해야 할 점은, 하위 컴포넌트가 상단 컴포넌트에 영향을 주게 될 경우를 최소화해야 하는 것이다.

(콜백으로 상단 컴포넌트의 get 관련 state 변경.)

이 경우 atom과 같이 불변 레퍼런스를 전달하여, 수정 및 시각화 대한 책임을 하위 컴포넌트로 전달할 수도 있다.

Redux의 경우 selector를 useCallback으로 감싸 하위 컴포넌트로 전달하는 방법으로 위 방법을 흉내낼 수 있다.

 

리코일이나 jotai가 필드 단위로 getter, setter를 제공하면서, 최적화를 쌓아가는 좋은 패턴인건 알겠는데,

atomEffect의 개념이 상당히 애매하다.

react-query나 swr을 따로 사용하는것 외에 비동기와 동기 데이터의 api를 recoil로 단일화 하는 방법에 대해 생각해 보았는데, 리코일 팀도 그렇고, jotai 쪽도 mutation 관련 integration 기능 추가에 소극적인걸 봐선, 그냥 atom은 클라이언트 쪽 데이터만 관리하는게 맞는것 같기도 하다.

jotai 개발자의 대답 : jotai가 react-query를 대체할 수 있을까?

jotai 개발자의 이슈 : atomWithMutation (1년이 넘은 이슈지만, 지금까지 기능 추가 안됨.)

 

recoil 대신 jotai를 선택한 이유에 대해 잘 소개한 글이 있어 링크를 걸어둔다.

https://devblog.kakaostyle.com/ko/2022-01-13-1-frontend-state-management/

 

반응형