💡 시작하기
현재 Redux만 사용하고 있는데, 다른 프로젝트에서 React Query와 Zustand를 사용하자고 해서 공부를 해야됐었다. 근데 마침 우아콘2023 컨퍼런스에 저 두 개의 상태 관리 도구를 사용하는 이유와 로직을 설명하는 세션이 있었다! 그래서 냅다 선정하고 듣기로 했다.
1️⃣ 이야기의 시작은 고민과 함께
프론트엔드 상태관리
State : A Component’s Memory
- 컴포넌트 : UI/UX와 함께 많은 발전을 거치게 되며 많은 인터렉션과 많은 일 등을 수행
ex) 어떤 팝업을 화면에 띄울지 말지, 주문 / 배달의 진행은 어떤지
-> 상태관리 필수!!
프론트엔드 상태관리 방법의 변화
Redux → MobX → Context API → Post Redux(Recoil, Zustand, …) → 원격 상태관리(React Query, SWR, …)
초기에는 Redux, MobX를 많이 사용하였으나, 다양한 한계점 존재
- Store가 너무 크고 복잡함
- Store에 상태관리보다 API 호출 코드가 더 많음
- Redux나 MobX가 비동기 통신에 적합한 도구인지에 대한 의문이 생김
⇒ React-Query 사용 시작
BUT!! 여전히 고민
- Store는 간단한데 컴포넌트가 더 복잡해짐
- Client Store를 간단하게 관리해도 됨
- 비즈니스 로직이 대부분 컴포넌트에 존재 → 컴포넌트 복잡해짐
⇒ Zustand 사용 시작
2️⃣ React Query & Zustand 살펴보기
React Query
https://tanstack.com/query/latest
강력한 비동기 상태 관리의 도구
유용한 옵션과 인터페이스, 리액트 훅같은 간단한 사용법, 캐싱, 동기화 등 다양한 기능을 제공합니다.
- 공식 문서 -
공식 코드
function Todos() {
// Access the client
const queryClient = useQueryClient()
// Query
const query = useQuery({ queryKey: ['todos'], queryFn: getTodos })
// Mutations
const mutation = useMutation({
mutationFn: postTodo,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<div>
<ul>
{query.data?.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<button
onClick={() => {
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
})
}}
>
Add Todo
</button>
</div>
)
}
render(<App />, document.getElementById('root'))
- Query & Mutation 선언
- Query : CRUD에서 Read에 대응되는 애들을 다룸
- Mutation : CRUD에서 Create, Update, Delete에 대응되는, 즉 변화를 일으키는 애들을 다룸
function Todos() {
// Access the client
const queryClient = useQueryClient()
// Query
const query = useQuery({ queryKey: ['todos'], queryFn: getTodos })
// Mutations
const mutation = useMutation({
mutationFn: postTodo,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
- Query 데이터 사용
<ul>
{query.data?.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
- Mutation 사용
- 자동으로 호출되지 않아, 이벤트 핸들러 등을 통해 직접 호출시켜 사용함 → Mutation을 발생시킨다고 말함
<button onClick={() => { mutation.mutate({ id: Date.now(), title: 'Do Laundry', }) }} > Add Todo </button>
Zustand
client state를 관리하기 위해 쓰는 도구
- 적은 보일러 플레이트 코드
- 직관적인 사용법
- 작은 패키지 사이즈
↓ 예제 코드
import { create } from 'zustand'
type State = {
firstName: string
lastName: string
}
type Action = {
updateFirstName: (firstName: State['firstName']) => void
updateLastName: (lastName: State['lastName']) => void
}
// Create your store, which includes both state and (optionally) actions
const usePersonStore = create<State & Action>((set) => ({
firstName: '',
lastName: '',
updateFirstName: (firstName) => set(() => ({ firstName: firstName })),
updateLastName: (lastName) => set(() => ({ lastName: lastName })),
}))
// In consuming app
function App() {
// "select" the needed state and actions, in this case, the firstName value
// and the action updateFirstName
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>
)
}
- 스토어 선언
const usePersonStore = create<State & Action>((set) => ({
firstName: '',
lastName: '',
updateFirstName: (firstName) => set(() => ({ firstName: firstName })),
updateLastName: (lastName) => set(() => ({ lastName: lastName })),
}))
- State : firstName, lastName
- Action : updateFirstName, updateLastName
- 스토어 컴포넌트 연결
const firstName = usePersonStore((state) => state.firstName)
const updateFirstName = usePersonStore((state) => state.updateFirstName)
각 상태 관리 도구를 선택한 이유?
React Query | Zustand |
Server State 관리 | Client State 관리 |
API 호출 코드로 비대해진 Store를 목적에 맞게 분리 | 컴포넌트 밖에서도 상태 변경이 가능 |
리액트 훅과 비슷한 직관적인 사용성 | 사용성이 단순해 러닝커브가 낮음 |
여러 인터페이스 & 옵션을 제공해 적은 코드로 강력한 동작 | 상태관리에 필요한 코드도 적음 |
자체 개발도구 제공 | Redux Devtools 확장 프로그램 활용 가능 |
⇒ React Query는 팀 내 도메인들이 서버와 유기적으로 얽혀있으면서 비동기 호출 전략이 요구되므로 해당 역할에 적합
⇒ Zustand는 외부 상태관리 도구의 의존도가 낮은 팀 내 코드와 전역 상태를 최소화하는 팀의 방향성에 적합
3️⃣ 상태관리는 프로덕트에 어떻게 녹아있을까?
원래의 배달의 민족 → 개발자, 프로덕트, 도메인이 다 구별되어있었음
⇒ 코드 구조와 기술 스택이 모두 다름
최근에 하나로 합치며, 팀 내 표준 개발 환경 프로젝
트 런칭
- Business Layer
- hooks : 비즈니스 로직을 담고 있는 hook의 모음
- services : 비즈니스 로직이 함수의 형태로 되어있는 것들의 모음 (helper function의 모음)
- Store Layer
- queries : Reacy Query의 선언부가 담겨 있음
- stores : Zustand의 store의 선언부가 담겨 있음
queries와 stores는 어떻게 활용될까요?
PocketListContainer(Component Layer)
→ usePocketListViewModel(Business Layer)
→ useFetchBaeminpayPocketListQuery(Store Layer - queries)
→ useBaeminpayModuleStore(Store Layer - stores)
- 분홍색 박스 : store 호출코드
- 노란색 박스 : hooks 호출 코드
- 연두색 박스 : services 호출 코드
- hook들을 컴포넌트랑 분리
- query에서도 store 호출
- 최종 로직
레이어화된 아키텍처로 설계한 이유 ?
- 컴포넌트에 집중되어있는 로직을 레이어층에 책임을 분산
→ 가독성 높임, 개발자 경험 개선, 유지보수 용이
⇒ 사업적 가치, 고객 가치에 기민한 대응 가능 - 장점
- 비즈니스 로직이 역할에 맞게 적절히 분산
- 컴포넌트의 동작 유추와 로직 파악이 용이해짐
4️⃣ 나도 쓰는 게 좋을지 고민된다면
도입 이후 프로덕트는
- Store는 Client State vs Server State
- Component는 각 Layer의 유기적인 결합
- Product는 유연하고 변경하기 편한 구조
📝 정리
React의 상태관리 도구는 매우 많다. 하지만 왜 React Query와 Zustand를 사용하는 지에 대해 요즘 트렌드와 결합하여 알게 되었다. 확실히 코드가 간단하다는 점이 큰 장점으로 다가왔다. 앞으로 있을 프로젝트에서 React Query와 Zustand를 사용할 때, 세션에서 설명해준 팁들과 고려사항을 곱씹어보며 사용해봐야겠다!
🔗 참고
'Conference' 카테고리의 다른 글
[Conference] FEConf 2023 - use 훅이 바꿀 리액트 비동기 처리의 미래 맛보기 (4) | 2024.12.05 |
---|