-
Notifications
You must be signed in to change notification settings - Fork 6
전역 상태 관리
아래의 글을 참고하여 작성하였습니다.
mobx-with-react-hooks-typescript
-
mobx-react는 observable한 상태들, computed로 계산된 결과를 메모이즈 해둘 수 있는 기능들 등 리덕스와는 다른 매력을 가지고 있는 라이브러리이다.
-
mobx는 리덕스와 다르게 관심사에 따라서 여러개의 스토어를 만들고 필요에 따라서 컴포넌트 위에 프로바이더를 만들어서 사용할 수 있다.
-
observer 함수 자체가 클래스 컴포넌트를 리턴하는 hoc(higher order component)으로 클래스를 위해서 만들어진 함수이기 때문에,
mobx-react v5에서는 hooks와는 호환되지 않았는데 이런 구조들을 개선하기 위해서 mobx-react-lite가 나왔다.(mobx-react v6 이상에서도 가능하다.)
-
mobx-react-lite는 훅을 지원하기 위해 함수형 컴포넌트에서만 사용할 수 있는 API만 제공하고 있으며 React.createContext API를 사용해서 store를 가져올 수 있다. (v6이상의 mobx-react 모듈을 사용하는 것으로 수정)
FE/src/store 밑에 폴더를 만들고 다음과 같이 파일을 구성한다.
- store.ts
- context.tsx
- hook.ts
(context와 hook은 한 단계 추상화하여 사용할 수 있을 것 같다. 변경시에 수정 예정!)
- store.ts
- store파일에서는 전역으로 관리하고 싶은 데이터와 그 데이터에 대한 action을 가진 객체를 만든다.
- 위에서 만든 객체를 return해주는 createStore 함수를 export한다.
- createStore의 return type을 함께 export한다. (context를 생성하거나 rootData 훅을 만들때 등 createStore함수의 타입을 지정할때 사용)
- context.tsx
- 만든 store를 다른 컴포넌트들이 사용하도록 하기 위해 context를 만들어 리턴한다.
- useLocalStore를 사용하여 observable한 state를 만든다. (useLocalStore를 통해 return된 객체의 모든 property는 자동적으로 observable될 수 있다.)
- store를 공유할 컴포넌트들의 최상위를 감싸줄 provider를 만든다.
=> 위에서 만든 context의 provider value에 observable한 state값을 넣고 children을 감싸도록 하여 return한다.
=> 이 provider로 감싸지는 컴포넌트, 즉 children의 위치에 들어갈 컴포넌트들은 모두 value값(observable한 state값)을 사용할 수 있다.
- 여기서 만든 context에 접근하여 데이터를 가져오기 위해서는 해당 컴포넌트에서 useContext를 이용하면 된다.
- hook.ts
- context를 사용하는 컴포넌트에서 useObserver를 통해 변화하는 state에 따라 리렌더링할 수 있다.
- 하지만, 위와같이 구현하면 store에 여러 데이터가 들어갈경우 문제가 생긴다.
=> 예를들어 store에 a,b,c,d,e데이터가 있고 A컴포넌트가 관심있는 state는 a, B컴포넌트가 관심있는 state는 b,c라고 가정해보자.
이때 A에 의해 store에 있는 a가 변경이 되면, a에 관심이없는 B컴포넌트까지 리렌더링 될 수 있다.
이와같이 무의미한 리렌더링은 옳지 않다.
- 따라서, 각 컴포넌트에서는 store의 데이터중 해당 컴포넌트가 관심있는 데이터의 변화에만 반응하도록 만들어 주면 좋을 것이다. 따라서 hook 파일을 만들게 되었다.
- hook파일에서는 useRootData라는 함수를 export해 준다.
useRootData는 추상화된 함수인 useStoreData를 사용하여 각 컴포넌트에서 관심있는 state를 useObserver로 wrapping하여 return해준다.
hook파일을 제대로 이해하고 사용해야 할 것같아서 덧붙이게 되었다.
hook파일은 useStoreData함수와 이 함수의 반환값을 받을 수 있는 함수로 나뉘어져 있다. (useRootData라고 명명했지만, 서로다른 이름을 갖게 해야 될 것 같다.)
store데이터를 사용하려는 컴포넌트에서는 useRootData를 통해 observable로 만들 데이터를 결정하고 , 자신은 observer가 된다.
사용방법은 아래와 같다.
const dateInfo = useRootData(store => store.nowCalendarInfo) ;
위와 같이 작성하면 해당 store의 nowCalendarInfo는 observable한 값이 되고, 해당 컴포넌트는 이 값에 반응하는 observer가 된다.
이것이 가능한 이유는 useRootData가 받은 값을 useStoreData를 통해 어떠한 로직을 거치기 때문이다.
export const useStoreData = <Selection, ContextData, Store>(
context: React.Context<ContextData>,
storeSelector: (ContextData: ContextData) => Store,
dataSelector: (store: Store) => Selection,
) => {
const value = React.useContext(context);
if (!value) throw new Error();
const store = storeSelector(value);
return useObserver(() => {
return dataSelector(store);
});
};
useStoreData는 위와 같이 생겼다.
제네릭하게 타입을 받는데, 어떤 context의 어떤 store의 어떤 data를 사용할지를 모두 제네릭하게 받고있다.
예제에서 가장 재사용가능한 hook파일로 만들기 위한 방법으로 위와같은 방법이 쓰였는데,
사실 store를 여러개 두고 각각의 hook파일을 두는 지금방식에서는 굳이 제네릭하게 할 필요가 있는지 모르겠다.(논의 후 수정할 사항)
현재 각 store마다 hook파일을 따로 두는 이유는, 결국 제네릭하게 타입을 건네려면 useRootData에 context를 넘겨주어야 할텐데
useRootData를 호출하는 컴포넌트쪽 코드가 복잡해지는것 보다는 폴더구조를 통해 파일을 분산하는 편이 나을 것 같았기 때문이다.
useRootData에서 context, storeSelector, dataSelector를 받는데,
두번째 인자인 storeSelector부분이 아주 헷갈렸다.
useRootData에서는 contextData => contextData! 라는 함수를 넘겨주고 있는데, 이 contextData가 context파일에서 내보낸 Context값이 되어있었다.
제네릭의 신비로움을 느낌과 동시에 아리송함을 느끼는 포인트였다. 팀원들과 상의후 내용을 추가해야겠다.
mobx-react의 버전이 업그레이드 되면서 mobx-react-lite를 사용할 필요가 없어졌다.
마이그레이션이 복잡하지 않기 때문에 아래의 글을 참고하여 변경해보면 좋을 것 같다.
mobx-react와 React Hooks API 함께 사용하기