이러한 고민 끝에 React Query라는 도구를 도입하게됩니다.
이러한 고민 끝에 Zustand라는 도구를 도입하게됩니다.
React Query는 강력한 비동기 상태관리 도구이며 아래와 같은 다양한 장점을 가지고 있습니다.
간단한 예시를 보겠습니다.
function Todos() {
// Access the client
const queryClient = useQueryClient()
// Queries 선언
const query = useQuery('todos', getTodos)
// Mutations 선언
const mutation = useMutation(postTodo, {
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries('todos')
},
})
return (
<div>
<ul> // Query 데이터 사용
{query.data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<button // Mutation 사용
onClick={() => {
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
})
}}
>
Add Todo
</button>
</div>
)
}
Zustand는 Client State를 관리하기 위해 사용한 도구이고 아래와 같은 장점은 가지고 있습니다.
간단한 예시를 보겠습니다.
// 스토어 선언
const usePersonStore = create<State & Action>((set) => ({
firstName: '',
lastName: '',
updateFirstName: (firstName) => set(() => ({ firstName: firstName })),
updateLastName: (lastName) => set(() => ({ lastName: lastName })),
}))
// In consuming app
function App() {
// 스토어 컴포넌트 연결
const firstName = usePersonStore((state) => state.firstName)
const updateFirstName = usePersonStore((state) => state.updateFirstName)
return (
<main>
<label>
First name
<input
// Update the "firstName" state
onChange={(e) => updateFirstName(e.currentTarget.value)}
value={firstName}
/>
</label>
<p>
Hello, <strong>{firstName}!</strong>
</p>
</main>
)
}
외부 상태 관리 도구의 의존도가 낮은 팀 내 코드와 전역 상태를 최소화하는 팀의 방향성에 적합
팀 내 도메인들이 서버와 유기적으로 얽혀져있으면서 비동기 호출 전략이 요구되므로 해당 역할에 적합
Store Layer 내의 장바구니 queries 파일 예제
queryKey를 생각하는 것도 정말 큰 비용이죠 위와 같은 라이브러리를 사용하시면 해당 비용을 절약하실 수 있습니다.
Store Layer 내의 팝업 스토어 예제 코드
const PoketListContainer: FC<PocketListContainerProps> = () => {
const containerRef = useRef<HTMLDivElement | null>(null);
const poketRef = useRef<HTMLDivElement | null>(null);
const { handler } = useBaeminpayModuleStore();
const { selectedPoket, onSelectPoket } = usePayMethodStore();
const { poketList } = usePoketListViewModel();
const sortedPocketList = useMemo(() => {...
}, [pocketList]);
const handleSelectPocket = async (pocket: PocketItemViewModel) => {
sendPocketSelecLog();
if (handler?.vaildatePocketSelect) {
const result = await handler.validatePocketSelect(pocket);
if (!result) return;
}
onSelectPocket(pocket)
}
useEffect(() => {
//...
}, [selectedPocket?.pocketNo])
}
가독성이 굉장히 좋아보입니다. 그렇다면 이 hooks 내부 안으로 들어가보겠습니다.
query를 4개나 호출하고 있습니다. pocketList라는 데이터를 만들기 위해 쿼리 4개와 store를 조합한 후 convertPocketPocketListViewModel이라는 함수를 통해 데이터를 전처리한 후 반환하는데요.
이러한 로직이 아까 그 컴포넌트에 있었다고 생각해보면 컴포넌트가 굉장히 복잡해지고 한 눈에 무슨일을 하는지 알 수가 없겠죠.
useQuery를 사용하고 있고 Store를 호출하고 있습니다.
stores 또한 일반적인 Zustand Store와 같은 모습을 하고 있습니다.
여기서 이런 의문이 생길 수 있습니다. "그러면 너희는 간단한 로직도 이렇게 컴포넌트에서 스토어까지 레이어를 가지게 구현을 하니?"
결론부터 말씀드리자면 아닙니다! 이 아키텍처의 기본적인 룰은 최상위 레이어에서 그 이하의 레이어만 호출할 수 있다입니다.
어떤 페이지는 API를 하나 찔러서 다른 페이지로 리다이렉트 시켜주는 페이지라고 생각해봅시다. 이 페이지에는 hooks와 component가 낀다면 코드의 복잡성만 늘어나고 큰 효율이 없을 겁니다.
제가 드리고 싶은 말씀은 형식에 집중하지 말고 본질을 바라봐주셨으면 좋겠다는 의미입니다. 저희가 레이어화된 아키텍처를 선택한 이유는 컴포넌트에 집중되어 있는 로직을 레이어층에 적절히 책임을 분산해서 가독성을 높이고 개발자 겸험을 개선하면서 유지보수가 용이하게 코드를 변경하고 이를 통해 사업적 가치와 고객 가치에대한 기민한 대응을 할 수있도록 입니다.
그러면 이런 질문이 나올 수 있을 거 같습니다. "너희는 처음부터 이렇게 해왔으니까 이게 좋은 거 아니냐?" 대답부터 드리자면 아닙니다! 간단한 예시를 말씀드리겠습니다.
위 사진을 보시면 아시겠지만 Github star 수가 높은 것은 물론이고 npm 트렌드 또한 꾸준히 성장하고 있습니다.
React Query & Zustand on 팀 내 표준 개발 환경
팀이 고민하고 있는 문제와 해결 방안은 팀마다 다릅니다. 만약 저희와 같이 React Query를 사용해서 Server State를 분리한다면 Zustand는 좋은 대안이 될 수 있습니다.
하지만 React Query같은 라이브러리를 통해 Server State를 분리하지 않는다면 여전히 Redux와 같은 정통적인 상태관리 라이브러리가 더 좋을 수 있습니다.
브라우저와 JS는 여전히 변화중이며 현재 문제를 해결한다고 해서 끝이 아니라 다음 문제가 생깁니다. 하지만 저희는 현재의 문제를 React Query와 Zustand라는 도구를 통해 해결한 것일 뿐이죠.
이번 영상에서는 기존의 상태관리 방식에서 벗어나 Server State와 Client State를 분리하여 관리하고 이를 위해 React Query와 Zustand라는 도구를 사용했다는 내용과 이를 통해 배민에서 실제로 사용하고 있는 Layer 아키텍처와 코드를 보면서 예시를 보여줬습니다. React Query와 Zustand는 이미 공부한 경험이 있고 실제로 프로젝트에서도 사용한 경험이 있습니다. 하지만 배민에서 사용하는 Layer 아키텍처는 상당히 흥미로웠는데요. 일반적인 디렉터리 구조와는 다른 구조였고, 관심사를 적절하게 분리하여 컴포넌트의 가독성을 높이고, 유지 보수성을 높일 수 있다는 부분에서 상당히 많은 매력을 느꼈습니다.