This post is not yet available in English and is shown in Korean.
프로미스 객체가 생기는 시점과 비동기 요청이 이루어지는 시점은 일반적으로
동일하다. 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('...');
// select까지 적용된 쿼리를 실행
let { data, error } = await query;
query = query.order('created_at', { ascending: true });
// order까지 적용된 쿼리를 실행
{ 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>;
}
그렇다면 PromiseLike를 구현한 PostgrestBuilder도 then 메서드를
구현했을테니 살펴보자.
export default abstract class PostgrestBuilder {
...
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 메서드에서 이루어짐을 확인할 수 있었다.
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을 사용할 수 있습니다.