검색엔진 최적화를 하면서 가장 답답한 부분은 뭘까. 매일 Google Search Console에 들어가서 수동으로 순위를 확인하는 것이다. 특히 관리하는 키워드가 100개를 넘어가면 손으로 일일이 체크하기는 불가능하다. 다만 대부분의 개발자들은 Search Console이 UI를 제공할 뿐 API를 통한 자동화가 가능하다는 걸 모르거나, 알아도 구글 API 인증과 권한 설정이 복잡해서 미루고 미룬다.
이번에는 Google Search Console API를 PHP에서 직접 연동해서, 매일 정해진 시간에 자동으로 검색 순위와 클릭수, 노출수를 수집하고 데이터베이스에 저장하는 시스템을 완벽하게 구축해보자. 구글 API 인증 설정부터 시작해서 실제 데이터 수집, 그리고 대시보드에 표시하는 것까지 다룬다.
Google Search Console API를 사용하려면 먼저 Google Cloud 프로젝트를 만들고 API를 활성화해야 한다. 직접 해보니 이 부분에서 실수하는 개발자가 많다.
① Google Cloud Console 접속
https://console.cloud.google.com 으로 이동해서 새 프로젝트를 생성한다. 프로젝트 이름은 아무거나 상관없지만, 명확하게 "SEO-Monitor" 같은 이름으로 지으면 나중에 찾기 편하다.
② Search Console API 활성화
좌측 메뉴에서 "API 및 서비스" → "라이브러리"를 클릭하고, "Google Search Console API"를 검색해서 "활성화"를 누른다. 그러면 약 1분 후에 API가 활성화된다.
③ 서비스 계정 생성
"API 및 서비스" → "사용자 인증 정보"로 이동하고, 상단의 "+ 사용자 인증 정보 만들기"를 클릭한다. 그 다음 "서비스 계정"을 선택한다.
서비스 계정 이름을 입력(예: "seo-monitor-bot")하고 "만들기 및 계속"을 누른다. 그 다음 화면에서 "편집자" 역할을 선택하고 계속 진행한다. 마지막 단계에서 "키 만들기"를 클릭하고, JSON 형식으로 다운로드한다. 이 파일은 굉장히 중요하니 안전한 곳에 보관해야 한다.
서비스 계정을 만들었어도 아직 Search Console에 접근할 수 없다. Google Search Console 설정에서 직접 권한을 줘야 한다.
Google Search Console(https://search.google.com/search-console)에 로그인하고, 모니터링할 웹사이트를 선택한다. 그 다음 "설정" → "사용자 및 권한"으로 이동해서 "사용자 추가"를 클릭한다. 방금 다운로드한 JSON 파일을 열면 "client_email"이라는 항목이 있는데, 그 이메일 주소를 복사해서 입력한다. 권한은 "소유자"나 "편집자" 중 하나를 선택하고 초대를 보낸다.
PHP에서 Google Search Console API를 연동하려면 공식 Google API 클라이언트 라이브러리가 필요하다. Composer로 설치한다.
composer require google/apiclient
설치가 완료되면, 다운로드한 서비스 계정 JSON 파일을 프로젝트 디렉토리에 복사한다. 보안상 이유로 웹에서 접근할 수 없는 폴더(예: /config)에 넣는 것이 좋다.
<?php
require 'vendor/autoload.php';
$client = new Google_Client();
$client->setAuthConfig('config/service-account.json');
$client->addScope('https://www.googleapis.com/auth/webmasters');
$service = new Google_Service_Webmasters($client);
// 매일 모든 데이터를 처음부터 조회 - 비효율적
$request = new Google_Service_Webmasters_SearchAnalyticsQueryRequest();
$request->setStartDate(date('Y-m-d', strtotime('-30 days')));
$request->setEndDate(date('Y-m-d'));
$request->setRowLimit(25000);
$response = $service->searchanalytics->query('sc-domain:example.com', $request);
echo json_encode($response);
?>
이 코드의 문제점은 매번 전체 기간의 데이터를 다시 조회한다는 것. API 쿼터를 빠르게 소모하고 처리 속도도 느리다.
<?php
require 'vendor/autoload.php';
class SearchConsoleMonitor {
private $client;
private $service;
private $siteUrl = 'sc-domain:example.com';
private $pdo;
public function __construct($keyPath, $dsn, $dbUser, $dbPass) {
// Google API 클라이언트 초기화
$this->client = new Google_Client();
$this->client->setAuthConfig($keyPath);
$this->client->addScope('https://www.googleapis.com/auth/webmasters');
$this->service = new Google_Service_Webmasters($this->client);
// 데이터베이스 연결
$this->pdo = new PDO($dsn, $dbUser, $dbPass);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
// 어제 데이터만 조회 - 매일 실행해도 효율적
public function fetchYesterdayData() {
$yesterday = date('Y-m-d', strtotime('-1 day'));
$request = new Google_Service_Webmasters_SearchAnalyticsQueryRequest();
$request->setStartDate($yesterday);
$request->setEndDate($yesterday);
$request->setRowLimit(25000);
// 쿼리별 집계
$request->setDimensions(['query']);
try {
$response = $this->service->searchanalytics->query($this->siteUrl, $request);
if (isset($response['rows'])) {
$this->saveData($response['rows'], $yesterday);
return "Successfully saved " . count($response['rows']) . " queries";
}
return "No data found";
} catch (Exception $e) {
return "Error: " . $e->getMessage();
}
}
private function saveData($rows, $date) {
// 기존 데이터 삭제(중복 방지)
$stmt = $this->pdo->prepare(
"DELETE FROM search_ranking WHERE ranking_date = ?"
);
$stmt->execute([$date]);
// 새로운 데이터 삽입
$insertStmt = $this->pdo->prepare(
"INSERT INTO search_ranking (keyword, clicks, impressions, ctr, avg_position, ranking_date)
VALUES (?, ?, ?, ?, ?, ?)"
);
foreach ($rows as $row) {
$insertStmt->execute([
$row['keys'][0], // 검색어
$row['clicks'] ?? 0,
$row['impressions'] ?? 0,
($row['ctr'] ?? 0) * 100, // 백분율로 변환
$row['position'] ?? 0,
$date
]);
}
}
// 특정 키워드의 최근 30일 추이 조회
public function getKeywordTrend($keyword, $days = 30) {
$startDate = date('Y-m-d', strtotime("-{$days} days"));
$endDate = date('Y-m-d');
$stmt = $this->pdo->prepare(
"SELECT ranking_date, clicks, impressions, avg_position
FROM search_ranking
WHERE keyword = ? AND ranking_date BETWEEN ? AND ?
ORDER BY ranking_date ASC"
);
$stmt->execute([$keyword, $startDate, $endDate]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
// 사용 예제
$monitor = new SearchConsoleMonitor(
'config/service-account.json',
'mysql:host=localhost;dbname=seo_db;charset=utf8mb4',
'root',
'password'
);
echo $monitor->fetchYesterdayData();
?>
이 코드의 핵심 개선점은 3가지다. 첫째, 어제 데이터만 조회하므로 매일 실행해도 API 쿼터를 최소한으로 사용한다. 둘째, 데이터를 MySQL에 저장해서 시간 경과에 따른 순위 변화 추적이 가능하다. 셋째, getKeywordTrend() 함수로 특정 키워드의 추이를 쉽게 조회할 수 있다.
데이터를 저장할 테이블을 먼저 만들어야 한다.
CREATE TABLE search_ranking (
id INT AUTO_INCREMENT PRIMARY KEY,
keyword VARCHAR(255) NOT NULL,
clicks INT DEFAULT 0,
impressions INT DEFAULT 0,
ctr DECIMAL(5, 2) DEFAULT 0,
avg_position DECIMAL(5, 2) DEFAULT 0,
ranking_date DATE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_keyword_date (keyword, ranking_date),
INDEX idx_keyword (keyword),
INDEX idx_date (ranking_date)
);
UNIQUE KEY unique_keyword_date 제약으로 같은 키워드와 날짜 조합이 중복 저장되는 것을 방지한다.
매일 자동으로 데이터를 수집하려면 cron 작업을 설정해야 한다. 보통 밤 12시 30분에 실행하는 것이 좋다(Google Search Console 데이터가 자정 이후 업데이트되기 때문).
우선 PHP 실행 파일을 만든다.
<?php
// cron-fetch-ranking.php
require 'src/SearchConsoleMonitor.php';
$monitor = new SearchConsoleMonitor(
'/home/user/config/service-account.json',
'mysql:host=localhost;dbname=seo_db;charset=utf8mb4',
'root',
'password'
);
$result = $monitor->fetchYesterdayData();
file_put_contents('/var/log/ranking-sync.log', date('Y-m-d H:i:s') . " - " . $result . PHP_EOL, FILE_APPEND);
?>
그 다음 crontab에 등록한다.
crontab -e
에디터가 열리면 다음 줄을 추가한다.
30 0 * * * /usr/bin/php /home/user/public_html/cron-fetch-ranking.php
이렇게 설정하면 매일 자정 30분에 어제 데이터를 자동으로 수집한다. 로그 파일에 실행 결과가 기록되므로, 나중에 문제 발생 시 디버깅하기 쉽다.
수집한 데이터를 웹에서 볼 수 있게 간단한 대시보드를 만들어보자.
<?php
session_start();
require 'src/SearchConsoleMonitor.php';
$pdo = new PDO(
'mysql:host=localhost;dbname=seo_db;charset=utf8mb4',
'root',
'password'
);
// 최근 7일간 상위 10개 키워드(임프레션 기준)
$stmt = $pdo->query(
"SELECT keyword, SUM(impressions) as total_impressions,
SUM(clicks) as total_clicks, AVG(avg_position) as avg_pos
FROM search_ranking
WHERE ranking_date BETWEEN DATE_SUB(NOW(), INTERVAL 7 DAY) AND CURDATE()
GROUP BY keyword
ORDER BY total_impressions DESC
LIMIT 10"
);
$topKeywords = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 순위 변화를 추적할 키워드 선택
$selectedKeyword = $_GET['keyword'] ?? null;
$trendData = [];
if ($selectedKeyword) {
$monitor = new SearchConsoleMonitor(
'config/service-account.json',
'mysql:host=localhost;dbname=seo_db;charset=utf8mb4',
'root',
'password'
);
$trendData = $monitor->getKeywordTrend($selectedKeyword, 30);
}
?>
<!DOCTYPE html>
<html>
<head>
<title>SEO Ranking Monitor</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
th { background-color: #f2f2f2; }
tr:hover { background-color: #f9f9f9; }
.metric { display: inline-block; margin: 10px; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
</style>
</head>
<body>
<h1>SEO Ranking Monitor</h1>
<h2>Top 10 Keywords (Last 7 Days)</h2>
<table>
<thead>
<tr>
<th>Keyword</th>
<th>Impressions</th>
<th>Clicks</th>
<th>Avg Position</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($topKeywords as $kw): ?>
<tr>
<td><?= htmlspecialchars($kw['keyword']) ?></td>
<td><?= number_format($kw['total_impressions']) ?></td>
<td><?= number_format($kw['total_clicks']) ?></td>
<td><?= round($kw['avg_pos'], 1) ?></td>
<td><a href="?keyword=<?= urlencode($kw['keyword']) ?>">View Trend</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if ($selectedKeyword && !empty($trendData)): ?>
<h2>Ranking Trend: <?= htmlspecialchars($selectedKeyword) ?></h2>
<canvas id="trendChart"></canvas>
<script>
const ctx = document.getElementById('trendChart').getContext('2d');
const labels = <?= json_encode(array_column($trendData, 'ranking_date')) ?>;
const positions = <?= json_encode(array_column($trendData, 'avg_position')) ?>;
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Average Position',
data: positions,
borderColor: 'rgb(75, 192, 192)',
tension: 0.1,
fill: false
}]
},
options: {
responsive: true,
scales: {
y: {
reverse: true, // 낮을수록 좋으므로 역순
beginAtZero: true
}
}
}
});
</script>
<?php endif; ?>
</body>
</html>
이 대시보드는 최근 7일간의 상위 키워드를 표로 보여주고, 특정 키워드를 선택하면 지난 30일간의 순위 변화를 그래프로 표시한다.
✗ 권한 설정 누락 - Search Console API 활성화는 했는데 Search Console 설정에서 서비스 계정 이메일을 추가하지 않으면 "Access Denied" 에러가 난다. 반드시 두 곳 모두 설정해야 한다.
✓ Google Cloud Console과 Search Console 둘 다에서 권한 설정하기
✗ API 쿼터 초과 - Search Console API는 월별 쿼터가 있다(기본 200 요청/일). 전체 기간 데이터를 매일 다시 조회하면 금방 넘어간다.
✓ 어제 데이터만 조회하거나, 저장된 데이터를 캐시로 활용하기
✗ 타임존 설정 미흡 - Google Search Console의 데이터는 PST(태평양 표준시) 기준이다. 한국에서 자정을 기준으로 수집하려면 시간 차이를 고려해야 한다.
✓ cron 실행 시간을 UTC 기준으로 조정하거나, PHP에서 명시적으로 date_default_timezone_set()을 사용하기
Google Search Console API는 SEO 데이터를 자동화하는 가장 직접적인 방법이다. 초기 설정이 복잡해 보이지만, 한 번 구축하면 매일 자동으로 순위 데이터가 쌓인다. 그 데이터를 분석해서 어떤 키워드는 상승 추세인지, 어떤 키워드는 하락했는지 한눈에 파악할 수 있다. 이 글의 코드를 참고해서 한 번 시도해보면, 수동 모니터링 시간을 절약하면서 동시에 SEO 데이터에 기반한 의사결정이 가능할 것이다.