Google Maps 리뷰 API: 알맞은 진입점을 고르고 장소 리뷰를 기록하는 방법
Business Profile, Places API, EveryInfra 장소 리뷰 조회의 차이를 구분하고, 장소 식별 정보, 언어, 기간 필터를 검증하고, 평점과 업체 답글의 변화를 보존하며, 샘플·저장·표시의 범위를 명확히 하는 방법을 다룹니다.
“Google Maps 리뷰를 가져오고 싶다”는 말은 세 가지 다른 작업을 뜻할 수 있습니다. 자신의 매장 리뷰를 관리하는 것, 제품에 장소 정보를 표시하는 것, 여러 장소의 사용자 피드백을 조사하는 것입니다. 세 작업 모두 리뷰를 다루지만 권한, 엔드포인트, 보존 규칙은 서로 다릅니다. 진입점을 잘못 고르면 나중에 요청량을 늘리거나 파싱 방식을 바꿔도 바로잡을 수 없습니다.
이 가이드는 먼저 워크플로를 고르는 방법을 안내한 뒤, EveryInfra의 google_maps_reviews.reviews로 장소를 검증하고, 소규모 요청을 구성하고, 평점과 업체 답글을 처리하고, 지속적인 관측을 구성하는 방법을 설명합니다. 핵심은 모든 장소의 과거 리뷰를 전부 내보낼 수 있다고 약속하는 것이 아니라, 각 데이터 레코드가 올바른 장소에 연결되도록 하고 샘플의 공백을 설명하는 것입니다.
인덱스와 구현 확인 기준일은 2026-09-04입니다. 본문에서 언급한 유료 요청은 이번에 실행하지 않았으며, 코드의 합성 레코드는 필드 변경 검증을 보여 주는 용도로만 사용합니다. 해당 데이터의 조회, 처리, 저장, 표시 용도에 대한 권한을 확인한 뒤에만 템플릿을 프로덕션에 사용하십시오.
진입점 고르기: 업체 관리, 장소 표시, 조사용 조회
운영 권한이 있는 매장을 관리하려면 먼저 Google Business Profile API를 검토하십시오. 공식 리뷰 가이드는 계정과 장소 단위로 읽기 및 업체 답글 작업을 구성하며, 연동하려면 앱 등록과 OAuth 사용자 인증 정보가 필요합니다. 사용자 리뷰에 답글을 달 때는 임의의 지도 링크에 일반적인 쓰기 요청을 보내지 말고, 적절한 관리 권한으로 작업을 수행하십시오.
공식 가이드의 deleteReply 함수는 사용자의 원본 리뷰가 아니라 업체 답글을 삭제합니다. 이 차이는 운영 워크플로에서 중요합니다. 직접 작성한 잘못된 답글을 철회하는 작업과 부적절한 사용자 리뷰를 처리하는 작업은 각각의 절차를 따라야 하며, DELETE 예시를 잘못 적용해서는 안 됩니다.
제품에 장소 정보를 표시하려면 Google Places API의 Place Details 엔드포인트를 참고하십시오. 이 엔드포인트는 장소 ID로 조회하며 FieldMask로 필요한 필드를 선택합니다. 이 장소 표시 경로를 과거 리뷰 전체를 일괄 내보내는 인터페이스로 바로 쓸 수는 없습니다. Places 콘텐츠를 사용할 때는 캐싱, 저장, 출처 표시 요구 사항도 준수해야 합니다.
EveryInfra는 리뷰를 읽는 별도의 방법을 제공합니다. platform은 google_maps_reviews, action은 reviews이며, 입력값은 장소 URL입니다. 아래 섹션은 이 조회 경로만 설명하며, 매장 프로필 수정, 답글 작성, 삭제는 다루지 않습니다. 또한 이 경로를 Google 공식 API로 지칭하지 않습니다.
서드파티 인터페이스를 선택한다고 데이터 사용 제한이 사라지지는 않습니다. Google Maps 추가 약관에는 복사, 재배포, 대량 다운로드에 대한 제한이 있으며, Places API에도 자체 콘텐츠 규정이 있습니다. 요청 한 번으로 데이터가 반환된다고 해서 장기간 쌓아 두거나, 대량으로 재판매하거나, 임의로 다시 표시해도 된다는 뜻은 아닙니다. 계획한 용도가 허용 범위를 벗어난다면 기술로 라이선스 판단을 우회하지 말고 계획을 조정하십시오.
공식 업체 관리 경로의 구체적인 파라미터가 필요하다면 reviews.list를 직접 확인하십시오. 이 메서드는 계정과 장소를 대상으로 리뷰 목록, 전체 평균 평점, 총 리뷰 수, 다음 페이지 토큰을 제공합니다. 이 구조를 보면 “이 페이지에서 가져온 항목 수”와 “이 장소의 총 리뷰 수”를 구분해야 하는 이유를 알 수 있습니다. 다만 이 메서드의 필드나 페이지네이션 규칙을 EveryInfra에 대한 보장으로 옮겨 적용하지 마십시오.
1단계: 매장 이름만 보지 말고 장소 식별 정보 확인하기
매장 이름은 안정적인 고유 식별자가 아닙니다. 같은 브랜드가 여러 지점을 운영할 수 있고, 같은 상권에 이름이 비슷한 매장이 있을 수 있습니다. 매장 이름이 바뀌었다고 기존 레코드가 반드시 새 매장의 것이 되는 것도 아닙니다. 리뷰 요청을 시작하기 전에 비즈니스 목표 체크리스트에서 장소 참조 정보, 주소, 사업체 소유 관계를 확인하십시오.
요청에 사용한 전체 장소 URL과 신뢰할 수 있는 방법으로 얻은 place_id를 함께 보관하십시오. 동일한 장소임이 확인된 데이터만 병합하고, 지리적으로 가깝거나 이름이 같거나 문자열이 비슷하다는 이유만으로 두 매장의 리뷰를 합치지 마십시오. 확인되지 않은 대상은 검토 체크리스트로 보내, 이후 분석에서 잘못 식별된 매장 데이터를 불만 추세로 취급하지 않도록 합니다.
현재 조회 구현은 URL에서 식별 가능한 장소 정보를 활용하지만, 모든 지도 주소 형식이 대상 지정에 똑같이 적합하지는 않습니다. 매장 이름과 지도 뷰포트 좌표만 담긴 링크를 그 자체로 확인된 장소 ID로 취급해서는 안 됩니다. 지도 페이지처럼 보이는 임시 주소를 조합하기보다 검증된 전체 원본 링크를 보관하는 것을 우선하십시오.
여기에도 인터페이스 경계가 있습니다. 구현 내부에서 일부 장소 정보를 인식하더라도, 공개 요청에 placeId나 place_id 파라미터를 임의로 추가할 수는 없습니다. 현재 reviews의 필수 파라미터는 url이며, 클라이언트는 공개 문서에 따라 파라미터를 전달해야 합니다.
2단계: 언어, 정렬, 기간 조건 확인하기
curl -fsS --max-time 30 \
'https://api.everyinfra.com/api/v1/social/catalog?platform=google_maps_reviews' \
| jq -e '.capabilities[] | select(.action == "reviews") | {
platform, action, required_params, optional_params, param_meanings,
mode, returns_list, default_limit, max_limit, response_fields
}'이 디렉터리에서 url은 필수이며, 동기 방식으로 목록을 반환하고, 기본 항목 수는 20개, 상한은 100개입니다. 선택 파라미터로 lang, language, since, sort를 지원합니다. 정확한 플랫폼 식별자는 google_maps_reviews이며, 글 제목을 보고 다른 플랫폼 이름으로 줄여 쓰지 마십시오.
newest와 relevance는 서로 다른 정렬 의도를 나타냅니다. 전자는 최근 콘텐츠를, 후자는 관련성 기준 샘플을 대상으로 하며, 서로 바꿔 쓸 수 있는 페이지 데이터가 아닙니다. 매장을 비교할 때는 선정 기준을 일관되게 유지하고 실제로 가져온 리뷰 ID를 저장하십시오. 정렬 순서를 바꾸면 샘플 구성이 달라질 수 있습니다.
언어 필드는 작성자의 국적이나 거주지로 필터링하지 않습니다. lang에 허용되는 값은 인터페이스 열거형을 따르며, 현재 en, zhcn, zhtw가 있습니다. 포괄적인 설명을 근거로 임의의 언어 코드를 넣어서는 안 됩니다. lang과 language를 무작정 함께 채우지 마십시오. 현재 매핑에서는 lang이 language 설정보다 우선하므로, 초기 구현에서는 명시적으로 지원되는 옵션 하나만 선택해야 합니다.
since는 리뷰 게시일의 하한을 나타내며, 디렉터리에서는 YYYY-MM-DD 형식을 권장합니다. 이는 매장 개업일이나 리뷰 수정 시각이 아니며, 처리 진행 상황을 나타내는 신뢰할 수 있는 커서도 아닙니다. 필요할 때 필터 조건으로 사용하고, 경계 날짜는 실제 결과와 대조해 따로 검증하십시오.
3단계: 사용 권한이 확인된 장소 하나로 소량의 결과 샘플 테스트하기
서버에 EVERYINFRA_API_KEY와 확인된 EVERYINFRA_MAPS_PLACE_URL을 설정했다고 가정합니다. 예시 요청은 한 번만 전송하고, limit은 params 안에 넣으십시오. 요청 최상위에 두면 항목 수가 제어되지 않습니다.
: "${EVERYINFRA_API_KEY:?set the API Key first}"
: "${EVERYINFRA_MAPS_PLACE_URL:?set the authorized full place URL first}"
jq -cn --arg url "${EVERYINFRA_MAPS_PLACE_URL}" '{
platform: "google_maps_reviews",
action: "reviews",
params: {url: $url, sort: "newest", limit: 3}
}' | curl --silent --show-error --include --max-time 180 \
'https://api.everyinfra.com/api/v1/social' \
-H "Authorization: Bearer ${EVERYINFRA_API_KEY}" \
-H 'Content-Type: application/json' --data-binary @-limit: 3은 테스트용 소량 샘플 설정이며, 정확히 3개 항목이 반환된다고 보장하지 않고 장소에 리뷰가 3개뿐이라는 뜻도 아닙니다. 180초는 클라이언트 대기 시간의 예시일 뿐 서비스 보장 값이 아닙니다. 먼저 HTTP 상태와 오류를 확인한 다음, 결과가 예상한 목록인지, 대상 장소와 일치하는지 검증하십시오.
빈 배열을 곧바로 “이 매장에는 리뷰가 없다”로 표시할 수는 없습니다. 이번 호출에서 조회가 실패했을 수도 있고, 대상, 필터, 접근 가능 여부를 확인해야 할 수도 있습니다. 실제 원인이 불분명하면 알 수 없음 상태를 유지하십시오. 클라이언트가 타임아웃되었을 때 이전 시도가 실행되지 않았거나 과금되지 않았다고 가정하고 POST 요청을 자동으로 재시도해서는 안 됩니다.
4단계: 장소 평점, 개별 평점, 답글 분리하기
현재 레코드 필드에는 id, text, rating, rating_scale, posted_at, place_id, place_name, owner_response, owner_response_at이 있습니다. 장소 정보의 평균 평점과 개별 리뷰 평점은 서로 다른 객체입니다. 빈 값을 채우려고 매장 평균 점수를 모든 리뷰에 복사하지 마십시오.
curl -fsS --max-time 30 \
'https://api.everyinfra.com/api/v1/social/fields?platform=google_maps_reviews&action=reviews' \
| jq '{fields, undocumented}'평점과 본문도 따로 처리해야 합니다. 텍스트 없이 평점만 있는 레코드는 평점 샘플로는 쓸모가 있지만, 텍스트 주제 분류에 그대로 넣을 수는 없습니다. rating이 없으면 알 수 없음 상태를 유지하십시오. 여러 플랫폼을 비교할 때는 rating_scale을 평점과 함께 평가하고, 척도가 다른 숫자를 평균하지 마십시오.
detailed_rating이 반환되면 세부 항목 점수가 담기지만, 모든 장소나 리뷰가 같은 세부 항목을 가진다고 가정해서는 안 됩니다. is_local_guide와 과거 리뷰 수를 “실제 사용자” 인증이나 신뢰도 점수로 바로 취급해서도 안 됩니다. 이는 플랫폼이 제공하는 속성일 뿐, 특정 리뷰 내용이 사실인지 검증한 결과가 아닙니다.
매장 답글은 따로 저장합니다. owner_response가 없다는 것은 이번 실행에서 이 값을 가져오지 못했다는 뜻일 뿐, 업체가 답글을 단 적이 없다는 증거는 아닙니다. 나중에 답글을 새로 확보했더라도 사용자의 원래 리뷰가 새로 게시되었다는 뜻은 아닙니다. 새로 확보한 답글과 기존 리뷰를 모두 새 리뷰로 취급하면 의미 없는 신규 항목 모니터링 알림이 발생합니다.
5단계: 식별자, 관측, 변경 기록을 따로 만들기
- 장소 식별자: 확인된 place_id, 출처 참조, 사업자 소유 관계. 중복 매장은 이름으로 대충 병합하지 말고 식별자부터 확인합니다.
- 리뷰 식별자: 장소 ID와 리뷰 ID의 조합. 신뢰할 수 있는 ID가 없는 레코드는 따로 처리하며, 작성자 닉네임을 고유 키로 쓰지 않습니다.
- 관측 로그: 요청 ID, 실제 수집 타임스탬프, 정렬 및 언어/날짜 조건, 가져온 필드, 누락된 요소.
- 변경 로그: 같은 리뷰의 내용, 평점, 업체 답글에서 관측된 차이. 알 수 없는 필드를 삭제로 해석하지 않습니다.
비교에 필요한 필드는 허용된 범위와 기간 내에서만 저장하십시오. 텍스트를 저장할 수 없다면 레코드를 허용된 최소 상태로 조정하고, “감사가 쉽다”는 이유로 무기한 보관하지 마십시오. 다음 섹션에서는 가상의 장소, 리뷰, 텍스트로 값 변경, 처음 확보한 필드, 확보하지 못한 데이터를 구분하는 방법을 보여 줍니다.
function compareReview(before, after) {
const identity = row => {
if (typeof row.place_id !== "string" || !row.place_id
|| typeof row.id !== "string" || !row.id) {
throw new Error("place and review ids required");
}
return JSON.stringify([row.place_id, row.id]);
};
if (identity(before) !== identity(after)) throw new Error("different review");
const t0 = Date.parse(before.observed_at);
const t1 = Date.parse(after.observed_at);
if (!Number.isFinite(t0) || !Number.isFinite(t1) || t1 <= t0) {
throw new Error("observations must have increasing times");
}
const changed = [], firstObserved = [], notObserved = [];
for (const field of ["text", "rating", "owner_response", "owner_response_at"]) {
if (!Object.hasOwn(after, field) || after[field] == null) {
notObserved.push(field);
} else if (!Object.hasOwn(before, field) || before[field] == null) {
firstObserved.push(field);
} else if (before[field] !== after[field]) {
changed.push(field);
}
}
return {changed, firstObserved, notObserved};
}
console.log(compareReview(
{place_id: "synthetic-place", id: "synthetic-review",
text: "synthetic review", rating: 3, observed_at: "2026-09-01T00:00:00Z"},
{place_id: "synthetic-place", id: "synthetic-review",
text: "synthetic review", rating: 3, owner_response: "synthetic reply",
observed_at: "2026-09-02T00:00:00Z"}
));이 예시에서는 owner_response를 새 리뷰로 기록하지 않고 firstObserved로 기록하며, 답글 시각이 없으면 notObserved로 기록합니다. text가 changed에 나타나더라도 두 번의 조회에서 가져온 텍스트가 달랐다는 뜻일 뿐이며, 번역이나 표현 방식의 변화일 수 있습니다. 뒷받침할 근거 없이 사용자가 원문을 수정했다고 단정하지 마십시오.
시간순 확인은 오래된 관측 로그가 더 최신 관측을 덮어쓰는 것을 막지만, 서버 데이터가 실시간으로 최신이라는 것을 증명하지는 않습니다. 기록한 observed_at은 응답을 확인한 시점이며, 플랫폼 필드의 업데이트 시각이 아닙니다.
여러 시간대에 걸쳐 관측 레코드를 저장할 때는 오프셋을 포함한 RFC 3339 시간 형식을 사용하고, 원본 값은 따로 보존하십시오. 형식을 통일하면 시점 간 비교가 쉬워지지만, 관측 시각이 리뷰 업데이트 시각이 되거나 현재 조회의 커버리지가 늘어나는 것은 아닙니다.
6단계: 날짜만 앞으로 옮기지 말고 증분 관측의 커버리지를 설명하기
지속적인 모니터링에서는 허용된 조건에서 고정된 장소 집합을 반복 관측하고, 날짜 필터에 적절한 중복 구간을 두며, 리뷰 식별자로 중복을 제거할 수 있습니다. 중복 구간의 길이는 관측된 지연 레코드와 작업 빈도에 따라 달라지며, 소량 샘플에서 보편적인 일수를 바로 도출할 수는 없습니다.
하지만 중복 구간으로 모든 누락을 해결할 수는 없습니다. 한 번에 반환되는 결과 수에는 제한이 있고, 오래된 리뷰에 나중에 업체 답글이 달릴 수 있으며, 정렬에 따라 반환되는 샘플이 달라집니다. 무엇보다 since가 업데이트된 오래된 리뷰를 모두 잡아낸다고 가정하지 마십시오. since는 게시 시각을 필터링할 뿐, 완전한 변경 스트림이 아닙니다.
실행할 때마다 확인할 예정이었던 장소, 레코드를 확보한 장소, 실패했거나 상태를 알 수 없는 장소, 장소별 반환 항목 수를 기록하십시오. 커버리지를 논하려면 수집 범위, 정렬, 잘림 조건을 알아야 합니다. 반환 개수를 알 수 없는 전체 개수로 나눠 그럴듯하게 정밀한 커버리지 비율을 만들어서는 안 됩니다.
매장을 비교할 때도 신중해야 합니다. 소수의 최근 리뷰로 낸 평균 점수는 지도상 장소의 전체 평점과 같지 않으며, 샘플 구성이 바뀐 것만으로는 점수 하락을 확인할 수 없습니다. 문제 유형을 비교하려면 먼저 분류 기준을 정의하고, 매장별 샘플 수집 시점과 누락 요소를 함께 표시하십시오.
7단계: 분석, 내부 처리, 공개 표시에 필요한 권한은 서로 다르다
언어 선택은 콘텐츠 표현 방식에 영향을 줄 수 있지만, 반환된 모든 리뷰가 해당 언어로 쓰인 원문이라고 보장하지는 않습니다. 번역문과 원문은 서로 독립된 사용자 의견 두 개가 아닙니다. 분석 결과에는 적용한 언어 조건을 기록하고, 언어 간 차이는 수동 검토로 확인하십시오.
Places API 데이터를 공개적으로 표시할 때는 텍스트를 자체 페이지에 복사하지 말고, 현재 정책에 따라 콘텐츠 출처 표시, 작성자 정보, 원본으로 연결되는 진입점에 관한 요구 사항을 처리하십시오. 데이터를 다른 경로로 얻었다면 적용되는 라이선스를 미리 확인하고, Places 요구 사항을 자동으로 적용하거나 면제된 것으로 간주하지 마십시오. 이 문서의 인메모리 예시는 저장이나 재배포 권한을 부여하지 않습니다.
내부 분석에서는 작업에 꼭 필요한 필드만 처리하고, 열람자와 보존 기간을 제한하십시오. 모델의 분류와 긴급도는 파생 판단으로 표시해야 합니다. 시스템이 사람의 검토 대기열을 정리할 수는 있지만, 낮은 평점이 자동 답글, 작성자에 대한 공개 연락, 매장 운영 상태 변경을 일으켜서는 안 됩니다.
워크플로를 확장하기 전에 장소 하나를 끝까지 검증하기
첫 연동을 마치면 다음을 설명할 수 있어야 합니다. 이 데이터를 처리할 권한이 있는 이유, 대상 매장, 선택한 정렬과 언어 옵션, 근거가 확인된 필드, 답글 변경을 식별하는 방법, 이번 실행에서 누락된 요소입니다. 또한 출처를 알 수 없는 리뷰 파일만 남기는 대신, 문제를 요청 ID까지 추적하고 실제 과금을 확인할 수 있어야 합니다.
이 질문에 답한 후 장소 목록을 늘리거나 정기 실행을 예약하십시오. 지속 가능한 리뷰 워크플로는 데이터를 성공적으로 읽는 데 그치지 않고, 장소 식별자, 용도, 샘플 범위, 변경 기록을 일관되게 명확히 유지합니다.
참고 LF-05