TIL 025 자동으로 동기적인 api 보내기

조성현·2021년 8월 16일
0

🤷‍♂️ 상황설명

다음과 같은 설정을 해보자, 한 곳에 api를 보내서 토큰을 받고, 또 그 토큰을 보내서 api를 보내고, 또 그 토큰을 보내고.

const token1 = await api1()
const token2 = await api2(token1)
const result = await api3(token2)

위 처럼 몇 개가 있는 지 미리 알 수 있다면 await로 간단하게 구현할 수 있지만. 몇 개의 요청이 있는지 모르는 상황에서 자동으로 순서대로 토큰을 기다렸다가 api 요청을 보내도록 할 수 있을까?

#try1

우선 다음과 같은 api가 있다고 하고 해결법을 찾아보자. 각 토큰에 따라 다른 응답을 보낸다.

const callAPI = token => {
    console.log("[요청] 요청한 token:", token);
    return new Promise(res =>
        setTimeout(() => {
            if (!token) {
                res(["첫요청결과", 1]);
            } else if (token === 3) {
                res(["마지막요청결과", null]);
            } else {
                res([token + "번 토큰에대한 요청결과", token + 1]);
            }
        }, 1000)
    );
};

첫 번째로 생각한 해결 방법은 싱글톤으로 이전 promise를 기록하는 것이었다. 그래서 이전 promise가 있다면 그 promise의 응답을 기다렸다가 요청을 보내는 것이다.

class Save {
    static instance;
    constructor() {
        if (Save.instance) return Save.instance;
        this.promise = null;
        Save.instance = this;
    }
}

async function solution(callAPI) {
    const save = new Save();
    if (save.promise === null) {
        const promise = callAPI();
        save.promise = promise;
        const [result, token] = await promise;
        console.log(result, token);
        return result;
    } else {
        save.promise.then(async ([_, tokenBefore]) => {
            const promise = callAPI(tokenBefore);
            save.promise = promise;
            const [result, token] = await promise;
            console.log(result, token);
            return result;
        });
    }
}

이렇게 했을 경우, 첫번째 응답을 기다리는데는 성공을 했지만, .then 이후에 저장한 promise가 바뀌기 때문에 두 번째, 세 번째 요청도 첫번째 프로미스를 기다리게 되었다.

#try2

그렇다면 큐를 만들어 요청이 들어오면 담고, 요청이 들어오면 앞의 큐의 프로미스를 기다리게 해보았다.

class Save {
    static instance;
    constructor() {
        if (Save.instance) return Save.instance;
        this.queue = [];
        this.cnt = 0;
        Save.instance = this;
    }
}

async function solution(callAPI) {
    const save = new Save();

    if (save.queue.length === 0) {
        save.queue.push(callAPI());
        save.cnt += 1;
        const [result, token] = await save.queue[0];
        console.log(result,token);
        return result;
    } else {
        console.log("이전 프로미스: " + save.queue[save.cnt-1])
        const cur = save.cnt
        save.queue[save.cnt-1].then(async ([_, tokenBefore]) => {
            save.queue.push(callAPI(tokenBefore));
            const [result, token] = await save.queue[cur];
            console.log(result,token);
            return result;
        });
        // save.queue[cur] = new Promise();
        save.cnt += 1;
    }
}

queue[cur-1] 로 접근을 해서 Promise가 배열에는 없지만 식별자로 접근해 .then이 작동하기를 기대했지만 undefined에는 작동하지 않았다. 실제로 promise가 채워지는 것은 역시 앞선 promise가 완료되었을 때이기 때문이다. 혹시 몰라 빈 new Promise를 미리 넣어도 보았지만 resolve 함수를 제대로 해놓지 않았기 때문에 작동하지 않았다.

#try3

api를 요청하는 프로미스는 결국 앞선 응답이 모두 완료되 시점에서 보내져야 된다는 것을 깨닫고, 뒤늦게 api를 요청하는 방법을 생각해야 했다.

그래서 트리거라는 배열을 만들어 프로미스의 resolve를 담은 함수를 담아서 api 요청을 대기시켰다. 이 후 앞선 api 요청이 완료된다면 배열에서 다음 순서의 트리거를 찾아 하나씩 실행시켰다.

class SyncAPI {
    static instance;
    constructor() {
        if (SyncAPI.instance) return SyncAPI.instance;
        this.tokens = [];
        this.triggers = [];
        this.cnt = 0;
        SyncAPI.instance = this;
    }

    async firstAPI(callAPI) {
        this.cnt += 1;
        const [result, token] = await callAPI();
        console.log("[응답] " + result, token);
        this.tokens[0] = token;
        if (this.triggers[1]) this.triggers[1]();
        return [result, token];
    }

    async wait(callAPI) {
        const cur = this.cnt;
        this.cnt += 1;
        const waitPromise = new Promise(res => (this.triggers[cur] = () => res()));
        await waitPromise;
        const [result, token] = await callAPI(this.tokens[cur - 1]);
        console.log("[응답] " + result, token);
        this.tokens[cur] = token;
        if (this.triggers[cur + 1]) this.triggers[cur + 1]();
        return [result, token];
    }

    async addAPI(callAPI) {
        if (this.cnt === 0) return await this.firstAPI(callAPI);
        else return await this.wait(callAPI);
    }
}

async function solution(callAPI) {
    const syncAPI = new SyncAPI();
    const [result, token] = await syncAPI.addAPI(callAPI);
    return [result, token];
}

solution(callAPI);
solution(callAPI);
solution(callAPI);
solution(callAPI);

순서대로 동기적으로 잘 작동한다!

물론, 위 함수를 제대로 쓰려면 token이 이미 받아져있을때는 제외처리 해야한다.

📘 TIL

  1. 미래의 만들어질 프로미스를 같은 식별자라도 미리 then, await로 대기시킬 수는 없다. (결국 실행 시점에서 어떤 resolve를 참조하느냐가 중요한듯)

  2. 이미 응답 받은 promise에 대해서는 await 시켜도 즉시 리턴값을 가져다 쓴다.

2번에 대해서는 궁금해서 실험을 해봤다.

const fulfiledPromise = new Promise(res => res("fulfiled"));

console.log(fulfiledPromise);

const test = async () => {
    const get = await fulfiledPromise;
    console.log(get);
};

test();

이미 fufiled된 promise에 대해서는 await가 바로 그 리턴값을 가져다가 사용하는 모습이다.

(추가)

이를 더욱 더 아름답게 해결할 수 있는 것이 프로미스 패턴이다.
이 글을 통해 더 공부한 내용을 적어놨다.

profile
Jazzing👨‍💻

0개의 댓글

관련 채용 정보