블로그워드프레스에서 공공데이터 API 활용하기 (1) — 미세먼지 실시간 조회 서비스

워드프레스에서 공공데이터 API 활용하기 (1) — 미세먼지 실시간 조회 서비스

작년 겨울, 미세먼지가 심한 날 네이버에서 미세먼지 수치를 확인하다가 문득 이런 생각이 들었다. “이 데이터, 나도 직접 가져올 수 있지 않나?” 공공데이터포털(data.go.kr)에 들어가보니 에어코리아 대기오염정보 API가 있었다. 신청하고, 코드 짜고, 삽질하고— 그 과정을 처음부터 끝까지 정리한다.

이 글은 시리즈의 첫 번째 편이다. 워드프레스 mu-plugin으로 미세먼지 실시간 조회 서비스를 만드는 과정을 실제 코드와 함께 다룬다.

공공데이터포털 API 키 발급

data.go.kr에 가입한 뒤 “에어코리아 대기오염정보 조회 서비스”를 검색해서 활용 신청을 한다. 승인은 자동이다. 발급받은 인코딩 키를 wp-config.php에 상수로 넣어둔다.

// wp-config.php
define( 'NALKKUL_DATA_GO_KR_KEY', '발급받은_인코딩키_여기에' );

이 키 하나로 대기오염, 약국, 부동산 등 여러 API를 동시에 쓸 수 있다. 단, API마다 별도 활용 신청이 필요한 건 따로 있다. 나중에 3편에서 자세히 다룬다.

왜 mu-plugin인가

일반 플러그인(wp-content/plugins/)과 mu-plugin(wp-content/mu-plugins/)의 차이가 뭘까. mu-plugin은:

  • 관리자 화면에서 비활성화할 수 없다 (실수로 끄는 사고 방지)
  • 하위 디렉토리의 파일은 자동 로드되지 않는다 (파일 하나 = 플러그인 하나)
  • 일반 플러그인보다 먼저 로드된다

공공데이터 API 서비스는 사이트의 핵심 기능이라서 누가 실수로 끄면 안 된다. 그래서 mu-plugin을 선택했다. 파일 하나에 모든 로직을 담아 wp-content/mu-plugins/air-quality.php에 넣으면 자동으로 활성화된다.

에어코리아 API 분석

사용할 API 엔드포인트는 getCtprvnRltmMesureDnsty다. 시도명을 넘기면 해당 시도의 모든 측정소 데이터를 JSON으로 돌려준다.

http://apis.data.go.kr/B552584/ArpltnInforInqireSvc/getCtprvnRltmMesureDnsty
?serviceKey={인코딩된_키}
&returnType=json
&numOfRows=100
&pageNo=1
&sidoName=서울
&ver=1.0

응답 구조는 response > body > items 배열 안에 각 측정소별 데이터가 들어온다. 주요 필드:

  • stationName — 측정소명 (예: 강남구)
  • pm10Value — 미세먼지 (PM10) 수치
  • pm25Value — 초미세먼지 (PM2.5) 수치
  • o3Value — 오존 수치
  • dataTime — 측정 시각

PHP에서 API 호출하기: rawurlencode 함정

여기서 제일 먼저 삽질한 부분이다. 공공데이터포털 API 키에는 +, /, = 같은 특수문자가 포함되어 있다. URL에 넣을 때 반드시 rawurlencode()를 써야 한다.

$url = 'http://apis.data.go.kr/B552584/ArpltnInforInqireSvc/getCtprvnRltmMesureDnsty'
     . '?serviceKey=' . rawurlencode( NALKKUL_DATA_GO_KR_KEY )
     . '&returnType=json'
     . '&numOfRows=100'
     . '&pageNo=1'
     . '&sidoName=' . rawurlencode( $sido )
     . '&ver=1.0';

$resp = wp_remote_get( $url, array( 'timeout' => 15 ) );

주의: urlencode()가 아니라 rawurlencode()다. urlencode()는 공백을 +로 바꾸고, rawurlencode()%20으로 바꾼다. 공공데이터 API 서버는 +를 그대로 해석해서 인증이 실패한다. 처음에 http_build_query()를 썼다가 403 에러가 나서 한참 헤맸다.

17개 시도 데이터 구조

에어코리아는 17개 시도별로 데이터를 제공한다. 시도 목록을 별도 함수로 분리했다.

function nalkkul_aq_sido_list() {
    return array(
        '서울', '부산', '대구', '인천', '광주', '대전', '울산', '세종',
        '경기', '강원', '충북', '충남', '전북', '전남', '경북', '경남', '제주',
    );
}

사용자 입력값을 이 목록과 대조해서 유효하지 않으면 기본값 ‘서울’로 폴백한다. 화이트리스트 방식이라 SQL 인젝션이나 파라미터 변조를 원천 차단할 수 있다.

미세먼지 등급 판정 로직

환경부 기준에 따라 PM10과 PM2.5 각각의 등급을 판정한다. 실제 코드다:

function nalkkul_aq_pm10_grade( $val ) {
    if ( $val === null || $val === '' || $val === '-' )
        return array( 'label' => '정보없음', 'color' => '#999', 'bg' => '#f5f5f5', 'level' => 0 );
    $v = intval( $val );
    if ( $v  '좋음',     'color' => '#1976d2', 'bg' => '#e3f2fd', 'level' => 1 );
    if ( $v  '보통',     'color' => '#388e3c', 'bg' => '#e8f5e9', 'level' => 2 );
    if ( $v  '나쁨',     'color' => '#f57c00', 'bg' => '#fff3e0', 'level' => 3 );
    return                 array( 'label' => '매우나쁨', 'color' => '#d32f2f', 'bg' => '#fbe9e7', 'level' => 4 );
}

function nalkkul_aq_pm25_grade( $val ) {
    if ( $val === null || $val === '' || $val === '-' )
        return array( 'label' => '정보없음', 'color' => '#999', 'bg' => '#f5f5f5', 'level' => 0 );
    $v = intval( $val );
    if ( $v  '좋음',     'color' => '#1976d2', 'bg' => '#e3f2fd', 'level' => 1 );
    if ( $v  '보통',     'color' => '#388e3c', 'bg' => '#e8f5e9', 'level' => 2 );
    if ( $v  '나쁨',     'color' => '#f57c00', 'bg' => '#fff3e0', 'level' => 3 );
    return                 array( 'label' => '매우나쁨', 'color' => '#d32f2f', 'bg' => '#fbe9e7', 'level' => 4 );
}

PM10과 PM2.5 기준이 다르다는 점에 주의하자. PM10은 30/80/150 기준이고, PM2.5는 15/35/75 기준이다. 종합 등급은 둘 중 나쁜 쪽을 따른다:

function nalkkul_aq_overall_grade( $pm10_level, $pm25_level ) {
    $max = max( $pm10_level, $pm25_level );
    // $max에 해당하는 등급 반환
}

각 등급에 색상 코드(color, bg)를 함께 넣은 건 의도적인 설계다. 프론트엔드에서 “좋음이면 파랑, 나쁨이면 주황” 같은 조건문을 쓰는 대신, 서버에서 색상까지 내려보내면 코드가 훨씬 깔끔해진다.

API 응답 파싱과 평균값 계산

에어코리아 API는 측정소별 데이터를 배열로 준다. 서울의 경우 약 40개 측정소 데이터가 온다. 개별 측정소 데이터도 보여주지만, 시도 전체 평균값도 필요하다.

$pm10_sum = 0; $pm25_sum = 0;
$pm10_cnt = 0; $pm25_cnt = 0;

foreach ( $items as $item ) {
    $pm10 = isset( $item['pm10Value'] ) && $item['pm10Value'] !== '-' && $item['pm10Value'] !== ''
            ? intval( $item['pm10Value'] ) : null;
    $pm25 = isset( $item['pm25Value'] ) && $item['pm25Value'] !== '-' && $item['pm25Value'] !== ''
            ? intval( $item['pm25Value'] ) : null;

    if ( $pm10 !== null ) { $pm10_sum += $pm10; $pm10_cnt++; }
    if ( $pm25 !== null ) { $pm25_sum += $pm25; $pm25_cnt++; }

    $stations[] = array(
        'name'       => $item['stationName'] ?? '',
        'pm10'       => $pm10,
        'pm25'       => $pm25,
        'o3'         => $o3,
        'pm10_grade' => nalkkul_aq_pm10_grade( $pm10 ),
        'pm25_grade' => nalkkul_aq_pm25_grade( $pm25 ),
        'dataTime'   => $item['dataTime'] ?? '',
    );
}

$avg_pm10 = $pm10_cnt > 0 ? round( $pm10_sum / $pm10_cnt ) : null;
$avg_pm25 = $pm25_cnt > 0 ? round( $pm25_sum / $pm25_cnt ) : null;

여기서 '-'와 빈 문자열을 모두 체크하는 이유는 API가 데이터가 없을 때 어떤 필드는 '-'을, 어떤 필드는 빈 문자열을 보내기 때문이다. 문서에는 안 나와 있고, 실제로 데이터를 받아봐야 안다.

트랜지언트 캐싱: API 호출 비용을 아끼자

공공데이터 API는 일일 호출 횟수 제한이 있다 (보통 하루 1,000회). 방문자가 페이지를 열 때마다 API를 호출하면 금방 한도를 넘긴다. WordPress의 Transient API로 캐싱하면 해결된다.

$cache_key = 'nalkkul_aq_' . md5( $sido );
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
    return $cached;  // 캐시 히트! API 호출 안 함
}

// ... API 호출 후 ...
$ttl = defined('NALKKUL_CACHE_AIR') ? NALKKUL_CACHE_AIR : 1800;
set_transient( $cache_key, $data, $ttl );

캐시 시간은 wp-config.php에서 상수로 관리한다. 미세먼지 데이터는 30분(1800초)마다 갱신된다. 에어코리아 자체가 1시간 단위로 업데이트하므로 30분이면 충분하다.

// wp-config.php
define( 'NALKKUL_CACHE_AIR', 1800 );  // 미세먼지: 30분

API 장애 대비: 폴백 데이터

공공데이터 API는 가끔 점검을 하거나 응답이 느려진다. 이때 사용자에게 빈 화면을 보여줄 순 없다. 마지막으로 성공한 데이터를 wp_options에 저장해두고, API가 실패하면 그걸 보여준다.

// API 성공 시: 폴백 데이터 업데이트
update_option( 'nalkkul_aq_fallback_' . md5( $sido ), $data, false );

// API 실패 시: 폴백 데이터 사용
if ( is_wp_error( $resp ) || wp_remote_retrieve_response_code( $resp ) !== 200 ) {
    $fallback = get_option( 'nalkkul_aq_fallback_' . md5( $sido ) );
    if ( $fallback ) {
        return $fallback;
    }
    return new WP_Error( 'api_fail', '대기오염 정보를 불러올 수 없습니다.' );
}

update_option의 세 번째 인자 false는 autoload를 끄는 설정이다. 모든 페이지에서 이 옵션을 로드할 필요가 없으니까.

AJAX로 도시 변경 시 새로고침 없이 갱신

드롭다운에서 시도를 바꾸면 페이지 전체를 새로고침하지 않고 AJAX로 해당 시도 데이터만 가져온다.

// PHP: AJAX 핸들러 등록
add_action( 'wp_ajax_nalkkul_aq_fetch',        'nalkkul_aq_ajax_handler' );
add_action( 'wp_ajax_nopriv_nalkkul_aq_fetch',  'nalkkul_aq_ajax_handler' );

function nalkkul_aq_ajax_handler() {
    $sido = isset( $_GET['sido'] ) ? sanitize_text_field( wp_unslash( $_GET['sido'] ) ) : '서울';
    $data = nalkkul_aq_fetch_data( $sido );

    if ( is_wp_error( $data ) ) {
        wp_send_json_error( $data->get_error_message() );
    }

    ob_start();
    nalkkul_aq_render_content( $data );
    $html = ob_get_clean();

    wp_send_json_success( array( 'html' => $html ) );
}

wp_ajax_nopriv_가 중요하다. 이걸 빼먹으면 로그인하지 않은 일반 방문자는 AJAX가 동작하지 않는다. 한참 테스트하다 시크릿 모드에서 안 되길래 발견했다.

// JavaScript: 도시 변경 이벤트
sel.addEventListener('change', function(){
    var sido = sel.value;
    spinner.style.display = 'inline';
    box.style.opacity = '0.5';

    var xhr = new XMLHttpRequest();
    xhr.open('GET', ajaxUrl + '?action=nalkkul_aq_fetch&sido=' + encodeURIComponent(sido));
    xhr.onload = function(){
        spinner.style.display = 'none';
        box.style.opacity = '1';
        try {
            var res = JSON.parse(xhr.responseText);
            if(res.success && res.data.html){
                box.innerHTML = res.data.html;
            }
        } catch(e){
            box.innerHTML = '<div class="taq-error">오류가 발생했습니다.</div>';
        }
    };
    xhr.send();
});

반응형 그리드와 다크모드

측정소 카드는 CSS Grid로 3열 → 2열 → 1열로 반응한다.

.taq-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }

@media (max-width: 900px) {
    .taq-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
    .taq-grid { grid-template-columns: 1fr; }
    .taq-hero { flex-direction: column; text-align: center; }
}

다크모드는 body.dark-mode 클래스를 감지해서 배경/텍스트 색상을 오버라이드한다. 카드 배경은 #1f2937, 텍스트는 #e4e4e7로 바꾼다. 등급 색상은 opacity만 살짝 낮춰서 눈이 편하게 했다.

실제 겪은 문제들

1. 403 에러 — API 키 인코딩 이슈

처음에 http_build_query()로 URL을 만들었더니 계속 403이 떴다. http_build_query()는 내부적으로 urlencode()를 쓰는데, 이게 +%2B로 바꾸지 않는다. 결국 서버에서 키를 인식하지 못한다. 수동으로 rawurlencode()를 써서 해결.

2. 빈 값의 다양한 형태

API 응답에서 데이터가 없는 필드가 null, "", "-" 세 가지 형태로 나온다. 처음에 empty()만 체크했다가 "-"를 걸러내지 못해서 숫자 0으로 표시되는 버그가 생겼다.

3. 시도명 인코딩

sidoName=서울에서 한글 인코딩도 rawurlencode()로 처리해야 한다. sidoName=%EC%84%9C%EC%9A%B8 형태가 되어야 정상 응답이 온다.

숏코드로 배포

완성된 서비스는

보통

서울 대기질

미세먼지 58 ㎍/㎥ (보통) 초미세먼지 35 ㎍/㎥ (보통)

측정시간: 2026-03-30 13:00 | 측정소 40곳

중구 나쁨
미세먼지 (PM10)
56
보통
초미세먼지 (PM2.5)
41
나쁨
오존 (O3)
0.041 ppm
한강대로 나쁨
미세먼지 (PM10)
60
보통
초미세먼지 (PM2.5)
39
나쁨
오존 (O3)
0.043 ppm
종로구 나쁨
미세먼지 (PM10)
48
보통
초미세먼지 (PM2.5)
40
나쁨
오존 (O3)
0.045 ppm
청계천로 나쁨
미세먼지 (PM10)
68
보통
초미세먼지 (PM2.5)
40
나쁨
오존 (O3)
0.039 ppm
종로 나쁨
미세먼지 (PM10)
60
보통
초미세먼지 (PM2.5)
39
나쁨
오존 (O3)
0.037 ppm
용산구 나쁨
미세먼지 (PM10)
64
보통
초미세먼지 (PM2.5)
42
나쁨
오존 (O3)
0.049 ppm
광진구 보통
미세먼지 (PM10)
51
보통
초미세먼지 (PM2.5)
31
보통
오존 (O3)
0.041 ppm
성동구 정보없음
미세먼지 (PM10)
-
정보없음
초미세먼지 (PM2.5)
-
정보없음
오존 (O3)
-
강변북로 나쁨
미세먼지 (PM10)
73
보통
초미세먼지 (PM2.5)
40
나쁨
오존 (O3)
0.024 ppm
중랑구 보통
미세먼지 (PM10)
41
보통
초미세먼지 (PM2.5)
28
보통
오존 (O3)
0.055 ppm
동대문구 보통
미세먼지 (PM10)
50
보통
초미세먼지 (PM2.5)
27
보통
오존 (O3)
0.048 ppm
홍릉로 보통
미세먼지 (PM10)
58
보통
초미세먼지 (PM2.5)
33
보통
오존 (O3)
0.034 ppm
성북구 보통
미세먼지 (PM10)
51
보통
초미세먼지 (PM2.5)
27
보통
오존 (O3)
0.054 ppm
정릉로 나쁨
미세먼지 (PM10)
52
보통
초미세먼지 (PM2.5)
36
나쁨
오존 (O3)
0.043 ppm
도봉구 보통
미세먼지 (PM10)
50
보통
초미세먼지 (PM2.5)
30
보통
오존 (O3)
0.05 ppm
은평구 보통
미세먼지 (PM10)
38
보통
초미세먼지 (PM2.5)
19
보통
오존 (O3)
0.054 ppm
서대문구 보통
미세먼지 (PM10)
44
보통
초미세먼지 (PM2.5)
19
보통
오존 (O3)
0.057 ppm
마포구 나쁨
미세먼지 (PM10)
62
보통
초미세먼지 (PM2.5)
39
나쁨
오존 (O3)
0.042 ppm
신촌로 나쁨
미세먼지 (PM10)
55
보통
초미세먼지 (PM2.5)
41
나쁨
오존 (O3)
0.037 ppm
강서구 나쁨
미세먼지 (PM10)
62
보통
초미세먼지 (PM2.5)
36
나쁨
오존 (O3)
0.036 ppm
공항대로 나쁨
미세먼지 (PM10)
84
나쁨
초미세먼지 (PM2.5)
41
나쁨
오존 (O3)
0.03 ppm
구로구 보통
미세먼지 (PM10)
64
보통
초미세먼지 (PM2.5)
34
보통
오존 (O3)
0.038 ppm
영등포구 보통
미세먼지 (PM10)
54
보통
초미세먼지 (PM2.5)
35
보통
오존 (O3)
0.041 ppm
영등포로 보통
미세먼지 (PM10)
60
보통
초미세먼지 (PM2.5)
28
보통
오존 (O3)
0.039 ppm
동작구 나쁨
미세먼지 (PM10)
58
보통
초미세먼지 (PM2.5)
37
나쁨
오존 (O3)
0.037 ppm
동작대로 중앙차로 나쁨
미세먼지 (PM10)
66
보통
초미세먼지 (PM2.5)
41
나쁨
오존 (O3)
0.022 ppm
관악구 보통
미세먼지 (PM10)
56
보통
초미세먼지 (PM2.5)
31
보통
오존 (O3)
0.035 ppm
강남구 나쁨
미세먼지 (PM10)
54
보통
초미세먼지 (PM2.5)
37
나쁨
오존 (O3)
0.04 ppm
서초구 나쁨
미세먼지 (PM10)
66
보통
초미세먼지 (PM2.5)
44
나쁨
오존 (O3)
0.039 ppm
도산대로 보통
미세먼지 (PM10)
59
보통
초미세먼지 (PM2.5)
29
보통
오존 (O3)
-
강남대로 나쁨
미세먼지 (PM10)
69
보통
초미세먼지 (PM2.5)
38
나쁨
오존 (O3)
0.021 ppm
송파구 나쁨
미세먼지 (PM10)
69
보통
초미세먼지 (PM2.5)
48
나쁨
오존 (O3)
0.033 ppm
강동구 보통
미세먼지 (PM10)
49
보통
초미세먼지 (PM2.5)
31
보통
오존 (O3)
0.062 ppm
천호대로 나쁨
미세먼지 (PM10)
57
보통
초미세먼지 (PM2.5)
39
나쁨
오존 (O3)
0.046 ppm
금천구 나쁨
미세먼지 (PM10)
62
보통
초미세먼지 (PM2.5)
41
나쁨
오존 (O3)
0.035 ppm
시흥대로 나쁨
미세먼지 (PM10)
70
보통
초미세먼지 (PM2.5)
36
나쁨
오존 (O3)
0.017 ppm
강북구 보통
미세먼지 (PM10)
51
보통
초미세먼지 (PM2.5)
26
보통
오존 (O3)
0.048 ppm
양천구 보통
미세먼지 (PM10)
51
보통
초미세먼지 (PM2.5)
31
보통
오존 (O3)
0.044 ppm
노원구 보통
미세먼지 (PM10)
55
보통
초미세먼지 (PM2.5)
34
보통
오존 (O3)
0.049 ppm
화랑로 나쁨
미세먼지 (PM10)
67
보통
초미세먼지 (PM2.5)
46
나쁨
오존 (O3)
0.032 ppm
숏코드로 어디든 삽입할 수 있다. 기본값은 서울이고,
보통

부산 대기질

미세먼지 53 ㎍/㎥ (보통) 초미세먼지 32 ㎍/㎥ (보통)

측정시간: 2026-03-30 13:00 | 측정소 36곳

태종대 보통
미세먼지 (PM10)
56
보통
초미세먼지 (PM2.5)
28
보통
오존 (O3)
-
청학동 나쁨
미세먼지 (PM10)
51
보통
초미세먼지 (PM2.5)
39
나쁨
오존 (O3)
-
전포동 나쁨
미세먼지 (PM10)
62
보통
초미세먼지 (PM2.5)
37
나쁨
오존 (O3)
0.017 ppm
온천동 보통
미세먼지 (PM10)
62
보통
초미세먼지 (PM2.5)
28
보통
오존 (O3)
0.055 ppm
명장동 보통
미세먼지 (PM10)
19
좋음
초미세먼지 (PM2.5)
21
보통
오존 (O3)
0.055 ppm
대연동 보통
미세먼지 (PM10)
46
보통
초미세먼지 (PM2.5)
25
보통
오존 (O3)
0.047 ppm
용호동 정보없음
미세먼지 (PM10)
-
정보없음
초미세먼지 (PM2.5)
-
정보없음
오존 (O3)
-
학장동 나쁨
미세먼지 (PM10)
66
보통
초미세먼지 (PM2.5)
42
나쁨
오존 (O3)
0.038 ppm
덕천동 보통
미세먼지 (PM10)
57
보통
초미세먼지 (PM2.5)
27
보통
오존 (O3)
0.049 ppm
화명동 보통
미세먼지 (PM10)
50
보통
초미세먼지 (PM2.5)
32
보통
오존 (O3)
0.048 ppm
삼락동 나쁨
미세먼지 (PM10)
53
보통
초미세먼지 (PM2.5)
40
나쁨
오존 (O3)
0.036 ppm
우동 나쁨
미세먼지 (PM10)
71
보통
초미세먼지 (PM2.5)
39
나쁨
오존 (O3)
-
감천동 보통
미세먼지 (PM10)
55
보통
초미세먼지 (PM2.5)
22
보통
오존 (O3)
0.027 ppm
청룡동 보통
미세먼지 (PM10)
47
보통
초미세먼지 (PM2.5)
24
보통
오존 (O3)
-
좌동 정보없음
미세먼지 (PM10)
-
정보없음
초미세먼지 (PM2.5)
-
정보없음
오존 (O3)
-
재송동 보통
미세먼지 (PM10)
54
보통
초미세먼지 (PM2.5)
34
보통
오존 (O3)
0.068 ppm
장림동 보통
미세먼지 (PM10)
44
보통
초미세먼지 (PM2.5)
26
보통
오존 (O3)
0.043 ppm
대저동 보통
미세먼지 (PM10)
45
보통
초미세먼지 (PM2.5)
33
보통
오존 (O3)
0.045 ppm
녹산동 나쁨
미세먼지 (PM10)
55
보통
초미세먼지 (PM2.5)
37
나쁨
오존 (O3)
0.059 ppm
명지동 나쁨
미세먼지 (PM10)
61
보통
초미세먼지 (PM2.5)
39
나쁨
오존 (O3)
0.066 ppm
연산동 나쁨
미세먼지 (PM10)
54
보통
초미세먼지 (PM2.5)
37
나쁨
오존 (O3)
0.058 ppm
기장읍 보통
미세먼지 (PM10)
53
보통
초미세먼지 (PM2.5)
31
보통
오존 (O3)
0.076 ppm
용수리 보통
미세먼지 (PM10)
41
보통
초미세먼지 (PM2.5)
26
보통
오존 (O3)
0.064 ppm
수정동 보통
미세먼지 (PM10)
48
보통
초미세먼지 (PM2.5)
17
보통
오존 (O3)
0.028 ppm
회동동 보통
미세먼지 (PM10)
44
보통
초미세먼지 (PM2.5)
26
보통
오존 (O3)
0.035 ppm
광안동 보통
미세먼지 (PM10)
55
보통
초미세먼지 (PM2.5)
31
보통
오존 (O3)
0.084 ppm
대신동 보통
미세먼지 (PM10)
45
보통
초미세먼지 (PM2.5)
28
보통
오존 (O3)
0.061 ppm
덕포동 나쁨
미세먼지 (PM10)
63
보통
초미세먼지 (PM2.5)
40
나쁨
오존 (O3)
0.042 ppm
개금동 보통
미세먼지 (PM10)
48
보통
초미세먼지 (PM2.5)
26
보통
오존 (O3)
0.052 ppm
당리동 보통
미세먼지 (PM10)
49
보통
초미세먼지 (PM2.5)
27
보통
오존 (O3)
0.045 ppm
부산항 나쁨
미세먼지 (PM10)
76
보통
초미세먼지 (PM2.5)
51
나쁨
오존 (O3)
0.018 ppm
부산신항 정보없음
미세먼지 (PM10)
-
정보없음
초미세먼지 (PM2.5)
-
정보없음
오존 (O3)
-
부산북항 나쁨
미세먼지 (PM10)
58
보통
초미세먼지 (PM2.5)
36
나쁨
오존 (O3)
0.057 ppm
부산감만 보통
미세먼지 (PM10)
62
보통
초미세먼지 (PM2.5)
35
보통
오존 (O3)
0.032 ppm
광복동 나쁨
미세먼지 (PM10)
53
보통
초미세먼지 (PM2.5)
36
나쁨
오존 (O3)
0.062 ppm
초량동 나쁨
미세먼지 (PM10)
59
보통
초미세먼지 (PM2.5)
43
나쁨
오존 (O3)
0.044 ppm
처럼 시도를 지정할 수도 있다.

add_shortcode( 'nalkkul_airquality', 'nalkkul_aq_shortcode' );

function nalkkul_aq_shortcode( $atts ) {
    $atts = shortcode_atts( array( 'sido' => '서울' ), $atts );
    $sido = sanitize_text_field( $atts['sido'] );
    $data = nalkkul_aq_fetch_data( $sido );
    // ... 렌더링
}

마무리

정리하면 이 서비스의 핵심 구조는 이렇다:

  1. wp-config.php에 API 키 상수 정의
  2. mu-plugin에서 API 호출 + 트랜지언트 캐싱 + 폴백
  3. 등급 판정 로직은 색상 코드까지 포함하는 배열 반환
  4. AJAX로 도시 변경 시 비동기 갱신
  5. 숏코드로 아무 페이지에나 삽입 가능

다음 편에서는 건강보험심사평가원 API를 연동해서 약국/병원 찾기 서비스를 만든다. 캐스케이딩 드롭다운(시도 → 시군구)과 API 문서와 실제 응답이 다른 문제를 다룰 예정이다.

시리즈 목차

데이터 출처: 한국환경공단 에어코리아(airkorea.or.kr), 공공데이터포털(data.go.kr)

이 글이 도움이 되셨다면 공유해주세요!

댓글 남기기

무엇이든 물어보세요! 💬