본문 바로가기
행복바이러스 행복바이러스

Bubble.io Custom Plugin 개발을 위한 JavaScript 비동기 데이터 처리 기법

읽는 시간 약 20분

Bubble.io는 코드를 거의 사용하지 않고 웹 애플리케이션을 만들 수 있는 노코드 플랫폼입니다. 하지만 외부 API 연동이나 복잡한 데이터 가공처럼 기본 기능만으로 구현하기 어려운 작업에서는 Custom Plugin이 필요합니다.

특히 플러그인에서 외부 서버의 데이터를 가져오거나 Bubble 데이터 목록을 처리할 때는 JavaScript의 비동기 실행 방식을 정확히 이해해야 합니다. 비동기 처리가 잘못되면 결과값이 반환되기 전에 워크플로가 끝나거나, API 요청이 중복 실행되는 문제가 발생할 수 있습니다.

이번 글에서는 Bubble.io Custom Plugin을 개발할 때 자주 사용하는 JavaScript 비동기 데이터 처리 방법을 실제 코드 중심으로 살펴보겠습니다.

1. 비동기 데이터 처리가 필요한 이유

다음과 같은 작업은 실행 결과가 즉시 반환되지 않습니다.

  • 외부 REST API 호출
  • Bubble 데이터 목록 불러오기
  • 이미지 또는 파일 업로드
  • 결제 서버 응답 대기
  • 여러 데이터의 순차 처리
  • 외부 데이터베이스 조회

예를 들어 상품 정보를 제공하는 외부 API를 호출한다고 가정해 보겠습니다.

const response = fetch("https://api.example.com/products");
console.log(response);

위 코드에서 response에는 실제 상품 데이터가 아니라 Promise 객체가 들어갑니다. 서버의 응답을 기다리지 않고 다음 코드가 먼저 실행되기 때문입니다.

따라서 async와 await를 사용해 응답이 완료될 때까지 기다려야 합니다.

async function loadProducts() {
    const response = await fetch("https://api.example.com/products");
    const data = await response.json();

    console.log(data);
}

await는 Promise의 처리가 완료될 때까지 해당 함수의 실행을 잠시 기다리게 합니다. 단, await는 일반적으로 async 함수 안에서 사용해야 합니다.

2. Bubble 서버 측 액션에서 async·await 사용하기

Bubble Plugin Editor에서 Server-side Action을 작성한다면 최상위 함수를 async function으로 선언할 수 있습니다.

async function(properties, context) {
    const response = await fetch(properties.api_url);

    if (!response.ok) {
        throw new Error(
            `API 요청 실패: ${response.status} ${response.statusText}`
        );
    }

    const data = await response.json();

    return {
        result_text: JSON.stringify(data),
        status_code: response.status
    };
}

이 코드의 실행 순서는 다음과 같습니다.

  1. Bubble에서 전달받은 api_url로 요청합니다.
  2. 서버 응답이 올 때까지 기다립니다.
  3. 정상 응답이 아니면 오류를 발생시킵니다.
  4. 응답 본문을 JSON으로 변환합니다.
  5. Bubble 워크플로에서 사용할 결과를 반환합니다.

Bubble Plugin API v4는 Node 18 환경과 Promise 기반 비동기 처리를 사용합니다. Bubble Thing의 get(), Bubble List의 get()과 length()도 Promise를 반환하므로 await를 적용해야 합니다.

3. API 오류를 안전하게 처리하는 방법

외부 API 요청은 항상 성공한다고 가정하면 안 됩니다. 서버 점검, 인증키 오류, 호출 한도 초과 또는 네트워크 장애가 발생할 수 있기 때문입니다.

async function(properties, context) {
    try {
        const response = await fetch(properties.api_url, {
            method: "GET",
            headers: {
                "Authorization": `Bearer ${context.keys.API_KEY}`,
                "Accept": "application/json"
            }
        });

        if (!response.ok) {
            throw new Error(`HTTP 오류: ${response.status}`);
        }

        const data = await response.json();

        return {
            success: true,
            result_text: JSON.stringify(data),
            error_message: ""
        };

    } catch (error) {
        return {
            success: false,
            result_text: "",
            error_message: error.message
        };
    }
}

try...catch를 사용하면 플러그인 전체가 예기치 않게 중단되는 것을 줄일 수 있습니다. Bubble 워크플로에서는 success 값이 yes일 때만 다음 단계를 실행하도록 조건을 설정할 수 있습니다.

API 키는 코드나 입력 필드에 직접 작성하지 말고 Plugin Editor의 비공개 키 설정을 이용해 context.keys로 불러오는 편이 안전합니다.

4. Bubble Thing과 List를 비동기로 처리하기

Plugin API v4에서는 Bubble Thing의 필드를 가져올 때도 await가 필요합니다.

async function(properties, context) {
    const userName = await properties.user_thing.get("name");
    const userEmail = await properties.user_thing.get("email");

    return {
        user_name: userName || "",
        user_email: userEmail || ""
    };
}

Bubble List는 전체 데이터를 한 번에 불러오기보다 필요한 범위만 나누어 처리하는 것이 좋습니다.

async function(properties, context) {
    const total = await properties.product_list.length();
    const limit = Math.min(total, 100);
    const products = await properties.product_list.get(0, limit);

    const names = [];

    for (const product of products) {
        const name = await product.get("name");
        names.push(name);
    }

    return {
        product_count: total,
        product_names: names
    };
}

Bubble List는 데이터가 많을 수 있어 모든 항목을 처음부터 메모리에 불러오지 않습니다. 공식 문서에서도 List의 length()와 get()이 Promise를 반환한다고 설명합니다.

목록이 수천 건이라면 한 번에 처리하지 말고 50~100개 단위로 분할하거나 Backend Workflow를 이용하는 것이 안정적입니다.

5. 순차 처리와 병렬 처리의 차이

다음 코드는 API 요청을 하나씩 순서대로 실행합니다.

const results = [];

for (const id of productIds) {
    const response = await fetch(
        `https://api.example.com/products/${id}`
    );

    const product = await response.json();
    results.push(product);
}

항목이 10개이고 각 요청에 1초가 걸린다면 전체 처리 시간이 약 10초가 될 수 있습니다.

각 요청이 서로 의존하지 않는다면 Promise.all()을 이용해 병렬로 처리할 수 있습니다.

const requests = productIds.map(async (id) => {
    const response = await fetch(
        `https://api.example.com/products/${id}`
    );

    if (!response.ok) {
        throw new Error(`상품 ${id} 조회 실패`);
    }

    return response.json();
});

const results = await Promise.all(requests);

병렬 처리는 빠르지만 요청을 지나치게 많이 보내면 외부 API의 호출 제한에 걸릴 수 있습니다. 따라서 대량 데이터는 일정한 크기로 나누어 처리해야 합니다.

async function processInBatches(ids, batchSize = 5) {
    const results = [];

    for (let i = 0; i < ids.length; i += batchSize) {
        const batch = ids.slice(i, i + batchSize);

        const batchResults = await Promise.all(
            batch.map(async (id) => {
                const response = await fetch(
                    `https://api.example.com/products/${id}`
                );

                if (!response.ok) {
                    throw new Error(`상품 ${id} 조회 실패`);
                }

                return response.json();
            })
        );

        results.push(...batchResults);
    }

    return results;
}

이 방식은 한 번에 실행되는 요청 수를 제한하면서도 순차 처리보다 빠르게 데이터를 가져올 수 있다는 장점이 있습니다.

6. 응답 시간 제한 추가하기

외부 서버가 응답하지 않으면 요청이 지나치게 오래 유지될 수 있습니다. AbortController를 사용하면 제한 시간이 지난 요청을 중단할 수 있습니다.

async function fetchWithTimeout(url, timeout = 8000) {
    const controller = new AbortController();

    const timer = setTimeout(() => {
        controller.abort();
    }, timeout);

    try {
        const response = await fetch(url, {
            signal: controller.signal
        });

        if (!response.ok) {
            throw new Error(`HTTP 오류: ${response.status}`);
        }

        return await response.json();

    } finally {
        clearTimeout(timer);
    }
}

사용 방법은 다음과 같습니다.

async function(properties, context) {
    try {
        const data = await fetchWithTimeout(
            properties.api_url,
            8000
        );

        return {
            success: true,
            result_text: JSON.stringify(data),
            error_message: ""
        };

    } catch (error) {
        const message =
            error.name === "AbortError"
                ? "외부 서버의 응답 시간이 초과되었습니다."
                : error.message;

        return {
            success: false,
            result_text: "",
            error_message: message
        };
    }
}

결제, 로그인, AI API처럼 응답 지연 가능성이 있는 서비스와 연동할 때 유용한 방식입니다.

7. Client-side Element의 중복 요청 방지

Bubble Element의 update 함수는 속성값이 변경될 때 여러 번 호출될 수 있습니다. update 안에서 조건 없이 API를 호출하면 같은 요청이 반복될 가능성이 있습니다.

function(instance, properties, context) {
    if (!properties.api_url) return;

    if (instance.data.lastUrl === properties.api_url) {
        return;
    }

    instance.data.lastUrl = properties.api_url;

    loadData();

    async function loadData() {
        try {
            instance.publishState("is_loading", true);

            const response = await fetch(properties.api_url);

            if (!response.ok) {
                throw new Error(`HTTP 오류: ${response.status}`);
            }

            const data = await response.json();

            instance.publishState(
                "result",
                JSON.stringify(data)
            );

            instance.triggerEvent("data_loaded");

        } catch (error) {
            instance.publishState(
                "error_message",
                error.message
            );

            instance.triggerEvent("data_failed");

        } finally {
            instance.publishState("is_loading", false);
        }
    }
}

마지막으로 요청한 URL을 instance.data.lastUrl에 저장하면 동일한 값으로 반복 호출되는 것을 줄일 수 있습니다. 로딩 상태와 오류 메시지를 별도의 Exposed State로 제공하면 Bubble 앱 제작자가 워크플로를 구성하기도 쉬워집니다.

8. 플러그인 성능을 높이는 실무 원칙

비동기 코드를 작성할 때는 다음 기준을 적용하는 것이 좋습니다.

  • 서로 의존하는 작업은 await로 순차 실행합니다.
  • 독립적인 소수의 요청은 Promise.all()로 처리합니다.
  • 대량 요청은 일정한 크기의 배치로 나눕니다.
  • 모든 HTTP 응답에서 response.ok를 확인합니다.
  • API 요청에는 제한 시간을 설정합니다.
  • update 함수의 중복 실행을 방지합니다.
  • 긴 작업은 클라이언트보다 Server-side Action이나 Backend Workflow에서 처리합니다.
  • API 키와 비밀값은 브라우저 코드에 노출하지 않습니다.
  • 필요한 필드와 데이터만 반환해 응답 크기를 줄입니다.
  • 실패 원인을 Bubble에서 확인할 수 있도록 오류값을 반환합니다.

Bubble의 Backend Workflow는 페이지와 독립적으로 서버에서 실행되므로 사용자가 브라우저를 닫은 이후에도 계속 실행해야 하는 작업에 적합합니다.

9. Plugin API v3에서 v4로 변경할 때 주의할 점

기존 플러그인이 API v3 이하에서 작성됐다면 v4로 변경하기 전에 비동기 관련 코드를 확인해야 합니다.

기존 코드는 다음처럼 작성됐을 수 있습니다.

function(properties, context) {
    const count = properties.my_list.length();

    return {
        count: count
    };
}

Plugin API v4에서는 다음과 같이 수정해야 합니다.

async function(properties, context) {
    const count = await properties.my_list.length();

    return {
        count: count
    };
}

또한 기존 context.request와 context.async는 v4에서 이전 호환용 영역으로 이동했으며, Bubble은 Promise 기반 코드와 fetch, Node의 util.promisify 사용을 안내하고 있습니다. 기존 플러그인을 업데이트할 때는 API 버전만 변경하지 말고 관련 함수를 함께 수정해야 합니다.

마무리

Bubble.io Custom Plugin에서 비동기 데이터 처리는 외부 API 연동과 대량 데이터 처리의 안정성을 결정하는 중요한 요소입니다.

단순히 코드 앞에 async와 await를 붙이는 것만으로는 충분하지 않습니다. 오류 처리, 요청 시간 제한, 중복 실행 방지, 병렬 처리 범위와 데이터 분할까지 함께 설계해야 실제 서비스에서도 안정적으로 작동합니다.

처음에는 하나의 API를 호출해 결과를 반환하는 간단한 Server-side Action부터 만들어보는 것이 좋습니다. 이후 오류 상태와 배치 처리 기능을 추가하면 재사용성과 완성도가 높은 Bubble 플러그인으로 발전시킬 수 있습니다.

youngji
함께 보면 좋은 글

댓글 0

첫 댓글을 남겨보세요.

광고 차단 알림

광고 클릭 제한을 초과하여 광고가 차단되었습니다.

단시간에 반복적인 광고 클릭은 시스템에 의해 감지되며, IP가 수집되어 사이트 관리자가 확인 가능합니다.