Supabase로 db를 조회하던 코드를 짜던 중 비동기 요청은 어느 시점에 되는지에 대한 궁금증이 생겼습니다. 평소에 프로미스 객체를 쓸 때와는 다른 경험이었거든요.
프로미스 객체가 생기는 시점과 비동기 요청이 이루어지는 시점은 일반적으로
동일합니다. fetch
나 setTimeout
를 사용할 때를 생각해보세요. 예를 들어
setTimeout
을 사용해 만든 아래 delay
함수는 프로미스 객체가 생성되는 즉시
비동기 작업이 시작됩니다.
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
delay(1000); // await 유무와 무관하게 여기서 바로 1초를 세기 시작
하지만 supabase를 사용할 때는 await 이전까지는 비동기 요청을 하지 않습니다. 이 특성 덕분에 조건부로 쿼리를 추가한다음 쿼리가 완성되면 await로 비동기 요청을 시작할 수도 있습니다.
let query = supabase.from('...').select('...');
if (tagIds) query = query.in('id', tagIds);
const { data, error } = await query;
하나 더 특이한 점은 await를 여러번 한다고 해도 그때그때의 쿼리에 맞춰 응답이 잘 온다는 점입니다.
let query = supabase.from('...').select('...');
let { data, error } = await query; // ✅
query = query.order('created_at', { ascending: true });
{ data, error } = await query; // ✅
select
나 order
메서드들의 타입을 보면 PostgrestTransformBuilder
,
PostgrestFilterBuilder
같이 프로미스가 아닌 커스텀 객체를 반환합니다. 쿼리를
단계별로 만들기위해
빌더 패턴를 활용한건
알겠는데 어떻게 프로미스가 아닌 객체에 await을 할 수 있는걸까요?
supabase 소스코드를
확인해보면 PostgrestTransformBuilder
나 PostgrestFilterBuilder
모두
PostgrestBuilder
를 상속(extends)받은 클래스이며 PostgrestBuidler
는
PromiseLike
를 구현(implements)하고 있습니다.
export default abstract class PostgrestBuilder<
Result,
ThrowOnError extends boolean = false,
> implements
PromiseLike<
ThrowOnError extends true
? PostgrestResponseSuccess<Result>
: PostgrestSingleResponse<Result>
> {
// ...
}
TypeScript 소스코드를
보면 PromiseLike
란 then
메서드를 구현한 객체를 의미합니다.
interface PromiseLike<T> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult1 = T, TResult2 = never>(
onfulfilled?:
| ((value: T) => TResult1 | PromiseLike<TResult1>)
| undefined
| null,
onrejected?:
| ((reason: any) => TResult2 | PromiseLike<TResult2>)
| undefined
| null,
): PromiseLike<TResult1 | TResult2>;
}
그렇다면 PostgrestBuilder
는 then
메서드를 구현했을테니 살펴봅시다.
// 일부 코드 생략
export default abstract class PostgrestBuilder {
constructor(builder: PostgrestBuilder) {
//...
if (builder.fetch) {
this.fetch = builder.fetch;
} else if (typeof fetch === 'undefined') {
this.fetch = nodeFetch;
} else {
this.fetch = fetch;
}
}
then(onfulfilled?, onrejected?): PromiseLike<TResult1 | TResult2> {
// ...
const _fetch = this.fetch;
let res = _fetch(this.url.toString(), {
method: this.method,
headers: this.headers,
body: JSON.stringify(this.body),
signal: this.signal,
}).then(async (res) => {
// ...
});
// ...
return res.then(onfulfilled, onrejected);
}
}
실제 네트워크 요청은 then
메서드에서 이루어짐을 확인할 수 있었습니다.
생성자에서 supabase를 사용하는 환경별로 fetch
함수를 설정해주는 것도
인상깊네요.
await의 대상이 모두 프로미스 객체여야한다는 고정관념을 버려야겠습니다. 아래 코드도 잘 동작합니다.
const obj = {
then(resolve, reject) {
resolve('hello');
},
};
async function test() {
const res = await obj;
console.log(res);
}
test();
mdn의 Promise 문서에도 참고할만한 내용이 있어서 가져왔어요:
JavaScript 생태계는 프로미스가 언어의 일부가 되기 훨씬 전부터 여러 가지 프로미스 구현을 만들어왔습니다. 내부적으로 다르게 표현되기는 하지만, 최소한 모든 프로미스와 유사한 객체는 Thenable 인터페이스를 구현합니다. thenable은 두 개의 콜백(하나는 프로미스가 이행될 때, 다른 하나는 거부될 때)과 함께 호출되는 .then() 메서드를 구현합니다. 프로미스 또한 thenable입니다.
기존 프로미스 구현과 상호 운용하기 위해 언어에서는 프로미스 대신 thenables을 사용할 수 있습니다.
댓글을 작성하려면 이 필요합니다.
아직 댓글이 없습니다. 첫 댓글을 남겨보세요!