애드센스가 "가치 없는 콘텐츠"라고 했다 — 페이지를 57개에서 40개로 줄여서 대응한 이야기

2026년 8월 28일. 다국어 이미지 도구 사이트가 애드센스 심사에서 "주의 필요 · 가치가 별로 없는 콘텐츠" 판정을 받았다. 원인을 측정해 보니 글이 부족한 게 아니라 URL이 너무 많았다.

상황

editimg.online은 브라우저에서만 도는 이미지 리사이즈·크롭·변환 도구다. Astro 정적 사이트에 React 아일랜드 하나를 얹었고, 이미지는 서버로 올라가지 않는다. 7개 언어를 지원한다.

애드센스 콘솔이 준 정보는 이게 전부였다.

항목
승인 상태 주의 필요
상태 세부정보 가치가 별로 없는 콘텐츠
Ads.txt 상태 승인됨

ads.txt는 통과했다는 게 중요한 단서다. 기술적 설정 문제가 아니라 콘텐츠 자체를 보고 내린 판단이라는 뜻이다.

첫 번째 본능은 틀렸다

"가치 없는 콘텐츠"를 보면 보통 "글을 더 쓰자"로 간다. 나도 그랬다. 그런데 숫자를 먼저 재 보기로 했다.

사이트맵 URL      57개
고유 원문         8편  (가이드 4편 × 영어·한국어)
비율              14%

57개 중 8개만 진짜 글이었다. 나머지 49개는 같은 템플릿의 번역이거나 복제였다. 글이 부족한 게 아니라 글 대비 URL이 너무 많았다.

이 관점으로 보니 원인 세 개가 나왔다.

원인 1 — 도구 페이지 4개가 사실상 같은 문서였다

/, /resize/, /crop/, /convert/가 컴포넌트 하나를 공유했다. 페이지마다 다른 건 <title>, h1, 부제뿐이었고 기능 카드 3개, 사용법 3단계, FAQ 5문항이 글자 그대로 동일했다.

/resize/  고유 문장 5/ 전체 84

7개 언어니까 28개 페이지가 이 상태였다.

원인 2 — 번역 안 된 법적 페이지가 영어 그대로 색인되고 있었다

이게 제일 뼈아팠다. 번역 함수가 이렇게 생겼다.

export function t(locale: Locale, key: string): string {
  return DICTS[locale]?.[key] ?? DICTS.en[key] ?? key;   // ← 없으면 영어
}

키가 없으면 영어로 폴백한다. UI 문자열에는 합리적인 동작이다. 문제는 개인정보처리방침·이용약관이 영어와 한국어만 번역돼 있었다는 것.

결과적으로 /ja/privacy/, /zh/terms/, /es/privacy/10개 URL이 영어 원문을 그대로 뿌리면서 self-canonical에 사이트맵 등재 상태였다. 구글 입장에서는 같은 문서 10부다.

더 뼈아픈 건, 가이드 글에는 이미 같은 문제를 해결해 둔 코드가 있었다는 점이다. 번역 없는 언어의 가이드 URL은 noindex 처리하고 사이트맵에서도 뺐다. 법적 페이지에만 그 처리를 빠뜨린 거였다.

원인 3 — 가이드 목록이 막다른 길이었다

/es/guides/는 색인 대상이었다. 그런데 거기 걸린 글 4편이 전부 영어 폴백이라 noindex였다.

색인되는 목록 페이지 → 전부 색인 안 되는 글. 크롤러 입장에서 도달할 콘텐츠가 0이다. 이런 목록이 5개 언어에 있었다.

대응 — 규칙을 하나로 만들었다

세 문제를 따로 고치는 대신, 질문 하나로 묶었다.

이 언어로 쓰인 글이 실제로 있는가?

기존 코드는 locale === 'en' || locale === 'ko' 같은 하드코딩이었다. 이러면 언어를 추가할 때마다 고쳐야 할 곳이 흩어진다. 대신 사전과 파일 시스템에 직접 물어보게 바꿨다.

/**
 * 이 언어의 사전에 키가 실제로 있는지. t()는 없으면 영어로 돌려주므로
 * 그걸로는 "번역이 있다"와 "영어가 나왔다"를 구분할 수 없다.
 */
export function hasTranslation(locale: Locale, key: string): boolean {
  return key in DICTS[locale];
}

/** 이 법률 문서가 번역된 언어 목록 */
export function docLocales(page: DocSlug): Locale[] {
  return LOCALES.filter((l) => hasTranslation(l, `${page}.s1.b`));
}

핵심은 페이지와 사이트맵이 같은 신호를 읽는다는 것이다. 사이트맵 필터는 빌드 설정에서 파일 시스템을 직접 읽는다.

function isIndexable(url) {
  const { locale, route } = parseUrl(url);
  if (locale === DEFAULT_LOCALE || route === '') return true;
  if (route in docLocales) return docLocales[route].has(locale);
  if (route === 'guides') return CONTENT_LOCALES.has(locale);
  if (toolContent.has(`${DEFAULT_LOCALE}/${route}`)) return toolContent.has(`${locale}/${route}`);
  const key = guideKey(url);
  return key === null || key in guideDates;
}

이걸 안 맞추면 사이트맵에 올려놓고 noindex를 주는 상태가 되고, 서치 콘솔에 "제출된 URL이 noindex로 표시됨" 오류가 그대로 쌓인다.

hreflang도 같은 기준으로 좁혔다. 색인하지 않을 URL을 대체 언어 버전으로 선언하면 서로 모순된 신호가 된다.

중요한 건 언어 전환기는 손대지 않았다는 점이다. 7개 언어 페이지는 전부 그대로 존재하고 정상 동작한다. 색인에서만 뺐다.

그다음 — 밀도를 올렸다

URL을 줄이는 것만으로는 절반이다. 남은 페이지의 밀도를 올려야 했다.

도구 페이지를 각자 다른 문서로. 도구별 고유 해설과 전용 FAQ를 마크다운 컬렉션으로 분리했다. 리사이즈면 보간 방식과 DPI 오해, 크롭이면 비율별 용도, 변환이면 포맷 선택 기준.

여기서 한 가지 규칙을 뒀다. 가이드와 달리 도구 원고는 영어로 폴백하지 않는다. 반쯤 영어인 도구 페이지가 얕은 페이지보다 나쁘기 때문이다. 원고가 없는 언어는 기존 공용 구성으로 렌더되고 색인에서 빠진다.

/resize/  고유 문장 5/ 84줄  →  20/ 30

구조화 데이터도 같이 고쳤다. FAQ가 페이지마다 달라졌는데 FAQPage JSON-LD는 공용 5문항을 그대로 내보내고 있었다. 화면과 구조화 데이터가 다르면 리치 결과 위반이다. 화면에 실제로 보이는 FAQ에서 스키마를 생성하도록 바꿨다.

/about//contact/를 실제 페이지로. 그전까지 모달이라 심사자가 클릭해서 확인할 주소가 없었다.

가이드를 4편에서 7편으로. 크롭 전용 가이드가 하나도 없어서 화면 비율 글을 썼고, 검색 수요가 큰 HEIC→JPG 변환과 일괄 리사이즈를 추가했다. 영어·한국어 각각이므로 글 자체는 14편이다.

결과

이전 이후
사이트맵 URL 57 40
고유 원문 8편 22편
원문 비율 14% 55%
/resize/ 고유 문장 5/84 20/30

페이지를 늘려서가 아니라 줄이면서 밀도를 올려서 나온 숫자다.

덤 — 언어를 추가해도 비율이 안 나빠진다

작업 중에 독일어를 추가했다. 원래는 재심사 전에 언어를 늘리면 중복 도구 페이지만 4개 늘어나서 방향이 반대다.

그런데 색인 규칙을 만들어 둔 덕에 결과가 이랬다.

사이트맵 3940 URL   (독일어가 늘린 색인 URL: /de/ 하나)

/de/          → index      사이트를 소개하는 홈
/de/resize/   → noindex    도구 원고 없음
/de/about/    → noindex    번역 없음

중복 페이지가 0개 늘었다. 독일 사용자는 완전한 독일어 UI로 도구를 쓸 수 있고, 색인에는 홈 하나만 올라간다. 나중에 독일어 원고를 쓰면 그 순간 자동으로 색인 대상이 된다.

언어 추가가 "UI 번역 → 도구 원고 → 문서 → 가이드" 4단계가 되고, 채우는 만큼만 색인이 늘어나는 구조다.

정리하면

  1. "가치 없는 콘텐츠"를 글 부족으로 읽지 말 것. 먼저 URL 대비 고유 원문 비율을 재라. 분모가 문제인 경우가 많다.
  2. 번역 폴백은 조용히 중복을 만든다. 키가 없을 때 영어를 돌려주는 함수는 UI에는 맞지만 본문에는 위험하다. "영어가 나온 것"과 "번역이 있는 것"을 구분할 방법이 코드에 있어야 한다.
  3. 색인 판정은 한 곳에서. 페이지의 noindex, 사이트맵 필터, hreflang이 같은 신호를 읽어야 한다. 세 군데에 따로 적으면 반드시 갈라진다.
  4. 한 컴포넌트가 여러 URL을 렌더한다면 의심하라. 편해서 그렇게 만들지만, 검색엔진에는 같은 문서를 여러 번 낸 것으로 보인다.

재심사는 아직 넣지 않았다. 승인 결과가 나오면 후속 글로 정리할 예정이다. 이 글은 "이렇게 하면 통과한다"가 아니라 "판정 근거를 어떻게 측정하고 무엇을 고쳤는가"에 대한 기록이다.