기본적으로 나의 공부를 위한 글이면서 Zustand, Recoil, Jotai, RQ에 대한 리뷰가 궁금한 사람들이 보면 좋을 것 같다.
서비스를 만들 때, 조금만 복잡해져도 상태관리에 대한 니즈는 무조건 생긴다.
사실 실무에서는 상태관리 라이브러리를 무.조.건. 사용해야 고통받지않고 서비스를 개발할 수 있다.
React 진영에서는 다양한 상태관리에 대한 선택지가 존재한다. 나에게 맞는 상태관리 라이브러리를 알아보자!
React를 처음 접했을 때, 별생각 없이 점유율이 높은 Redux를 사용했었는데, 방대한 코드량, 익숙하지 않은 원리와 직관적이지 않은 코드들 때문에... 도저히 못 쓰겠어서 Redux의 대체 라이브러리를 찾고자 함이 여러 라이브러리를 탐구한 진짜 이유이다. 😂
경우에 따라서 다르겠지만, 실무에서는 비동기처리가 필수적이기 때문에 상태관리 라이브러리 1개와 비동기처리 특화 라이브러리 1개를 조합해서 사용한다. (ex. Redux, Redux-saga)
Recoil은 페이스북에서 서포트하는 라이브러리이다. 글로벌하게 상태관리할 데이터를 잘게 쪼게어서 관리하게 된다. 그래서 그 하나 하나의 단위를 atom
이라고 부른다.
아무 위치에 아무 이름으로된 js파일에 선언해서 사용가능하다.
js파일에 atom을 선언한 후, 필요한 곳에서 import해서 사용하면 된다. atom의 key는 고유값이어야한다.
import { atom } from 'recoil'
export const textState = atom({
key: 'textState',
default: '',
})
React의 useState훅처럼 useRecoilState훅을 사용할 수 있다.
값만 조회할 수 있는 훅과 set메서드만 사용할 수 있는 훅도 제공한다.
사용법은 이렇게 매우 간단하다.
import { useState } from 'react'
import { useRecoilState } from 'recoil'
import { textState } from '../store/post'
const SearchBox = () => {
const [temp, setTemp] = useState('')
const [text, setText] = useRecoilState(textState) // Recoil
const handleChange = (e) => {
const v = e.target.value
setTemp(v)
setText(v)
}
return <input type="text" value={temp} onChange={handleChange} />
}
export default SearchBox
Recoil만으로도 비동기처리가 가능하긴하다. 간단한 기능만 사용하면 좋을 것 같다.
(나는 안쓸 거)
selector를 async/await으로 감싼 후 사용한다.
/* selector 선언 */
import { selector } from 'recoil'
import axios from 'axios'
export const postsState = selector({
key: 'postsState',
get: async ({ get }) => {
return (await axios.get('/posts')).data
},
set: () => {}
})
useRecoilValueLoadable
을 사용하면, return받는 데이터 뿐만 아니라 로딩, 에러발생등등의 상태 또한 받아볼 수 있다.
/* 컴포넌트에서 사용 */
import React from 'react'
import { useRecoilValueLoadable } from 'recoil'
import { postsState } from '../store/post'
const Posts = () => {
const { state, contents } = useRecoilValueLoadable(postsState) // Recoil
switch (state) {
case 'hasValue':
return (
<div>
<div>Posts</div>
{contents.map((post) => {
return <div key={post.id}>{JSON.stringify(post)}</div>
})}
</div>
)
case 'loading':
return <div>Loading...</div>
case 'hasError':
return 'Err'
}
}
export default Posts
심플 & Atom: 매우 심플하게 글로벌한 데이터를 관리할 수 있어서 좋다는 생각을 했다. 사실 Atomic패턴으로 상태관리를 하는 건 Recoil을 통해서 처음 접하는데, 굉장히 매력적이다. (근데, 밑에서 다룰 Jotai는 atom에서 key값 마저 빼버려서... 난 Jotai를 쓸 거다 ㅎㅎㅎ)
클라이언트 상태관리 추천: 실무에서는 CRUD를 조합한 다양한 비동기 처리가 필요한데, 이 복잡한 것을 Recoil로 처리하기에는 무리가 있을 것이라고 판단이 된다.즉, 클라이언트 사이드에서의 상태관리로는 아주 좋은 선택지가 될 것 같다.
Powered by Facebook: 급변하는 신기술의 바다에서 페이스북팀의 공식 서포트는 무시할 수 없는 매력적인 요소이다.
Jotai, Zustand, Valtio를 만든 일본의 천재 개발자가 만든 상태관리 라이브러리이다.
Recoil의 atomic패턴을 보고 영감을 받았다고 한다. 그래서인지 사용법이 매~우 유사하다.
아래 코드 2줄이면 jotai를 사용할 준비는 끝났다.
나는 미니멀 & 심플함을 추구하는데... 코드가 너무 매력적으로 보인다...
거추장스러운 설정 코드 없이 단순히 '선언'하는 것 만으로 상태관리가 가능하다니...
import { atom } from 'jotai'
export const textAtom = atom('')
Recoil과 마찬가지로 useState훅과 유사한 형태로 사용할 수 있다. 또, Value만 조회하거나, set메서드만 사용할 수 있는 훅도 제공한다.
import { useState } from 'react'
import { useAtom } from 'jotai'
import { textAtom } from '../store/jotai'
const SearchBox = () => {
const [temp, setTemp] = useState('')
const [text, setText] = useAtom(textAtom) // Jotai
const handleChange = (e) => {
const v = e.target.value
setTemp(v)
setText(v)
}
return <input type="text" value={temp} onChange={handleChange} />
}
export default SearchBox
Jotai의 비동기 처리 방식은 생략한다. Recoil과 비슷하게 사용하고, 같은 문제들을 같고 있다.
심플 & 심플: 미니멀리즘의 끝.
나 이거 할래: 심플함에 매료되어 이거 쓰기로 함 ㅎ
Integration: Jotai는 다른 라이브러리들과의 통합 Hook들을 제공한다. 이 훅을 사용하지 않더라도, 다른 기술과 매끄러운 통합을 고려한 게 엿보인다. 실제로 RQ와 같이 써봤는데 버그 없이 한 큐에 동작될만큼(?) 어디에든 잘 붙는 라이브러리인 것 같다.
❤
위에서 언급한 일본의 천재 개발자가 만든 또 하나의 상태관리 라이브러리이다. Redux와 같은 Flux패턴을 사용하고 있어서, 가장 친숙한 모양새를 갖고 있다.
또, Flux패턴에서는 state와 action이 나뉘어지기 때문에 action을 활용한다면, 굳이 RQ나 SWR을 사용하지 않아도 비동기 처리가 원활히 가능하다.
Zustand 또한 Store를 선언하고 사용할 곳에서 import해서 쓰면 된다.
import create from 'zustand'
import axios from 'axios'
// set, get 메서드를 제공해서 state의 값을 읽기, 쓰기할 수 있다.
export const useGameStore = create((set, get, _state) => ({
games: [],
getGames: async (params) => {
const games = (await axios.get('/games', { params })).data
set({ games })
return games
},
gameIds: () => {
const games = get().games
return games.map((v) => v.id).join(',')
},
}))
Store에서 필요한 값/메서드를 가져와서 사용할 수 있다.
Store의 action에 async/await을 감싸고 해당 action을 비동기 api로 쓸 수 있다.
(단, loading, error 상태값 조회, 캐싱등등의 편의 기능은... 직접 구현.. ㅎ)
import { useState } from 'react'
import { useGameStore } from '../store/zustand'
const Games = () => {
const [term, setTerm] = useState('')
const games = useGameStore((state) => state.games) // Zustand
const getGames = useGameStore((state) => state.getGames) // Zustand
const handleChange = async (e) => {
const q = e.target.value
setTerm(q)
const res = await getGames({q})
console.log(res)
}
useEffect(() => {
getGames()
}, [])
return <>
<input type="text" value={term} onChange={handleChange} />
<div>{JSON.stringify(games)}</div>
</>
}
export default Games
State & Action: 모든 Flux패턴의 라이브러리들이 state와 action으로 나뉘어져서 동작하지만, Zustand는 간단 명료하게 이 패턴을 구현하였다.
비동기처리 가능: RQ에서 제공하는 캐싱과 같은 기능은 사용할 수 없지만, 기본적으로 비동기 처리가 가능하다. 컴팩트하게 기술스펙을 가져가고 싶다면, RQ나 SWR없이 Zustand만 써도 될 것 같다.
구독: 위에서 설명하지는 않았지만, 컴포넌트에서 각 스토어를 구독할 수 있고, 값의 변동을 리스닝하면서 특정 함수들을 실행하게 해줄 수 있다.
Flux패턴: Redux를 대체할 Flux패턴 라이브러리를 찾고 계시다면 강력히 추천한다.
RQ의 시작은 "클라이언트와 서버에 대한 상태관리는 분리되어야한다" 라는 가치관에서 시작한다. 그리고 RQ는 서버에 대한 상태관리의 '명수'다. 🔥
캐싱, API의 상태 관리(로딩, 에러, 성공등등), 폴링, SSR, API병렬처리(동시에 여러 api를 수행해야하는 경우)등등 실무에서 서버와 통신할 때 벌어지는 거의 모든 경우를 커버한다.
그래서 이 글에서 모든 내용을 다루긴 어렵고, 기본적인 것들만 다뤄보도록 하겠다.
RQ는 위에서 얘기한 라이브러리들과 달리 최상위 또는 글로벌하게 관리하고 싶은 상위 컴포넌트에 Client를 주입해줘야한다.
/* main.jsx */
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { QueryClient, QueryClientProvider } from 'react-query'
import { ReactQueryDevtools } from 'react-query/devtools'
import './main.css'
const root = document.getElementById('root')
// RQ 클라이언트 선언 및 옵션설정
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 0,
useErrorBoundary: true,
staleTime: Infinity,
// cacheTime: Infinity,
},
mutations: {
useErrorBoundary: true,
},
},
})
ReactDOM.createRoot(root).render(
// 앱 최상위에 client 주입
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools />
<App />
</QueryClientProvider>,
)
이렇게 하면 <App />
컴포넌트와 그 하위의 모든 컴포넌트들이 RQ의 통제(?)범위에 들어오게 된다.
또, query를 js파일에 선언한 후, 사용하고자 하는 파일 또는 컴포넌트에서 import해서 사용할 수 있다.
import { useQuery } from 'react-query'
import axios from 'axios'
const getSports = (params) => axios.get('/sports', { params })
export const useGetSportsQuery = (p) => {
return useQuery('get-sports', () => getSports(p), {
onSuccess: (data) => {
// jotai, zustand, recoil 과 같은 상태관리 라이브러리를 이용하여 값을 set할 수 있다.
},
})
}
RQ에 데이터를 조회하는 query에 대해서만 살펴보겠다.
(데이터 수정/생성/삭제는 Mutation
으로 할 수 있다.)
아래와 같이 query(api)의 상태를 알 수 있고, 캐싱도 옵션에 따라 알아서 해준다 ㅎ
또, 위에서 사용한 useQuery
와 같은 RQ에서 제공하는 훅들이 비동기 처리도 해줘서, 따로 asycn/await을 감싸줄 필요는 없다.
import React, { useEffect, useState } from 'react'
import { useGetSportsQuery } from '../queries'
const Sports = () => {
const { isLoading, isError, data } = useGetSportsQuery() // RQ의 Query
if (isLoading || isFetching) return <div>Loading...</div>
if (isError) return <div>Error...</div>
return <div>{JSON.stringify(data)}</div>
}
export default Sports
죄송합니다: 더 많은 기능들이 있지만, 이 글에 다 담기에는 너무 많아요 😂
초!강력: RQ에서 제공하는 기능들은 매우매우 강력한 것 같다. 개발자의 피로도는 확 줄여주고, 생산성은 확 올려준다.
캐싱: 서버API가 캐싱이 된다는 것은 앱 최적화, UX개선을 의미한다. 엄청나군.
Integration: RQ는 딱 비동기통신 및 서버API에 특화되어 있고, 클라이언트 단의 상태관리는 취향에 맞는 라이브러리를 매끄럽게 사용할 수 있다.
이번 탐구(?)의 가장 큰 수확은 RQ와 Atomic패턴을 접한 것이 아닐까 싶다. 그래서 다음 프로젝트는 Jotai
와 RQ
의 조합으로 프로젝트를 구성해볼까 한다. ㅎㅎ
이로써 Jotai, Recoil, Zustand, RQ의 리뷰를 마친다.