🔔 웹훅 연동 가이드

웹훅은 이벤트 발생 시 등록된 URL로 HTTP POST 요청을 보내 실시간으로 알림을 전달합니다. 각 요청에는 HMAC-SHA256 서명이 포함되어 있어 페이로드의 무결성과 출처를 검증할 수 있습니다.

📌 사전 준비
웹훅을 사용하려면 콘솔 > 개발자 > 웹훅 관리에서 웹훅 URL과 시크릿 키를 등록해야 합니다. 웹훅 관리에 대한 자세한 내용은 콘솔 매뉴얼을 참고하세요.

수신 HTTP 헤더

웹훅 요청에 포함되는 헤더 목록입니다.

헤더명설명
Content-Typeapplication/json페이로드 형식
User-AgentFingerpushLink-Webhook/1.0핑거푸시 웹훅 에이전트 식별
X-Webhook-Id웹훅 ID이벤트를 발생시킨 웹훅의 고유 ID
X-Webhook-Event이벤트 타입이벤트 종류 (click, campaign_click, test 등)
X-Signature-256sha256={hex}HMAC-SHA256 서명 (페이로드 검증용)
X-Delivery-IdUUID개별 전송의 고유 식별자 (중복 수신 방지용)

이벤트 타입

수신 가능한 이벤트 목록입니다. 웹훅 설정 시 원하는 이벤트만 선택하여 수신할 수 있습니다.

이벤트 타입설명
click단축 URL 클릭 이벤트
campaign_click캠페인 URL 클릭 이벤트
goal_completed딥링크 퍼널 목표 달성 이벤트
funnel_stage_changed딥링크 퍼널 단계 전환 이벤트
session_event크로스 플랫폼 세션 이벤트
test테스트 이벤트 (콘솔에서 테스트 발송 시)

공통 페이로드 구조

모든 이벤트는 동일한 래퍼 구조로 전송됩니다.

JSON — 공통 구조
{
  "event": "이벤트 타입",
  "timestamp": "2026-01-15T14:30:00+09:00",
  "delivery_id": "550e8400-e29b-41d4-a716-446655440000",
  "organization_id": 1,
  "data": { ... 이벤트별 데이터 ... }
}
필드타입설명
eventString이벤트 타입 (위 표 참고)
timestampString이벤트 발생 시각 (ISO 8601, KST)
delivery_idString전송 고유 ID (UUID, 중복 방지용)
organization_idNumber조직 ID
dataObject이벤트별 상세 데이터

이벤트별 페이로드 예제

JSON — click
{
  "event": "click",
  "timestamp": "2026-01-15T14:30:00+09:00",
  "delivery_id": "550e8400-e29b-41d4-a716-446655440000",
  "organization_id": 1,
  "data": {
    "url": {
      "id": 123,
      "short_url": "abc123",
      "original_url": "https://example.com/landing",
      "title": "랜딩 페이지"
    },
    "click": {
      "ip_hash": "a1b2c3d4",
      "country": "KR",
      "browser": "Chrome",
      "os": "Android",
      "device_type": "mobile",
      "referrer": "https://example.com/page"
    }
  }
}
JSON — campaign_click
{
  "event": "campaign_click",
  "timestamp": "2026-01-15T14:30:00+09:00",
  "delivery_id": "550e8400-e29b-41d4-a716-446655440000",
  "organization_id": 1,
  "data": {
    "campaign": {
      "id": 45,
      "name": "신년 프로모션",
      "alias": "new-year-2026",
      "short_url": "c/new-year-2026"
    },
    "url": {
      "id": 123,
      "short_url": "abc123",
      "original_url": "https://example.com/landing"
    },
    "click": {
      "ip_hash": "a1b2c3d4",
      "country": "KR",
      "browser": "Chrome",
      "os": "Android",
      "device_type": "mobile",
      "referrer": "https://example.com/page",
      "cid": "promo-123",
      "tracker_alias": "naver_blog"
    }
  }
}
JSON — goal_completed
{
  "event": "goal_completed",
  "timestamp": "2026-01-15T14:35:00+09:00",
  "delivery_id": "660e8400-e29b-41d4-a716-446655440000",
  "organization_id": 1,
  "data": {
    "funnel": {
      "id": 789,
      "session_id": "sess-abc-123",
      "external_user_id": "user-001",
      "current_stage": "GOAL_COMPLETED",
      "goal_type": "purchase",
      "goal_value": 29900,
      "platform": "android",
      "device_type": "mobile",
      "country_code": "KR",
      "total_duration_seconds": 185
    },
    "ip_hash": "a1b2c3d4",
    "url": {
      "id": 123,
      "short_url": "abc123",
      "original_url": "https://example.com/landing"
    },
    "campaign": {
      "id": 45,
      "name": "신년 프로모션",
      "alias": "new-year-2026"
    }
  }
}
JSON — funnel_stage_changed
{
  "event": "funnel_stage_changed",
  "timestamp": "2026-01-15T14:32:00+09:00",
  "delivery_id": "770e8400-e29b-41d4-a716-446655440000",
  "organization_id": 1,
  "data": {
    "stage_transition": {
      "previous_stage": "CLICKED",
      "current_stage": "APP_OPENED"
    },
    "funnel": {
      "id": 789,
      "session_id": "sess-abc-123",
      "external_user_id": "user-001",
      "platform": "ios",
      "device_type": "mobile",
      "country_code": "KR"
    }
  }
}
JSON — session_event
{
  "event": "session_event",
  "timestamp": "2026-01-15T14:33:00+09:00",
  "delivery_id": "880e8400-e29b-41d4-a716-446655440000",
  "organization_id": 1,
  "data": {
    "event": {
      "id": 456,
      "session_id": "sess-abc-123",
      "event_type": "page_view",
      "event_name": "상품 상세 페이지",
      "created_at": "2026-01-15 14:33:00"
    },
    "event_data": {
      "page_url": "https://example.com/product/123",
      "product_id": "prod-123"
    },
    "session": {
      "platform": "web",
      "last_platform": "web",
      "platform_transitions": 0,
      "total_events": 5,
      "external_user_id": "user-001"
    }
  }
}

서명 검증

X-Signature-256 헤더의 HMAC-SHA256 서명을 검증하여 요청의 무결성과 출처를 확인합니다. 서명은 sha256={hex} 형식으로 전달됩니다.

⚠️ 보안 주의
서명 검증 없이 웹훅을 처리하면 위변조된 요청을 수신할 수 있습니다. 반드시 서명을 검증하세요.
Java — HMAC-SHA256 검증
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.HexFormat;

public boolean verifySignature(String payload, String secret, String signatureHeader) {
    try {
        String expected = signatureHeader.replace("sha256=", "");
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
        String actual = HexFormat.of().formatHex(mac.doFinal(payload.getBytes("UTF-8")));
        return MessageDigest.isEqual(expected.getBytes(), actual.getBytes());
    } catch (Exception e) {
        return false;
    }
}
Python — HMAC-SHA256 검증
import hmac
import hashlib

def verify_signature(payload: str, secret: str, signature_header: str) -> bool:
    expected = signature_header.replace("sha256=", "")
    actual = hmac.new(
        secret.encode("utf-8"),
        payload.encode("utf-8"),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, actual)
Node.js — HMAC-SHA256 검증
const crypto = require('crypto');

function verifySignature(payload, secret, signatureHeader) {
    const expected = signatureHeader.replace('sha256=', '');
    const actual = crypto
        .createHmac('sha256', secret)
        .update(payload, 'utf8')
        .digest('hex');
    return crypto.timingSafeEqual(
        Buffer.from(expected, 'hex'),
        Buffer.from(actual, 'hex')
    );
}
PHP — HMAC-SHA256 검증
<?php
function verifySignature(string $payload, string $secret, string $signatureHeader): bool {
    $expected = str_replace('sha256=', '', $signatureHeader);
    $actual = hash_hmac('sha256', $payload, $secret);
    return hash_equals($expected, $actual);
}
?>

수신 서버 구축 예제

각 언어/프레임워크별 완전한 웹훅 수신 서버 예제입니다. 서명 검증, 중복 방지, 이벤트 라우팅을 포함합니다.

Java — Spring Boot 웹훅 수신 서버
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.HexFormat;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;

@RestController
@RequestMapping("/webhook")
public class WebhookReceiverController {

    private static final String WEBHOOK_SECRET = "your-webhook-secret";
    private final ObjectMapper objectMapper = new ObjectMapper();

    @PostMapping
    public ResponseEntity<String> handleWebhook(
            @RequestBody String payload,
            @RequestHeader("X-Signature-256") String signature,
            @RequestHeader("X-Webhook-Event") String eventType,
            @RequestHeader("X-Delivery-Id") String deliveryId) {

        // 1. 서명 검증
        if (!verifySignature(payload, WEBHOOK_SECRET, signature)) {
            return ResponseEntity.status(401).body("Invalid signature");
        }

        // 2. 중복 방지 (deliveryId 기반)
        if (isDuplicate(deliveryId)) {
            return ResponseEntity.ok("Already processed");
        }

        // 3. 빠르게 200 응답 후, 실제 처리는 비동기로
        processAsync(eventType, payload);
        return ResponseEntity.ok("OK");
    }

    @SuppressWarnings("unchecked")
    private void processAsync(String eventType, String payload) {
        // @Async 또는 별도 스레드로 처리 권장
        try {
            Map<String, Object> body = objectMapper.readValue(payload, Map.class);
            Map<String, Object> data = (Map<String, Object>) body.get("data");

            switch (eventType) {
                case "click" -> handleClick(data);
                case "campaign_click" -> handleCampaignClick(data);
                case "goal_completed" -> handleGoalCompleted(data);
                case "funnel_stage_changed" -> handleFunnelStageChanged(data);
                case "session_event" -> handleSessionEvent(data);
            }
        } catch (Exception e) {
            // 에러 로깅
        }
    }

    @SuppressWarnings("unchecked")
    private void handleClick(Map<String, Object> data) {
        Map<String, Object> url = (Map<String, Object>) data.get("url");
        Map<String, Object> click = (Map<String, Object>) data.get("click");
        System.out.println("Click: " + url.get("short_url")
                + " from " + click.get("country"));
    }

    @SuppressWarnings("unchecked")
    private void handleCampaignClick(Map<String, Object> data) {
        Map<String, Object> campaign = (Map<String, Object>) data.get("campaign");
        Map<String, Object> click = (Map<String, Object>) data.get("click");
        System.out.println("Campaign: " + campaign.get("name")
                + ", CID: " + click.get("cid")
                + ", Tracker: " + click.get("tracker_alias"));
    }

    @SuppressWarnings("unchecked")
    private void handleGoalCompleted(Map<String, Object> data) {
        Map<String, Object> funnel = (Map<String, Object>) data.get("funnel");
        System.out.println("Goal completed: " + funnel.get("goal_type")
                + ", Value: " + funnel.get("goal_value"));
    }

    @SuppressWarnings("unchecked")
    private void handleFunnelStageChanged(Map<String, Object> data) {
        Map<String, Object> transition = (Map<String, Object>) data.get("stage_transition");
        System.out.println("Stage: " + transition.get("previous_stage")
                + " → " + transition.get("current_stage"));
    }

    @SuppressWarnings("unchecked")
    private void handleSessionEvent(Map<String, Object> data) {
        Map<String, Object> event = (Map<String, Object>) data.get("event");
        Map<String, Object> session = (Map<String, Object>) data.get("session");
        System.out.println("Session event: " + event.get("event_type")
                + ", Platform: " + session.get("platform")
                + ", Transitions: " + session.get("platform_transitions"));
    }

    private boolean verifySignature(String payload, String secret, String signatureHeader) {
        try {
            String expected = signatureHeader.replace("sha256=", "");
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
            String actual = HexFormat.of().formatHex(mac.doFinal(payload.getBytes("UTF-8")));
            return MessageDigest.isEqual(expected.getBytes(), actual.getBytes());
        } catch (Exception e) {
            return false;
        }
    }

    private boolean isDuplicate(String deliveryId) {
        // Redis 또는 DB로 중복 체크 구현
        return false;
    }
}
Python — Flask 웹훅 수신 서버
from flask import Flask, request, jsonify
import hmac
import hashlib

app = Flask(__name__)
WEBHOOK_SECRET = "your-webhook-secret"
processed_deliveries = set()  # 프로덕션에서는 Redis 사용 권장

def verify_signature(payload: bytes, secret: str, signature_header: str) -> bool:
    expected = signature_header.replace("sha256=", "")
    actual = hmac.new(
        secret.encode("utf-8"),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, actual)

@app.route("/webhook", methods=["POST"])
def handle_webhook():
    # 1. 서명 검증
    signature = request.headers.get("X-Signature-256", "")
    if not verify_signature(request.data, WEBHOOK_SECRET, signature):
        return jsonify({"error": "Invalid signature"}), 401

    # 2. 중복 방지
    delivery_id = request.headers.get("X-Delivery-Id", "")
    if delivery_id in processed_deliveries:
        return jsonify({"status": "already processed"}), 200
    processed_deliveries.add(delivery_id)

    # 3. 이벤트 처리
    event_type = request.headers.get("X-Webhook-Event", "")
    body = request.get_json()
    data = body.get("data", {})

    handlers = {
        "click": handle_click,
        "campaign_click": handle_campaign_click,
        "goal_completed": handle_goal_completed,
        "funnel_stage_changed": handle_funnel_stage_changed,
        "session_event": handle_session_event,
    }

    handler = handlers.get(event_type)
    if handler:
        handler(data)

    return jsonify({"status": "ok"}), 200

def handle_click(data):
    url_info = data.get("url", {})
    click = data.get("click", {})
    print(f"Click: {url_info.get('short_url')} from {click.get('country')}")

def handle_campaign_click(data):
    campaign = data.get("campaign", {})
    click = data.get("click", {})
    print(f"Campaign: {campaign.get('name')}, "
          f"CID: {click.get('cid')}, Tracker: {click.get('tracker_alias')}")

def handle_goal_completed(data):
    funnel = data.get("funnel", {})
    print(f"Goal: {funnel.get('goal_type')}, Value: {funnel.get('goal_value')}")

def handle_funnel_stage_changed(data):
    transition = data.get("stage_transition", {})
    print(f"Stage: {transition.get('previous_stage')} → {transition.get('current_stage')}")

def handle_session_event(data):
    event = data.get("event", {})
    session = data.get("session", {})
    print(f"Session: {event.get('event_type')}, "
          f"Platform: {session.get('platform')}, "
          f"Transitions: {session.get('platform_transitions')}")

if __name__ == "__main__":
    app.run(port=3000)
Node.js — Express 웹훅 수신 서버
const express = require('express');
const crypto = require('crypto');

const app = express();
const WEBHOOK_SECRET = 'your-webhook-secret';
const processedDeliveries = new Set(); // 프로덕션에서는 Redis 사용 권장

// raw body를 보존하기 위해 express.raw() 사용
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const payload = req.body.toString('utf8');

    // 1. 서명 검증
    const signature = req.headers['x-signature-256'] || '';
    if (!verifySignature(payload, WEBHOOK_SECRET, signature)) {
        return res.status(401).json({ error: 'Invalid signature' });
    }

    // 2. 중복 방지
    const deliveryId = req.headers['x-delivery-id'] || '';
    if (processedDeliveries.has(deliveryId)) {
        return res.json({ status: 'already processed' });
    }
    processedDeliveries.add(deliveryId);

    // 3. 빠르게 응답 후 비동기 처리
    res.json({ status: 'ok' });

    const body = JSON.parse(payload);
    const eventType = req.headers['x-webhook-event'] || '';
    const data = body.data || {};

    switch (eventType) {
        case 'click':
            console.log(`Click: ${data.url?.short_url} from ${data.click?.country}`);
            break;
        case 'campaign_click':
            console.log(`Campaign: ${data.campaign?.name}, ` +
                `CID: ${data.click?.cid}, Tracker: ${data.click?.tracker_alias}`);
            break;
        case 'goal_completed':
            console.log(`Goal: ${data.funnel?.goal_type}, Value: ${data.funnel?.goal_value}`);
            break;
        case 'funnel_stage_changed':
            console.log(`Stage: ${data.stage_transition?.previous_stage} → ` +
                `${data.stage_transition?.current_stage}`);
            break;
        case 'session_event':
            console.log(`Session: ${data.event?.event_type}, ` +
                `Platform: ${data.session?.platform}, ` +
                `Transitions: ${data.session?.platform_transitions}`);
            break;
    }
});

function verifySignature(payload, secret, signatureHeader) {
    const expected = signatureHeader.replace('sha256=', '');
    const actual = crypto
        .createHmac('sha256', secret)
        .update(payload, 'utf8')
        .digest('hex');
    try {
        return crypto.timingSafeEqual(
            Buffer.from(expected, 'hex'),
            Buffer.from(actual, 'hex')
        );
    } catch (e) {
        return false;
    }
}

app.listen(3000, () => console.log('Webhook receiver on port 3000'));
JSP — 웹훅 수신 서버
<%@ page contentType="application/json; charset=UTF-8" %>
<%@ page import="java.io.*, java.util.*, java.security.*, javax.crypto.*, javax.crypto.spec.*" %>
<%@ page import="com.google.gson.JsonObject, com.google.gson.JsonParser" %>
<%
// FingerpushLink 웹훅 수신 서버 (JSP)
// 필요: gson 라이브러리 (WEB-INF/lib/gson-x.x.x.jar)

final String WEBHOOK_SECRET = "YOUR_WEBHOOK_SECRET_HERE";

// 1. POST 메서드 확인
if (!"POST".equalsIgnoreCase(request.getMethod())) {
    response.setStatus(405);
    out.print("{\"error\": \"Method Not Allowed\"}");
    return;
}

// 2. 요청 본문 읽기
StringBuilder sb = new StringBuilder();
BufferedReader reader = request.getReader();
String line;
while ((line = reader.readLine()) != null) {
    sb.append(line);
}
String body = sb.toString();

// 3. 서명 검증
String signatureHeader = request.getHeader("X-Signature-256");
if (signatureHeader == null || signatureHeader.isEmpty()) {
    response.setStatus(401);
    out.print("{\"error\": \"Missing signature\"}");
    return;
}

String signature = signatureHeader.replace("sha256=", "");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(WEBHOOK_SECRET.getBytes("UTF-8"), "HmacSHA256"));
byte[] hash = mac.doFinal(body.getBytes("UTF-8"));
StringBuilder hexStr = new StringBuilder();
for (byte b : hash) {
    hexStr.append(String.format("%02x", b));
}
String expected = hexStr.toString();

if (!MessageDigest.isEqual(expected.getBytes(), signature.getBytes())) {
    response.setStatus(401);
    out.print("{\"error\": \"Invalid signature\"}");
    return;
}

// 4. 중복 방지 (Delivery ID 확인)
String deliveryId = request.getHeader("X-Delivery-Id");
// 실제 프로젝트에서는 DB/Redis에 저장하여 중복 체크

// 5. 이벤트 처리
String eventType = request.getHeader("X-Webhook-Event");
JsonObject payload = JsonParser.parseString(body).getAsJsonObject();
JsonObject data = payload.getAsJsonObject("data");

switch (eventType != null ? eventType : "") {
    case "click":
        String shortUrl = data.getAsJsonObject("url").get("short_url").getAsString();
        application.log("[click] shortUrl=" + shortUrl);
        break;
    case "campaign_click":
        String alias = data.getAsJsonObject("campaign").get("alias").getAsString();
        application.log("[campaign_click] alias=" + alias);
        break;
    case "goal_completed":
        JsonObject funnel = data.getAsJsonObject("funnel");
        application.log("[goal_completed] goalType=" + funnel.get("goal_type").getAsString());
        break;
    case "session_event":
        JsonObject event = data.getAsJsonObject("event");
        application.log("[session_event] eventName=" + event.get("event_name").getAsString());
        break;
    case "funnel_stage_changed":
        JsonObject transition = data.getAsJsonObject("stage_transition");
        application.log("[funnel_stage_changed] " +
            transition.get("previous_stage").getAsString() +
            " -> " + transition.get("current_stage").getAsString());
        break;
}

// 6. 성공 응답
response.setStatus(200);
out.print("{\"status\": \"ok\", \"deliveryId\": \"" + deliveryId + "\"}");
%>
PHP — 웹훅 수신 서버
<?php
// FingerpushLink 웹훅 수신 서버 (PHP)
// PHP 7.4+ 필요, 추가 라이브러리 불필요

header('Content-Type: application/json; charset=UTF-8');

$WEBHOOK_SECRET = 'YOUR_WEBHOOK_SECRET_HERE';

// 1. POST 메서드 확인
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['error' => 'Method Not Allowed']);
    exit;
}

// 2. 요청 본문 읽기
$body = file_get_contents('php://input');
if (empty($body)) {
    http_response_code(400);
    echo json_encode(['error' => 'Empty body']);
    exit;
}

// 3. 서명 검증
$signatureHeader = $_SERVER['HTTP_X_SIGNATURE_256'] ?? '';
if (empty($signatureHeader)) {
    http_response_code(401);
    echo json_encode(['error' => 'Missing signature']);
    exit;
}

$signature = str_replace('sha256=', '', $signatureHeader);
$expected = hash_hmac('sha256', $body, $WEBHOOK_SECRET);
if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// 4. 중복 방지 (Delivery ID 확인)
$deliveryId = $_SERVER['HTTP_X_DELIVERY_ID'] ?? '';
// 실제 프로젝트: DB/Redis에 저장 후 중복 체크

// 5. 이벤트 타입별 처리
$eventType = $_SERVER['HTTP_X_WEBHOOK_EVENT'] ?? '';
$payload = json_decode($body, true);
$data = $payload['data'];

switch ($eventType) {
    case 'click':
        $url = $data['url'];
        error_log("[클릭] shortUrl={$url['short_url']}");
        break;
    case 'campaign_click':
        $campaign = $data['campaign'];
        $click = $data['click'];
        error_log("[캠페인] alias={$campaign['alias']}, cid={$click['cid']}");
        break;
    case 'goal_completed':
        $funnel = $data['funnel'];
        error_log("[목표달성] goalType={$funnel['goal_type']}");
        break;
    case 'session_event':
        $event = $data['event'];
        error_log("[세션이벤트] eventName={$event['event_name']}");
        break;
    case 'funnel_stage_changed':
        $transition = $data['stage_transition'];
        error_log("[퍼널변경] {$transition['previous_stage']} -> {$transition['current_stage']}");
        break;
}

// 6. 성공 응답
http_response_code(200);
echo json_encode([
    'status' => 'ok',
    'deliveryId' => $deliveryId,
]);
?>

재시도 정책

항목
최대 재시도 횟수최대 5회 (설정 가능, 기본: 3회)
성공 조건200~299 범위의 HTTP 상태 코드
자동 비활성화연속 50회 실패 시 웹훅 자동 비활성화
타임아웃기본 5000ms (1000~30000ms 설정 가능)

베스트 프랙티스

💡 웹훅 운영 권장사항
  1. 빠른 응답 — 수신 서버는 200 OK를 빠르게 응답하고, 실제 처리는 비동기로 수행하세요.
  2. 중복 방지X-Delivery-Id를 저장하여 중복 수신을 방지하세요.
  3. 서명 검증 필수X-Signature-256 헤더를 반드시 검증하여 위변조를 방지하세요.
  4. 시크릿 관리 — 시크릿 키는 안전하게 보관하고, 노출 시 즉시 재생성하세요.
  5. 타임아웃 설정 — 수신 서버의 타임아웃은 웹훅 타임아웃보다 짧게 설정하세요.
  6. 필터링 — 필터 기능을 활용하여 필요한 이벤트만 수신하면 불필요한 트래픽을 줄일 수 있습니다.