[구글 애드센스 #10] 글 쓰면서 SEO 신경 안 쓰는 법 — 점검 도구를 만들었습니다
지난 글에서 SEO 항목을 정리하고 나니 다음 문제가 남았습니다. 이걸 글 쓸 때마다 어떻게 챙기느냐입니다.
실제로 해보면 글이 이상해집니다. 키워드를 넣어야 한다는 생각으로 쓰면 문장이 뒤틀리고, 소제목을 몇 개 넣을지 세다 보면 흐름이 끊깁니다. 주객이 전도되는 거죠.
쓰기와 점검을 분리하기
결론부터 말하면 글 쓸 때는 신경 쓰지 않는 게 맞습니다.
자유롭게 쓰고, 다 쓴 뒤에 점검합니다. 순서를 나누면 문장이 뒤틀릴 일이 없습니다. 앞서 서식을 본문에서 빼내 테마로 옮겼던 것과 같은 발상입니다. 두 가지를 동시에 하려니 어려웠던 것이지, 나눠서 하면 각각은 간단합니다.
항목을 두 종류로 나눈다
점검할 것도 줄일 수 있었습니다. 대부분의 항목은 한 번 설정하면 끝나는 것이었습니다.
| 구조 항목 · 한 번만 | 글 항목 · 매번 |
|---|---|
| 사이트맵 제출 | 제목 |
| 반응형 테마 | 퍼머링크 |
| 보안 연결 | 검색 설명 |
| 검색 설명 기능 활성화 | 이미지 대체 텍스트 |
| 이미지 표시 규칙 | 링크와 링크 텍스트 |
| 라벨 구조 | 소제목 구성 |
※ 왼쪽은 이미 끝난 작업입니다. 매번 볼 것은 오른쪽 여섯 개뿐입니다.
이렇게 나누고 보니 실제로 매번 확인할 건 여섯 개였고, 그중 넷은 이미 발행 전 순서에 들어 있었습니다. 스무 가지를 챙겨야 할 것 같던 느낌은 두 종류를 구분하지 않고 나열한 설명들 때문이었습니다.
점검 도구를 만들어봤습니다
여섯 개도 매번 눈으로 세려니 번거로웠습니다. 워드프레스에는 이 역할을 하는 플러그인이 있는데 블로그스팟에는 없습니다.
그래서 만들었습니다. 제목, 퍼머링크, 검색 설명, 본문을 붙여넣으면 항목별로 확인해주는 페이지입니다. 검색 노출 점검기에서 바로 쓰실 수 있고, 코드는 이 글 아래에 전문을 올려두었습니다.
구역을 셋으로 나눈 이유
만들면서 가장 고민한 부분입니다. 검사 항목을 한 줄로 나열하면 순위 요소와 아닌 것이 섞여 버립니다. 그러면 도구가 오히려 오해를 키웁니다.
| 구역 | 의미 | 검사하는 예 |
|---|---|---|
| 1구역 | 검색 노출에 영향 | 대체 텍스트, 링크 텍스트 |
| 2구역 | 클릭 여부에 영향 | 제목·설명 길이, 퍼머링크 |
| 3구역 | 노출과 무관 | 본문 분량, 인라인 스타일 |
3구역 항목에는 "순위와는 무관하지만"이라는 설명을 붙였습니다. 챙길 이유가 없다는 뜻이 아니라 이유가 다르다는 걸 보이게 하려고요. 소제목 위계는 접근성 때문에, 본문 분량은 애드센스 관행 때문에, 인라인 스타일은 편집기 문제 때문에 봅니다.
이름을 SEO 점검기로 하지 않은 이유
처음에는 그렇게 부르려고 했습니다. 검색하는 사람이 쓰는 말이니까요.
그런데 이름을 SEO로 달면 3구역까지 SEO 항목처럼 보입니다. 이 도구를 만든 이유가 그 구분을 드러내는 것이었는데, 이름에서부터 뭉개지는 셈이죠. 그래서 검색 노출 점검기로 정했습니다.
블로그스팟 사용자를 위한 항목
일반 도구에는 없는 검사를 두 개 넣었습니다.
인라인 스타일 검출 — 본문에 서식이 직접 들어 있으면 저장할 때 사라집니다. 앞서 겪었던 문제라 아예 자동으로 잡게 했습니다.
본문 속 H1 검출 — 글 제목이 이미 최상위 제목이라 본문에 또 쓰면 중복입니다.
블로그에 올리면서 겪은 것
도구를 만드는 것보다 블로그에 올리는 게 더 까다로웠습니다.
문제는 편집기가 저장할 때 본문의 서식을 정리한다는 점이었습니다. 앞선 글에서 다뤘던 그 문제입니다. 도구를 통째로 붙여넣으면 CSS가 날아가고, 최악의 경우 JS까지 사라집니다.
그래서 셋을 나눠서 넣었습니다.
| 구성 | 넣은 곳 |
|---|---|
| CSS | 테마 맞춤설정의 CSS 추가 칸 |
| HTML | 페이지 본문 |
| JS | 페이지 본문 (HTML 모드에서만) |
CSS를 테마로 옮길 때 한 가지 더 신경 쓸 게 있었습니다. 이름이 버튼이나 상자처럼 흔하면 테마가 이미 쓰고 있는 이름과 충돌합니다. 그래서 이 도구의 이름은 전부 같은 접두어로 시작하게 만들었습니다.
글이 아니라 페이지로 올린 이유
도구는 콘텐츠가 아니라 기능입니다. 글로 올리면 시간이 지나며 목록에서 밀려나 찾기 어려워집니다. 페이지로 두면 메뉴에서 언제든 접근됩니다.
다만 도구만 덩그러니 있으면 내용이 얇은 페이지가 됩니다. 무엇을 하는 도구인지, 구역이 무슨 의미인지 설명을 함께 넣었습니다.
코드 전문
파일 하나로 되어 있어, 복사해서 .html 확장자로 저장하면 각자 브라우저에서 쓰실 수 있습니다. 블로그에 올릴 필요도 인터넷 연결도 없습니다.
블로그에 넣으실 거라면 위에 적은 대로 CSS 부분만 따로 떼어 테마에 넣는 편이 안전합니다.
전체 코드 펼쳐보기
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>잇다. 글 점검기</title>
<style>
:root{
--paper:#F2EBDC;
--ink:#24462F;
--body:#2B2B28;
--muted:#6B6459;
--line:#DCD3C0;
--surface:#FDFCF9;
--ok:#1E7A5A;
--warn:#A8730F;
--no:#B03A31;
}
*{box-sizing:border-box;}
body{
margin:0;
background:var(--paper);
color:var(--body);
font-family:-apple-system,BlinkMacSystemFont,"Apple SD Gothic Neo","Malgun Gothic","맑은 고딕",sans-serif;
line-height:1.6;
font-size:15px;
}
.wrap{max-width:860px;margin:0 auto;padding:40px 20px 80px;}
header{border-bottom:2px solid var(--ink);padding-bottom:18px;margin-bottom:28px;}
h1{font-size:26px;margin:0 0 6px;color:var(--ink);letter-spacing:-.02em;}
header p{margin:0;color:var(--muted);font-size:14px;}
.note{
border-left:3px solid var(--ink);
background:var(--surface);
padding:12px 16px;
margin:0 0 28px;
font-size:14px;
color:#4A4A45;
}
.note b{color:var(--ink);}
fieldset{border:1px solid var(--line);border-radius:6px;padding:18px;margin:0 0 18px;background:var(--surface);}
legend{font-weight:700;color:var(--ink);padding:0 8px;font-size:14px;}
label{display:block;font-size:13px;color:var(--muted);margin:14px 0 5px;}
label:first-of-type{margin-top:0;}
input,textarea{
width:100%;
border:1px solid var(--line);
border-radius:4px;
padding:9px 11px;
font-size:14px;
font-family:inherit;
background:#fff;
color:var(--body);
}
textarea{min-height:180px;font-family:Consolas,Monaco,monospace;font-size:13px;line-height:1.5;resize:vertical;}
input:focus,textarea:focus{outline:2px solid var(--ink);outline-offset:1px;}
.row{display:flex;gap:12px;flex-wrap:wrap;align-items:center;margin-top:18px;}
button{
background:var(--ink);color:var(--paper);border:0;border-radius:4px;
padding:11px 22px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;
}
button:hover{background:#1B3524;}
button.ghost{background:transparent;color:var(--ink);border:1px solid var(--line);font-weight:500;}
#out{margin-top:34px;}
.tier{margin-bottom:30px;}
.tier-head{border-bottom:1.5px solid var(--ink);padding-bottom:7px;margin-bottom:2px;}
.tier-head h2{font-size:17px;margin:0;color:var(--ink);}
.tier-head p{margin:3px 0 0;font-size:13px;color:var(--muted);}
.item{display:flex;gap:12px;padding:12px 2px;border-bottom:1px solid var(--line);align-items:flex-start;}
.mark{
flex:none;width:22px;height:22px;border-radius:5px;
display:flex;align-items:center;justify-content:center;
font-size:12px;font-weight:800;margin-top:1px;
}
.m-ok{background:#E1F0E9;color:var(--ok);}
.m-warn{background:#F7EEDA;color:var(--warn);}
.m-no{background:#F8E6E4;color:var(--no);}
.item-body strong{display:block;font-size:14px;color:var(--body);margin-bottom:2px;}
.item-body span{font-size:13px;color:var(--muted);}
.summary{
display:flex;gap:0;border:1px solid var(--line);border-radius:6px;
overflow:hidden;margin-bottom:30px;background:var(--surface);
}
.summary div{flex:1;text-align:center;padding:14px 8px;border-right:1px solid var(--line);}
.summary div:last-child{border-right:0;}
.summary b{display:block;font-size:22px;font-weight:800;}
.summary span{font-size:12px;color:var(--muted);}
footer{margin-top:44px;padding-top:18px;border-top:1px solid var(--line);font-size:13px;color:var(--muted);}
footer a{color:var(--ink);}
@media (max-width:600px){
.wrap{padding:26px 14px 60px;}
h1{font-size:22px;}
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>잇다. 글 점검기</h1>
<p>발행 전 2분 점검 · 브라우저에서만 동작하며 아무것도 전송되지 않습니다</p>
</header>
<div class="note">
<b>먼저 알아둘 것.</b> 여기서 모두 통과해도 검색 순위가 오르지는 않습니다.
구글은 유용하고 고유한 콘텐츠가 다른 어떤 항목보다 큰 영향을 준다고 밝히고 있습니다.
이 도구는 <b>빠뜨린 것을 잡아주는 역할</b>까지입니다.
</div>
<fieldset>
<legend>글 정보</legend>
<label for="title">제목</label>
<input id="title" type="text" placeholder="예: [구글 애드센스 #09] SEO라고 알려진 것들">
<label for="slug">퍼머링크 (도메인 뒤 부분만)</label>
<input id="slug" type="text" placeholder="예: seo-basics-09">
<label for="desc">검색 설명</label>
<input id="desc" type="text" placeholder="검색 결과에서 제목 아래 나오는 문장">
<label for="html">본문 HTML</label>
<textarea id="html" placeholder="편집기 HTML 모드에서 전체 복사해 붙여넣으세요"></textarea>
<div class="row">
<button onclick="run()">점검하기</button>
<button class="ghost" onclick="clearAll()">비우기</button>
</div>
</fieldset>
<div id="out"></div>
<footer>
판정 기준은 구글 검색 센터 <a href="https://developers.google.com/search/docs/fundamentals/seo-starter-guide?hl=ko" target="_blank" rel="noopener">SEO 기본 가이드</a>를 따릅니다.
같은 문서의 '무시해야 할 사항'에 해당하는 항목은 3구역으로 분류했습니다.
</footer>
</div>
<script>
const OK=0, WARN=1, NO=2;
function esc(s){return String(s).replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]));}
function textLen(s){return (s||'').replace(/\s/g,'').length;}
function run(){
const title=document.getElementById('title').value.trim();
const slug=document.getElementById('slug').value.trim();
const desc=document.getElementById('desc').value.trim();
const html=document.getElementById('html').value;
const doc=new DOMParser().parseFromString('<div id="root">'+html+'</div>','text/html');
const root=doc.getElementById('root');
const bodyText=root ? root.textContent : '';
const imgs=root?[...root.querySelectorAll('img')]:[];
const links=root?[...root.querySelectorAll('a[href]')]:[];
const inlineStyled=root?[...root.querySelectorAll('[style]')]:[];
const heads=root?[...root.querySelectorAll('h1,h2,h3,h4,h5,h6')]:[];
const A=[],B=[],C=[];
/* ── 1구역 : 검색 노출에 실제로 영향 ── */
// 대체 텍스트
const noAlt=imgs.filter(i=>!i.getAttribute('alt')||!i.getAttribute('alt').trim());
if(imgs.length===0){
A.push([OK,'이미지 대체 텍스트','이미지가 없습니다.']);
}else if(noAlt.length===0){
A.push([OK,'이미지 대체 텍스트','이미지 '+imgs.length+'개 모두 대체 텍스트가 있습니다.']);
}else{
A.push([NO,'이미지 대체 텍스트','이미지 '+imgs.length+'개 중 '+noAlt.length+'개에 대체 텍스트가 없습니다. 구글이 이미지를 이해하는 근거가 됩니다.']);
}
// 이미지 주변 텍스트
if(imgs.length>0){
const lonely=imgs.filter(img=>{
let n=img.closest('div')||img;
const prev=n.previousElementSibling, next=n.nextElementSibling;
const has=el=>el && textLen(el.textContent)>20;
return !(has(prev)||has(next));
});
if(lonely.length===0){
A.push([OK,'이미지 주변 텍스트','모든 이미지 앞뒤에 설명이 될 문단이 있습니다.']);
}else{
A.push([WARN,'이미지 주변 텍스트',lonely.length+'개 이미지가 텍스트에서 떨어져 있습니다. 이미지 근처 텍스트가 맥락을 알려줍니다.']);
}
}
// 앵커 텍스트
const bad=/^(여기|클릭|링크|더보기|자세히|바로가기|here|click|link|이 글|이곳)$/i;
const badLinks=links.filter(a=>{
const t=a.textContent.trim();
return t.length===0 || bad.test(t) || /^https?:\/\//.test(t);
});
if(links.length===0){
A.push([WARN,'링크','링크가 하나도 없습니다. 관련 글이나 근거 자료로 연결하면 발견 경로가 늘어납니다.']);
}else if(badLinks.length===0){
A.push([OK,'링크 텍스트','링크 '+links.length+'개의 텍스트가 목적지를 설명합니다.']);
}else{
A.push([WARN,'링크 텍스트',badLinks.length+'개 링크가 "여기", "클릭" 같은 텍스트이거나 주소 그대로입니다. 어떤 페이지인지 알 수 있게 바꾸세요.']);
}
// 외부 링크 rel
const ext=links.filter(a=>/^https?:\/\//.test(a.getAttribute('href')||''));
const extNoRel=ext.filter(a=>!(a.getAttribute('rel')||'').trim());
if(ext.length===0){
A.push([OK,'외부 링크','외부 링크가 없습니다.']);
}else if(extNoRel.length===0){
A.push([OK,'외부 링크','외부 링크 '+ext.length+'개에 rel 속성이 지정돼 있습니다.']);
}else{
A.push([WARN,'외부 링크',ext.length+'개 중 '+extNoRel.length+'개에 rel이 없습니다. 신뢰하기 어려운 곳이면 nofollow를 붙이세요.']);
}
// 제목 존재
if(!title){
A.push([NO,'제목','제목이 비어 있습니다.']);
}else{
A.push([OK,'제목','입력됨. 다른 글과 겹치지 않는지는 직접 확인하세요.']);
}
/* ── 2구역 : 검색 결과에 어떻게 보이는가 ── */
const tl=title.length;
if(!title){
B.push([NO,'제목 길이','제목이 없습니다.']);
}else if(tl<=32){
B.push([OK,'제목 길이',tl+'자. 검색 결과에서 잘리지 않습니다.']);
}else if(tl<=45){
B.push([WARN,'제목 길이',tl+'자. 모바일에서 뒷부분이 잘립니다. 중요한 말이 앞에 있는지 보세요.']);
}else{
B.push([NO,'제목 길이',tl+'자. 상당 부분이 잘립니다.']);
}
const dl=desc.length;
if(!desc){
B.push([NO,'검색 설명','비어 있습니다. 비워두면 본문 앞부분이 잘려 노출됩니다.']);
}else if(dl<30){
B.push([WARN,'검색 설명',dl+'자. 조금 짧습니다. 40~60자 권장.']);
}else if(dl<=70){
B.push([OK,'검색 설명',dl+'자. 적당합니다.']);
}else{
B.push([WARN,'검색 설명',dl+'자. 뒤가 잘립니다. 첫 40자에 핵심을 두세요.']);
}
if(title&&desc){
const norm=s=>s.replace(/[\[\]()·—\-–,.\s]/g,'');
const t=norm(title), d=norm(desc);
let overlap=0;
for(let i=0;i<t.length-3;i++){ if(d.includes(t.substr(i,4))) overlap++; }
const ratio=t.length>4?overlap/(t.length-3):0;
if(ratio>0.45){
B.push([WARN,'제목과 설명 중복','설명이 제목을 되풀이합니다. 제목이 못 담은 정보를 넣으세요.']);
}else{
B.push([OK,'제목과 설명 중복','서로 다른 정보를 담고 있습니다.']);
}
}
if(!slug){
B.push([WARN,'퍼머링크','비어 있습니다. 자동으로 두면 의도와 다른 주소가 생깁니다.']);
}else{
const issues=[];
if(/[^a-z0-9\-]/.test(slug)) issues.push('영문 소문자·숫자·하이픈 외 문자');
if(/\s/.test(slug)) issues.push('공백');
const words=slug.split('-').filter(Boolean).length;
if(words>6) issues.push('단어가 '+words+'개로 김');
if(issues.length===0){
B.push([OK,'퍼머링크','형식 이상 없음. 발행 후에는 되돌리기 어려우니 지금 확정하세요.']);
}else{
B.push([NO,'퍼머링크',issues.join(' / ')+'. 발행하면 되돌리기 어렵습니다.']);
}
}
/* ── 3구역 : 순위와 무관 (접근성·관리) ── */
const len=textLen(bodyText);
if(len===0){
C.push([NO,'본문 분량','본문이 비어 있습니다.']);
}else if(len>=1000){
C.push([OK,'본문 분량',len.toLocaleString()+'자. 애드센스 관행 기준을 넘습니다.']);
}else{
C.push([WARN,'본문 분량',len.toLocaleString()+'자. 순위와는 무관하지만 애드센스 심사에서는 통용 기준이 있습니다.']);
}
const h1s=heads.filter(h=>h.tagName==='H1');
if(h1s.length>0){
C.push([WARN,'본문 속 H1','본문에 H1이 '+h1s.length+'개 있습니다. 글 제목이 이미 H1이라 중복입니다.']);
}else{
C.push([OK,'본문 속 H1','없습니다.']);
}
let skip=0, prev=1;
heads.forEach(h=>{
const lv=+h.tagName[1];
if(lv-prev>1) skip++;
prev=lv;
});
if(heads.length===0){
C.push([WARN,'소제목','소제목이 없습니다. 긴 글은 섹션으로 나누는 편이 읽기 좋습니다.']);
}else if(skip===0){
C.push([OK,'소제목 위계','단계를 건너뛴 곳이 없습니다. (구글 순위와는 무관, 스크린 리더에 유효)']);
}else{
C.push([WARN,'소제목 위계',skip+'곳에서 단계를 건너뜁니다. 순위와는 무관하지만 스크린 리더 사용자에게 불편합니다.']);
}
if(inlineStyled.length===0){
C.push([OK,'인라인 스타일','없습니다. 저장해도 서식이 유지됩니다.']);
}else{
C.push([WARN,'인라인 스타일',inlineStyled.length+'곳에 style 속성이 있습니다. 블로그스팟 편집기가 저장할 때 지울 수 있습니다.']);
}
render(A,B,C);
}
function render(A,B,C){
const all=[...A,...B,...C];
const n=[0,0,0];
all.forEach(x=>n[x[0]]++);
const item=([s,name,msg])=>{
const cls=['m-ok','m-warn','m-no'][s];
const ch=['O','~','X'][s];
return '<div class="item"><div class="mark '+cls+'">'+ch+'</div>'+
'<div class="item-body"><strong>'+esc(name)+'</strong><span>'+esc(msg)+'</span></div></div>';
};
const tier=(title,sub,arr)=>
'<section class="tier"><div class="tier-head"><h2>'+title+'</h2><p>'+sub+'</p></div>'+
arr.map(item).join('')+'</section>';
document.getElementById('out').innerHTML =
'<div class="summary">'+
'<div><b style="color:var(--ok)">'+n[0]+'</b><span>이상 없음</span></div>'+
'<div><b style="color:var(--warn)">'+n[1]+'</b><span>확인 필요</span></div>'+
'<div><b style="color:var(--no)">'+n[2]+'</b><span>고쳐야 함</span></div>'+
'</div>'+
tier('1구역 · 검색 노출에 영향','구글 가이드가 실제로 권장하는 항목입니다.',A)+
tier('2구역 · 검색 결과에 보이는 모습','순위가 아니라 클릭 여부를 좌우합니다.',B)+
tier('3구역 · 순위와 무관','구글이 순위 요소가 아니라고 밝힌 항목입니다. 접근성이나 관리 목적으로 봅니다.',C);
document.getElementById('out').scrollIntoView({behavior:'smooth',block:'start'});
}
function clearAll(){
['title','slug','desc','html'].forEach(id=>document.getElementById(id).value='');
document.getElementById('out').innerHTML='';
}
</script>
</body>
</html>
도구가 못 하는 것
이 부분을 도구 첫 화면에도 적어뒀습니다. 모두 통과해도 순위가 오르지는 않습니다.
구글이 가장 큰 영향을 준다고 밝힌 항목은 유용하고 고유한 콘텐츠인데, 이건 기계가 판단할 수 없습니다. 글이 실제로 도움이 되는지, 검색한 사람이 원한 답인지는 세어볼 수가 없으니까요.
점검 도구는 빠뜨린 것을 잡아주는 역할까지입니다. 대체 텍스트를 안 넣었다든가, 검색 설명을 비워뒀다든가 하는 것들이요. 실수를 막아주지 좋은 글을 만들어주지는 않습니다.
아직 검증되지 않았습니다
솔직하게 적자면 이 도구가 효과가 있는지는 아직 모릅니다.
만든 지 얼마 안 됐고, 이 블로그는 유입 데이터가 쌓이기 전입니다. 판정 기준도 공식 문서를 근거로 잡았지만 제가 해석한 결과이고, 실제로 어떤 항목이 의미 있었는지는 시간이 지나야 알 수 있습니다.
그래서 지금은 만들어봤고 쓰고 있다까지가 사실입니다. 몇 달 뒤에 검색 데이터를 놓고 다시 점검해볼 생각입니다. 어떤 항목은 빠지고 어떤 항목은 추가되겠죠.
다만 하나는 분명해졌습니다. 글을 쓰면서 검색 노출을 신경 쓰지 않게 됐다는 것. 그것만으로도 만든 값은 했다고 봅니다.
댓글
댓글 쓰기