SWR-Style Fetching with XState State Machines
Using state machines to intelligently refresh your data
imfeld.dev
SWR이 무엇이고 왜 유용한가요?
최신 데이터 보여주기
초기 로드 시 더 나은 동작
처음으로 데이터를 로드하는 것은 작지만 여전히 중요한 과제입니다.
일부 사이트는 서버 사이드 렌더링(SSR)을 사용하여 첫 페이지를 서버에서 통합하여 대기 시간을 줄입니다.
그러나 이것이 항상 훌륭한 해결책은 아닙니다.
로드되는 페이지의 초기 데이터는 빌드하는 데 시간이 걸리거나 사용 중인 웹 프레임워크가 SSR을 지원하지 않을 수 있습니다.
- 데이터가 로드되는 동안 아무 작업도 수행하지 않고(또는 로드 표시기를 표시) 데이터가 도착하면 페이지를 전환합니다.
- 페이지를 즉시 전환하되 데이터를 기다리는 동안 로딩 표시기를 표시합니다.
- 페이지에서 마지막으로 보여준 것을 저장하고, 새로운 데이터가 도착하기를 기다리는 동안 로컬 캐시에서 로드합니다.
SWR의 동작
- 로컬에 캐시된 데이터가 있는 경우 사용자가 즉시 유용한 것을 볼 수 있도록 먼저 해당 데이터를 반환합니다.
- 로컬로 캐시된 데이터를 가져온 후 충분한 시간이 지나면 "stale"이라고 부르고 데이터를 다시 가져옵니다.
- 주기적으로 SWR 프로세스가 활성화되어 있는 한 데이터가 오래되면 다시 가져옵니다.
Overview of the design (전체적인 디자인)
- "온라인 상태" 및 "브라우저 탭 포커스 상태"를 추적하여 새로 고침을 일시 중지할 때를 알 수 있습니다. 네트워크 연결이 없거나 사용자가 응용 프로그램을 사용하지 않는 경우 새로고침하고 싶지 않습니다.
- 라이브러리 클라이언트는 상태 시스템에 이벤트를 보내 현재 가져오지 않아야 함을 나타낼 수 있습니다.
- 사용자가 로그인되어 있지 않거나 특정 데이터 접근 권한이 없습니다.
- 우리는 이 데이터가 필요하지 않은 애플리케이션의 상태에 존재할 수 있습니다. (필요한 데이터만)
- 새로 고침 시간 간격을 설정할 수 있습니다.
- 데이터의 특성에 따라 새로 고침 사이에 몇 초, 1분, 1시간 또는 하루가 소요될 수 있습니다.
- 상태 머신이 활성화된 경우 지정된 시간이 경과하면 자동으로 데이터를 다시 가져옵니다.
- 클라이언트는 "부실" 데이터가 있는 경우 이를 가져오기 위해 처음에 호출되는 함수를 제공할 수 있습니다.
- 데이터 가져오기에 대한 세부 사항은 클라이언트에게 맡겨집니다. 유일한 요구 사항은 fetch 함수가 데이터를 resolve하는 promise를 반환한다는 것입니다.
- 가져오기(fetch) 함수는 또한 새로운 데이터가 존재하지 않았음을 나타내기 위해 specialUNMODIFIED 값을 반환할 수 있습니다.
- 이는 일반적으로 가져오기 요청이 etags 또는 If-Modified-Since 헤더를 사용하고 서버에서 데이터가 변경되지 않았음을 나타낼 때 발생합니다.
- fetcher 함수에는 새로운 데이터가 도착하거나 오류가 발생했을 때 호출하는 함수를 제공할 수 있습니다.
- 오류가 발생하면 지수 백오프 타이머(exponential backoff timer)를 사용하여 가져오기가 자동으로 재시도됩니다.
- 캐시 관리
- 단일 상태 머신으로 여러 클라이언트 관리
- 페이징/무한 "추가 가져오기" 기능.
- 서버에서 받은 마지막 데이터가 있는 데이터에 보류 중인 돌연변이를 병합합니다.
When to Fetch
Retrying on error
Quick XState Overview
Events
Actions
More details about actions at Actions | XState Docs.
상태 기계 컨텍스트
Implementation(구현)
Fetcher 생성 옵션
- fetcher는 데이터를 가져오는 함수입니다. 상태 머신은 새로 고칠 때마다 이 함수를 호출합니다.
- receive는 일부 데이터를 수신했거나 오류가 발생했을 때 페처에서 호출하는 함수입니다. 부작용과 관련한, fetcher의 출력입니다.
- initialData는 첫 번째 가져오기가 성공하기 전에 사용할 데이터를 반환하는 선택적 함수입니다. 해당 함수가 제공된 경우 페처는 처음 생성될 때 이 함수를 호출합니다. 이것은 일반적으로 일종의 캐시에서 읽힙니다.
- key는 fetcher 및 initialData 함수에 전달되는 값입니다. fetcher는 해당 키가 주어지지 않으면 키를 사용하지 않습니다.
- name은 디버그 출력에 사용되는 문자열입니다. 제공되지 않으면 기본적으로 key로 설정됩니다.
- autoRefreshPeriod는 데이터를 다시 새로 고칠 때까지 기다려야 하는 시간을 결정합니다.
- maxBackoff는 오류 후 재시도할 때 가져오기 사이에 대기하는 가장 긴 시간입니다.
- initialPermitted 및 initialEnabled는 페처가 생성될 때 허용되고 활성화되어야 하는지 여부를 나타냅니다. 기본값은 true이지만 false인 경우 상태 시스템은 관련 이벤트를 가져올 수 있을 때까지 기다립니다.
상태 기계 컨텍스트 구현
fetcher는 컨텍스트에 다음 값을 유지합니다.
- lastRefresh는 이전 새로 고침이 발생한 시간을 기록합니다. 이를 통해 다음 새로 고침이 언제 발생해야 하는지 계산할 수 있습니다.
- retries는 가져오기에 실패하고 다시 시도한 횟수입니다.
- reportedError는 우리가 실패하고 가져오기 오류를 보고했는지 여부를 나타냅니다. 이는 동일한 오류를 반복해서 보고하지 않도록 하기 위한 것입니다.
- storeEnabled, browserEnabled 및 permitted는 저장소를 새로 고칠 수 있는지 여부를 추적합니다. 이것들은 또한 머신의 상태와 연관되어 있지만 일부 이벤트는 새로 고침을 강제할 수 있으며 이러한 플래그를 보고 새로 고침이 완료된 후 어떤 상태로 돌아갈지 확인하는 것이 유용합니다.
상태 기계 상태 구현
이 모든 설명과 디자인 작업에도 불구하고 실제 상태 머신은 상당히 간단합니다.maybeStart
maybeStart: {
always: [
{ cond: 'not_permitted_to_refresh', target: 'notPermitted' },
{ cond: 'can_enable', target: 'waitingForRefresh' },
{ target: 'disabled' },
],
},
XState Guards
이러한 전환은 전환이 실행되기 위해 참이어야 하는 조건을 나타내는 cond 키워드를 사용합니다.
XState는 이러한 조건 가드를 호출하며 상태 머신 설정은 다음과 같습니다.
guards: {
not_permitted_to_refresh: (ctx) => !ctx.permitted,
permitted_to_refresh: (ctx) => ctx.permitted,
can_enable: (ctx) => {
if (!ctx.storeEnabled || !ctx.permitted) {
return false;
}
if (!ctx.lastRefresh) {
// Refresh if we haven’t loaded any data yet.
return true;
}
// Finally, we can enable if the browser tab is active.
return ctx.browserEnabled;
},
},
글로벌 이벤트 핸들러
on: {
FETCHER_ENABLED: { target: 'maybeStart', actions: 'updateStoreEnabled' },
SET_PERMITTED: { target: 'maybeStart', actions: 'updatePermitted' },
BROWSER_ENABLED: {
target: 'maybeStart',
actions: 'updateBrowserEnabled',
},
},
권한 없음(notPermitted), 비활성화(desabled)
// Not permitted to refresh, so ignore everything except the global events that might permit us to refresh.
notPermitted: {
entry: ['clearData', 'clearLastRefresh'],
},
// Store is disabled, but still permitted to refresh so we honor the FORCE_REFRESH event.
disabled: {
on: {
FORCE_REFRESH: {
target: 'refreshing',
cond: 'permitted_to_refresh',
},
},
},
waitingForRefresh
새로 고침이 활성화된 동안 상태 시스템은 새로 고칠 시간이 될 때까지 waitForRefresh 상태에서 기다립니다.
FORCE_REFRESH 이벤트는 여전히 새로 고침을 즉시 트리거할 수 있습니다.
waitingForRefresh: {
on: {
FORCE_REFRESH: 'refreshing',
},
after: {
nextRefreshDelay: 'refreshing',
},
}
Delays
after: {
400: 'slowLoading'
}
delays: {
nextRefreshDelay: (context) => {
let timeSinceRefresh = Date.now() - context.lastRefresh;
let remaining = autoRefreshPeriod - timeSinceRefresh;
return Math.max(remaining, 0);
},
errorBackoffDelay: /* details later */,
},
리프레시
refreshing: {
on: {
// Ignore the events while we're refreshing but still update the
// context so we know where to go next.
FETCHER_ENABLED: { target: undefined, actions: 'updateStoreEnabled' },
SET_PERMITTED: { target: undefined, actions: 'updatePermitted' },
BROWSER_ENABLED: {
target: undefined,
actions: 'updateBrowserEnabled',
},
},
// An XState "service" definition
invoke: {
id: 'refresh',
src: 'refresh',
onDone: {
target: 'maybeStart',
actions: 'refreshDone',
},
onError: {
target: 'errorBackoff',
actions: 'reportError',
},
},
},
Global Event Handler Overrides
on: {
FETCHER_ENABLED: { target: 'maybeStart', actions: 'updateStoreEnabled' },
SET_PERMITTED: { target: 'maybeStart', actions: 'updatePermitted' },
BROWSER_ENABLED: {
target: 'maybeStart',
actions: 'updateBrowserEnabled',
},
},
XState Services
- Promise가 실행된 다음 resolve하거나 reject합니다.
- rxjs 라이브러리에 구현된 것과 같은 Observable은 여러 이벤트를 보낸 다음 완료할 수 있습니다.
- 서비스는 또한 현재 상태 시스템과 앞뒤로 통신하는 전체 상태 머신이 될 수도 있습니다.
- 호출된 머신이 최종 상태가 되면 서비스가 완료된 것으로 간주됩니다.
상태의 invoke 개체는 서비스를 정의합니다. src 키는 호출할 서비스를 나타내며 서비스 타입에 따라 onDone 및 onError는 수행할 다음 전환 및 액션을 정의합니다.
여기서는 클라이언트가 제공한 fetcher 함수를 호출하고 프라미스를 반환하는 하나의 서비스만 사용합니다.
결과 다루기
onDone: {
target: 'maybeStart',
actions: 'refreshDone',
},
refreshDone: assign((context, event) => {
let lastRefresh = Date.now();
let updated = {
lastRefresh,
retries: 0,
reportedError: false,
};
if(event.data !== UNMODIFIED && context.permitted) {
receive({ data: event.data, timestamp: lastRefresh });
}
return updated;
})
onError: {
target: 'errorBackoff',
actions: 'reportError',
},
reportError: assign((context: Context, event) => {
// Ignore the error if it happened because the browser went offline while fetching.
// Otherwise report it.
if (
!context.reportedError &&
browserStateModule.isOnline() // See the Github repo for this function
) {
receive({ error: event.data });
}
return {
reportedError: true,
};
}),
errorBackoff
errorBackoff: {
entry: ‘incrementRetry’,
after: {
errorBackoffDelay: ‘refreshing’,
},
},
incrementRetry: assign({ retries: (context) => context.retries + 1 }),
delays: {
errorBackoffDelay: (context, event) => {
const baseDelay = 200;
const delay = baseDelay * (2 ** context.retries);
return Math.min(delay, maxBackoff);
},
}
앱에서 사용하기
import { writable } from 'svelte/store';
export function autoFetchStore({url, interval, initialDataFn}) {
var store = writable({}, () => {
// When we get our first subscriber, enable the store.
f.setEnabled(true);
// Then disable it when we go back to zero subscribers.
return () => f.setEnabled(false);
});
var f = fetcher({
key: url,
autoRefreshPeriod: interval,
fetcher: () => fetch(url).then((r) => r.json()),
receive: store.set,
initialData: initialDataFn,
initialEnabled: false,
});
return {
subscribe: store.subscribe,
destroy: f.destroy,
refresh: f.refresh,
};
}
주 : 위의 복잡한 내용의 구현이 얼마나 읽기 쉬운지 repo에 들어가서 확인해보자!
'FrontEnd' 카테고리의 다른 글
Recoil로 Todo List 만들기. (0) | 2022.05.14 |
---|---|
Recoil 배경 및 기본 알아보기 (0) | 2022.05.14 |
제어의 역전을 활용해 타입 친화적인 컨텍스트를 디자인하는 방법 (0) | 2022.05.05 |
XState와 React를 활용한 기본 UI 예제 구현(7-GUIs) (0) | 2022.05.05 |
XState : 타입스크립트와 같이 활용하기 (0) | 2022.05.05 |