# 리코일? ## React 내부 상태 관리의 한계 - 컴포넌트들 사이에서 state를 공유하려면 공통의 조상으로 끌어올려야하는데 그러면 리렌더링을 할 필요가 없는 것도 리렌더링을 해야할 수 있다. - `React.memo`를 사용해서 리렌더링을 막을 수는 있다. - `context`는 하나의 value만 저장할 수 있다. 각 consumer에서 사용되는 value들의 집합을 가질 수는 없다. - 위 두 개의 문제는 state가 있는 곳과 state가 사용되는 곳 사이의 코드 분리가 어렵다. ## 그냥 context api를 사용하면? context는 낮은 빈도의 업데이트에 사용하면 좋다. ![](https://i.imgur.com/5xx16dP.png) 위와 같은 서비스가 있다고 가정해보자. 이미지 목록이 있고 오른쪽에는 이미지 목록에서 이미지를 클릭하면 메타데이터를 보여주는 컴포넌트를 렌더링한다. 서비스에서는 이미지의 이름을 변경할 수 있는데 이 때 이미지 컴포넌트와 메타데이터 컴포넌트만 다시 렌더링을 하는 것이 좋다. **하지만 context는 Provider 하위의 모든 consumer들이 provider 속성이 변경될 때마다 다시 렌더링이 되기 때문에 일부만 다시 렌더링이 되게 하기가 어렵다. ** ## 리코일의 장점 - 리코일은 배우기 쉽다. hook을 사용하고 있는 사람들에게 익숙하다. - 컴포넌트가 사용하는 데이터 조각만 사용할 수 있다. - 계산된 selector를 선언할 수 있다. - 비동기 데이터 흐름을 위한 내장 솔루션을 제공한다. ## Recoil의 기본 개념 recoil에서 데이터의 흐름은 `atome`에서 `selector`, 리액트 컴포넌트로 이어진다. `atom`은 컴포넌트가 구독할 수 있는 state의 기본 유닛이다. `selector`는 state를 동기적으로 또는 비동기적으로 transform 한다. ## 리코일 기본 ## RecoilRoot recoil state를 사용하는 컴포넌트의 부모 컴포넌트의 어디엔가 `RecoilRoot`를 적용해야한다. ### Atom Atom은 컴포넌트가 구독할 수 있는 react state라고 볼 수 있다. atom의 값을 변경하면 구독하고 있는 컴포넌트들이 모두 다시 렌더링된다. 런타임에 생성이 될 수도 있다. atom은 고유한 key 값이 필요하다. atom을 생성할 때는 고유한 키 값과 default 값을 설정한다. ```javascript= export const nameState = atom({ key: 'nameState', default: 'Jane Doe' }); ``` ### useRecoilState atom의 값을 구독하여 업데이트할 수 있는 hook. `useState와` 동일한 방식으로 사용한다. ```javascript= const [fontSize, setFontSize] = useRecoilState(fontSizeState); ``` ### useRecoilValue setter 없이 atom의 값만을 반환한다. ### useSetRecoilState setter 함수만들 반환한다. ```javascript= import {nameState} from './someplace' // useRecoilState const NameInput = () => { const [name, setName] = useRecoilState(nameState); const onChange = (event) => { setName(event.target.value); }; return <> <input type="text" value={name} onChange={onChange} /> <div>Name: {name}</div> </>; } // useRecoilValue const SomeOtherComponentWithName = () => { const name = useRecoilValue(nameState); return <div>{name}</div>; } // useSetRecoilState const SomeOtherComponentThatSetsName = () => { const setName = useSetRecoilState(nameState); return <button onClick={() => setName('Jon Doe')}>Set Name</button>; } ``` ### selector selector는 상태에서 파생된 데이터로 다른 atom에 의존하는 동적인 데이터를 만들 수 있게한다. `selector`는 다른 `atom`이나 `selector`를 input으로 받는 순수함수이다. 만약 이 input으로 받은 `atom`이나 `selector`가 업데이트 되면 `selector`가 re-evaluated된다. 컴포넌트에서 `atom`을 구독하는 것처럼 `selector`를 구독할 수 있다. `selector`를 만들어 recoil에서는 최소한의 state 집합만 atom에 저장이 되게 해서 불필요한 state를 줄일 수 있다. ```javascript= const fontSizeLabelState = selector({ key: 'fontSizeLabelState', get: ({get}) => { const fontSize = get(fontSizeState); const unit = 'px'; return `${fontSize}${unit}`; }, }); ``` `get` 프로퍼티는 computed 되는 함수이다. 매개변수로 `get`이라는 값을 받는데 이것을 이용해서 다른 `atom`이나 `selector`에 dependency를 만들 수 있다. `set`이 없으면 writable 하지 않기 때문에 `useRecoilValue()`를 사용해서 값을 읽는다. ### atomFamily atomFamily는 atom과 동일하지만 다른 인스턴스와 구분이 가능한 매개변수를 받는다. ```javascript= // store의 코드 const getImage = async id => { return new Promise(resolve => { const url = `http://someplace.com/${id}.png`; let image = new Image(); image.onload = () => resolve({ id, name: `Image ${id}`, url, metadata: { width: `${image.width}px`, height: `${image.height}px` } }); image.src = url; }); }; export const imageState = atomFamily({ key: "imageState", default: async id => getImage(id) }); // 컴포넌트의 코드 const Images = () => { const imageList = useRecoilValue(imageListState); return ( <div className="images"> {imageList.map(id => ( <Suspense key={id} fallback="Loading..."> <Image id={id} /> </Suspense> ))} </div> ); }; // 단일 이미지 const Image = ({ id }) => { const { name, url } = useRecoilValue(imageState(id)); return ( <div className="image"> <div className="name">{name}</div> <img src={url} alt={name} /> </div> ); }; ``` ## Asynchronous Data Query 리코일은 state와 derived state를 리액트 컴포넌트에 data-flow 그래프를 통해서 연결하는 방식을 제공한다. 그런데 그래프의 함수가 asynchronous일 수도 있다는 점에서 매우 강력하다. 이것을 통해서 동기식의 리액트 컴포넌트 render 함수에서 비동기 함수를 사용하기가 쉬워진다. 리코일은 `selector`에서 동기 함수와 비동기 함수를 섞어서 사용하기 용이하게 해준다. `selector`의 `get` callback에서 값이 아니라 promise를 반환해서 interface를 동일하게 유지할 수 있다. `selector`는 비동기적인 데이터를 리코일의 data-flow graph에 연결하는데 사용된다. `selector`는 주어진 input이 있으면 항상 같은 결과를 반환한다. 이것은 `selector`의 evaluation이 캐싱되고 재실행되고 여러번 사용될 수 있다는 점에서 중요하다. 이러한 점 때문에 `selector`는 read-only DB query를 작성하는데 좋은 방법이 된다. 변할 수 있는 데이터에서는 **Query Refresh**를 사용할 수 있고 synchronize mutable state나 persist state, 다른 side-effect에 대해서는 **Atom Effects** api를 고려할 수 있다. ### asynchronous 예제 ```jsx= const currentUserNameQuery = selector({ key: 'CurrentUserName', get: async ({get}) => { const response = await myDBQuery({ userID: get(currentUserIDState), }); return response.name; }, }); function CurrentUserInfo() { const userName = useRecoilValue(currentUserNameQuery); return <div>{userName}</div>; } ``` `selector`를 사용하는 컴포넌트 입장에서는 `selector` 내부가 비동기인지 아닌지를 알 필요가 없다. 하지만 리액트의 render 함수가 동기적이기 때문에 promise가 resolve 되기 전의 처리를 해줄 필요가 있다. #### React Suspense 사용. `React Suspense`를 사용하면 자식들 중에 pending 상태인 것을 catch 할 수 있다 만약 에러가 있는 경우에는 `<ErrorBoundary>`를 사용해서 에러를 잡을 수 있다. 만약 `React Suspense`를 사용하고 싶지 않은 경우에는 `useRecoilValueLoadable`을 사용할 수 있다. ### Queries with Parameters 만약 query를 할 때 derived state 외에도 매개변수를 받아서 query를 하고 싶으면 `selectorFamily`를 사용할 수 있다. ```jsx= const userNameQuery = selectorFamily({ key: 'UserName', get: userID => async () => { const response = await myDBQuery({userID}); if (response.error) { throw response.error; } return response.name; }, }); function UserInfo({userID}) { const userName = useRecoilValue(userNameQuery(userID)); return <div>{userName}</div>; } function MyApp() { return ( <RecoilRoot> <ErrorBoundary> <React.Suspense fallback={<div>Loading...</div>}> <UserInfo userID={1}/> <UserInfo userID={2}/> <UserInfo userID={3}/> </React.Suspense> </ErrorBoundary> </RecoilRoot> ); } ``` `selector`로 query를 할 때 state, derived state, query를 섞어서 data-flow graph를 만들 수 있다. 이 그래프는 state가 업데이트 되면 자동으로 업데이트 되고 컴포넌트를 리렌더링 시킨다. ### Pre-fetching 성능을 위해서 렌더링 이전에 fetching을 시작하고 싶은 경우에 다음과 같이 할 수 있다. ```jsx= function CurrentUserInfo() { const currentUser = useRecoilValue(currentUserInfoQuery); const friends = useRecoilValue(friendsInfoQuery); const changeUser = useRecoilCallback(({snapshot, set}) => userID => { snapshot.getLoadable(userInfoQuery(userID)); // pre-fetch user info set(currentUserIDState, userID); // change current user to start new render }); return ( <div> <h1>{currentUser.name}</h1> <ul> {friends.map(friend => <li key={friend.id} onClick={() => changeUser(friend.id)}> {friend.name} </li> )} </ul> </div> ); } ``` ### Query Default Atom Values `atom`이 local state를 표현하지만 default value를 `selector`를 이용해서 query 하는 것은 매우 많이 사용하는 패턴이다. ```jsx= const currentUserIDState = atom({ key: 'CurrentUserID', default: selector({ key: 'CurrentUserID/Default', get: () => myFetchCurrentUserID(), }), }); ``` ### Query Refresh `selector`를 이용해서 데이터를 가져올 때 같은 state에 대해서 `selector` evaluation은 같은 결과를 준다는 것을 기억해야한다. 결과는 캐싱 되어 여러번 실행되어도 같은 값을 반환한다. 이 말은 어플리케이션의 life time 동안에 result가 변화하는 query에는 하나의 `selector`를 사용하면 안된다는 의미가 된다. mutable 데이터에 사용할 수 있는 패턴은 몇 가지가 있다. #### Request Id 사용 같은 input에 대해서 같은 `selector` evaluation을 반환하기 때문에 request id를 dependency로 추가해서 보내는 방법이 있다. ```jsx= const userInfoQueryRequestIDState = atomFamily({ key: 'UserInfoQueryRequestID', default: 0, }); const userInfoQuery = selectorFamily({ key: 'UserInfoQuery', get: userID => async ({get}) => { get(userInfoQueryRequestIDState(userID)); // Add request ID as a dependency const response = await myDBQuery({userID}); if (response.error) { throw response.error; } return response; }, }); function useRefreshUserInfo(userID) { setUserInfoQueryRequestID = useSetRecoilState(userInfoQueryRequestIDState(userID)); return () => { setUserInfoQueryRequestID(requestID => requestID + 1); }; } function CurrentUserInfo() { const currentUserID = useRecoilValue(currentUserIDState); const currentUserInfo = useRecoilValue(userInfoQuery(currentUserID)); const refreshUserInfo = useRefreshUserInfo(currentUserID); return ( <div> <h1>{currentUser.name}</h1> <button onClick={refreshUserInfo}>Refresh</button> </div> ); } ``` #### Atom 사용 다른 방법은 `selector` 대신에 `atom`을 사용하는 방법이 있다. `atom`은 현재 promise를 지원하지 않기 때문에 query refresh가 pending 상태일 때 React Suspense를 사용하지 못한다. 하지만 수동으로 result와 status를 객체 안에 넣어서 사용할 수도 있다. ## 참고 링크 https://ui.toast.com/weekly-pick/ko_20200616 https://recoiljs.org/docs/introduction/motivation/ ###### tags: `tech sharing`