# Next.js에서 1C-Bitrix로의 비동기 리드 전송: 큐와 워커 없는 패턴
REST API를 통해 CRM으로 동기식 리드 제출은 치명적인 문제를 초래합니다: 사용자가 외부 요청 완료까지 응답을 기다려야 하고, 1C-Bitrix API의 불안정성으로 타임아웃과 오류가 발생합니다. 우리는 Next.js 16에서 큐와 백그라운드 프로세스를 없애는 솔루션을 제안합니다. 이 구현은 리드를 로컬 PostgreSQL 데이터베이스에 저장한 후 사용자에게 즉시 응답하고, HTTP 응답 전송 후 백그라운드 처리를 위한 메커니즘인 after()를 통해 Bitrix와의 통합을 처리합니다.
큐 없는 아키텍처: 세 층의 책임 분리
핵심 원칙은 작업을 동기식과 비동기식 부분으로 분리하는 것입니다. 폼을 받으면:
- zod를 통한 데이터 유효성 검사
- bitrix_id=NULL로 PostgreSQL에 저장
- 즉시 200 OK 응답
- after()를 통한 백그라운드 Bitrix 전송
이렇게 하면:
- Bitrix REST API 안정성에 독립적
- 사용자 인터페이스 차단 없음
- SELECT * FROM leads WHERE bitrix_id IS NULL로 전달 실패 리드 수동 처리 가능
이 접근의 핵심은 Redis/BullMQ를 버리는 것입니다. 모든 작업이 Next.js API 라우트에 맞고, 응답 후 처리를 위해 내장 after()를 사용합니다. 이는 인프라 복잡성을 줄이고 VPS에서 systemd/nginx를 통한 배포를 간소화합니다.
Next.js 16의 after(): 기술적 구현
after() 메서드는 서버리스 환경에서 "fire-and-forget" 문제를 해결합니다. Promise.resolve().then()과 달리 응답 전송 후에도 작업 실행을 보장하며, 작업 완료까지 프로세스를 종료하지 않습니다. 폼 처리 예시:
export async function POST(request: Request) {
// ... validatsiya and antidubl
const [lead] = await pgQuery(...);
after(async () => {
try {
const bitrixId = await sendToBitrix24(payload);
await pgQuery(
`UPDATE leads SET bitrix_id = $1 WHERE id = $2`,
[bitrixId, lead.id]
);
} catch (error) {
console.error("[Leads API] Error otpravki in Bitriks:", error);
}
});
return Response.json({ ok: true, id: lead.id });
}
주요 주의점:
- PostgreSQL의 10분 윈도우를 통한 중복 방지 구현(Bitrix는 경고 없이 중복 허용)
- 비동기 작업 특성을 반영해 bitrix_id를 nullable text로 저장
- 통합 오류는 journalctl에 로그로 기록, 사용자 응답에 영향 없음
1C-Bitrix를 위한 안정적인 HTTP 클라이언트
불안정한 API와 안정적으로 작업하기 위한 핵심 매개변수:
- MAX_ATTEMPTS = 2 — 신뢰성과 부하 균형
- REQUEST_TIMEOUT_MS = 8000 — Bitrix p95 지연(400-700 ms) 초과
- RETRY_DELAY_MS = 1500 — 지수적 복잡도 없는 선형 백오프
클라이언트 구현:
async function bitrixRequest(method: string, payload: Record<string, unknown>) {
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
try {
const response = await fetch(url, {
signal: controller.signal,
// ...
});
const text = await response.text();
const json = text ? JSON.parse(text) : {};
if (!response.ok || json.error) {
throw new Error(json.error_description || `HTTP ${response.status}`);
}
return json;
} catch (error) {
if (attempt >= MAX_ATTEMPTS) throw error;
await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
} finally {
clearTimeout(timer);
}
}
}
세 가지 핵심 포인트:
- response.text()를 통한 수동 파싱 — 빈 응답 오류 우회
- HTTP 상태와 CRM 내부 오류 두 유형 처리
- finally에서의 타이머 정리 보장 — 메모리 누수 방지
인증 기능과 데이터 스키마
통합을 위해 OAuth 대신 인바운드 웹훅 선택:
- Marketplace와 승인 의존성 없음
- BITRIX_WEBHOOK_URL 환경 변수 간단 관리
- Bitrix 관리 패널에서 스코프 설정
URL 내 토큰 유출 위험은 로그와 코드에서 전체 URL이 아닌 API 메서드만 표시하는 규칙으로 최소화.
leads 테이블 스키마:
CREATE TABLE leads (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
phone text NOT NULL,
source text,
bitrix_id text,
created_at timestamptz NOT NULL DEFAULT now()
);
bitrix_id를 text로 한 이유, integer가 아닌?
- Bitrix가 JSON에서 ID를 문자열로 반환
- 타입 변환 불필요
- PostgreSQL UUID로 멱등성 보장
주요 포인트
- 단일 진실 원천 — 로컬 DB: Bitrix 연락 전에 리드 저장으로 CRM 실패 시에도 데이터 보장
- 응답 후 처리: after()가 큐를 대체해 인프라 복잡성 제거
- 오류 처리: 타임아웃 재시도와 journalctl 로깅으로 관찰 가능성 보장
- DB 수준 중복 방지: 전화번호 기준 10분 윈도우로 CRM과 무관하게 중복 방지
- 안전한 인증: 웹훅은 엄격한 URL 로깅 제어 필요
이 패턴은 분당 50건 이하 요청 프로젝트에 적합합니다. 고부하 시스템에서는 SELECT COUNT(*) FROM leads WHERE bitrix_id IS NULL로 모니터링 추가와 cron을 통한 자동 재시도 고려. 현재 구현에서는 자동 재시도가 임시 실패 시 리드 중복 위험을 줄이기 위해 수동 오류 처리 선호.
— Editorial Team
아직 댓글이 없습니다.