본문 바로가기

프로그래밍

Node.js에서 ChatGPT API 연동 A→Z (2025 최신, Responses API 기준)

반응형

Node.js에서 ChatGPT API 연동 A→Z (2025 최신, Responses API 기준)

chat gpt Api와 NodeJS연동하기

목표: 실제 서비스에 바로 넣을 수 있는 프로덕션 품질 Node.js 연동 템플릿과 설계 원칙, 스트리밍, 함수(툴) 호출, Structured Outputs(JSON Schema 강제), 임베딩, 레이트리밋 대응까지 한 번에 정리.

 


0) 전제 & 개요

  • 권장 런타임: Node.js 20 LTS 이상. OpenAI 공식 JS SDK는 Node 20+를 명시적으로 지원한다.
  • 공식 SDK: openai (ESM). Responses API가 주요 진입점이다.
  • 절대 금지: 브라우저에서 직접 API 키를 노출하지 말 것(서버 프록시 필수).

1) 빠른 시작 (설치 → 키 발급 → 첫 호출)

1-1. 프로젝트 초기화 & 패키지 설치

npm init -y
npm pkg set type=module
npm i openai express zod dotenv pino
npm i -D typescript tsx @types/node
npx tsc --init

 

 

1-2. API 키 발급 & 환경변수

# .env
OPENAI_API_KEY=sk-...

 

 

1-3. 최소 호출 (Responses API)

// src/lib/openai.ts
import OpenAI from "openai";
import "dotenv/config";

export const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});
// src/examples/minimal.ts
import { openai } from "../lib/openai";

const res = await openai.responses.create({
  model: "gpt-4o-mini",
  input: "Node.js에서 OpenAI API를 쓰는 방법을 한 줄로 설명해줘.",
});

console.log(res.output_text);

 


2) 구조적 설계(서비스/컨트롤러 분리)

src/
  config/
    env.ts
  lib/
    openai.ts
  services/
    llm.service.ts
  routes/
    chat.route.ts
    sse.route.ts
  server.ts
// src/config/env.ts
import "dotenv/config";

export const ENV = {
  OPENAI_API_KEY: process.env.OPENAI_API_KEY ?? "",
  PORT: Number(process.env.PORT ?? 3000),
};
// src/services/llm.service.ts
import { openai } from "../lib/openai";

export async function generateText(input: string, system?: string) {
  const res = await openai.responses.create({
    model: "gpt-4o-mini",
    instructions: system ?? "You are a concise assistant.",
    input,
  });
  return res.output_text;
}
// src/routes/chat.route.ts
import { Router } from "express";
import { generateText } from "../services/llm.service";

export const chatRouter = Router();

chatRouter.post("/chat", async (req, res) => {
  const { message, system } = req.body as { message: string; system?: string };
  const text = await generateText(message, system);
  res.json({ text });
});
// src/server.ts
import express from "express";
import { ENV } from "./config/env";
import { chatRouter } from "./routes/chat.route";
import pino from "pino";

const app = express();
const logger = pino({ level: "info" });

app.use(express.json());
app.use("/api", chatRouter);

app.listen(ENV.PORT, () => {
  logger.info(`server listening on http://localhost:${ENV.PORT}`);
});

 


3) 스트리밍(SSE)로 “타자치는 느낌” 구현

// src/routes/sse.route.ts
import { Router } from "express";
import { openai } from "../lib/openai";

export const sseRouter = Router();

sseRouter.get("/stream", async (req, res) => {
  const prompt = String(req.query.q ?? "스트리밍 데모 문장 1줄.");
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");

  const stream = await openai.responses.create({
    model: "gpt-4o-mini",
    input: prompt,
    stream: true,
  });

  for await (const event of stream) {
    if ("type" in event && event.type === "response.output_text.delta") {
      res.write(`data: ${event.delta}\n\n`);
    }
    if ("type" in event && event.type === "response.completed") {
      res.write(`event: done\ndata: [DONE]\n\n`);
      break;
    }
  }

  res.end();
});
// src/server.ts (추가)
import { sseRouter } from "./routes/sse.route";
app.use("/api", sseRouter);

 


4) 함수 호출(도구 호출)로 외부 시스템 연동

// src/services/tools.service.ts
import { openai } from "../lib/openai";

const tools = [
  {
    type: "function" as const,
    function: {
      name: "get_weather",
      description: "도시의 현재 날씨를 조회",
      strict: true,
      parameters: {
        type: "object",
        properties: {
          city: { type: "string", description: "도시명 (예: Seoul)" },
          unit: { type: "string", enum: ["c", "f"], default: "c" },
        },
        required: ["city"],
        additionalProperties: false,
      },
    },
  },
];

async function execTool(name: string, args: any) {
  if (name === "get_weather") {
    return { city: args.city, unit: args.unit ?? "c", temp: 28, cond: "Cloudy" };
  }
  throw new Error(`unknown tool: ${name}`);
}

export async function askWithTools(userQuery: string) {
  const first = await openai.responses.create({
    model: "gpt-4o-mini",
    input: userQuery,
    tools,
  });

  const toolCalls = first.output?.[0]?.content
    ?.filter((c: any) => c.type === "tool_call")
    ?.map((c: any) => c.tool_call);

  if (!toolCalls?.length) return first.output_text;

  const toolOutputs = await Promise.all(
    toolCalls.map(async (call: any) => ({
      tool_call_id: call.id,
      output: JSON.stringify(await execTool(call.name, call.arguments)),
    }))
  );

  const second = await openai.responses.create({
    model: "gpt-4o-mini",
    tool_choice: "none",
    input: [
      { role: "user", content: userQuery },
      ...toolOutputs.map((o) => ({
        role: "tool" as const,
        content: [{ type: "output_text" as const, text: o.output }],
        tool_call_id: o.tool_call_id,
      })),
    ],
  });

  return second.output_text;
}

 


5) Structured Outputs: JSON 스키마 준수

// src/examples/structured.ts
import { openai } from "../lib/openai";

const productSchema = {
  name: "ProductCard",
  schema: {
    type: "object",
    properties: {
      title: { type: "string" },
      price_krw: { type: "number" },
      tags: { type: "array", items: { type: "string" } },
      in_stock: { type: "boolean" },
    },
    required: ["title", "price_krw", "tags", "in_stock"],
    additionalProperties: false,
  },
  strict: true,
};

const res = await openai.responses.create({
  model: "gpt-4o-mini",
  input: "가격 3만원대 개발자 굿즈(머그컵) 상품카드 JSON을 만들어줘.",
  response_format: {
    type: "json_schema",
    json_schema: productSchema,
  },
});

console.log(res.output_parsed);

 


6) 임베딩(RAG 준비)

// src/examples/embedding.ts
import { openai } from "../lib/openai";

const embed = await openai.embeddings.create({
  model: "text-embedding-3-small",
  input: "챗봇에 넣을 문서 조각입니다.",
});

console.log(embed.data[0].embedding.length);

 


7) 레이트 리밋·안정성

// src/lib/openai.ts
import OpenAI from "openai";
export const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  maxRetries: 2,
  timeout: 20_000,
  logLevel: "warn",
});

 


8) 실제 서비스용 레이어드 템플릿

// src/services/ai.service.ts
import { openai } from "../lib/openai";

export async function askPlain(userInput: string) {
  const res = await openai.responses.create({
    model: "gpt-4o-mini",
    instructions: "Answer in Korean, keep it short.",
    input: userInput,
  });
  return res.output_text;
}

export async function askStructured(userInput: string) {
  const schema = {
    name: "AnswerWithReasons",
    schema: {
      type: "object",
      properties: {
        answer: { type: "string" },
        reasons: { type: "array", items: { type: "string" } },
        confidence: { type: "number" },
      },
      required: ["answer", "reasons", "confidence"],
      additionalProperties: false,
    },
    strict: true,
  };

  const res = await openai.responses.create({
    model: "gpt-4o-mini",
    input: userInput,
    response_format: { type: "json_schema", json_schema: schema },
  });

  return res.output_parsed as {
    answer: string;
    reasons: string[];
    confidence: number;
  };
}
// src/routes/stream.route.ts
import { Router } from "express";
import { openai } from "../lib/openai";

export const streamRouter = Router();

streamRouter.get("/ask/stream", async (req, res) => {
  const q = String(req.query.q ?? "");
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");

  const stream = await openai.responses.create({
    model: "gpt-4o-mini",
    input: q,
    stream: true,
  });

  for await (const ev of stream) {
    if ("type" in ev && ev.type === "response.output_text.delta") {
      res.write(`data: ${ev.delta}\n\n`);
    }
    if ("type" in ev && ev.type === "response.completed") {
      res.write(`event: done\ndata: [DONE]\n\n`);
      break;
    }
  }

  res.end();
});
// src/server.ts
import express from "express";
import pino from "pino";
import { ENV } from "./config/env";
import { apiRouter } from "./routes/api.route";
import { streamRouter } from "./routes/stream.route";

const app = express();
const logger = pino({ level: "info" });

app.use(express.json());
app.use("/api", apiRouter);
app.use("/api", streamRouter);

app.listen(ENV.PORT, () => {
  logger.info(`http://localhost:${ENV.PORT}`);
});

 


위 코드에 대한 글로된 설명

1) 빠른 시작 – 설치부터 첫 응답까지

Node.js에서 OpenAI API를 사용하려면 먼저 프로젝트를 초기화하고, openai 공식 SDK와 필수 라이브러리를 설치해야 합니다. npm init -y로 기본 설정을 만든 뒤, npm pkg set type=module로 ESM 모듈 방식을 활성화합니다. API 키는 절대 코드에 직접 쓰지 않고 .env 파일에 저장하며, dotenv로 로드해 사용합니다.
Responses API의 장점은 단일 메서드 호출로도 완전한 결과를 얻을 수 있다는 점입니다. openai.responses.create()에 model과 input만 지정하면 바로 응답을 받을 수 있으며, 이를 console.log로 출력하면 첫 결과를 확인할 수 있습니다.
결론: 가장 빠른 방법은 환경설정과 SDK 설치 후 responses.create() 한 줄로 시작하는 것입니다.


2) 구조적 설계 – 서비스와 컨트롤러 분리

규모가 커질수록 API 호출 로직과 HTTP 요청 처리 로직을 분리하는 것이 필수입니다. lib 폴더에 OpenAI 인스턴스를 초기화하고, services 폴더에 API 호출 함수를 정의합니다. routes 폴더에는 Express 라우터를 두어 HTTP 요청을 받고, 필요한 서비스 함수를 호출하도록 구성합니다.
이렇게 하면 테스트하기 쉽고, 나중에 모델 변경이나 기능 확장이 필요할 때 수정 범위가 최소화됩니다. 특히 다수의 모델이나 API를 조합할 경우, 서비스 계층에서 일관성 있게 관리할 수 있습니다.
결론: API 호출과 HTTP 처리를 분리하면 유지보수성과 확장성이 크게 향상됩니다.


3) 스트리밍(SSE) – 실시간 타자 효과 구현

일반 API 호출은 결과를 한 번에 받지만, SSE(Server-Sent Events)를 사용하면 모델이 생성하는 텍스트를 단어 단위로 실시간 전송할 수 있습니다. Responses API에서 stream: true를 주면 SDK가 이벤트 스트림을 async iterator 형태로 제공하며, 이를 for await 루프에서 순차적으로 클라이언트로 전달할 수 있습니다.
이 방식은 채팅 UI에서 타자 치는 듯한 자연스러운 효과를 줄 수 있어 사용자 경험을 크게 향상시킵니다. 서버는 텍스트 조각(delta)을 지속적으로 전송하고, 완료 시점에 연결을 종료하면 됩니다.
결론: 스트리밍은 UX를 한 단계 높여주는 핵심 기능이니 적극 고려해야 합니다.


4) 함수 호출(툴 호출) – 외부 시스템과 안전하게 연동

함수 호출 기능은 모델이 직접 외부 기능을 사용할 수 있도록 하는 안전한 인터페이스입니다. 먼저 함수명, 설명, 매개변수를 JSON Schema로 정의합니다. 모델은 필요할 때 이 함수를 호출하는 형태로 파라미터를 생성하고, 서버는 이를 받아 실제 로직(API 호출, DB 조회 등)을 실행한 뒤 결과를 모델에 다시 전달합니다.
이 과정은 2단계로 진행됩니다. 첫 번째 응답에서 모델이 호출할 함수와 파라미터를 정하고, 두 번째 호출에서 그 결과를 기반으로 최종 답변을 생성합니다.
결론: 함수 호출은 LLM을 외부 서비스와 연결하는 표준화된 안전한 방법입니다.


5) Structured Outputs – 스키마 검증으로 데이터 품질 확보

Structured Outputs 기능을 사용하면 모델이 반드시 지정된 JSON Schema 형태로 응답하도록 강제할 수 있습니다. response_format 옵션에서 type: "json_schema"를 사용하고, strict: true를 설정하면 스키마에 맞지 않는 응답은 거부됩니다.
이를 활용하면 LLM 응답을 후처리 없이 그대로 API나 DB에 반영할 수 있으며, 데이터 파싱 오류를 최소화할 수 있습니다. 특히 전자상거래, 금융, 예약 서비스 등 데이터 구조가 엄격해야 하는 분야에서 강력합니다.
결론: 데이터 신뢰성을 확보하려면 Structured Outputs를 적극 활용하세요.


6) 임베딩 – 검색과 RAG를 위한 기반

임베딩은 텍스트를 고정 길이 벡터로 변환하는 기술입니다. embeddings.create() 메서드에 텍스트를 입력하면 모델이 이를 수백 차원의 벡터로 반환합니다. 이 벡터를 벡터 DB에 저장해두면 코사인 유사도 검색 등으로 유사 문서를 빠르게 찾을 수 있습니다.
RAG(Retrieval-Augmented Generation) 시스템을 만들 때 필수로 사용되며, 지식 검색 챗봇, 추천 시스템, 문서 검색 등에 응용됩니다.
결론: 임베딩은 고품질 검색과 문맥 보강을 위한 핵심 도구입니다.


7) 레이트 리밋·안정성 – 대규모 트래픽 대비

트래픽이 많으면 429(Too Many Requests) 오류가 발생할 수 있습니다. SDK에서 maxRetries와 timeout을 설정해 자동 재시도와 요청 시간 제한을 적용할 수 있습니다. 또한 p-limit이나 Bottleneck 같은 라이브러리로 동시 요청 수를 제어하면 안정성이 높아집니다.
프롬프트 최적화로 토큰 수를 줄이고, 자주 쓰는 응답은 캐싱해 불필요한 호출을 줄이는 것도 비용 절감과 속도 향상에 효과적입니다.
결론: 안정적 운영을 위해 재시도, 큐잉, 캐싱 전략을 반드시 준비해야 합니다.


8) 실제 서비스 템플릿 – 확장 가능한 구조

프로덕션 환경에서는 단순한 호출 예제를 넘어선 구조화가 필요합니다. 서비스 계층에서는 텍스트 응답, 구조화 응답, 툴 호출, 스트리밍 기능을 각각 함수로 분리합니다. API 라우트는 이 서비스 함수를 호출하는 역할만 하도록 구성해, 로직 중복을 없애고 유지보수를 쉽게 합니다.
이 방식은 모델 교체, 파라미터 변경, 기능 추가 시 수정 범위를 최소화해 개발 효율을 높입니다.
결론: 기능별로 모듈화된 서비스-컨트롤러 구조가 장기적으로 가장 효율적입니다.

반응형