이글은 독자를 Recoil을 써보지 않았지만, 리액트 hooks에 익숙한사람으로 설정합니다.
간단한 설명을 위해 디테일한 코드 및 설명은 생략됩니다.
3분만에 recoil을 훑어보고, Redux와 장단점을 비교해봅니다.
우선 Recoil의 기본개념부터 알아보죠
Recoil의 핵심 개념은 atoms
, selectors
로 나눠져요.
atom은 상태 단위로 업데이트, 구독이 가능합니다.
우선은 리액트의 state를 컴포넌트 밖으로 빼놨다고 생각하세요.
const $counter = atom<number>({ // number는 아래 default값에서 추론되기때문에 생략가능합니다
key: 'counter',
default: 0,
})
컴포넌트에서 이를 가져다 쓰기위해서는 useRecoilState()
함수를 사용합니다.
const [counter, setCounter] = useRecoilState($counter)
React의 useState()
hook과 사용성이 비슷하지만 상태를 컴포넌트간 공유한다는 차이점이 있어요
어떤 컴포넌트에서든 $counter
를 구독할 수 있고 상태가 업데이트되면 react state와 마찬가지로 reRender됩니다.
selector는 atom
또는 selector
를 구독해서 derived value
을 리턴하는 순수함수입니다.
// $counter가 1이면 $doubleCounter는 2입니다.
const $doubleCounter = selector({
key: 'doubleCounter',
get: ({ get }) => {
const counter = get($counter)
return counter * 2
},
})
위 예시를 보면 derived value에 대해서 이해가 될거에요. $counter
값으로부터 유도된 새로운 값이죠.
$doubleCounter
는 $counter
를 구독하게 됩니다. $counter가 업데이트되면 $doubleCounter도 당연히 같이 업데이트 되겠죠?
최소한의 상태만 atom
에 저장하고 이로부터 유도된 값은 selector
를 통해서 관리하게 됩니다.
그리고 위에서 언급했다시피 selector
는 selector
도 구독할 수 있습니다.
$doubleCounter
를 구독해서 2배값을 derive 한다면 $quadraCounter
라는 selector도 만들 수 있겠죠?
다음으로 살펴볼것들은 hooks 입니다.
위에서도 한번 보여드린 useRecoilState()
같은 것들이 몇개 더 있는데요.
주로 사용되는 hook은 다음과 같습니다:
useRecoilState()
: 상태를 읽고 쓰려고 할 때 사용. 컴포넌트는 atom을 구독함useRecoilValue()
: 상태를 읽기만 할 때 사용. 컴포넌트는 atom을 구독함useSetRecoilState()
: 상태를 쓰려고만 할 때 사용.useResetRecoilState()
: 초기화 함수 제공. atom선언시 넘겼던 default값으로 초기화useSetRecoilState()
, useResetRecoilState()
처럼 set류 hook은 리코일 상태를 구독하지 않습니다.
따라서 리렌더링으로부터 자유롭습니다. 아래처럼 최적화를 하는것이 좋습니다.
const [menu, setMenu] = useRecoilState($menu)
setMenu({ ...menu, food: 'pizza' }) // ❌
const setMenu = useSetRecoilState($menu)
setMenu((prevMenu) => ({ ...prevMenu, food: 'pizza' })) // ✅
공식문서에서 등장하는 비동기 처리 패턴은 아닙니다. 이름도 사실 제가 지은거에요...
명령적 프로그래밍을 해야할 경우, 쓰기 편하고 직관적입니다. 👍
명령적 프로그래밍을 해야만 하는 경우가 가끔 있는데요
이런경우는 아래의 async selector를 쓰면 문제가 복잡해집니다. (캐싱문제가 얽히기도 합니다)
리코일 문서에서 이를 해결하는법을 알려주고, 여러 컨퍼런스 영상을 찾아봤지만 (불편하다는 의견 다수)
개인적으로 이런경우는 그냥 action hooks를 쓰는게 낫습니다.
const $menu = atom({
key: 'menu',
default: {
food: '',
price: 0,
isLoading: false,
},
})
const useMenuAction = () => {
const setMenu = useSetRecoilState($menu)
const fetchMenu = async (id: number) => {
setMenu((prev) => ({ ...prev, isLoading: true })) 👈🏻
const menu = await 서버에서_메뉴_가져오기(id)
setMenu((prev) => ({ ...menu, isLoading: false })) 👈🏻
}
return {
fetchMenu,
}
}
const Menu = () => {
const menu = useRecoilState($menu)
const { fetchMenu } = useMenuAction() 👈🏻
if (menu.isLoading) return <LoadingSkeleton />
return (
<div> // redux의 dispatch(fetchMenu(id)) 와 유사하죠?
<button onClick={() => fetchMenu(id)}> 👈🏻
여기를 눌러서 메뉴정보를 가져오세요
</button>
<div>{menu.food} : {menu.price}원</div>
</div>
)
}
공식문서에 등장하는 패턴입니다.
선언적 프로그래밍을 할 경우 편리합니다. 👍
인자를 받는 경우는 selectorFamily
, 인자가 없는 경우는 selector
를 사용합니다.
지면관계상 selectorFamily만 다루겠습니다. selector도 궁금하다면 recoil:selector 공식 문서를 참고하세요.
const fetchMenu = selectorFamily({
key: 'fetchMenu',
get: (id: number) => async ({}) => {
const menu = await 서버에서_메뉴_가져오기(id) 👈🏻
return menu
},
})
const Menu = () => {
const menu = useRecoilValue(fetchMenu(10)) 👈🏻
return (
<div>
<div>{menu.food} : {menu.price}원</div>
</div>
)
}
뭔가 이상함을 눈치 채셨을텐데요, menu는 비동기적으로 결정되는 값입니다.
하지만 menu를 동기적으로 결정된 값으로 취급해서 렌더링을 하는데요.
이는 react에 실험적으로 도입된 Suspense API와 깊은 연관이 있습니다.
프로그래머가 비동기적인 값을 동기값으과 동일하게 취급해서 다루고, 리액트의 Suspense API가 이를 마법처럼 해결해주는 방식입니다.
물론 Suspense API를 위한 추가적인 코드 작성이 필요합니다.
const App = () => {
return ( // menu값이 resolve 되기 전까지 LoadingSkeleton을 렌더링
<Suspense fallback={<LoadingSkeleton />}> 👈🏻
<Menu />
</Suspense>
)
}
위 예시처럼 프로그래머가 loading 처리를 if (loading)
처럼 명령형으로 표현할 필요가 없습니다.
Suspense를 통해 선언형으로 로딩상태를 표현할 수 있다는것이 장점이죠.
리액트팀은 Suspense API를 통해서 다음과같은 목표를 이루려합니다: 👇🏻
Suspense사용 코드를 살펴보면 async/await의 try, catch문과 유사한 구조를 찾을 수 있습니다.
<ErrorBoundary fallback={<ErrorView />}>
<Suspense fallback={<LoadingSkeleton />}>
<Menu />
</Suspense>
</ErrorBoundary>
try {
await fetchMenu()
} catch (error) {
// 에러 처리
}
우리가 모든 실패할 수 있는 함수에 try, catch문을 감싸지 않는것처럼
Suspense를 일으키는 모든 컴포넌트에 Suspense, ErrorBoundary를 붙이기보다는
적당한 부분단위로 에러와 로딩상태를 한번에 처리하게 됩니다.
또한 Suspense대신 useRecoilValueLoadable()함수를 통해서 명령적으로 처리할 수 있는 옵션도 제공합니다.
리코일팀은 문서에서 동시성 모드(Concurrent Mode)를 비롯한 다른 새로운 React의 기능들과의 호환
을 강조하고 있습니다.
Suspense는 며칠전까지는 실험적기능이였지만 React 18이 릴리즈됨에 따라서 리액트의 정식 스펙이 되었습니다.
이런것을 보며 페이스북에서 개발하기 때문에 리액트의 개발 방향성을 따라가는것도 리코일의 장점
이라고 느꼈습니다.
renderToString()
대신 pipeToNodeWritable()
로 바꿔주면 된다.
Great post! I wonder do you love playing online game? Play tiny fishing, bro. It's fun!!!