작년 겨울, 미세먼지가 심한 날 네이버에서 미세먼지 수치를 확인하다가 문득 이런 생각이 들었다. “이 데이터, 나도 직접 가져올 수 있지 않나?” 공공데이터포털(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 형태가 되어야 정상 응답이 온다.
숏코드로 배포
완성된 서비스는 측정시간: 2026-03-30 16:00 | 측정소 40곳 측정시간: 2026-03-30 16:00 | 측정소 36곳
숏코드로 어디든 삽입할 수 있다. 기본값은 서울이고, 서울 대기질
처럼 시도를 지정할 수도 있다.부산 대기질
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 );
// ... 렌더링
}
마무리
정리하면 이 서비스의 핵심 구조는 이렇다:
- wp-config.php에 API 키 상수 정의
- mu-plugin에서 API 호출 + 트랜지언트 캐싱 + 폴백
- 등급 판정 로직은 색상 코드까지 포함하는 배열 반환
- AJAX로 도시 변경 시 비동기 갱신
- 숏코드로 아무 페이지에나 삽입 가능
다음 편에서는 건강보험심사평가원 API를 연동해서 약국/병원 찾기 서비스를 만든다. 캐스케이딩 드롭다운(시도 → 시군구)과 API 문서와 실제 응답이 다른 문제를 다룰 예정이다.
시리즈 목차
- [현재 글] 1편: 미세먼지 실시간 조회 서비스
- 2편: 약국/병원 찾기 서비스
- 3편: 기업정보 조회와 사업자등록 확인
- 4편: 수익화 전략과 운영 노하우
데이터 출처: 한국환경공단 에어코리아(airkorea.or.kr), 공공데이터포털(data.go.kr)