취소 가능한 async 작업 및 SolidJS와 LazyPromise를 사용한 타입된 서버 오류
Source: Dev.to
취소 가능한 비동기 작업
왜 비동기 작업을 취소해야 할까요?
서버 요청을 보냈다면 이미 네트워크를 통해 전송된 상태이지만, 브라우저에서 이를 중단해야 하는 경우가 있습니다.
예시: OTP 코드 인증 흐름
- 사용자가 이메일을 입력하고 로그인 코드를 요청합니다.
- 앱은 사용자가 받은 코드를 입력할 수 있는 페이지를 표시합니다.
- 코드는 서버로 전송되고, 서버는
Set‑Cookie헤더를 반환해 사용자를 인증하고 대시보드로 리다이렉트합니다.
코드와 함께 보낸 요청이 아직 진행 중인데 사용자가 뒤로 가기를 눌러 다른 이메일로 로그인하려고 하면, 남아 있던 요청이 결국 잘못된 쿠키를 설정하게 됩니다. 이런 상황에서는 브라우저 수준에서 요청을 중단해야 합니다.
LazyPromise와 Solid
얼마 전 LazyPromise라는 원시 개념을 만들었습니다. API는 네이티브 Promise와 비슷하지만 취소가 가능합니다. LazyPromise는 .subscribe(handleValue, handleError) 메서드를 제공하며, 이 메서드는 해제 핸들을 반환합니다.
React에서 Solid로 앱을 포팅하면서, 해제 핸들을 Solid의 onCleanup에 전달해 Solid 스코프가 폐기될 때 비동기 작업이 즉시 취소되도록 하는 useLazyPromise 라는 연결 함수를 작성했습니다:
useLazyPromise(myLazyPromise, handleValue, handleError);
왜 AbortSignal만 사용하지 않을까?
네이티브 Promise를 AbortSignal로 감쌀 수 있습니다:
useAbortablePromise(async (abortSignal) => {
// fetch things
});
LazyPromise는 이러한 래퍼라고 볼 수 있습니다. 유틸리티 lazy는 async 함수를 LazyPromise로 변환합니다:
useLazyPromise(
lazy(async (abortSignal) => {
// fetch things
}),
handleValue,
handleError
);
async (abortSignal) => …와 같은 함수를 작성하면 이미 지연된 프라미스를 다루는 것이며, 실제 LazyPromise 객체를 사용하면 아래에서 논의하는 몇 가지 추가 이점을 얻을 수 있습니다.
타입이 지정된 서버 오류
내 프로젝트에서는 타입 시스템이 서버 엔드포인트가 발생시킬 수 있는 오류를 반영합니다. 서버 핸들러는 일반 async/await를 사용하고, 오류가 발생하면 { __error: … } 형태의 객체를 반환하므로 결과 타입은 Data | { __error: Error }(또는 { data: Data } | { error: Error })가 됩니다.
클라이언트는 tRPC를 통해 통신하고, 래퍼가 tRPC 응답을 LazyPromise로 변환합니다 (gist).
const lazyPromise = trpcLazyPromise(api.authn.checkOtpCode.mutate)({
/* params */
});
lazyPromise는 Data와 Error에 대해 제네릭을 가집니다. 반환 타입을 Data | { __error: Error }로 유지하면 커스텀 프라미스 없이도 오류 타입을 포착할 수 있지만, Promise.all 같은 유틸리티가 깨지고 코드 가독성이 떨어집니다.
Effect(React)와 같은 라이브러리도 타입이 지정된 오류를 제공하지만 완전히 새로운 API를 요구합니다. LazyPromise는 친숙한 Promise‑유사 API를 유지하면서 취소 가능성과 타입이 지정된 오류를 추가합니다.
Solid와 LazyPromise 사용
지연성, 취소 가능성 및 타입이 지정된 오류
LazyPromise가 네이티브 Promise와 다른 두 가지 점을 살펴봤습니다:
- 지연성 / 취소 가능성 – Solid 스코프가 폐기될 때 작업을 중단할 수 있습니다.
- 타입이 지정된 오류 – 오류 타입이 프라미스의 제네릭 파라미터에 포함됩니다.
동기 해결
세 번째 차이점: LazyPromise는 동기적으로(같은 틱 안에서) 해결됩니다. 이는 Solid의 미세 입자 반응성에 잘 맞습니다.
useLazyPromiseValue
useLazyPromiseValue는 Solid의 createResource와 유사한 원시 버전입니다. 로딩 심볼을 먼저 반환하고, 이후에 해결된 값을 반환하는 accessor를 반환합니다.
// `value` is an accessor that first returns a Symbol loadingSymbol,
// then the resolved value.
const value = useLazyPromiseValue(() => getLazyPromise(mySignal()));
지연 프라미스가 동기적으로 해결될 때 동작은 useMemo와 유사합니다:
// `resolved` is like Promise.resolve.
useLazyPromiseValue(() => resolved(mySignal()));
// behaves the same as:
useMemo(() => mySignal());
결론
이 방식을 직접 사용해 보고 싶다면, LazyPromise 라이브러리는 메모리 누수 테스트를 포함해 충분히 검증되었으며 안정적입니다 (source, introductory article). SolidJS 바인딩은 실험 단계이지만 here에서 확인할 수 있습니다. 특히 다가오는 async signals 기능과 이 접근 방식이 어떻게 상호 작용할지에 대한 의견을 댓글로 남겨 주세요.