웹 서비스나 앱을 개발하면서 사용자의 현재 위치에 맞는 실시간 날씨 데이터를 보여줘야 하는 상황을 경험해봤을까? 공공데이터포털에서 제공하는 기상청 단기예보 API는 대한민국 전역의 날씨 정보를 무료로 활용할 수 있는 가장 확실한 도구다. 다만 API를 신청하고 코드를 작성했음에도 인증키 오류(SERVICE_KEY_IS_NOT_REGISTERED_ERROR)가 발생하거나 데이터 파싱에 실패하여 막히는 경우가 많다.

이번에는 기상청 단기예보 API의 작동 원리부터 가장 자주 발생하는 ServiceKey 인코딩 오류 해결법, 그리고 PHP cURL을 이용해 실시간 날씨 데이터를 가져오는 방법까지 완벽하게 정리해서 소개하겠다.

 

1. 기상청 단기예보 API 이해하기

기상청 단기예보 API는 격자 좌표(X, Y)를 기준으로 실시간 기상 실황, 초단기예보, 단기예보(구 동네예보) 데이터를 JSON 또는 XML 형태로 제공한다. 위도와 경도 좌표를 기상청 전용 격자 좌표로 변환한 뒤 요청을 보내면 기온, 강수 형태, 습도, 풍속 등의 세부 기상 정보를 얻을 수 있다.

공공데이터포털 API를 연동할 때 가장 핵심이 되는 개념은 인증키(ServiceKey)와 요청 시각(base_date, base_time)이다. 기상청 서버는 정해진 시간마다 데이터를 업데이트하므로, 매시간 정해진 발표 시각에 맞춰 요청을 보내야 정확한 응답을 받을 수 있다.

 

2. ServiceKey 인코딩 오류와 해결 방법

공공데이터포털에서 API 승인을 받은 후 가장 흔하게 겪는 문제가 바로 LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDED 또는 SERVICE_KEY_IS_NOT_REGISTERED_ERROR 에러다. 이 에러의 90% 이상은 인증키의 URL 인코딩 문제로 인해 발생한다.

공공데이터포털 마이페이지에서는 인증키를 'Encoding 인증키''Decoding 인증키' 두 가지 형태로 제공한다. cURL 요청을 보낼 때 어떤 키를 사용하는지에 따라 인코딩 처리 방식이 달라진다.

 

인증키 유형별 사용법 비교
구분인증키 형태PHP cURL 처리 방법
Encoding 인증키이미 %2F, %3D 등으로 인코딩된 상태http_build_query() 사용 금지 (이중 인코딩 방지), URL에 직접 결합
Decoding 인증키원문 문자열 (+, /, = 등 포함)rawurlencode() 또는 http_build_query()를 통해 인코딩 후 전달

 

3. PHP 실전 구현 예제

다음은 PHP cURL을 이용하여 기상청 초단기실황조회 API를 호출하고, 현재 기온(T1H)과 강수형태(PTY)를 파싱하는 실전 코드다.

✗ 잘못된 코드 (http_build_query 사용으로 이미 인코딩된 ServiceKey가 이중 인코딩되는 경우):

<?php
// 이미 인코딩된 ServiceKey 사용 시 이중 인코딩 발생
$encodingServiceKey = 'aBC123%2Fdef456%3D%3D';

$params = [
    'serviceKey' => $encodingServiceKey, // http_build_query가 %를 %25로 다시 인코딩함!
    'pageNo' => '1',
    'numOfRows' => '10',
    'dataType' => 'JSON',
    'base_date' => '20231025',
    'base_time' => '0600',
    'nx' => '55',
    'ny' => '127'
];

$url = 'http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtNcst?' . http_build_query($params);
// 결과적으로 serviceKey=aBC123%252Fdef456%253D%253D 가 되어 인증 실패 발생!
?>

✓ 올바른 코드 (인증키 파라미터를 안전하게 조립하여 요청하는 방식):

<?php
function getKmaWeather($nx, $ny) {
    // 공공데이터포털에서 발급받은 Decoding 인증키 사용
    $decodingServiceKey = 'aBC123/def456==';
    
    // 현재 날짜 및 시각 기준 매개변수 생성 (초단기실황은 매시 40분 이후 업데이트)
    $baseDate = date('Ymd');
    $currentHour = (int)date('H');
    $currentMinute = (int)date('i');
    
    if ($currentMinute < 40) {
        $currentHour -= 1;
        if ($currentHour < 0) {
            $currentHour = 23;
            $baseDate = date('Ymd', strtotime('-1 day'));
        }
    }
    $baseTime = sprintf('%02d00', $currentHour);

    $endpoint = 'http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtNcst';
    
    // query string 조립 (serviceKey는 rawurlencode 직접 적용)
    $queryParams = '?' . rawurlencode('serviceKey') . '=' . rawurlencode($decodingServiceKey);
    $queryParams .= '&' . rawurlencode('pageNo') . '=' . rawurlencode('1');
    $queryParams .= '&' . rawurlencode('numOfRows') . '=' . rawurlencode('100');
    $queryParams .= '&' . rawurlencode('dataType') . '=' . rawurlencode('JSON');
    $queryParams .= '&' . rawurlencode('base_date') . '=' . rawurlencode($baseDate);
    $queryParams .= '&' . rawurlencode('base_time') . '=' . rawurlencode($baseTime);
    $queryParams .= '&' . rawurlencode('nx') . '=' . rawurlencode($nx);
    $queryParams .= '&' . rawurlencode('ny') . '=' . rawurlencode($ny);

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $endpoint . $queryParams);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode !== 200 || !$response) {
        return null;
    }

    $data = json_decode($response, true);
    
    // 응답 데이터 정상 여부 검증
    if (!isset($data['response']['body']['items']['item'])) {
        return null;
    }

    $weatherData = [];
    foreach ($data['response']['body']['items']['item'] as $item) {
        // T1H: 기온(℃), PTY: 강수형태, REH: 습도(%)
        $weatherData[$item['category']] = $item['obsrValue'];
    }

    return $weatherData;
}

// 서울 중구 (nx: 60, ny: 127) 날씨 조회
$weather = getKmaWeather(60, 127);
print_r($weather);
?>

출력 결과 (정상 응답 시):

Array
(
    [PTY] => 0
    [REH] => 55
    [RN1] => 0
    [T1H] => 18.5
    [UUU] => 1.2
    [VVV] => -0.8
    [WSD] => 1.4
)

 

4. 실무 연동 시 흔한 실수와 주의사항

기상청 API 연동 실무에서 흔히 발생하는 대표적인 문제와 체크포인트는 다음과 같다.

  • 발표 시각 처리 오류: 초단기실황 API는 매시간 30분마다 데이터가 생성되고 40분에 확정된다. 매시 00분에 직전 시각 데이터를 호출하면 데이터가 조회되지 않으므로, 분 단위 조건 체크가 필수적이다.
  • 기상청 API 활용 신청 직후 오류: 공공데이터포털에서 API 승인을 받은 직후에는 시스템 동기화까지 최대 1~2시간이 소요될 수 있다. 승인 즉시 요청 시 SERVICE_KEY_IS_NOT_REGISTERED_ERROR가 발생할 수 있으므로 잠시 기다린 후 테스트해야 한다.
  • HTTP / HTTPS 프로토콜 혼동: 기상청 API 엔드포인트는 HTTP와 HTTPS를 모두 지원하지만, 일부 서버 환경에서 SSL 암호화 통신 문제로 연결이 지연될 수 있다. cURL 설정 시 CURLOPT_SSL_VERIFYPEER 옵션을 적절히 다루어야 한다.

 

5. 마무리 및 요약

공공데이터포털 기상청 단기예보 API 연동의 핵심은 정확한 발표 시각 계산ServiceKey의 올바른 URL 인코딩 처리다. 인증키 중복 인코딩으로 인한 오류만 방지하더라도 API 연동 과정의 대부분의 트러블슈팅을 해결할 수 있다.

안정적인 날씨 정보 제공은 외부 API 연동 서비스의 품질을 높이는 중요한 요소다. 디코딩된 인증키를 안전하게 관리하고 발표 시각 예외 처리를 꼼꼼히 구현하는 작은 최적화 습관이 모여서 서비스의 신뢰성을 만든다는 점을 잊지 말자. 이 글의 PHP 예제 코드를 참고해 작성 중인 프로젝트에 적용해 보면, 오류 없이 실시간 날씨 데이터를 연동하는 최종 결과를 얻을 수 있을 것이다.