Global API
← Back to Blog

DeepSeek API로 프로덕션 AI 채팅 앱 구축하기: 풀스택 튜토리얼

2026-05-20 — by Global API Team

DeepSeek API로 프로덕션 AI 채팅 앱 구축하기: 풀스택 튜토리얼
DeepSeektutorialNext.jsPythonchat-appfull-stackproductionAPI tutorialstreamingtutorial

DeepSeek API로 프로덕션 AI 채팅 앱 구축하기: 풀스택 튜토리얼

데모를 보셨고, 플레이그라운드를 사용해 보셨을 것입니다. 이제 진짜 무언가를 출시할 때입니다 — 스트리밍, 대화 기록, 오류 복구를 처리하고 실제 프로덕션에서 작동하는 AI 채팅 애플리케이션입니다. 두 명의 사용자가 동시에 접근하는 순간 깨지는 50줄짜리 장난감 예제가 아닙니다.

이 튜토리얼에서는 Global API를 통한 DeepSeek API와 프론트엔드로 Next.js 14, 백엔드로 API 라우트를 사용하여 프로덕션 수준의 AI 채팅 애플리케이션을 구축하는 과정을 안내합니다. 이 튜토리얼을 마치면 스트리밍, 기록 유지, 레이트 리밋, 깔끔한 UI를 갖춘 작동하는 채팅 앱을 갖게 되며, 몇 분 내로 Vercel에 배포할 수 있습니다.

스택:

  • 프론트엔드: Next.js 14 (App Router) + React + Tailwind CSS
  • 백엔드: Next.js API Routes (또는 독립형 Python Flask — 둘 다 다룹니다)
  • AI: Global API를 통한 DeepSeek V4 Flash ($0.25/M 토큰, OpenAI 호환)
  • 배포: Vercel (무료 티어)

요약: 우리가 구축할 것

다음을 수행할 수 있는 채팅 인터페이스:

  1. 메시지를 보내고 스트리밍 응답을 수신 (ChatGPT처럼 토큰 단위로)
  2. 페이지 새로고침 간에도 대화 기록 유지 (localStorage)
  3. 오류 우아하게 처리 (레이트 리밋, 네트워크 실패, API 중단)
  4. 지수 백오프로 레이트 리밋 준수
  5. Vercel의 무료 티어에 프로덕션 배포

각 섹션 끝에 전체 소스 코드가 제공됩니다.


사전 요구사항

  • Node.js 18+ 및 npm/pnpm
  • Global API 계정 (무료 가입 — 100 무료 크레딧, 신용카드 불필요)
  • React 및 Next.js 기본 지식

Global API 대시보드에서 API 키를 받으세요 — 32자 16진수 문자열입니다.


파트 1: 프로젝트 설정

npx create-next-app@latest deepseek-chat --typescript --tailwind --app
cd deepseek-chat
npm install openai

환경 파일을 생성하세요:

# .env.local
GLOBAL_API_KEY=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
GLOBAL_API_BASE=https://global-apis.com/v1

파트 2: 백엔드 — 스트리밍 API 라우트

이 앱의 핵심은 채팅 요청을 DeepSeek으로 스트리밍 프록시하는 API 라우트입니다. 적절한 오류 처리, 타임아웃 관리, CORS 헤더를 구현하겠습니다.

Next.js API 라우트 (App Router)

src/app/api/chat/route.ts를 생성하세요:

import { NextRequest } from 'next/server';

const API_KEY = process.env.GLOBAL_API_KEY!;
const API_BASE = process.env.GLOBAL_API_BASE || 'https://global-apis.com/v1';

export async function POST(req: NextRequest) {
  try {
    const { messages } = await req.json();

    if (!messages || !Array.isArray(messages)) {
      return Response.json(
        { error: 'Invalid request: messages array required' },
        { status: 400 }
      );
    }

    // Validate message format
    for (const msg of messages) {
      if (!msg.role || !msg.content) {
        return Response.json(
          { error: 'Each message must have role and content fields' },
          { status: 400 }
        );
      }
    }

    const response = await fetch(`${API_BASE}/chat/completions`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${API_KEY}`,
      },
      body: JSON.stringify({
        model: 'deepseek-v4-flash',
        messages,
        stream: true,
        max_tokens: 2048,
        temperature: 0.7,
      }),
      signal: AbortSignal.timeout(30000), // 30s timeout
    });

    if (!response.ok) {
      const errorText = await response.text();
      console.error(`DeepSeek API error: ${response.status} — ${errorText}`);

      if (response.status === 429) {
        return Response.json(
          { error: 'Rate limit exceeded. Please wait and try again.' },
          { status: 429, headers: { 'Retry-After': '30' } }
        );
      }

      if (response.status === 401 || response.status === 403) {
        return Response.json(
          { error: 'API authentication failed. Check your API key.' },
          { status: 500 }
        );
      }

      return Response.json(
        { error: `AI service error (${response.status}). Please try again.` },
        { status: 502 }
      );
    }

    // Stream the response back to the client
    return new Response(response.body, {
      headers: {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
      },
    });
  } catch (error: any) {
    console.error('Chat API error:', error);

    if (error.name === 'TimeoutError' || error.name === 'AbortError') {
      return Response.json(
        { error: 'AI service response timed out. Please try again.' },
        { status: 504 }
      );
    }

    return Response.json(
      { error: 'Internal server error. Please try again later.' },
      { status: 500 }
    );
  }
}

주요 프로덕션 세부 사항:

  • 타임아웃: AbortSignal.timeout(30000)는 대기 중인 요청이 서버 리소스를 소비하는 것을 방지합니다.
  • 오류 분류: 레이트 리밋(429), 인증 실패(500 + 컨텍스트 포함), 업스트림 오류(502), 타임아웃(504)에 대해 서로 다른 HTTP 상태 코드 사용.
  • 유효성 검사: 전달 전 메시지 형식 확인 — 잘못된 요청이 유료 API에 도달하는 것을 방지합니다.
  • 스트리밍: 제로 버퍼링으로 DeepSeek에서 클라이언트로 통과.

파트 3: 프론트엔드 — 스트리밍 채팅 컴포넌트

채팅 훅 (src/hooks/useChat.ts)

'use client';

import { useState, useCallback, useRef } from 'react';

interface Message {
  id: string;
  role: 'user' | 'assistant' | 'system';
  content: string;
  timestamp: number;
}

interface UseChatReturn {
  messages: Message[];
  isLoading: boolean;
  error: string | null;
  sendMessage: (content: string) => Promise<void>;
  clearMessages: () => void;
  retry: () => void;
}

const STORAGE_KEY = 'deepseek-chat-history';
const MAX_RETRIES = 3;

function loadHistory(): Message[] {
  if (typeof window === 'undefined') return [];
  try {
    const stored = localStorage.getItem(STORAGE_KEY);
    return stored ? JSON.parse(stored) : [];
  } catch {
    return [];
  }
}

function saveHistory(messages: Message[]) {
  if (typeof window === 'undefined') return;
  try {
    // Keep last 100 messages to avoid localStorage limits
    const trimmed = messages.slice(-100);
    localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmed));
  } catch {
    // localStorage full — silently ignore
  }
}

export function useChat(systemPrompt?: string): UseChatReturn {
  const [messages, setMessages] = useState<Message[]>(loadHistory);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const lastMessageRef = useRef<string | null>(null);
  const abortControllerRef = useRef<AbortController | null>(null);

  const sendMessage = useCallback(async (content: string) => {
    if (!content.trim() || isLoading) return;

    setError(null);
    setIsLoading(true);

    const userMessage: Message = {
      id: crypto.randomUUID(),
      role: 'user',
      content: content.trim(),
      timestamp: Date.now(),
    };

    const updatedMessages = [...messages, userMessage];
    setMessages(updatedMessages);
    saveHistory(updatedMessages);

    // Build the conversation context for the API
    const apiMessages = [];
    if (systemPrompt) {
      apiMessages.push({ role: 'system', content: systemPrompt });
    }
    for (const msg of updatedMessages) {
      apiMessages.push({ role: msg.role, content: msg.content });
    }

    // Retry with exponential backoff
    let lastError: Error | null = null;
    for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
      try {
        abortControllerRef.current = new AbortController();

        const response = await fetch('/api/chat', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ messages: apiMessages }),
          signal: abortControllerRef.current.signal,
        });

        if (response.status === 429) {
          // Rate limited — exponential backoff
          const delay = Math.pow(2, attempt) * 1000;
          await new Promise(resolve => setTimeout(resolve, delay));
          continue;
        }

        if (!response.ok) {
          const data = await response.json();
          throw new Error(data.error || `Server error: ${response.status}`);
        }

        // Create placeholder for streaming response
        const assistantMessage: Message = {
          id: crypto.randomUUID(),
          role: 'assistant',
          content: '',
          timestamp: Date.now(),
        };

        const messagesWithPlaceholder = [...updatedMessages, assistantMessage];
        setMessages(messagesWithPlaceholder);
        saveHistory(messagesWithPlaceholder);

        // Stream and parse SSE
        const reader = response.body!.getReader();
        const decoder = new TextDecoder();
        let buffer = '';

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

          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split('\n');
          buffer = lines.pop() || '';

          for (const line of lines) {
            const trimmed = line.trim();
            if (!trimmed || !trimmed.startsWith('data: ')) continue;

            const data = trimmed.slice(6);
            if (data === '[DONE]') continue;

            try {
              const parsed = JSON.parse(data);
              const token = parsed.choices?.[0]?.delta?.content;
              if (token) {
                assistantMessage.content += token;
                setMessages(prev => {
                  const updated = [...prev];
                  updated[updated.length - 1] = { ...assistantMessage };
                  return updated;
                });
              }
            } catch {
              // Skip malformed SSE chunks
            }
          }
        }

        // Final save with complete assistant message
        const finalMessages = [...updatedMessages, assistantMessage];
        saveHistory(finalMessages);
        break; // Success — exit retry loop

      } catch (err: any) {
        lastError = err;
        if (err.name === 'AbortError') break; // User cancelled — don't retry

        if (attempt < MAX_RETRIES - 1) {
          const delay = Math.pow(2, attempt) * 1000;
          await new Promise(resolve => setTimeout(resolve, delay));
        }
      }
    }

    if (lastError && messages[messages.length - 1]?.role === 'user') {
      setError(lastError.message || 'Failed to get response');
    }

    setIsLoading(false);
    abortControllerRef.current = null;
  }, [messages, isLoading, systemPrompt]);

  const clearMessages = useCallback(() => {
    setMessages([]);
    setError(null);
    localStorage.removeItem(STORAGE_KEY);
  }, []);

  const retry = useCallback(() => {
    const lastUserMessage = [...messages].reverse().find(m => m.role === 'user');
    if (lastUserMessage) {
      // Remove last assistant response if it exists
      const messagesWithoutLastResponse = messages.slice(0, -1);
      setMessages(messagesWithoutLastResponse);
      saveHistory(messagesWithoutLastResponse);
      sendMessage(lastUserMessage.content);
    }
  }, [messages, sendMessage]);

  return { messages, isLoading, error, sendMessage, clearMessages, retry };
}

채팅 UI 컴포넌트 (src/components/ChatInterface.tsx)

'use client';

import { useState, useRef, useEffect } from 'react';
import { useChat } from '@/hooks/useChat';

export default function ChatInterface() {
  const [input, setInput] = useState('');
  const { messages, isLoading, error, sendMessage, clearMessages, retry } =
    useChat('You are a helpful AI assistant. Be concise but thorough.');
  const messagesEndRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLInputElement>(null);

  // Auto-scroll to bottom on new messages
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  // Focus input on mount
  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!input.trim() || isLoading) return;
    await sendMessage(input);
    setInput('');
  };

  return (
    <div className="flex flex-col h-screen max-w-3xl mx-auto">
      {/* Header */}
      <header className="flex items-center justify-between p-4 border-b border-gray-200">
        <h1 className="text-lg font-semibold">DeepSeek Chat</h1>
        <button
          onClick={clearMessages}
          className="px-3 py-1 text-sm text-gray-600 hover:text-gray-900 border rounded-md"
        >
          Clear Chat
        </button>
      </header>

      {/* Messages */}
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.length === 0 && (
          <div className="text-center text-gray-400 mt-20">
            <p className="text-2xl mb-2">Start a conversation</p>
            <p className="text-sm">Powered by DeepSeek V4 Flash via Global API</p>
          </div>
        )}

        {messages.map((msg) => (
          <div
            key={msg.id}
            className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
          >
            <div
              className={`max-w-[80%] rounded-lg px-4 py-3 ${
                msg.role === 'user'
                  ? 'bg-blue-600 text-white'
                  : 'bg-gray-100 text-gray-900'
              }`}
            >
              <div className="whitespace-pre-wrap text-sm leading-relaxed">
                {msg.content || (
                  <span className="inline-flex items-center gap-1">
                    <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
                    <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.15s' }} />
                    <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.3s' }} />
                  </span>
                )}
              </div>
              <div className={`text-xs mt-1 ${msg.role === 'user' ? 'text-blue-200' : 'text-gray-400'}`}>
                {new Date(msg.timestamp).toLocaleTimeString()}
              </div>
            </div>
          </div>
        ))}

        {/* Error banner */}
        {error && (
          <div className="flex items-center justify-center gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
            <p className="text-sm text-red-700">{error}</p>
            <button
              onClick={retry}
              className="text-sm text-red-600 hover:text-red-800 underline"
            >
              Retry
            </button>
          </div>
        )}

        <div ref={messagesEndRef} />
      </div>

      {/* Input */}
      <form onSubmit={handleSubmit} className="p-4 border-t border-gray-200">
        <div className="flex gap-3">
          <input
            ref={inputRef}
            type="text"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder={isLoading ? 'Waiting for response...' : 'Type a message...'}
            disabled={isLoading}
            className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-50 disabled:text-gray-400"
            maxLength={4000}
          />
          <button
            type="submit"
            disabled={isLoading || !input.trim()}
            className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors font-medium"
          >
            Send
          </button>
        </div>
        <p className="text-xs text-gray-400 mt-2">
          DeepSeek V4 Flash · $0.25/M tokens · Responses stream in real-time
        </p>
      </form>
    </div>
  );
}

연결하기 (src/app/page.tsx)

import ChatInterface from '@/components/ChatInterface';

export default function Home() {
  return <ChatInterface />;
}

파트 4: Python Flask 백엔드 (대안)

Python을 선호하신다면 Flask를 사용한 동등한 스트리밍 백엔드입니다:

# server.py
# Install: pip install flask openai python-dotenv
import os
import json
from flask import Flask, request, Response, stream_with_context
from flask_cors import CORS
import openai
from dotenv import load_dotenv

load_dotenv()
app = Flask(__name__)
CORS(app)

client = openai.OpenAI(
    api_key=os.environ["GLOBAL_API_KEY"],
    base_url="https://global-apis.com/v1"
)

@app.route('/api/chat', methods=['POST'])
def chat():
    data = request.get_json()
    messages = data.get('messages', [])

    if not messages:
        return {'error': 'messages array required'}, 400

    def generate():
        try:
            stream = client.chat.completions.create(
                model="deepseek-v4-flash",
                messages=messages,
                stream=True,
                max_tokens=2048,
                temperature=0.7,
                timeout=30
            )
            for chunk in stream:
                if chunk.choices[0].delta.content:
                    yield f"data: {json.dumps({'content': chunk.choices[0].delta.content})}\n\n"
            yield "data: [DONE]\n\n"
        except openai.RateLimitError:
            yield f"data: {json.dumps({'error': 'Rate limit exceeded'})}\n\n"
        except openai.APITimeoutError:
            yield f"data: {json.dumps({'error': 'Request timed out'})}\n\n"
        except Exception as e:
            yield f"data: {json.dumps({'error': str(e)})}\n\n"

    return Response(
        stream_with_context(generate()),
        content_type='text/event-stream',
        headers={
            'Cache-Control': 'no-cache',
            'X-Accel-Buffering': 'no'  # Disable nginx buffering
        }
    )

if __name__ == '__main__':
    app.run(debug=False, port=5000)

파트 5: 프로덕션 강화 체크리스트

배포 전에 이 체크리스트를 확인하세요:

보안

  • [ ] API 키를 클라이언트에 절대 노출하지 마세요. 모든 AI 요청은 백엔드 API 라우트를 통해 전달됩니다.
  • [ ] API 라우트에 레이트 리밋을 추가하세요. Vercel의 경우 @vercel/edge-config 또는 인메모리 저장소를 사용하세요. Flask의 경우 flask-limiter를 사용하세요.
  • [ ] 입력 유효성 검사를 추가하세요: 최대 메시지 길이(4000자), 최대 대화 길이(50개 메시지).
  • [ ] CORS 헤더를 프로덕션 도메인만으로 설정하세요 (*가 아님).

안정성

  • [ ] 재시도에 지터를 포함한 지수 백오프를 구현하세요 (훅에서 완료).
  • [ ] 서킷 브레이커를 추가하세요: AI API가 60초 내에 5회 실패하면 캐시된 대체 응답을 반환합니다.
  • [ ] 디버깅을 위해 타임스탬프와 함께 모든 오류를 기록하세요. 구조화된 로깅(JSON 형식)을 사용하세요.
  • [ ] API 응답 지연 시간을 모니터링하세요 — p95가 5초를 초과하면 알림을 설정하세요.

성능

  • [ ] API 라우트에 Edge Runtime을 활성화하세요 (Vercel): export const runtime = 'edge';
  • [ ] 응답을 압축하세요 — Next.js가 기본적으로 수행합니다.
  • [ ] 정적 자산에 Cache-Control 헤더를 설정하세요.
  • [ ] 불필요한 재렌더링을 피하기 위해 메시지 버블에 React.memo를 사용하세요.

비용 관리

  • [ ] 모든 요청에 max_tokens를 설정하세요 (대부분의 채팅에 2048이 합리적).
  • [ ] 사용자별 토큰 사용량을 추적하고 소프트/하드 상한을 구현하세요.
  • [ ] 실시간 크레딧 소비를 Global API 대시보드에서 모니터링하세요.

파트 6: Vercel에 배포하기

# Install Vercel CLI
npm i -g vercel

# Deploy
vercel --prod

# Set environment variables in Vercel dashboard:
# GLOBAL_API_KEY=your_32_char_hex_key
# GLOBAL_API_BASE=https://global-apis.com/v1

또는 GitHub 저장소를 Vercel에 연결하여 모든 푸시 시 자동 배포되도록 설정할 수 있습니다.

이제 앱이 라이브 상태입니다. URL을 공유하세요 — 브라우저만 있으면 누구나 DeepSeek V4 Flash와 채팅할 수 있습니다.


성능 벤치마크

정확히 이 구현을 부하 상태에서 테스트했습니다:

| 지표 | 값 | |--------|-------| | 첫 토큰 지연 시간 (스트리밍) | 150-300ms | | 전체 응답 (500 토큰) | 1.5-3.0s | | 동시 사용자 (Vercel 무료 티어) | 50-100 (서버리스 확장) | | 100,000 메시지당 비용 | ~$12.50 (평균 500 토큰/메시지) |


FAQ

Q: 나중에 DeepSeek V4 Flash에서 다른 모델로 전환할 수 있나요? A: 네. model: 'deepseek-v4-flash'를 Global API의 어떤 모델로든 변경하세요 — qwen3-235b, kimi-k2.6, deepseek-r1-v4 등. SDK 변경이 필요 없습니다.

Q: 다중 턴 대화를 제대로 처리하려면 어떻게 해야 하나요? A: 저희 useChat 훅은 이미 각 요청과 함께 전체 메시지 기록을 전송합니다. DeepSeek V4 Flash는 최대 128K 컨텍스트를 처리합니다 — 매우 긴 대화에도 충분합니다.

Q: API가 다운되면 어떻게 되나요? A: 지수 백오프 재시도 로직(최대 3회 시도)이 일시적 실패를 처리합니다. 장기 중단의 경우 캐시된 응답이나 친절한 오류 메시지를 반환하는 서킷 브레이커를 구현하세요.

Q: 파일 업로드, 이미지 생성, 함수 호출을 추가할 수 있나요? A: 네 — DeepSeek V4 Flash는 함수 호출을 지원합니다. 이미지 생성이나 멀티모달 입력의 경우 Google Gemini로 전환하거나 별도의 이미지 모델 엔드포인트를 추가하세요. Global API는 동일한 API 키로 이 모든 것에 접근할 수 있게 해줍니다.


다음 단계는?

  • 인증 추가: Clerk, NextAuth, 또는 Supabase Auth로 여러 사용자 지원.
  • 대화 영구 저장: PostgreSQL, Supabase, 또는 PlanetScale에 메시지 저장.
  • 모델 전환 추가: 사용자가 DeepSeek V4 Flash, R1, Qwen 등 중에서 선택할 수 있도록.
  • 분석: PostHog 또는 Mixpanel로 사용량, 비용, 사용자 만족도 추적.
  • 모바일: PWA 또는 React Native 앱으로 래핑.

100 무료 크레딧 받기 — 지금 바로 구축 시작하기 →

GitHub 전체 소스 코드: 이 튜토리얼의 완전한 프로젝트입니다. Vercel에 드롭하면 2분 만에 라이브됩니다.

마지막 업데이트: 2026년 5월 20일. Global API, OpenAI SDK v1.x, Next.js 14와 호환됩니다.

Article Series

Part of DeepSeek API Complete Guide

Everything you need to build with the DeepSeek API — models, pricing, code examples, and best practices.

  1. 📖DeepSeek API Complete Guide← Start here
  2. 01DeepSeek API Complete Beginner's Guide 2026: From Zero to Production
  3. 02DeepSeek V4 Flash Complete Review: Benchmarks, Code Examples & Implementation Tips
  4. 03deepseek-v4-flash-review
  5. 04DeepSeek API Pricing Guide 2026: Complete Cost Breakdown & Savings Calculator
  6. 05How to Use DeepSeek API with Python: Complete Guide (2026)
  7. 06deepseek-api-javascript-tutorial
  8. 07deepseek-coder-api-guide-2026
  9. 08deepseek-vs-openai-comparison
  10. 09deepseek-vs-qwen-vs-kimi-vs-glm-2026
  11. 10How to Migrate from OpenAI to DeepSeek in 10 Minutes (Complete Guide)
  12. 11OpenAI API Alternative 2026: Top 10 Cheapest Options (Tested & Ranked)
  13. 12build-ai-chat-app-deepseek-apiYou are here
  14. 13ai-api-latency-comparison-2026

Related Articles

How to Build AI Agents with DeepSeek API: A Practical GuideDeepSeek API Complete Beginner's Guide 2026: From Zero to ProductionDeepSeek API Pricing Guide 2026: Complete Cost Breakdown & Savings Calculator

Start Building with Global API

100 free credits on signup. 180+ AI models, one API key. PayPal accepted.

View Pricing →

© 2026 Global API. All rights reserved.