← 활용 사례

오래 걸리는 작업의 진행률과 AI 답변 스트리밍

내보내기, 가져오기, 인코딩, AI 작업: 실제 작업을 따라가는 진행 막대, 완료되는 순간 나타나는 결과, 쓰이는 대로 보이는 답변.

내보내기, 동영상 인코딩, 보고서 생성, 가져오기, AI 작업은 10초에서 10분까지 걸릴 수 있습니다. 아무 정보 없는 스피너는 새로고침, 중복 클릭, 이탈을 부릅니다. 실제 진행률을 보여 주고 결과가 준비되는 순간 표시하면 사용자는 페이지에 머뭅니다. AI 기능에도 똑같이 쓸 수 있습니다. 작업이 백그라운드 잡에서 돌아가더라도 어시스턴트의 답변을 쓰이는 대로 스트리밍하세요.

사용자가 얻는 것

  • 실제 작업을 따라 움직이는 진행 막대: “10,000행 중 3,200행 처리 중”.
  • 다른 탭에 있어도 작업이 끝나는 순간 다운로드 링크나 결과가 나타납니다.
  • AI 답변이 오래 기다린 뒤 한꺼번에가 아니라 조금씩 나타납니다.
  • 페이지를 떠나도 괜찮습니다. 끝나면 알려 줍니다.

사용하는 기능

  • 사용자 인증과 sendToUser: 작업의 업데이트는 그 작업을 시작한 사람에게만, 모든 탭으로 전달됩니다.
  • 여러 사람이 같은 작업을 지켜볼 때는 프라이빗 채널(private-job-<id>).
  • 어떤 언어의 워커든: Node.js, PHP, Python, Go, Ruby, Java, .NET의 공식 Pusher 서버 라이브러리로 큐 워커에서 발행할 수 있습니다.
  • 앱을 닫은 사용자에게 긴 작업이 끝났음을 알리는 푸시 알림.

1단계: 작업 시작, ID 받기

app.post('/exports', requireLogin, async (req, res) => {
  const job = await queue.add('export', { userId: req.user.id, filters: req.body });
  res.status(202).json({ jobId: job.id });
});

2단계: 워커에서 진행률 보고

행마다가 아니라 의미 있는 단계마다 진행률을 보고합니다.

worker.process('export', async (job) => {
  const user = String(job.data.userId);
  const rows = await db.orders.query(job.data.filters);
  for (let i = 0; i < rows.length; i += 1000) {
    await writeChunk(rows.slice(i, i + 1000));
    await pusher.sendToUser(user, 'job-progress', { jobId: job.id, done: Math.min(i + 1000, rows.length), total: rows.length });
  }
  const url = await uploadAndSign();
  await pusher.sendToUser(user, 'job-done', { jobId: job.id, url });
});

pusher 패키지를 쓰는 Python 워커도 같습니다.

pusher_client.send_to_user(str(user_id), "job-progress", {"jobId": job_id, "done": done, "total": total})

3단계: 브라우저에 표시

사용자가 소켓에 로그인해 있다면(인앱 알림 참고):

const { jobId } = await fetch('/exports', { method: 'POST', body: form }).then((r) => r.json());

pusher.user.bind('job-progress', (p) => {
  if (p.jobId === jobId) progress.set(p.done / p.total);
});
pusher.user.bind('job-done', (p) => {
  if (p.jobId === jobId) showDownload(p.url);
});

AI 답변 스트리밍

AI 모델이 백그라운드 잡에서 답변을 쓸 때는 출력을 작은 묶음으로 사용자에게 전달하세요. 그러면 긴 답변도 수천 건이 아니라 수십 건의 메시지로 끝납니다.

let buffer = '';
let last = Date.now();
for await (const chunk of llm.stream(prompt)) {
  buffer += chunk.text;
  if (Date.now() - last > 150 || buffer.length > 2000) {
    await pusher.sendToUser(user, 'ai-delta', { id: answerId, text: buffer });
    buffer = '';
    last = Date.now();
  }
}
await pusher.sendToUser(user, 'ai-done', { id: answerId, text: buffer });
pusher.user.bind('ai-delta', ({ id, text }) => answers.get(id).append(text));
pusher.user.bind('ai-done', ({ id, text }) => answers.get(id).append(text).finish());

새로고침 후에도 남도록 전체 답변을 서버에도 저장하세요.

팁

  • 업데이트 하나가 메시지 1건입니다. 1~5%마다 또는 150밀리초마다 갱신하면 충분히 부드럽고, 작업 하나에 메시지 수십 건이면 됩니다.
  • 작업을 시작하기 전에 구독하거나, 페이지를 열 때 작업 상태를 한 번 조회하면 페이지가 연결되기 전에 보낸 업데이트를 놓치지 않습니다.
  • 사용자가 탭을 닫았다면 작업이 끝날 때 푸시 알림 가이드대로 푸시를 보내세요.