개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← Node Auction

낙찰 스케줄러 — 서버가 꺼졌다 켜져도 낙찰은 되어야 한다

2026년 6월 10일
node-schedulessesequelize

24시간이 지난 상품은 누가 자동으로 낙찰시켜줘야 한다. 근데 서버가 계속 켜져 있으리라는 보장이 없다. 이 문제를 어떻게 풀었는지가 이번 편의 전부다.

재시작 시 밀린 것부터 처리

const targets = await Good.findAll({
  where: { SoldId: null, createdAt: { [Op.lte]: yesterday } },
});
targets.forEach(async (good) => {
  const t = await sequelize.transaction();
  const success = await Auction.findOne({
    where: { GoodId: good.id },
    order: [["bid", "DESC"]],
    transaction: t,
  });
  await good.setSold(success.UserId, { transaction: t });
  await User.update(
    { money: sequelize.literal(`money - ${success.bid}`) },
    { where: { id: success.UserId }, transaction: t }
  );
  await t.commit();
});

서버가 꺼져있던 사이에 이미 24시간이 지나버린 상품들. 이런 애들은 스케줄을 걸 필요도 없이 서버 켜지자마자 바로 처리한다. 낙찰자 지정과 잔액 차감을 트랜잭션으로 묶은 이유는 간단하다 — 낙찰자는 지정됐는데 돈은 안 깎이거나, 그 반대가 되면 안 되니까.

아직 안 끝난 것들은 개별 예약

const ongoing = await Good.findAll({
  where: { SoldId: null, createdAt: { [Op.gte]: yesterday } },
});
ongoing.forEach((good) => {
  const end = new Date(good.createdAt);
  end.setDate(end.getDate() + 1);
  const job = scheduleJob(end, async () => {
    const success = await Auction.findOne({ where: { GoodId: good.id }, order: [["bid", "DESC"]] });
    await good.setSold(success.UserId);
    await User.update({ money: sequelize.literal(`money - ${success.bid}`) }, { where: { id: success.UserId } });
  });
});

node-schedule의 scheduleJob은 특정 시각을 넘겨주면 그 시각에 딱 한 번 실행되는 잡을 만든다. 상품마다 등록 시각 + 24시간을 계산해서 각자의 낙찰 시각에 예약을 건다. 상품이 100개면 예약된 잡도 100개가 떠 있는 셈이다.

여기서 신경 쓰인 부분: 이 예약은 메모리에만 떠 있다. 서버가 죽으면 예약도 같이 날아간다. 그래서 위의 "재시작 시 밀린 것부터 처리"가 필요한 거다 — 서버가 다시 켜지면 그사이 놓친 낙찰들을 한 번 쓸어담고, 그다음부터 다시 예약을 건다. 이 두 로직이 짝을 이뤄야 어떤 상황에서도 낙찰이 새지 않는다.

SSE로 서버 시간 뿌리기

sse.on("connection", (client) => {
  setInterval(() => client.send(Date.now().toString()), 1_000);
});

Socket.IO 말고 SSE(Server-Sent Events)도 하나 붙여봤다. 얘는 서버 → 클라이언트 단방향이라 양방향 통신이 필요 없는 "그냥 흘려보내기만 하면 되는 데이터"에 잘 맞는다. 지금은 서버 시간을 1초마다 흘려보내는 정도로만 써봤는데, 실전이라면 남은 경매 시간 카운트다운 같은 데 쓰기 좋을 것 같다.

app.js — 마지막 조립

checkAuction(); // 서버 켜질 때 낙찰 스케줄러 시작
...
const server = app.listen(app.get("port"), () => { ... });
webSocket(server, app); // socket.io
sse(server, app);       // sse

미들웨어, 라우터, 소켓, SSE, 스케줄러 — 따로따로 만들어둔 조각들을 여기서 순서대로 꽂는다. checkAuction()은 서버가 뜨자마자 호출해서 밀린 낙찰부터 처리하고, listen 이후에 소켓이랑 SSE를 붙인 건 둘 다 server 인스턴스가 필요해서다.

여기까지 하니 회원가입 → 상품 등록 → 실시간 입찰 → 자동 낙찰까지 한 바퀴가 다 돈다. Node.js 교과서 따라 만든 경매 사이트치고는 꽤 그럴싸하게 굴러간다.

← 이전 글실시간 입찰 — Socket.IO 방과 세 겹의 방어 로직