← 블로그 홈

네이버 파워링크 광고 클릭 사기 방지 전략 - 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 기반 탐지        │  매우어려움│
└─────────────┴────────────────────────────────────┴───────────┘

구현 우선순위:

  1. IP 기반 중복 클릭 감지 (필수) - 가장 효과적이고 구현 쉬움
  2. Referrer 검증 (권장) - 비정상 유입 경로 차단
  3. 사용자 행동 패턴 분석 (선택) - 봇 트래픽 감지
  4. 서버사이드 로깅 (권장) - 장기적 분석 및 패턴 발견

최종 체크리스트

  • IP 기반 중복 클릭 감지 구현
  • Referrer 검증 로직 추가
  • URL 파라미터 검증
  • 사용자 경고 UI 구현
  • 서버 로깅 시스템 구축
  • 개인정보 보호 정책 반영
  • False Positive 테스트
  • 모니터링 대시보드 구축

클릭 사기는 완벽하게 막을 수는 없지만, 적절한 감지 시스템을 구축하면 80% 이상의 악의적인 클릭을 방어할 수 있습니다. 가장 중요한 것은 정상 사용자의 경험을 해치지 않으면서도 효과적으로 사기를 감지하는 균형을 찾는 것입니다.

📚 참고 자료


이 포스트가 도움이 되셨다면 공유해주세요! 질문이나 제안사항이 있으시면 댓글로 남겨주세요. 🚀

이 글을 공유해보세요!