Airtable REST API를 백엔드에 연동해서 노코드 데이터베이스의 레코드를 실시간으로 가져오는 기능을 구현해봤을까? 서버 단에서 cURL로 API를 연속 호출할 때 429 Too Many Requests 에러가 발생하거나, 레코드가 100개 이상일 때 나머지 데이터가 잘려 나가는 현상을 한 번쯤 경험했을 것이다. 다만 많은 개발자들이 Airtable API의 요청 제한 기준과 offset 기반 페이지네이션 동작 방식을 정확히 모른 채 단발성 cURL 요청만 작성해 버그를 만들어낸다. 이번에는 Airtable REST API의 인증 및 동작 원리부터 Rate Limit을 우회하는 캐싱 전략, 그리고 100개 이상의 레코드를 완벽하게 수집하는 페이지네이션 처리까지 완벽하게 정리해서 소개하겠다.

 

1단계: Airtable REST API 인증 체계와 요청 제한 이해하기

Airtable은 노코드 엑셀 형태의 UI를 제공하지만 내부적으로는 표준 RESTful API를 완벽히 지원한다. 기존의 API Key 방식은 보충 단계를 거쳐 Personal Access Token(PAT) 방식으로 완전히 대체되었다. PAT 인증 방식을 사용할 때는 Bearer 토큰 헤더를 필수적으로 전송해야 한다.

Airtable API를 실무 서버에 적용할 때 반드시 알아야 할 가장 큰 제약 조건은 초당 요청 제한(Rate Limit)과 응답 데이터의 최대 개수 한계다.

구분제한 사항 및 동작 스펙비고
Rate Limit베이스(Base) 당 초당 최대 5회 요청 가능초과 시 HTTP Status 429 반환
1회 최대 조회 레코드단일 API 요청당 최대 100개 레코드 반환100개 초과 시 offset 값 전달됨
인증 방식Personal Access Token (PAT)Authorization: Bearer {token}
데이터 구조records 배열 내 id, fields, createdTime 구성JSON 포맷 응답

 

2단계: Offset 페이지네이션과 429 에러 방지 전략

Airtable 테이블에 저장된 레코드가 100개를 넘어갈 경우, API 응답 JSON 객체에는 offset 키가 포함되어 돌아온다. 다음 페이지의 데이터를 가져오려면 URL 쿼리 파라미터에 offset={offset_val}을 추가하여 재귀적 또는 반복적으로 요청을 보내야 한다.

동시에 초당 5회를 초과하는 요청을 보낼 경우 429 에러가 터지므로, 반복문 내부에 미세한 지연 시간(usleep)을 부여하거나 서버 단 캐싱(File, Redis 등)을 적용해 API 직접 호출 횟수를 최소화해야 한다.

 

3단계: PHP cURL 실전 코드 구현

실제 백엔드 환경에서 발생하기 쉬운 오답 코드와 이를 해결한 올바른 페이지네이션 수집 코드를 비교해보자.

 

✗ 잘못된 코드: 단일 요청만 처리하여 100개 초과 데이터 누락 및 에러 미처리

아래 코드는 offset 존재 여부를 검사하지 않아 100개 이상의 데이터가 존재할 때 이후 레코드를 모두 분실한다.

<?php
$api_token = "patXxxxxxx.Xxxxxxx";
$base_id = "appXxxxxxx";
$table_name = "Products";

$url = "https://api.airtable.com/v0/{$base_id}/{$table_name}";

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer {$api_token}"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$data = json_decode($response, true);
// 100개까지만 출력되고 나머지 레코드는 완전히 누락됨
print_r($data['records']);
?>

 

✓ 올바른 코드: offset 루프 수집 및 Rate Limit 지연 제어 적용

재귀적 반복문으로 offset을 추적하여 전체 레코드를 안전하게 병합하고, 요청 간 usleep을 추가해 429 방지를 완료한 코드다.

<?php
function fetchAllAirtableRecords($base_id, $table_name, $api_token) {
    $all_records = [];
    $offset = null;
    
    do {
        $query_params = [];
        if ($offset !== null) {
            $query_params['offset'] = $offset;
        }
        
        $url = "https://api.airtable.com/v0/{$base_id}/" . rawurlencode($table_name);
        if (!empty($query_params)) {
            $url .= '?' . http_build_query($query_params);
        }
        
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            "Authorization: Bearer {$api_token}",
            "Content-Type: application/json"
        ]);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        
        $response = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($http_code === 429) {
            // Rate Limit 발생 시 1초 대기 후 재시도
            sleep(1);
            continue;
        }
        
        if ($http_code !== 200) {
            throw new Exception("Airtable API Error: HTTP {$http_code} - {$response}");
        }
        
        $data = json_decode($response, true);
        if (isset($data['records'])) {
            $all_records = array_merge($all_records, $data['records']);
        }
        
        // 다음 페이지를 위한 offset 추출
        $offset = $data['offset'] ?? null;
        
        // 초당 5회 제한을 준수하기 위한 0.2초 지연
        if ($offset !== null) {
            usleep(200000);
        }
        
    } while ($offset !== null);
    
    return $all_records;
}

try {
    $token = "patXxxxxxx.Xxxxxxx";
    $records = fetchAllAirtableRecords("appXxxxxxx", "Products", $token);
    echo "총 " . count($records) . "개의 레코드를 성공적으로 수집했습니다.";
} catch (Exception $e) {
    echo "오류 발생: " . $e->getMessage();
}
?>

 

응답 결과 예시
총 254개의 레코드를 성공적으로 수집했습니다.

 

4단계: 실무 연동 시 흔한 실수와 주의사항

✗ 테이블 이름에 공백이나 특수문자가 포함되었을 때 URL 인코딩을 빠뜨리는 경우

rawurlencode($table_name) 함수를 사용해 공백이 %20 형태로 올바르게 변환되도록 처리해야 한다.

✗ 사용자가 페이지를 요청할 때마다 백엔드가 Airtable API를 매번 직접 호출하는 디자인

✓ 트래픽이 조금만 몰려도 429 에러가 발생한다. 수집된 결과를 파일 캐시나 Redis에 5~10분간 저장해두고 캐시 데이터를 반환하는 구조로 설계해야 한다.

 

5단계: 핵심 요약 및 정리

Airtable REST API는 노코드 플랫폼의 편의성과 강력한 데이터 관리 기능을 백엔드 시스템에 결합할 수 있는 훌륭한 도구다. 1회 최대 조회 제한 100개와 초당 5회 요청 제한이라는 명확한 스펙을 이해하고 페이지네이션과 예외 처리를 구현하는 습관이 모여 안정적인 외부 API 연동 서비스를 만든다는 점을 잊지 말자. 이 글의 3단계 cURL 반복문 코드를 참고해 백엔드 수집 로직을 개편하면, Rate Limit 끊김 없이 전체 레코드를 안전하게 동기화하는 최종 결과를 얻을 수 있을 것이다.