본문 바로가기

FrontEnd

Lean Recoil 강의 정리 : Async Data Fetching (비동기 데이터 가져오기)

반응형
selector와 selectorFamily를 사용하여 사용자 데이터를 가져오고 Recoil이 Suspense와 작동하는 방식을 배웁니다.
 

기본 사용법

api는 동기 비동기가 다르지 않다.

리턴값만 프로미스로 변경해주면 된다.

const userState = selectorFamily({
    key: 'user',
    get: (userId: number) => async () => {
        const userData = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`).then((res) => res.json())
        return userData
    },
})

해당 데이터는 promise이기 때문에, 정상적인 방법으론 렌더링할 수 없다.

이를 위해 React.Suspense를 사용한다.

해당 API를 사용하는 컴포넌트 상단을  Suspense로 감싸준다.

const UserData = ({userId}: {userId: number}) => {
    const user = useRecoilValue(userState(userId))
    if (!user) return null

    return (
        <div>
            <Heading as="h2" size="md" mb={1}>
                User data:
            </Heading>
            <Text>
                <b>Name:</b> {user.name}
            </Text>
            <Text>
                <b>Phone:</b> {user.phone}
            </Text>
        </div>
    )
}
// 상단 컴포넌트
{userId !== undefined && (
    <Suspense fallback={<div>Loading...</div>}>
        <UserData userId={userId} />
    </Suspense>
)}

Selector Family는 응답을 캐싱하며, 비동기 요청으로 데이터를 가져오기 때문에 효과적으로 여러 요청을 처리할 수 있다.

1번 유저 데이터가 다 나오기 전에 2번 유저 정보를 조회하면, 동시에 비동기로 처리하기 때문에 성능 이슈가 없다.

언제 selector / selectorFamily를 사용하는가

자바스크립트도 일단 const를 먼저 사용하듯

우선 atom으로 선언하고, 이후 파생은 다음 규칙에 따라 적용한다.

데이터 소스 (파생 원천) 아래를 사용하세요
atom / selector selector
React (component prop / useState / Redux ) selectorFamily
Both selectorFamily

async getter내에서 비동기 atom 조회

외부에서 가져다 쓰는 selector는 atom 동기인지 비동기인지 상관없이 동일하게 get으로 가져다 쓴다.

(내부적으로 generator를 사용하고 있을 것이라 생각함)

const weatherState = selectorFamily({
    key: 'weather',
    get: (userId: number) => async ({get}) => {
        get(weatherFetchIdState(userId))

        const user = get(userState(userId)) // 동기적으로 호출한다.
        const weather = await getWeather(user.address.city)
        return weather
    },
})

ErrorBoundary와 Data Refetch

ErrorBoundary

리코일과 suspense 덕택에 loading 플래그를 없앨 수 있었다.

이제 에러 처리를 컴포넌트에서 선언적으로 수행해보자.

먼저 아래의 라이브러리를 다운받는다.

(현재 리액트는 에러바운더리를 사용하려면 클래스 문법으로 직접 만들어야 한다. 만들어진걸 가져다 쓰자.)

import {ErrorBoundary, FallbackProps} from 'react-error-boundary'
// 4번 유저를 조회하면 에러를 리턴함. 
const userState = selectorFamily({
    key: 'user',
    get: (userId: number) => async () => {
        const userData = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`).then((res) => res.json())
        if (userId === 4) throw new Error('User does not exist')
        return userData
    },
})

const ErrorFallback = ({error, resetErrorBoundary}: FallbackProps) => {
    return (
        <div>
            <Heading as="h2" size="md" mb={1}>
                Something went wrong
            </Heading>
            <Text mb={1}>{error.message}</Text>
            <Button onClick={resetErrorBoundary}>Ok</Button>
        </div>
    )
}

// 사용 방법
<ErrorBoundary
    FallbackComponent={ErrorFallback}
    resetKeys={[userId]} // 해당 키 바뀌면 에러 없는것으로 처리
    onReset={() => { // resetErrorBoundary
        setUserId(undefined)
    }}
>
    {userId !== undefined && (
        <Suspense fallback={<div>Loading...</div>}>
            <UserData userId={userId} />
        </Suspense>
    )}
</ErrorBoundary>

Data Refetch

리코일의 get은 멱등성을 가진 연산이다. 즉, 같은 키로 여러번 조회하는 경우 항상 같은 결과를 리턴한다.

컴퓨팅에서 멱등 연산은 동일한 입력 매개변수로 두 번 이상 호출되는 경우 추가 효과가 없는 연산입니다.
예를 들어 집합에서 항목을 제거하는 것은 집합에 대한 멱등 연산으로 간주될 수 있습니다.

Idempotence: https://stackoverflow.com/questions/1077412/what-is-an-idempotent-operation

아톰키를 새로 만들어 데이터를 다시 가져와보자. atomEffect로 처리하는 방법을 추후에 알아볼 것이다.

 

논리는 다음과 같다.

weatherState는 userState와 weatherFetchIdState의 각 셀렉터/아톰에 의존한다.

refreshWeather를 사용하면 fetchId가 변경되어 weatherState가 업데이트 된다.

즉 아톰의 변경이 selector의 상태를 변경한다.

(atomFamily, selectorFamily 내부의 아톰은 전부 개별적이다.)

 

const weatherFetchIdState = atomFamily({
    key: 'weatherFetchId',
    default: 0,
})

const useRefreshWeather = (userId: number) => {
	// 사용자 id를 키로 아톰을 가져온다.
    const setFetchId = useSetRecoilState(weatherFetchIdState(userId))
    // 아톰의 값를 변경한다.
    return () => setFetchId((id) => id + 1)
}

// weatherState 각 셀렉터는 weatherFetchIdState의 각 사용자별 아톰(userState, fetchId)에 의존하고 있음.
const weatherState = selectorFamily({
    key: 'weather',
    get: (userId: number) => async ({get}) => {
    	// refetch를 위해 의존성 도입
        get(weatherFetchIdState(userId))
        const user = get(userState(userId))
        const weather = await getWeather(user.address.city)
        return weather
    },
})

 

반응형