Shopify에서 Meta 마케팅 API로 실시간 데이터 파이프라인 구축
Source: Dev.to
지난 몇 달 동안 Audience+ 를 구축했습니다 — Shopify 고객 데이터를 실시간으로 Meta의 광고 플랫폼과 동기화하는 도구입니다.
아래는 작동 방식, 우리가 해결한 과제, 그리고 비슷한 것을 구축하려는 분들에게 도움이 될 구체적인 코드 패턴을 명확하고 이해하기 쉽게 정리한 내용입니다.
우리가 해결하고자 하는 문제
Meta의 브라우저 기반 추적은 근본적으로 깨졌습니다.
- iOS 14.5 앱 추적 투명성(ATT)은 약 75 %의 아이폰 사용자를 숨깁니다.
- Meta 픽셀은 데이터 보관 기간이 180 일에 불과합니다.
- Meta는 대부분의 스토어에서 **실제 전환 데이터의 30–40 %**만을 최적화합니다.
해결책: 서버‑사이드 API를 사용해 Shopify에서 Meta로 1인당 고객 및 구매 데이터를 직접 전송합니다.
아키텍처 개요
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Shopify Store │──▶│ Audience+ API│──▶│ Meta API │
│ (Webhooks) │ │ (Processing) │ │ │
└───────────────┘ └───────────────┘ └───────────────┘
│
▼
┌───────────────┐
│ PostgreSQL │
│ (Customer DB)│
└───────────────┘
기술 스택
| 구성 요소 | 선택 |
|---|---|
| Framework | Next.js 15 (App Router) |
| Language | TypeScript |
| API Layer | tRPC |
| Database | PostgreSQL (Neon serverless) |
| ORM | Prisma |
| Auth | Better‑Auth + Shopify OAuth |
| Hosting | Vercel |
Shopify 웹훅 통합
Shopify는 고객 및 주문 라이프사이클 이벤트에 대한 웹훅을 전송합니다. 우리는 처리하기 전에 HMAC 서명을 사용하여 각 요청의 진위 여부를 확인합니다.
// app/api/webhooks/shopify/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
export async function POST(req: NextRequest) {
const body = await req.text();
const hmac = req.headers.get('x-shopify-hmac-sha256');
if (!verifyShopifyWebhook(body, hmac)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
const topic = req.headers.get('x-shopify-topic');
const payload = JSON.parse(body);
switch (topic) {
case 'orders/create':
await handleNewOrder(payload);
break;
case 'customers/create':
await handleNewCustomer(payload);
break;
case 'customers/update':
await handleCustomerUpdate(payload);
break;
}
return NextResponse.json({ received: true });
}
function verifyShopifyWebhook(body: string, hmac: string | null): boolean {
if (!hmac) return false;
const hash = crypto
.createHmac('sha256', process.env.SHOPIFY_WEBHOOK_SECRET!)
.update(body, 'utf8')
.digest('base64');
return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(hmac));
}
고객 데이터 정규화
고객 데이터는 Meta의 요구 사항(소문자, 공백 제거, SHA‑256)을 충족하도록 정규화 및 해시 처리됩니다.
// lib/customer-processor.ts
import crypto from 'crypto';
interface ShopifyCustomer {
id: number;
email: string;
phone?: string;
first_name?: string;
last_name?: string;
orders_count: number;
total_spent: string;
created_at: string;
}
interface MetaUserData {
em?: string;
ph?: string;
fn?: string;
ln?: string;
external_id?: string;
}
function processCustomerForMeta(customer: ShopifyCustomer): MetaUserData {
const userData: MetaUserData = {};
if (customer.email) {
userData.em = hashForMeta(customer.email.toLowerCase().trim());
}
if (customer.phone) {
userData.ph = hashForMeta(normalizePhone(customer.phone));
}
if (customer.first_name) {
userData.fn = hashForMeta(customer.first_name.toLowerCase().trim());
}
if (customer.last_name) {
userData.ln = hashForMeta(customer.last_name.toLowerCase().trim());
}
userData.external_id = hashForMeta(customer.id.toString());
return userData;
}
function hashForMeta(value: string): string {
return crypto.createHash('sha256').update(value).digest('hex');
}
/* Helper to normalize phone numbers – implementation omitted for brevity */
function normalizePhone(phone: string): string {
// e.g., strip non‑numeric characters, ensure E.164 format, etc.
return phone.replace(/\D+/g, '');
}
Source: …
Meta 마케팅 API 통합
우리는 두 개의 Meta API와 통합합니다:
- Custom Audiences API – 고객 목록을 동기화합니다.
- Conversions API – 실시간 서버‑사이드 이벤트를 전송합니다.
맞춤 잠재고객 동기화
// lib/meta-audience-sync.ts
import { chunk, sleep } from './utils';
const META_API_VERSION = 'v18.0';
interface AudienceUser {
email?: string;
phone?: string;
firstName?: string;
lastName?: string;
}
async function addUsersToAudience(
audienceId: string,
users: AudienceUser[],
accessToken: string
): Promise {
const BATCH_SIZE = 10_000;
const batches = chunk(users, BATCH_SIZE);
for (const batch of batches) {
const payload = {
schema: ['EMAIL', 'PHONE', 'FN', 'LN'],
data: batch.map(user => [
user.email ? hashForMeta(user.email.toLowerCase()) : '',
user.phone ? hashForMeta(normalizePhone(user.phone)) : '',
user.firstName ? hashForMeta(user.firstName.toLowerCase()) : '',
user.lastName ? hashForMeta(user.lastName.toLowerCase()) : '',
]),
};
const response = await fetch(
`https://graph.facebook.com/${META_API_VERSION}/${audienceId}/users`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payload, access_token: accessToken }),
}
);
if (!response.ok) {
const err = await response.text();
throw new Error(`Meta API error: ${err}`);
}
// Respect rate limits
await sleep(1000);
}
}
구매 이벤트 전송 (Conversions API)
// lib/meta-conversions.ts
import crypto from 'crypto';
interface ProcessedCustomer {
emailHash?: string;
phoneHash?: string;
// …other hashed fields
}
interface ShopifyOrder {
id: number;
created_at: string;
total_price: string;
currency: string;
// …other order fields
}
async function sendPurchaseEvent(
pixelId: string,
customer: ProcessedCustomer,
order: ShopifyOrder,
accessToken: string
) {
const event = {
event_name: 'Purchase',
event_time: Math.floor(new Date(order.created_at).getTime() / 1000),
event_id: `order_${order.id}`,
action_source: 'website',
user_data: {
em: customer.emailHash,
ph: customer.phoneHash,
// add other hashed identifiers as needed
},
custom_data: {
currency: order.currency,
value: parseFloat(order.total_price),
// optional: contents, order_id, etc.
},
};
const response = await fetch(
`https://graph.facebook.com/${META_API_VERSION}/${pixelId}/events`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
data: [event],
access_token: accessToken,
}),
}
);
if (!response.ok) {
const err = await response.text();
throw new Error(`Conversions API error: ${err}`);
}
}
요약
- First‑party data는 브라우저 기반 추적의 제한을 우회합니다.
- Server‑side hashing은 매치율을 유지하면서 Meta의 개인정보 보호 요구사항을 충족합니다.
- Batching & rate‑limit handling은 대규모 오디언스 목록을 처리할 때 필수적입니다.
유사한 파이프라인을 구축하고 있다면, 위의 패턴들이 탄탄한 출발점을 제공할 것입니다. 즐거운 해킹 되세요!
// Example: Sending a Conversions API event to Meta
const event = {
event_name: 'Purchase',
event_time: Math.floor(Date.now() / 1000),
user_data: {
em: customer.hashedEmail,
ph: customer.hashedPhone,
client_ip_address: order.client_details?.browser_ip,
client_user_agent: order.client_details?.user_agent,
},
custom_data: {
value: parseFloat(order.total_price),
currency: order.currency,
content_ids: order.line_items.map(i => i.product_id.toString()),
content_type: 'product',
num_items: order.line_items.reduce((s, i) => s + i.quantity, 0),
},
};
await fetch(
`https://graph.facebook.com/${META_API_VERSION}/${pixelId}/events`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: [event], access_token: accessToken }),
}
);
잠재고객 세분화
고객은 Meta와 동기화된 세그먼트로 자동 분류됩니다.
enum CustomerSegment {
NEW = 'new',
ENGAGED = 'engaged',
EXISTING = 'existing',
}
function classifyCustomer(customer: CustomerWithOrders): CustomerSegment {
if (customer.orders_count === 0) return CustomerSegment.ENGAGED;
if (customer.orders_count >= 1) return CustomerSegment.EXISTING;
return CustomerSegment.NEW;
}
주요 과제 및 해결책
속도 제한
Meta는 엄격한 제한을 적용합니다. 우리는 지수 백오프 재시도를 사용합니다.
async function withRetry(
fn: () => Promise,
maxRetries = 3
): Promise {
let lastError: Error;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (err) {
lastError = err as Error;
await sleep(2 ** i * 1000); // exponential back‑off
}
}
throw lastError!;
}
멱등 웹훅 처리
async function processWebhookOnce(
webhookId: string,
handler: () => Promise
): Promise {
const existing = await prisma.processedWebhook.findUnique({
where: { id: webhookId },
});
if (existing) return; // already processed
await handler();
await prisma.processedWebhook.create({
data: { id: webhookId, processedAt: new Date() },
});
}
결과
**Audience+**를 사용하는 스토어는 일반적으로 다음을 확인합니다:
- 50–100 % 더 많은 전환 (Meta에 표시)
- 10–20 % ROAS 향상
- 최초로 정확한 제외 및 리타게팅
내가 다르게 할 일
- 먼저 Conversions API부터 시작한다.
- 모니터링을 더 일찍 추가한다.
- 동기식 처리 대신 첫날부터 큐를 사용한다.
직접 사용해 보기
직접 빌드하지 않고 사용하고 싶다면, 다음을 확인하세요:
👉 https://www.audience-plus.com
10분 설정. 완전 자동화.
구현에 대해 질문이 있나요? 댓글에 남겨 주세요.