← 전체 글로 돌아가기

Next.js

Next.js 서버 액션에서 같은 주문이 두 번 만들어진 날

버튼을 잠갔는데도 주문이 중복 생성됐다. UI 제어와 서버 멱등성을 분리해 처리한 기록.

증상은 버튼 밖에서 시작됐다

주문 확인 버튼을 누르면 바로 disabled로 바꾸고 있었다. 그래서 중복 요청은 없을 거라 생각했다. 그런데 모바일에서 네트워크가 잠깐 끊긴 뒤 새로고침한 사용자의 주문이 두 건 생겼다. 브라우저가 보낸 요청을 완전히 제어할 수 없다는 사실을 그때 다시 확인했다.

먼저 DB에서 중복의 모양을 확인했다

주문 번호가 같은 것이 아니라, 같은 장바구니와 금액으로 서로 다른 주문 ID가 생성돼 있었다. 버튼 상태 문제가 아니라 서버 액션이 요청마다 새 레코드를 만들고 있었다.

select cart_id, amount, count(*)
from orders
group by cart_id, amount
having count(*) > 1;

멱등성 키를 주문 생성의 입력으로 넣었다

클라이언트가 제출 단위의 UUID를 만들고, 서버는 그 값을 주문 테이블의 유니크 키로 받게 했다. 이미 처리한 키면 새 주문을 만들지 않고 기존 결과를 돌려준다. 결제 API에도 같은 키를 전달했다.

const existing = await db.order.findUnique({ where: { idempotencyKey } })
if (existing) return existing

return db.order.create({ data: { cartId, amount, idempotencyKey } })

DB의 유니크 제약도 추가했다. findUniquecreate 사이에 요청 두 개가 겹칠 수 있기 때문이다.

alter table orders
add constraint orders_idempotency_key_unique unique (idempotency_key);

버튼 잠금은 그대로 두되 역할을 낮췄다

버튼 잠금은 빠른 연속 클릭을 줄이는 UX 장치다. 데이터 보호 장치로 취급하면 안 된다. 서버에서 중복을 막은 뒤에는 에러가 나도 같은 키로 재시도할 수 있도록 응답을 정리했다.

마무리로 확인한 항목은 세 가지였다.

  • 새로고침 후 같은 요청을 보내도 주문 수가 늘지 않는가
  • 유니크 충돌 시 기존 주문을 정상 응답하는가
  • 결제 제공사에도 동일한 멱등성 키가 전달되는가