크리에이터 링크 페이지를 커머스 유입 경로로 쓰기 — 추적과 전환 설계

이커머스

유입 추적UTM딥링크어트리뷰션크리에이터 커머스

이 글은 누구를 위한 것인가

  • 크리에이터·파트너의 링크 페이지에서 오는 유입을 다루는 커머스 팀
  • "인스타에서 왔다는데 GA에서는 direct로 잡힌다"는 문제를 겪는 경우
  • 크리에이터별 성과를 정산으로 연결해야 하는 상황

들어가며

크리에이터 커머스의 유입 경로는 대체로 이렇게 생겼다.

  인스타 프로필  →  링크 페이지  →  상품 페이지  →  구매
  (link in bio)     (외부 서비스)    (우리 몰)

가운데 링크 페이지가 외부 서비스라는 점이 모든 문제의 출발점이다. 우리가 심을 수 있는 코드가 없고, 리퍼러는 중간에 끊기며, 앱으로 넘어가면 파라미터가 사라진다.

그 결과 대시보드는 이렇게 나온다.

  direct / none          62%   ← 실제로는 대부분 링크 페이지 유입
  instagram.com          11%
  l.instagram.com         6%
  (기타 30여 개)          21%

이 상태로는 크리에이터별 성과를 말할 수 없고, 정산 근거도 만들 수 없다. 이 글은 그 62%를 걷어내는 방법을 다룬다.


1. 리퍼러는 신뢰할 수 없다

먼저 왜 유실되는지 알아야 대책이 선다.

[리퍼러가 사라지는 경우]

  · 인앱 브라우저 → 외부 브라우저 전환
  · HTTPS → 앱 스킴(deep link) 이동
  · Referrer-Policy 가 no-referrer / same-origin 인 출처
  · iOS 앱 간 이동 (인스타 → 사파리)
  · 사용자가 링크를 복사해 붙여넣기
  · QR 코드 스캔

출처가 리퍼러를 어디까지 보낼지는 Referrer-Policy가 결정하고, 우리가 통제할 수 없다. 결국 리퍼러 기반 어트리뷰션은 모바일에서 절반쯤 실패한다. 대안은 하나다 — URL 파라미터를 우리가 직접 심는 것.

  ❌ https://shop.example.com/products/1234
     → 리퍼러에 의존. 유실되면 direct

  ✅ https://shop.example.com/products/1234?utm_source=linkpage&utm_medium=bio&utm_campaign=creator_abc
     → 리퍼러가 없어도 파라미터는 남는다

파라미터는 크리에이터가 링크를 등록할 때 우리가 자동으로 붙여줘야 한다. "UTM 붙여서 등록해 주세요"라고 안내하면 대부분 안 붙이거나 틀리게 붙인다.


2. UTM 규칙을 먼저 못 박는다

UTM은 자유 문자열이라 규칙이 없으면 즉시 오염된다.

[실제로 자주 보는 오염]

  utm_source=Instagram / instagram / IG / insta / 인스타
  utm_medium=bio / link-in-bio / linkinbio / profile
  utm_campaign=여름세일 / summer_sale / SummerSale2026 / (빈값)

  → 대시보드에서 같은 유입이 5줄로 쪼개짐
[규칙 — 코드로 강제]

  utm_source    유입 플랫폼         instagram | youtube | tiktok | linkpage
  utm_medium    유입 형태           bio | post | story | dm
  utm_campaign  캠페인 슬러그       소문자·숫자·하이픈만
  utm_content   크리에이터 식별자   cr_{id}   ← 정산의 기준
  utm_term      (사용 안 함)

utm_content에 크리에이터 ID를 넣는 것이 실무의 핵심이다. utm_campaign에 크리에이터명을 넣으면 캠페인과 크리에이터가 뒤섞여, 나중에 "이 크리에이터의 전체 성과"를 뽑을 수 없다.

const ALLOWED_SOURCE = ['instagram', 'youtube', 'tiktok', 'linkpage', 'blog'] as const;

function buildTrackedUrl(target: string, ctx: {
  source: typeof ALLOWED_SOURCE[number];
  medium: 'bio' | 'post' | 'story' | 'dm';
  campaign: string;
  creatorId: string;
}): string {
  const u = new URL(target);
  // 이미 붙어 있으면 덮어쓰지 않는다 (크리에이터가 수동으로 넣었을 수 있음)
  if (!u.searchParams.has('utm_source')) {
    u.searchParams.set('utm_source', ctx.source);
    u.searchParams.set('utm_medium', ctx.medium);
    u.searchParams.set('utm_campaign', slugify(ctx.campaign));
    u.searchParams.set('utm_content', `cr_${ctx.creatorId}`);
  }
  return u.toString();
}

소문자 정규화를 수집 시점에도 한 번 더 하는 게 좋다. 어디선가 대문자가 섞여 들어오면 집계가 갈라진다.


3. 자체 단축 링크 — 중간 계층을 소유한다

파라미터를 붙이는 것만으로는 부족한 경우가 있다. 링크가 길어져 크리에이터가 직접 수정하거나, 플랫폼이 파라미터를 잘라내기도 한다.

자체 리다이렉트 계층을 두면 링크는 짧아지고, 클릭 자체를 우리가 기록할 수 있다.

  https://go.example.com/c/abc123
    → 302 → https://shop.example.com/products/1234?utm_...
// GET /c/:code
export async function GET(req: Request, { params }: { params: Promise<{ code: string }> }) {
  const { code } = await params;
  const link = await db.shortLink.findUnique({ where: { code } });
  if (!link) return new Response(null, { status: 404 });

  // 클릭 기록은 비동기로 — 리다이렉트를 지연시키지 않는다
  void recordClick({
    code,
    creatorId: link.creatorId,
    ua: req.headers.get('user-agent'),
    ref: req.headers.get('referer'),
    at: new Date(),
  });

  return new Response(null, {
    status: 302,                      // ★ 301 아님
    headers: {
      Location: link.targetUrl,
      'Cache-Control': 'no-store',    // ★ 중간 캐시 방지
    },
  });
}

두 가지가 중요하다.

  · 302 (임시) 를 쓴다
    301(영구)은 브라우저가 캐시해서, 목적지를 바꿔도 이전 주소로 간다
    그리고 캐시된 이동은 클릭 기록이 남지 않는다

  · Cache-Control: no-store
    CDN이나 프록시가 리다이렉트를 캐시하면 클릭 수가 실제보다 적게 잡힌다

클릭 기록을 리다이렉트보다 먼저 await 하지 않는 것도 포인트다. 기록 DB가 느려지면 사용자 이동이 지연된다. 유실을 줄이려면 큐에 넣고 비동기로 처리한다.


4. 앱 딥링크 — 파라미터가 사라지는 구간

모바일에서 상품 링크가 앱을 열도록 되어 있으면, 웹 파라미터가 앱으로 전달되지 않는 경우가 많다.

[유실 지점]

  브라우저: /products/1234?utm_content=cr_abc
     ↓ 앱 열기 (Universal Link / App Link)
  앱:      상품 1234 화면 — 파라미터 없음

  → 앱에서 구매하면 어트리뷰션이 끊긴다

해결은 앱이 원본 URL 전체를 받도록 하는 것이다. iOS는 Associated Domains 설정이 전제이며, 파라미터 파싱은 URLSearchParams에 대응하는 각 플랫폼 API로 처리한다. Universal Links / App Links는 원래 전체 URL을 전달하므로, 앱 쪽에서 쿼리 파라미터를 파싱해 저장하면 된다.

// iOS — 앱 진입 시 UTM 보관
func application(_ app: UIApplication, continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    guard let url = userActivity.webpageURL,
          let comps = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }

    let utm = comps.queryItems?.reduce(into: [String: String]()) { acc, item in
        if item.name.hasPrefix("utm_"), let v = item.value { acc[item.name] = v }
    }
    if let utm, !utm.isEmpty {
        AttributionStore.save(utm, ttlDays: 7)   // ★ 유효기간을 둔다
    }
    return true
}

유효기간(TTL)을 두는 것이 중요하다. 저장한 UTM을 영구 보관하면, 몇 달 뒤 자연 유입으로 산 구매까지 그 크리에이터 성과로 잡힌다. 7~30일이 일반적이다.

앱 미설치 사용자는 스토어를 거치면서 파라미터가 완전히 사라진다. 이 구간은 설치 후 첫 실행 시 클립보드나 지연 딥링크로 복원해야 하는데, 구현 비용이 커서 초기에는 웹 결제 경로를 유지하는 편이 낫다.


5. 첫 방문 값을 고정한다

같은 사용자가 여러 경로로 들어오면 어느 것을 인정할지 정해야 한다.

  1일차  크리에이터 A 링크로 방문 (구매 안 함)
  3일차  검색으로 재방문
  5일차  크리에이터 B 링크로 방문 → 구매

  → A? B? 둘 다? 나누기?

정책을 코드로 못 박고 문서화하는 것이 중요하다. 크리에이터 정산과 직결되므로 나중에 바꾸면 분쟁이 난다.

const ATTR_TTL_DAYS = 30;

function captureAttribution(url: URL) {
  const utm = readUtm(url);
  if (!utm.source) return;

  const prev = readCookie('attr');
  // 정책: 마지막 유입 인정(last non-direct click)
  //       단, 기존 값이 유효기간 내이고 같은 크리에이터면 갱신만
  writeCookie('attr', JSON.stringify({
    ...utm,
    firstSeenAt: prev?.firstSeenAt ?? Date.now(),
    lastSeenAt: Date.now(),
  }), { maxAge: ATTR_TTL_DAYS * 86400, sameSite: 'Lax' });
}
[정책 선택지]

  첫 클릭 인정   발굴 기여를 중시. 신규 크리에이터에게 유리
  마지막 클릭    구매 직전 기여를 중시. 계산이 단순 (가장 흔함)
  분할           공정하지만 정산 로직이 복잡해지고 분쟁도 늘어남

초기에는 마지막 클릭이 현실적이다. 계산이 단순하고 크리에이터도 이해하기 쉽다. 다만 정산 약관에 명시해야 한다 — "구매 전 30일 내 마지막 링크 클릭 기준"처럼.


6. 주문에 기록한다 — 쿠키만 믿지 않는다

어트리뷰션 값은 주문 생성 시점에 주문 레코드로 복사해야 한다.

ALTER TABLE orders
  ADD COLUMN attr_source     TEXT,
  ADD COLUMN attr_medium     TEXT,
  ADD COLUMN attr_campaign   TEXT,
  ADD COLUMN attr_creator_id TEXT,
  ADD COLUMN attr_captured_at TIMESTAMPTZ;

CREATE INDEX ON orders (attr_creator_id, created_at DESC)
  WHERE attr_creator_id IS NOT NULL;

쿠키에만 두면 정산 시점에 값이 없다. 쿠키는 만료되고, 사용자가 지우고, 기기가 바뀐다. 주문에 박아두면 그 순간의 진실이 영구 보존된다.

반품·취소도 반영해야 한다. 정산은 순매출 기준이어야 하므로, 주문 상태 변경이 크리에이터 정산에 연동되어야 한다.

-- 크리에이터별 확정 정산 대상
SELECT
  attr_creator_id,
  count(*) FILTER (WHERE status = 'completed')            AS orders,
  sum(net_amount) FILTER (WHERE status = 'completed')     AS net_sales,
  sum(net_amount) FILTER (WHERE status = 'refunded')      AS refunded
FROM orders
WHERE attr_creator_id IS NOT NULL
  AND created_at >= :from AND created_at < :to
GROUP BY attr_creator_id;

정산 확정 시점은 반품 가능 기간이 지난 뒤로 잡아야 한다. 구매 즉시 정산하면 반품분을 나중에 회수해야 하는데, 그게 가장 흔한 분쟁 원인이다.


7. 이벤트 스키마를 표준에 맞춘다

자체 지표를 만들기 전에 표준 이벤트 규격을 따르면, 도구를 바꿔도 데이터가 살아남는다.

// GA4 이커머스 이벤트 — 유입 정보를 함께 실어보낸다
gtag('event', 'purchase', {
  transaction_id: order.id,
  value: order.netAmount,
  currency: 'KRW',
  items: order.items.map(i => ({
    item_id: i.sku, item_name: i.name,
    price: i.price, quantity: i.qty,
  })),
});

이벤트명과 파라미터는 GA4 이커머스 규격을 그대로 쓰는 편이 낫다. 자체 이름을 쓰면 기본 리포트가 전부 비게 된다.

상품 페이지의 구조화 데이터도 링크 유입에 영향을 준다. 링크 페이지에서 넘어온 사용자가 검색으로 되돌아와 확인하는 경로가 흔하기 때문이다.

  Product 스키마 — 가격·재고·평점이 검색 결과에 표시
  → 링크 페이지에서 본 상품을 검색으로 재확인할 때 이탈 감소

구현 요건은 상품 구조화 데이터 가이드를 따른다.


8. 링크 페이지 쪽에서 할 수 있는 것

외부 링크 페이지 서비스를 쓰더라도, 어떤 서비스를 쓰느냐가 추적 가능성에 영향을 준다.

[확인할 것]

  □ 링크에 쿼리 파라미터를 그대로 유지하는가 (잘라내는 서비스가 있다)
  □ 리다이렉트를 몇 단계 거치는가 (많을수록 유실 위험)
  □ 자체 도메인 연결이 되는가 (리퍼러가 자사 도메인으로 잡힘)
  □ 클릭 통계를 내보낼 수 있는가 (우리 데이터와 대조용)

Linkme의 유즈케이스 페이지처럼 크리에이터 링크·포트폴리오·비즈니스 용도를 나눠 제시하는 서비스들이 있는데, 커머스 관점에서 실제로 봐야 할 것은 소개 문구가 아니라 위 네 가지다. 특히 파라미터 보존 여부는 직접 테스트해봐야 안다 — UTM을 붙인 링크를 등록하고, 실제로 클릭해서 도착지 URL에 파라미터가 남아 있는지 확인하면 5분이면 끝난다.


9. 정리

  1. 리퍼러에 의존하지 말고 URL 파라미터를 우리가 붙인다
  2. UTM 값은 enum으로 강제, utm_content에 크리에이터 ID
  3. 자체 단축 링크로 중간 계층 소유 (302 + no-store)
  4. 앱 딥링크는 원본 URL 파싱 + TTL 있는 저장
  5. 어트리뷰션 정책(마지막 클릭 / 30일)을 약관에 명시
  6. 쿠키가 아니라 주문 레코드에 기록, 반품 반영
  7. GA4 표준 이벤트 규격을 그대로 사용
  8. 링크 서비스의 파라미터 보존 여부를 직접 테스트

가장 먼저 할 것은 1번과 2번이다. 파라미터를 자동으로 붙이고 값을 강제하는 것만으로, direct로 뭉쳐 있던 유입의 상당 부분이 제자리를 찾는다. 나머지 최적화는 그 데이터가 생긴 뒤에 의미가 있다.