← 목록으로

85개국 공항 API 데이터: 7개 언어 항공편 상태를 완벽 정규화하며 배운 것들

2026. 4. 7.

85개국 공항 API 데이터: 7개 언어 항공편 상태를 완벽 정규화하며 배운 것들

85개국 이상의 공항 웹사이트에서 항공편 데이터를 긁어모으다 보면, 세상에 합의된 것이 하나도 없다는 사실을 금방 깨닫게 됩니다. 데이터 형식은 물론이고, 필드 이름, 심지어 항공편 상태를 나타내는 문자열까지 제각각이죠.

가령 "Departed"라는 단어 하나만 해도 SAL, FLY, CER, AIR, DEP, DEPARTED, Departed, Отправлен, Väljunud, DESPEGADO, gestartet, partito 등 공항 웹사이트마다 전혀 다른 표현을 사용합니다. 이런 혼돈 속에서 MyAirports를 구축한다는 것은 이 모든 표현을 처리하고, 단 하나의 예측 가능한 enum 값으로 매핑하는 상태 정규화 도구를 만드는 일과 같았습니다. 그 과정이 어떻게 진행되었고, 또 어떤 점들이 저를 놀라게 했는지 지금부터 이야기해 보죠.

우리가 목표한 스키마는 '단순함'

API가 반환하는 모든 항공편은 아래 목록 중 하나로 통일된 status 필드를 갖습니다.

scheduled | boarding | departed | arrived | delayed | cancelled | unknown

총 7가지 값. 이처럼 단순하고 예측 가능하며, 추가적인 매핑 없이 어떤 프런트엔드에서도 즉시 사용할 수 있는 형태가 우리의 목표였습니다. 현실 세계의 복잡한 데이터를 여기까지 끌어오는 것이 가장 어려운 부분이었죠.

레이어 1: 정확한 문자열 매칭

가장 직관적인 첫 번째 레이어는 바로 알려진 상태 문자열을 표준 값으로 변환하는 조회 테이블입니다.

const EXACT = {
  // English
  'scheduled': 'scheduled',
  'on time': 'scheduled',
  'departed': 'departed',
  'in flight': 'departed',
  'arrived': 'arrived',
  'landed': 'arrived',
  'delayed': 'delayed',
  'cancelled': 'cancelled',
  'boarding': 'boarding',
  'gate open': 'boarding',

  // Spanish (AENA codes)
  'SAL': 'departed',  // salida
  'AIR': 'departed',  // en aire
  'FLY': 'departed',
  'CER': 'departed',  // cerrado
  'LND': 'arrived',   // aterrizó
  'FNL': 'arrived',   // final
  'IBK': 'arrived',   // in-block
  'BOR': 'boarding',  // embarcando
  'EMB': 'boarding',
  'ULL': 'boarding',
  'OPE': 'boarding',
  'EST': 'scheduled', // estimado
  'PRG': 'scheduled', // programado
  'RET': 'delayed',   // retraso
  'CNL': 'cancelled',

  // Russian
  'Отправлен': 'departed',
  'Прилетел': 'arrived',
  'Посадка': 'boarding',
  'Задержан': 'delayed',
  'Отменён': 'cancelled',

  // Estonian
  'Väljunud': 'departed',
  'Saabunud': 'arrived',
  'Pardal': 'boarding',

  // Finnish (Finavia)
  'Lähtenyt': 'departed',
  'Saapunut': 'arrived',
  'Nousussa': 'boarding',
};

이 방법만으로도 실제 데이터의 60~70%는 처리할 수 있습니다. 하지만 나머지는 좀 더 복잡한 접근이 필요했죠.

레이어 2: 부분 문자열 패턴 매칭

일부 상태 문자열에는 실제 시간, 게이트 번호, 게이트 마감 시간 등 동적인 정보가 포함되어 있습니다. 이런 경우는 정확히 일치시킬 수 없으므로, 패턴을 이용한 매칭을 사용해야 합니다.

const SUBSTRING_PATTERNS = [
  // "Delayed 00:45"
  [/delay/i, 'delayed'],
  // 'cancel'로 시작하는 모든 언어의 "취소" 관련 문자열
  [/cancel/i, 'cancelled'],
  // AENA 스타일: "CERRADO" (closed), "PROGRAMADO" (scheduled)
  [/^cerr/i, 'departed'],
  [/^progr/i, 'scheduled'],
  // "Check-in open" → scheduled (아직 탑승 시작 전)
  [/check.?in/i, 'scheduled'],
];

레이어 3: 동적 콘텐츠를 위한 정규식 패턴

소수의 공항은 실제 타임스탬프나 특정 정보를 상태 문자열에 직접 삽입합니다. 피닉스 스카이 하버 공항이 좋은 예시인데, 지연된 출발 항공편의 상태 필드에 "Now 14:22" (실제 출발 시간)처럼 표시하죠. 이건 상태가 아니라 타임스탬프이며, 항공편이 출발했다는 의미로 해석해야 합니다.

const REGEX_PATTERNS = [
  // Phoenix: "Now 12:21 PM" → departed
  [/^now\s+\d/i, 'departed'],
  // Finavia: "Expected 14:35" → still scheduled
  [/^expected\s+\d/i, 'scheduled'],
  // Generic: "14:35"와 같이 시간만 있는 경우 → departed/arrived
  [/^\d{1,2}:\d{2}$/, 'arrived'],
];

레이어 4: 러시아어 어간 매칭

러시아어는 형태론적으로 매우 풍부한 언어입니다. 문법적 격, 수, 성에 따라 단어가 심하게 변형되죠. "Посадка" (boarding)는 문장 위치에 따라 "посадке", "посадкой", "посадку" 등으로 나타납니다. 정확한 매칭으로는 이런 변화형을 놓치기 십상입니다.

해결책은 거창한 NLP 라이브러리(단어 몇 개 때문에 쓰기엔 너무 과하죠)가 아니라, 바로 어간 접두사였습니다. 단어의 처음 4~5글자만으로도 대개 단어를 식별하기에 충분했죠.

const RUSSIAN_STEMS = {
  'посад': 'boarding',   // posad- (посадка, посадке...)
  'отпра': 'departed',   // otpra- (отправлен, отправился...)
  'прил':  'arrived',    // pril- (прилетел, прилетела...)
  'задер': 'delayed',    // zader- (задержан, задержка...)
  'отмен': 'cancelled',  // otmen- (отменён, отменённый...)
};

처음 러시아어 데이터를 접했을 땐 '이걸 다 어떻게 매핑하지?' 막막했지만, 실무에서 여러 시도를 해보니 굳이 거창한 NLP 라이브러리 없이도 접두사만으로 충분히 해결되는 경우가 많다는 걸 깨달았습니다. 과유불급이죠. 이 방식으로 흔한 변화형들을 오탐 없이 잡아낼 수 있었습니다.

예상치 못했던 복병, '객체' 가드

예상치 못했던 한 가지는 일부 공항 API가 상태를 문자열이 아닌 객체 형태로 반환한다는 점이었습니다.

{ "status": { "code": "DEP", "description": "Departed", "color": "#ff0000" } }

정규화 도구는 이런 경우를 위해 매칭을 시도하기 전에 codedescription 필드를 추출하도록 설계되었습니다. 이 object guard가 없으면, typeof status === 'object'인 경우 문자열 매칭 로직이 조용히 에러를 뿜어내게 됩니다.

AENA의 30개 이상의 상태 코드

AENA(스페인 공항 운영사로, 22개 스페인 공항을 관할)는 제가 접한 시스템 중 가장 정교한 상태 시스템을 가지고 있었습니다. 그들은 SIGER 지상 처리 시스템에서 가져온 두세 글자짜리 코드를 사용합니다.

FNL, LND, IBK → arrived
BOR, EMB, ULL, OPE, APE → boarding
SAL, AIR, FLY, CER, APA → departed
EST, PRG, INI, SCH, HOR → scheduled
RET, DEM, REM → delayed
CNL, DES, CAN → cancelled

각 코드는 IBK(= in-block), ULL(= 최종 탑승 호출), DEM(= 지상 지연)과 같은 특정 운영상의 의미를 가집니다. 정규화 도구는 이 모든 것을 6개의 표준 값으로 축소합니다. 이 과정에서 운영상의 상세 정보는 손실되지만, 공개 API의 목적을 고려할 때 이는 올바른 트레이드오프입니다.

실제로 'unknown'이 의미하는 바

unknown 값은 정규화 도구가 확신을 가지고 분류할 수 없는 모든 것을 포괄합니다. 실제 운영 환경에서는 다음과 같은 경우에 나타났습니다.

  • 빈 문자열이나 null 값 (일부 공항은 정시 항공편의 상태를 아예 반환하지 않습니다)
  • 아직 매핑되지 않은 공항별 고유 코드
  • 제가 아직 설정하지 않은 언어(아랍어, 일본어, 중국어 등)의 상태 문자열

unknown을 반환하는 것은 잘못된 값을 반환하는 것보다 훨씬 낫습니다. 프런트엔드에서는 알 수 없는 상태에 대해 "—"를 표시할 수 있고, 사용자들은 이것이 데이터 없음으로 이해할 수 있습니다.

정규화 도구의 실행

정규화 도구는 모든 스크래핑이 끝난 후, 필드 매퍼가 상태를 포함하는 JSON 필드를 식별한 직후에 실행됩니다. 이 도구는 순수 함수로 동작합니다.

normalizeStatus(raw) // → 'scheduled' | 'boarding' | 'departed' | 'arrived' | 'delayed' | 'cancelled' | 'unknown'

입력은 항상 문자열(객체 가드를 거친 후)이며, 출력은 항상 7가지 값 중 하나입니다. 예외는 발생시키지 않으며, 인식되지 않는 입력은 'unknown'을 반환합니다.

프로젝트를 통해 얻은 더 큰 교훈

이 프로젝트를 시작할 때 저는 스크래핑이 가장 어려운 부분일 거라고 예상했습니다. 하지만 정말로 어려운 부분은 바로 정규화였습니다. 즉, 너무나도 일관성 없는 소스에서 데이터를 가져와서 그것을 균일하게 유용하도록 만드는 과정이 진짜 난관이었죠.

항공편 상태 정규화는 그 한 가지 예시에 불과합니다. 같은 문제는 타임스탬프(공항은 UTC가 아닌 현지 시간대를 사용하며 형식도 제각각입니다), 위치 필드("MILAN, MALPENSA"를 IATA 필드에 넣는 경우), 항공사 코드(IATA, ICAO, 전체 이름 혼용) 등에서도 끊임없이 나타났습니다.

효과적이었던 원칙은 간단합니다. 먼저 엄격한 출력 스키마를 설계하고, 이를 강제하기 위한 정규화 레이어를 구축하는 것입니다. 그리고 모든 예외 케이스를 소비자가 특별히 처리할 문제가 아니라 정규화 도구에 추가해야 할 대상으로 간주하는 것이죠.

MyAirports API는 myairports.online/developers에서 라이브로 사용해 볼 수 있습니다. 1,000개 이상의 공항에 대한 출발 및 도착 정보를 위에서 설명한 7가지 상태로 정규화하여 제공하고 있습니다.


다음 시리즈: 628,000개 누적 항공편 기록이 항공사 지연 순위, 게이트 예측, 공항 혼잡도 히트맵으로 어떻게 전환되는지 다룹니다.


원문: https://dev.to/jfcaba/normalizing-flight-statuses-across-7-languages-what-i-learned-building-a-global-airport-api-po0 수집일: 2026-04-07 05:59:04