27장. Server Action을 실제 계약으로 만든다
form action은 UI 함수가 아니라 서버 진입점이다. 입력 schema, session, resource authorization, idempotency key, transaction, audit log, 사용자용 오류를 한 순서로 고정한다. React의 pending 상태는 중복 클릭을 줄일 뿐 network retry나 두 탭을 막지 못한다. 버튼 비활성화와 서버 멱등성을 둘 다 구현한다.
'use server'
export async function reserveClass(input: unknown) {
const user = await requireUser()
const command = ReserveSchema.parse(input)
return db.transaction(async (tx) => {
const previous = await tx.idempotency.find(command.requestId, user.id)
if (previous) return previous.response
const booking = await reserveOneSeat(tx, user.id, command.sessionId)
await tx.outbox.insert({ type: 'BOOKING_CONFIRMED', bookingId: booking.id })
return tx.idempotency.save(command.requestId, user.id, booking)
})
}
예상 가능한 SOLD_OUT, ALREADY_BOOKED는 사용자 행동을 안내하는 domain result로 돌리고, DB 단절 같은 오류는 correlation id와 함께 server log로 보낸다. action 응답에 ORM entity 전체를 직렬화하지 않고 화면에 필요한 필드만 반환한다.