네이버 API로 "황금 키워드" 발굴 도구 만들기 — 서명 생성부터 점수 공식까지 (PHP 실전)

·조회 3 ·약 7분

네이버 API로 "황금 키워드" 발굴 도구 만들기 — 서명 생성부터 점수 공식까지 (PHP 실전)

카테고리: AI 기반 개발 / 트러블슈팅 태그: 네이버검색광고api, 키워드분석, php, hmac, 블로그seo, 개발기


무엇을 만드나

"검색량은 많은데 경쟁은 적은 키워드"를 찾는 도구를 직접 만듭니다. 상용 키워드 도구가 하는 일을 API 2개로 재현하는 겁니다.

  • 네이버 검색광고 API (/keywordstool) → 월간 검색수(PC·모바일), 경쟁정도, 연관 키워드 대량 수집
  • 네이버 검색 API (/search/blog) → 그 키워드로 발행된 블로그 문서 수(= 공급/경쟁)

핵심 아이디어는 간단합니다. 검색 수요(검색광고 API) ÷ 공급(블로그 발행량) = 빈틈. 이 글은 그걸 PHP로 처음부터 짜면서, 제가 실제로 밟은 지뢰 3개까지 같이 보여드립니다.

두 API는 성격이 완전히 다릅니다. 이게 첫 번째 함정입니다.

검색광고 API 검색(오픈) API
인증 HMAC-SHA256 서명 + Customer ID Client ID/Secret 헤더
키 발급 검색광고센터 → 도구 → API 관리 개발자센터 앱 등록
호출 한도 계정별 하루 25,000회
base URL api.searchad.naver.com openapi.naver.com

1단계 — 검색광고 API 서명 생성 (여기서 다들 막힙니다)

검색광고 API는 오픈 API와 달리 요청마다 서명을 만들어야 합니다. 공식은 이겁니다.

signature = base64( HMAC-SHA256( secret_key,  timestamp + "." + method + "." + uri ) )
  • timestamp = Unix epoch 밀리초 (초 아님! 자주 틀리는 부분)
  • method = GET
  • uri = /keywordstool (쿼리스트링 제외, 경로만)

PHP로는 세 줄입니다.

function nv_searchad_signature(string $timestamp, string $method, string $uri, string $secret): string
{
    return base64_encode(hash_hmac('sha256', $timestamp . '.' . $method . '.' . $uri, $secret, true));
}

hash_hmac의 마지막 인자 true가 중요합니다(raw binary 출력 → base64). 이걸 빼면 hex가 나와서 서명이 안 맞습니다.


2단계 — 연관 키워드 + 검색량 가져오기

서명을 헤더 4종(X-Timestamp, X-API-KEY, X-Customer, X-Signature)에 실어 보냅니다.

function nv_searchad_rows(string $hintKeyword, string $apiKey, string $secret, string $customer): array
{
    $method = 'GET';
    $uri = '/keywordstool';
    $timestamp = (string) round(microtime(true) * 1000);   // 밀리초

    $url = 'https://api.searchad.naver.com' . $uri . '?' . http_build_query([
        'hintKeywords' => preg_replace('/\s+/u', '', $hintKeyword) ?: $hintKeyword,
        'showDetail'   => 1,
    ]);

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 12,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json; charset=UTF-8',
            'X-Timestamp: ' . $timestamp,
            'X-API-KEY: '   . $apiKey,
            'X-Customer: '  . $customer,
            'X-Signature: ' . nv_searchad_signature($timestamp, $method, $uri, $secret),
        ],
    ]);
    $body = curl_exec($ch);
    $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($code !== 200) return [];
    $json = json_decode($body, true);
    return $json['keywordList'] ?? [];
}

응답의 keywordList 각 항목에는 이런 필드가 옵니다.

네이버 키워드 도구를 직접 만들다가 "erp 맛집"이 나온 이유

{
  "relKeyword": "강남역맛집",
  "monthlyPcQcCnt": 12600,
  "monthlyMobileQcCnt": 88200,
  "compIdx": "높음",
  "monthlyAvePcClkCnt": 1.6,
  "plAvgDepth": 7
}

monthlyPcQcCnt + monthlyMobileQcCnt가 총검색량입니다. 주의: 검색수가 아주 적으면 "< 10" 같은 문자열이 옵니다. 숫자 변환 함수를 하나 둡니다.

function nv_num($v): int {
    $s = trim((string)$v);
    if ($s === '') return 0;
    if (strpos($s, '<') !== false) return 10;   // "< 10" → 10
    return (int) str_replace(',', '', $s);
}

3단계 — 블로그 발행량(공급) 가져오기

검색광고 API는 검색량은 주지만 "그 키워드로 블로그가 몇 개 있는지"는 안 줍니다. 그건 오픈 API의 블로그 검색으로 가져옵니다. total 필드가 곧 발행량입니다.

function nv_blog_total(string $keyword, string $clientId, string $clientSecret): ?int
{
    $url = 'https://openapi.naver.com/v1/search/blog.json?' . http_build_query([
        'query'   => $keyword,
        'display' => 1,     // 문서 1개만 받아도 total은 전체 개수
        'sort'    => 'sim',
    ]);
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 5,
        CURLOPT_HTTPHEADER     => [
            'X-Naver-Client-Id: '     . $clientId,
            'X-Naver-Client-Secret: ' . $clientSecret,
        ],
    ]);
    $body = curl_exec($ch);
    curl_close($ch);
    $json = json_decode($body, true);
    return isset($json['total']) ? (int)$json['total'] : null;
}

문제는 후보가 30~50개면 이걸 30~50번 호출해야 한다는 겁니다. 순차로 하면 몇 초씩 걸립니다. curl_multi로 5개씩 병렬 처리하고, 24시간 파일 캐시를 씌웁니다.

foreach (array_chunk($pending, 5) as $chunk) {
    $mh = curl_multi_init();
    $handles = [];
    foreach ($chunk as $kw) {
        $ch = curl_init('https://openapi.naver.com/v1/search/blog.json?' .
            http_build_query(['query' => $kw, 'display' => 1, 'sort' => 'sim']));
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5,
            CURLOPT_HTTPHEADER => [
                'X-Naver-Client-Id: ' . $clientId,
                'X-Naver-Client-Secret: ' . $clientSecret,
            ],
        ]);
        curl_multi_add_handle($mh, $ch);
        $handles[] = [$kw, $ch];
    }
    do { $status = curl_multi_exec($mh, $running); if ($running) curl_multi_select($mh, 0.5); }
    while ($running && $status === CURLM_OK);

    foreach ($handles as [$kw, $ch]) {
        $json = json_decode((string)curl_multi_getcontent($ch), true);
        $out[$kw] = isset($json['total']) ? (int)$json['total'] : null;
        curl_multi_remove_handle($mh, $ch);
        curl_close($ch);
    }
    curl_multi_close($mh);
    usleep(120000);   // 429 방지: 청크 사이 0.12초 쉼
}

하루 25,000회 한도가 있으니, 같은 키워드는 반드시 캐시해서 재호출을 막습니다.

여기까지가 "정상 경로"입니다. 이제 제가 실제로 밟은 지뢰들입니다.


지뢰 ① — erp를 쳤더니 "erp맛집"이 나왔다

업무용 소프트웨어 ERP가 궁금해서 소문자로 erp를 쳤습니다. 추천에 erp맛집, erp카페, erp가볼만한곳이 떴습니다. 블로그글이 1,200만 개라는 숫자까지.

API를 의심하기 전에 직접 찍어봤습니다.

var_dump(count(nv_searchad_rows('erp', ...)));   // 1
var_dump(count(nv_searchad_rows('ERP', ...)));   // 565
입력 네이버가 준 연관어
erp (소문자) 1개
ERP (대문자) 565개

네이버 키워드 도구는 erpERP다른 단어로 취급합니다. 소문자로 보내니 1개만 왔고, 그러면 제 코드가 "연관어가 부족하네" 하며 폴백을 켰습니다. 폴백은 검색어 뒤에 맛집·카페 같은 접미사를 붙여 후보를 지어내는 로직이었고요.

그래서 erp + 맛집이 태어난 겁니다. API가 아니라 제 코드가 만든 가짜 키워드였습니다.

고침은 한 줄입니다. 보내기 전에 대문자로 정규화(한글은 영향 없음).

$keyword = mb_strtoupper($keyword, 'UTF-8');   // 'erp' → 'ERP'

여기가 첫 번째 사고 지점이었습니다.


지뢰 ② — 정렬 기준을 잘못 잡으면 좋은 키워드가 잘려나간다

강남역 맛집을 쳤더니 추천에 월롱역맛집(파주에 있습니다)이 떴습니다. 또 찍어봤습니다.

네이버 키워드 도구를 직접 만들다가 "erp 맛집"이 나온 이유

  • 네이버가 강남역맛집에 준 키워드: 1,200개
  • 그중 강남역 포함: 75개 — 그것도 검색량이 큽니다 (강남역맛집 10만, 강남역술집 1.7만)

이 좋은 게 왜 화면에 없었을까요? 블로그 발행량 조회는 비싸서 30개만 잘라 쓰는데, 그 자르기 전 정렬이 "경쟁도 낮은 순"이었습니다.

// 잘못된 정렬 — 경쟁도 낮은 순
usort($items, fn($a, $b) =>
    nv_comp_rank($a['compIdx']) <=> nv_comp_rank($b['compIdx']));
$items = array_slice($items, 0, 30);

강남역맛집·강남역술집은 인기라 경쟁도가 "높음"이고, 검색량 몇백짜리 무명 키워드가 "낮음"입니다. 경쟁도 낮은 순으로 자르니, 검색량 큰 진짜 키워드가 30개 밖으로 밀려 사라졌습니다.

고침 — 검색어 관련성 먼저, 그 안에서 검색량 큰 순으로 자릅니다.

$core = nv_seed_core($keyword);   // '강남역 맛집' → '강남역'
usort($items, function ($a, $b) use ($core) {
    $am = mb_strpos($a['relKeyword'], $core) !== false ? 1 : 0;
    $bm = mb_strpos($b['relKeyword'], $core) !== false ? 1 : 0;
    if ($am !== $bm) return $bm <=> $am;              // 관련 키워드 먼저
    return (int)$b['total'] <=> (int)$a['total'];     // 그 안에서 검색량 순
});
$items = array_slice($items, 0, 50);

여기서 nv_seed_core가 핵심입니다. 강남역 맛집에서 맛집은 모든 맛집 키워드에 있으니 관련성 판단에 무용합니다. 구별되는 토큰(강남역)만 뽑아 기준으로 씁니다.


지뢰 ③ — 점수 공식이 사람 직관과 거꾸로였다

처음 점수 공식은 이랬습니다.

점수 = (검색량 ÷ 블로그글) × (1 - 경쟁도)

검색량이 비율로만 들어갑니다. 그래서 검색량 400짜리라도 블로그글이 유난히 적으면 점수가 튀어 추천 1위가 됐습니다. 블로그 글 쓰는 입장에서 검색량 400은 수요 자체가 없는데 말이죠.

우선순위를 상식대로 다시 짰습니다. ① 검색량(수요) → ② 블로그글 적을수록 → ③ 경쟁 낮을수록.

function nv_score_formula(int $total, string $comp, ?int $blog, float $intentWeight): float
{
    if ($total <= 0 || !$blog) return 0.0;
    $saturation = $total / ($total + $blog);   // 0~1, 블로그 적을수록 1
    // 총검색량이 규모를 지배 → 검색량 큰 게 위로. saturation이 포화도 보정.
    return round($total * $intentWeight * (1 - nv_comp_penalty($comp)) * $saturation, 1);
}

nv_comp_penalty는 경쟁도를 감점으로 바꿉니다 (낮음 0.15 / 중간 0.5 / 높음 0.85). 그리고 추천 최소 검색량을 100에서 1,000으로 올려, 검색량 몇백짜리가 1위에 못 오게 막았습니다.


결과

강남역 맛집 재검색:

강남역맛집    검색량 100,800   추천 1위
강남역데이트  검색량 5,490     추천 2위
강남역점심맛집 검색량 3,730    추천 3위

진짜 강남역 키워드가 검색량 순으로 정직하게 나옵니다.

작은 반례도 하나. 접미사에서 맛집·카페를 아예 빼봤더니, 이번엔 청계천에서 청계천솔루션 같은 게 나왔습니다. 장소에 사업 접미사가 붙은 거죠. 그래서 되돌렸습니다. 장소형·사업형을 함께 두고, 실제 검색량이 있는 것만 살아남게 하는 게 옳았습니다.


따라 만들 분들을 위한 체크리스트

  • [ ] 검색광고 API: 서명은 밀리초 timestamp + hash_hmac(..., true) (raw)
  • [ ] uri는 경로만(/keywordstool), 쿼리스트링 제외하고 서명
  • [ ] 영문 키워드는 대문자 정규화 후 호출
  • [ ] 검색량 "< 10" 문자열 방어
  • [ ] 블로그 발행량은 curl_multi 병렬 + 캐시 (하루 25,000회 한도)
  • [ ] 30~50개 자르기 정렬을 "관련성 → 검색량" 순으로
  • [ ] 점수는 검색량을 지배 인자로, 비율은 보정으로

두 개의 API를 붙이는 건 반나절이면 됩니다. 하지만 "쓸 만한 결과"가 나오게 만드는 건 위 지뢰 3개를 밟고 나서였습니다. 여러분은 키워드를 고를 때 검색량과 경쟁도 중 무엇을 먼저 보시나요?

네이버검색광고api #키워드분석 #php #hmac #블로그seo #개발기

댓글 0

비밀번호는 본인 댓글 삭제에 쓰입니다.
아직 댓글이 없습니다. 첫 댓글을 남겨보세요!
Powered by maocafe blog