네이버 파워링크 광고 클릭 사기 방지 전략 - IP와 Referrer 기반 악성 클릭 대응
디지털 마케팅을 운영하다 보면 가장 큰 골칫거리 중 하나가 바로 악의적인 광고 클릭입니다. 특히 네이버 파워링크 같은 검색 광고는 클릭당 비용(CPC)이 발생하기 때문에, 경쟁사나 악의적인 사용자의 반복 클릭으로 인해 광고비가...
디지털 마케팅을 운영하다 보면 가장 큰 골칫거리 중 하나가 바로 악의적인 광고 클릭입니다. 특히 네이버 파워링크 같은 검색 광고는 클릭당 비용(CPC)이 발생하기 때문에, 경쟁사나 악의적인 사용자의 반복 클릭으로 인해 광고비가 부당하게 소진될 수 있습니다. 이번 포스트에서는 IP 추적, Referrer 분석 등을 활용하여 클릭 사기를 감지하고 사용자에게 경고하는 방법을 고민해보겠습니다.
📊 클릭 사기(Click Fraud)란?
클릭 사기의 유형
클릭 사기 패턴 분류
┌─────────────────┬──────────────────────────────────┬─────────────┐
│ 유형 │ 설명 │ 위험도 │
├─────────────────┼──────────────────────────────────┼─────────────┤
│ 경쟁사 클릭 │ 경쟁업체가 의도적으로 반복 클릭 │ HIGH │
│ 봇 트래픽 │ 자동화된 스크립트/봇 클릭 │ HIGH │
│ 클릭팜 │ 저임금 인력을 동원한 수동 클릭 │ MEDIUM │
│ 실수 클릭 │ 사용자의 의도하지 않은 중복 클릭 │ LOW │
└─────────────────┴──────────────────────────────────┴─────────────┘
네이버 파워링크의 문제점
- 클릭당 최소 70원 ~ 수천원의 비용 발생
- 일일 예산 소진 시 광고 노출 중단
- 전환율 하락으로 인한 ROI 악화
🛡️ 클릭 사기 감지 전략
1. IP 기반 중복 클릭 감지
기본 아이디어
- 동일 IP에서 짧은 시간 내 반복 유입 감지
- 서버사이드 또는 클라이언트사이드에서 처리 가능
서버사이드 구현 (Flask 예제)
from flask import Flask, request, jsonify
from datetime import datetime, timedelta
from collections import defaultdict
import hashlib
app = Flask(__name__)
# IP별 클릭 이력 저장 (실제로는 Redis 등 사용 권장)
click_history = defaultdict(list)
# 설정값
CLICK_THRESHOLD = 3 # 허용 클릭 횟수
TIME_WINDOW = 60 # 시간 창(초)
@app.route('/track-click', methods=['POST'])
def track_click():
# 클라이언트 IP 가져오기
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
# IP 해싱 (개인정보 보호)
ip_hash = hashlib.sha256(client_ip.encode()).hexdigest()[:16]
current_time = datetime.now()
# 이전 클릭 이력 필터링 (시간 창 내의 클릭만)
click_history[ip_hash] = [
click_time for click_time in click_history[ip_hash]
if current_time - click_time < timedelta(seconds=TIME_WINDOW)
]
# 현재 클릭 추가
click_history[ip_hash].append(current_time)
click_count = len(click_history[ip_hash])
# 클릭 사기 여부 판단
is_suspicious = click_count > CLICK_THRESHOLD
return jsonify({
'is_suspicious': is_suspicious,
'click_count': click_count,
'threshold': CLICK_THRESHOLD,
'message': '비정상적인 클릭 패턴이 감지되었습니다.' if is_suspicious else '정상 클릭'
})
if __name__ == '__main__':
app.run(debug=True)
클라이언트사이드 구현 (LocalStorage 활용)
// 클릭 추적 및 사기 감지 함수
function detectClickFraud() {
const STORAGE_KEY = 'powerlink_clicks';
const THRESHOLD = 3; // 허용 클릭 횟수
const TIME_WINDOW = 60000; // 60초
// 현재 시간
const now = Date.now();
// 저장된 클릭 이력 가져오기
let clickHistory = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
// 시간 창 내의 클릭만 필터링
clickHistory = clickHistory.filter(timestamp =>
now - timestamp < TIME_WINDOW
);
// 현재 클릭 추가
clickHistory.push(now);
// 로컬 스토리지 업데이트
localStorage.setItem(STORAGE_KEY, JSON.stringify(clickHistory));
// 클릭 사기 여부 반환
return {
isSuspicious: clickHistory.length > THRESHOLD,
clickCount: clickHistory.length,
threshold: THRESHOLD
};
}
2. document.referrer 분석
Referrer를 통한 유입 경로 검증
// Referrer 기반 유입 검증
function validateReferrer() {
const referrer = document.referrer;
const validReferrerPatterns = [
/^https?:\/\/(www\.)?naver\.com/, // 네이버 메인
/^https?:\/\/search\.naver\.com/, // 네이버 검색
/^https?:\/\/m\.search\.naver\.com/ // 네이버 모바일 검색
];
// Referrer가 없는 경우 (직접 접속, 북마크 등)
if (!referrer) {
return {
isValid: false,
reason: 'no_referrer',
message: '유입 경로를 확인할 수 없습니다.'
};
}
// 네이버 검색 결과에서 유입되었는지 확인
const isValidReferrer = validReferrerPatterns.some(pattern =>
pattern.test(referrer)
);
if (!isValidReferrer) {
return {
isValid: false,
reason: 'invalid_referrer',
message: '비정상적인 유입 경로가 감지되었습니다.',
referrer: referrer
};
}
return {
isValid: true,
referrer: referrer
};
}
3. URL 파라미터 검증
네이버 광고 URL 파라미터 분석
// URL 파라미터 검증
function validateAdParameters() {
const urlParams = new URLSearchParams(window.location.search);
// 네이버 파워링크는 특정 파라미터를 포함
const requiredParams = ['n_media', 'n_query', 'n_rank', 'n_ad_group'];
const optionalParams = ['NaPm']; // 네이버 광고 매개변수
const hasRequiredParams = requiredParams.every(param =>
urlParams.has(param)
);
// 파라미터 패턴 검증
const nMedia = urlParams.get('n_media');
const validMediaTypes = ['27758', '27862']; // 네이버 검색광고 코드
return {
isValid: hasRequiredParams && validMediaTypes.includes(nMedia),
params: Object.fromEntries(urlParams.entries())
};
}
🚨 사용자 경고 시스템 구현
통합 감지 및 경고 시스템
// 통합 클릭 사기 감지 및 경고 시스템
class ClickFraudDetector {
constructor(config = {}) {
this.config = {
ipThreshold: config.ipThreshold || 3,
timeWindow: config.timeWindow || 60000,
enableReferrerCheck: config.enableReferrerCheck !== false,
enableParamCheck: config.enableParamCheck !== false,
warningMode: config.warningMode || 'modal', // 'modal', 'banner', 'silent'
apiEndpoint: config.apiEndpoint || '/api/track-click'
};
this.suspiciousScore = 0; // 의심 점수
}
// 전체 검증 실행
async detectFraud() {
const results = {
ipCheck: this.checkIPFrequency(),
referrerCheck: this.config.enableReferrerCheck ? validateReferrer() : { isValid: true },
paramCheck: this.config.enableParamCheck ? validateAdParameters() : { isValid: true },
timestamp: new Date().toISOString()
};
// 의심 점수 계산
this.calculateSuspiciousScore(results);
// 서버에 데이터 전송
if (this.config.apiEndpoint) {
await this.sendToServer(results);
}
// 경고 표시 여부 결정
if (this.suspiciousScore >= 50) {
this.showWarning(results);
}
return results;
}
// IP 클릭 빈도 체크
checkIPFrequency() {
return detectClickFraud(); // 앞서 정의한 함수 사용
}
// 의심 점수 계산
calculateSuspiciousScore(results) {
this.suspiciousScore = 0;
// IP 기반 점수
if (results.ipCheck.isSuspicious) {
this.suspiciousScore += 50;
} else if (results.ipCheck.clickCount >= 2) {
this.suspiciousScore += 20;
}
// Referrer 기반 점수
if (!results.referrerCheck.isValid) {
if (results.referrerCheck.reason === 'no_referrer') {
this.suspiciousScore += 30;
} else {
this.suspiciousScore += 40;
}
}
// URL 파라미터 기반 점수
if (!results.paramCheck.isValid) {
this.suspiciousScore += 40;
}
}
// 서버에 데이터 전송
async sendToServer(results) {
try {
const response = await fetch(this.config.apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
...results,
suspiciousScore: this.suspiciousScore,
userAgent: navigator.userAgent,
screenResolution: `${screen.width}x${screen.height}`
})
});
const data = await response.json();
return data;
} catch (error) {
console.error('서버 전송 실패:', error);
return null;
}
}
// 경고 표시
showWarning(results) {
switch (this.config.warningMode) {
case 'modal':
this.showModalWarning(results);
break;
case 'banner':
this.showBannerWarning(results);
break;
case 'silent':
console.warn('의심스러운 클릭 감지:', results);
break;
}
}
// 모달 경고
showModalWarning(results) {
const modal = document.createElement('div');
modal.innerHTML = `
<div style="
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
">
<div style="
background: white;
padding: 30px;
border-radius: 10px;
max-width: 500px;
text-align: center;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
">
<h2 style="color: #ff4444; margin-bottom: 15px;">⚠️ 비정상적인 접근 감지</h2>
<p style="color: #666; margin-bottom: 20px; line-height: 1.6;">
짧은 시간 내 반복적인 광고 클릭이 감지되었습니다.<br>
정상적인 이용이 아닌 경우 법적 책임이 발생할 수 있습니다.
</p>
<div style="background: #f5f5f5; padding: 15px; border-radius: 5px; margin-bottom: 20px;">
<strong>감지 정보:</strong><br>
<small style="color: #888;">
클릭 횟수: ${results.ipCheck.clickCount}회<br>
의심 점수: ${this.suspiciousScore}/100<br>
시간: ${new Date().toLocaleString()}
</small>
</div>
<button onclick="this.closest('div').parentElement.remove()" style="
background: #4CAF50;
color: white;
border: none;
padding: 12px 30px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
">확인</button>
</div>
</div>
`;
document.body.appendChild(modal);
}
// 배너 경고
showBannerWarning(results) {
const banner = document.createElement('div');
banner.innerHTML = `
<div style="
position: fixed;
top: 0;
left: 0;
right: 0;
background: #ff4444;
color: white;
padding: 15px;
text-align: center;
z-index: 9999;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
">
⚠️ 비정상적인 광고 클릭이 감지되었습니다. 반복 시 접근이 제한될 수 있습니다.
<button onclick="this.parentElement.remove()" style="
background: transparent;
border: 2px solid white;
color: white;
padding: 5px 15px;
margin-left: 20px;
cursor: pointer;
border-radius: 3px;
">닫기</button>
</div>
`;
document.body.insertBefore(banner, document.body.firstChild);
// 10초 후 자동 제거
setTimeout(() => banner.remove(), 10000);
}
}
// 사용 예제
document.addEventListener('DOMContentLoaded', () => {
const detector = new ClickFraudDetector({
ipThreshold: 3,
timeWindow: 60000,
warningMode: 'modal',
apiEndpoint: '/api/track-click'
});
// 페이지 로드 시 감지 실행
detector.detectFraud().then(results => {
console.log('클릭 사기 감지 결과:', results);
});
});
🔧 고급 감지 기법
1. 사용자 행동 패턴 분석
// 사용자 행동 패턴 추적
class UserBehaviorTracker {
constructor() {
this.events = [];
this.startTime = Date.now();
this.init();
}
init() {
// 마우스 움직임 추적
document.addEventListener('mousemove', this.trackMouseMove.bind(this));
// 스크롤 추적
document.addEventListener('scroll', this.trackScroll.bind(this));
// 클릭 추적
document.addEventListener('click', this.trackClick.bind(this));
// 키보드 입력 추적
document.addEventListener('keydown', this.trackKeypress.bind(this));
}
trackMouseMove(e) {
this.events.push({
type: 'mousemove',
x: e.clientX,
y: e.clientY,
time: Date.now() - this.startTime
});
}
trackScroll() {
this.events.push({
type: 'scroll',
scrollY: window.scrollY,
time: Date.now() - this.startTime
});
}
trackClick(e) {
this.events.push({
type: 'click',
x: e.clientX,
y: e.clientY,
time: Date.now() - this.startTime
});
}
trackKeypress() {
this.events.push({
type: 'keypress',
time: Date.now() - this.startTime
});
}
// 봇 여부 판단
isLikelyBot() {
const timeOnPage = Date.now() - this.startTime;
const mouseEvents = this.events.filter(e => e.type === 'mousemove').length;
const scrollEvents = this.events.filter(e => e.type === 'scroll').length;
// 휴리스틱 판단
// 1. 페이지 머문 시간이 너무 짧음 (< 2초)
if (timeOnPage < 2000) return true;
// 2. 마우스 움직임이 전혀 없음
if (mouseEvents === 0 && timeOnPage > 3000) return true;
// 3. 스크롤이 전혀 없고 즉시 이탈
if (scrollEvents === 0 && timeOnPage < 5000 && mouseEvents < 5) return true;
return false;
}
getBehaviorScore() {
const timeOnPage = Date.now() - this.startTime;
const uniqueEventTypes = new Set(this.events.map(e => e.type)).size;
let score = 0;
// 페이지 머문 시간
if (timeOnPage > 10000) score += 30;
else if (timeOnPage > 5000) score += 20;
else if (timeOnPage > 2000) score += 10;
// 다양한 이벤트 발생
score += uniqueEventTypes * 10;
// 이벤트 총 개수
score += Math.min(this.events.length, 40);
return Math.min(score, 100); // 최대 100점
}
}
2. 핑거프린팅 기법
// 브라우저 핑거프린팅
async function generateFingerprint() {
const components = [];
// 1. User Agent
components.push(navigator.userAgent);
// 2. 화면 해상도
components.push(`${screen.width}x${screen.height}x${screen.colorDepth}`);
// 3. 시간대
components.push(new Date().getTimezoneOffset());
// 4. 언어
components.push(navigator.language);
// 5. 플랫폼
components.push(navigator.platform);
// 6. 하드웨어 동시성
components.push(navigator.hardwareConcurrency);
// 7. Canvas 핑거프린팅
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillText('Browser Fingerprint', 2, 2);
components.push(canvas.toDataURL());
// 8. WebGL 핑거프린팅
const gl = document.createElement('canvas').getContext('webgl');
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
if (debugInfo) {
components.push(gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL));
components.push(gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL));
}
// 해시 생성
const fingerprint = await hashString(components.join('|||'));
return fingerprint;
}
// SHA-256 해시 함수
async function hashString(str) {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
📈 실전 적용 고려사항
1. False Positive 최소화
정상 사용자를 잘못 차단하지 않으려면:
- 임계값을 너무 낮게 설정하지 않기
- 복수의 지표를 조합하여 판단
- 점진적 경고 시스템 (경고 → 제한 → 차단)
// 점진적 대응 시스템
function getResponseLevel(suspiciousScore) {
if (suspiciousScore < 30) {
return { level: 'none', action: 'allow' };
} else if (suspiciousScore < 50) {
return { level: 'warning', action: 'warn' };
} else if (suspiciousScore < 70) {
return { level: 'suspicious', action: 'log_and_warn' };
} else if (suspiciousScore < 90) {
return { level: 'high', action: 'captcha' };
} else {
return { level: 'critical', action: 'block' };
}
}
2. GDPR 및 개인정보 보호
주의사항:
- IP 주소는 개인정보로 간주될 수 있음
- 해싱 또는 익명화 처리 필수
- 사용자에게 데이터 수집 고지
// IP 익명화
function anonymizeIP(ip) {
// IPv4: 마지막 옥텟 제거
if (ip.includes('.')) {
const parts = ip.split('.');
parts[parts.length - 1] = '0';
return parts.join('.');
}
// IPv6: 마지막 64비트 제거
if (ip.includes(':')) {
const parts = ip.split(':');
return parts.slice(0, 4).join(':') + '::';
}
return ip;
}
3. 네이버 광고 정책 준수
네이버의 무효 클릭 정책:
- 네이버는 자체적으로 무효 클릭 감지 시스템 운영
- 광고주가 자체 감지 시스템을 구축하는 것은 허용
- 단, 정상 사용자의 접근을 막아서는 안 됨
🎯 결론 및 권장사항
최적의 클릭 사기 방지 전략
보호 레벨별 추천 구성
┌─────────────┬────────────────────────────────────┬───────────┐
│ 레벨 │ 적용 기법 │ 난이도 │
├─────────────┼────────────────────────────────────┼───────────┤
│ 기본 │ IP 중복 체크 + LocalStorage │ 쉬움 │
│ 중급 │ + Referrer 검증 + URL 파라미터 │ 보통 │
│ 고급 │ + 행동 패턴 분석 + 핑거프린팅 │ 어려움 │
│ 최고급 │ + 서버 로깅 + ML 기반 탐지 │ 매우어려움│
└─────────────┴────────────────────────────────────┴───────────┘
구현 우선순위:
- IP 기반 중복 클릭 감지 (필수) - 가장 효과적이고 구현 쉬움
- Referrer 검증 (권장) - 비정상 유입 경로 차단
- 사용자 행동 패턴 분석 (선택) - 봇 트래픽 감지
- 서버사이드 로깅 (권장) - 장기적 분석 및 패턴 발견
최종 체크리스트
- IP 기반 중복 클릭 감지 구현
- Referrer 검증 로직 추가
- URL 파라미터 검증
- 사용자 경고 UI 구현
- 서버 로깅 시스템 구축
- 개인정보 보호 정책 반영
- False Positive 테스트
- 모니터링 대시보드 구축
클릭 사기는 완벽하게 막을 수는 없지만, 적절한 감지 시스템을 구축하면 80% 이상의 악의적인 클릭을 방어할 수 있습니다. 가장 중요한 것은 정상 사용자의 경험을 해치지 않으면서도 효과적으로 사기를 감지하는 균형을 찾는 것입니다.
📚 참고 자료
이 포스트가 도움이 되셨다면 공유해주세요! 질문이나 제안사항이 있으시면 댓글로 남겨주세요. 🚀