API DocsOverview

SSE 소비

Server-Sent Events 소비 패턴 및 예제

Server-Sent Events (SSE)

Server-Sent Events는 서버에서 클라이언트로 데이터를 실시간으로 스트리밍할 수 있게 해줍니다. 이 가이드는 DO 2.0 API에서 SSE를 소비하는 방법을 다룹니다.

SSE 소비 패턴

POST 기반 SSE (stream: true 와 함께 /api/chat/completions):

javascript
const response = await fetch('/api/chat/completions', {
method: 'POST',
headers: {
  'Authorization': `Bearer ${token}`,
  'Content-Type': 'application/json'
},
body: JSON.stringify({ 
  model: 'model-id', 
  messages: [...], 
  stream: true 
})
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
const { done, value } = await reader.read();
if (done) break;

const text = decoder.decode(value);
const lines = text.split('\n');

for (const line of lines) {
  if (line.startsWith('data: ')) {
    const data = line.slice(6);
    if (data === '[DONE]') {
      // 스트림 완료
      break;
    }
    try {
      const parsed = JSON.parse(data);
      console.log('청크:', parsed);
    } catch (e) {
      // JSON이 아닌 라인 건너뜀
    }
  }
}
}

핵심 포인트

EventSource 제한사항

인증된 SSE에는 EventSource를 사용하지 마세요.

EventSource API는 커스텀 헤더를 설정할 수 없어 인증에 적합하지 않습니다. 항상 ReadableStream 과 함께 fetch 를 사용하세요.

javascript
// ❌ 잘못된 방법 - EventSource는 Authorization 헤더를 설정할 수 없습니다
const eventSource = new EventSource('/api/stream');

// ✅ 올바른 방법 - ReadableStream과 함께 fetch 사용
const response = await fetch('/api/stream', {
  headers: { 'Authorization': `Bearer ${token}` }
});
const reader = response.body.getReader();

스트림 형식

SSE 스트림은 다음 형식으로 데이터를 전송합니다:

data: {"type": "content_block_delta", "delta": {"text": "Hello"}}
data: {"type": "content_block_delta", "delta": {"text": " world"}}
data: [DONE]

data: 로 시작하는 각 라인에는 JSON 객체가 포함됩니다. data: [DONE] 으로 스트림이 종료됩니다.

오류 처리

[DONE] 전에 스트림이 실패하거나 오류를 반환하는 경우:

javascript
const { done, value } = await reader.read();
if (done) {
  // 응답 상태 확인
  if (!response.ok) {
    const error = await response.json();
    console.error('스트림 오류:', error);
  }
  break;
}

스트리밍 채팅 예제

javascript
async function streamChat(messages) {
const response = await fetch('/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    model: 'gpt-4',
    messages,
    stream: true
  })
});

if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}

const reader = response.body.getReader();
const decoder = new TextDecoder();
let content = '';

try {
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const text = decoder.decode(value);
    const lines = text.trim().split('\n');

    for (const line of lines) {
      if (line.startsWith('data: ')) {
        const data = line.slice(6);
        if (data === '[DONE]') return content;
        
        try {
          const chunk = JSON.parse(data);
          if (chunk.choices?.[0]?.delta?.content) {
            content += chunk.choices[0].delta.content;
            // 실시간으로 스트리밍 콘텐츠 표시
            console.log(chunk.choices[0].delta.content);
          }
        } catch (e) {
          // 유효하지 않은 JSON 건너뜀
        }
      }
    }
  }
} finally {
  reader.cancel();
}

return content;
}

일반적인 실수

1. 파일 업로드 시 Content-Type 수동 설정

❌ 잘못된 방법:

javascript
const formData = new FormData();
formData.append('file', file);

fetch('/api/files', {
  method: 'POST',
  headers: { 'Content-Type': 'multipart/form-data' }, // ❌ 수동 설정 금지!
  body: formData
});

✅ 올바른 방법:

javascript
const formData = new FormData();
formData.append('file', file);

fetch('/api/files', {
  method: 'POST',
  // 브라우저가 올바른 boundary와 함께 Content-Type을 자동 설정
  body: formData
});

브라우저는 올바른 boundary 와 함께 Content-Type: multipart/form-data 를 자동으로 설정합니다. 수동으로 설정하면 파일 업로드가 실패합니다.

2. 인증이 필요한 엔드포인트에 EventSource 사용

❌ 잘못된 방법:

javascript
// EventSource는 Authorization 헤더를 설정할 수 없습니다!
const source = new EventSource('/api/v1/chat/stream');

✅ 올바른 방법:

javascript
const response = await fetch('/api/v1/chat/stream', {
  headers: { 'Authorization': `Bearer ${token}` }
});
const reader = response.body.getReader();

3. POST 엔드포인트 혼동

❌ 잘못된 방법:

javascript
// POST /api/v1/users는 새 사용자를 생성합니다
POST /api/v1/users
// 관리자 작업이어야 합니다

// POST /api/v1/users/[id]는 사용자를 업데이트합니다
POST /api/v1/users/123
  • POST /api/v1/users — 새 사용자 생성 (관리자 전용)
  • POST /api/v1/users/:id — 기존 사용자 업데이트 (관리자 전용)
  • GET /api/v1/users/:id — 사용자 상세 정보 조회 (자신 또는 관리자)

모범 사례

  1. 인증이 필요한 엔드포인트에는 항상 Authorization 헤더를 설정하세요
  2. 스트림을 읽기 전에 응답 상태를 확인하세요
  3. 부분 읽기를 처리하세요 — 스트림이 JSON 중간에서 분할될 수 있습니다
  4. 리더를 정리하세요 — 오류 또는 조기 종료 시 reader.cancel() 호출
  5. 재연결 시 지수 백오프를 구현하세요
  6. 장시간 실행 스트림에는 타임아웃을 설정하세요
  7. Content-Type을 올바르게 사용하세요 — 파일 업로드 시 브라우저가 설정하도록 허용