LangChain.js 완벽 가이드 — LLM 애플리케이션 개발·RAG·Next.js

LangChain.js 완벽 가이드 — LLM 애플리케이션 개발·RAG·Next.js

이 글의 핵심

LangChain.js는 TypeScript/JavaScript 생태계에서 LLM 호출, 프롬프트 구성, 검색 증강 생성(RAG), 벡터 스토어 연동, 스트리밍까지 한 흐름으로 묶어 주는 프레임워크입니다. 이 글에서는 핵심 개념(LCEL·Runnable)부터 Pinecone·Supabase 연동, Next.js App Router와의 결합, 실전 챗봇 패턴까지 단계적으로 정리합니다.

이 글의 핵심

LangChain.js는 JavaScript/TypeScript 환경에서 대규모 언어 모델(LLM) 애플리케이션을 구조화하기 위한 오픈소스 프레임워크입니다. 단순히 OpenAI 등의 SDK를 호출하는 것을 넘어, 프롬프트 템플릿, 체인(Chain), 검색기(Retriever), 벡터 스토어, 에이전트 등을 조합해 재사용 가능한 파이프라인으로 만드는 데 초점이 있습니다.

이 글에서는 다음을 다룹니다.

  • LangChain.js의 핵심 개념(모듈 구조, LCEL, Runnable)
  • 프롬프트 템플릿체인 구성 패턴
  • Pinecone, Supabase(pgvector) 와의 Vector Store 연동
  • RAG(Retrieval-Augmented Generation) 구현 흐름
  • 스트리밍토큰·컨텍스트 제한 대응
  • Next.js(App Router) 와의 통합 및 실전 챗봇 골격

전제: Node.js 18+ 환경, TypeScript 사용, OpenAI(또는 호환 API) 키, Pinecone·Supabase는 해당 서비스 계정이 있다고 가정합니다. 패키지 버전은 릴리스마다 import 경로가 바뀔 수 있으므로, 공식 문서의 JS 통합 가이드와 함께 확인하는 것이 좋습니다.


1. LangChain.js란 무엇인가

1.1 Python 버전과의 관계

LangChain은 원래 Python 생태계에서 널리 쓰이며, LangChain.js는 이를 JavaScript/TypeScript로 이식·확장한 구현입니다. 체인 구성, 프롬프트 관리, 문서 로더·분할기, 벡터 스토어 같은 개념은 대응하지만, 패키지 이름과 세부 API는 차이가 있으므로 Python 문서의 코드를 그대로 복사하면 동작하지 않는 경우가 많습니다. 새 프로젝트는 반드시 JS 전용 문서를 기준으로 합니다.

1.2 패키지 구조(개략)

실무에서는 보통 다음과 같이 역할이 나뉩니다.

영역대표 패키지역할
코어@langchain/coreRunnable, 메시지, 프롬프트, 출력 파서 등 추상화
통합@langchain/openai, @langchain/anthropic각 LLM·임베딩 제공자
커뮤니티@langchain/community다양한 로더·벡터 스토어·도구
상위 체인langchain검색 체인, 에이전트 등 조합 레이어
전용@langchain/pinecone특정 벡터 DB와의 얇은 어댑터

package.json에는 같은 메이저 계열로 맞추는 것이 안전합니다. 예: @langchain/corelangchain의 버전 호환은 릴리스 노트를 확인합니다.


2. 핵심 개념: Runnable과 LCEL

2.1 Runnable

LangChain.js의 실행 단위는 Runnable 입니다. LLM, 프롬프트, 파서, 검색기는 모두 Runnable로 취급되며, .pipe() 로 연결합니다. 이 패턴을 LCEL(LangChain Expression Language) 이라고 부릅니다.

장점은 다음과 같습니다.

  • 합성: 작은 블록을 조립해 복잡한 파이프라인을 만듦
  • 스트리밍: 동일한 체인에 대해 stream, streamEvents 등으로 부분 출력 전달
  • 배치·비동기: invoke, batch, invoke의 비동기 변형 등 일관된 인터페이스

즉, “한 번의 함수 호출”이 아니라 데이터가 왼쪽에서 오른쪽으로 흐르는 파이프로 사고하는 것이 LangChain.js 스타일입니다.

2.2 메시지와 채팅 모델

챗봇에서는 HumanMessage, SystemMessage, AIMessage채팅 메시지를 사용합니다. ChatOpenAI는 이러한 메시지 배열을 받아 응답 메시지를 반환합니다. 단순 문자열 입출력이 아니라 역할이 구분된 대화를 모델에 전달할 수 있어, 시스템 프롬프트·안전 지침을 안정적으로 넣을 수 있습니다.


3. 설치와 기본 설정

프로젝트 루트에서 의존성을 추가합니다. 사용하는 벡터 DB에 맞춰 선택적으로 설치합니다.

npm install langchain @langchain/core @langchain/openai @langchain/community @langchain/textsplitters
# Pinecone 사용 시
npm install @langchain/pinecone @pinecone-database/pinecone
# Supabase 벡터 사용 시
npm install @supabase/supabase-js

환경 변수는 .env에 두고, Node에서는 process.env로 읽습니다. Next.js에서는 NEXT_PUBLIC_ 접두사가 없는 변수만 서버(라우트 핸들러·서버 컴포넌트)에서 안전하게 쓸 수 있습니다. API 키는 클라이언트 번들에 넣지 마십시오.


4. 프롬프트 템플릿과 체인

4.1 ChatPromptTemplate

변수 자리를 둔 프롬프트는 ChatPromptTemplate 으로 정의합니다. 나중에 formatMessages 또는 체인에 넘길 입력 객체로 값이 채워집니다.

import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0.2,
});

const prompt = ChatPromptTemplate.fromMessages([
  [
    "system",
    "당신은 {domain} 분야 전문가입니다. 간결한 한국어로 답하세요.",
  ],
  ["human", "{question}"],
]);

// LCEL: prompt → model → 문자열 파서
const chain = prompt.pipe(model).pipe(new StringOutputParser());

const answer = await chain.invoke({
  domain: "클라우드 인프라",
  question: "LangChain.js에서 LCEL이 의미하는 바를 한 문장으로 설명해 주세요.",
});

위 코드에서 pipe 는 앞 단계의 출력 형식이 뒤 단계의 입력과 맞아야 합니다. ChatPromptTemplate은 메시지 목록을 만들고, ChatOpenAI는 메시지를 받아 AIMessage를 반환하며, StringOutputParser는 그중 텍스트만 뽑아 문자열로 넘깁니다. 이렇게 단계별 책임이 분리되면, 나중에 검색 단계만 끼워 넣어 RAG로 확장하기 쉽습니다.

4.2 체인이라는 말의 의미

문서에서는 “체인”이 순차 Runnable의 조합을 가리키는 경우가 많습니다. 예를 들어 “질문 분류 → 검색 → 답변 생성”은 세 개의 Runnable을 파이프로 이은 체인입니다. 별도의 LLMChain 클래스에 의존하기보다, LCEL 파이프가 최신 권장 패턴입니다.


5. 문서 분할과 임베딩

RAG에 앞서 원본 텍스트를 적절한 크기의 청크로 나누고, 각 청크를 임베딩 벡터로 변환해 벡터 스토어에 넣습니다. 청크가 너무 크면 검색 정밀도가 떨어지고, 너무 작으면 문맥이 잘려 답변 품질이 나빠질 수 있습니다.

import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { Document } from "@langchain/core/documents";
import { OpenAIEmbeddings } from "@langchain/openai";

const raw = "… 긴 문서 문자열 …";

const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000,
  chunkOverlap: 150,
});

const splits = await splitter.splitText(raw);
const docs = splits.map(
  (text, i) =>
    new Document({
      pageContent: text,
      metadata: { chunkIndex: i },
    })
);

const embeddings = new OpenAIEmbeddings({
  model: "text-embedding-3-small",
});

임베딩 모델은 검색 단계와 반드시 동일해야 합니다. 나중에 모델을 바꾸면 기존 인덱스는 호환되지 않으므로 재임베딩·재적재가 필요합니다.


6. Vector Store: Pinecone

Pinecone은 호스팅형 벡터 데이터베이스로, 인덱스·네임스페이스·메타데이터 필터링 등을 API로 다룹니다. LangChain.js에서는 PineconeStore 로 연동합니다.

사전 준비: Pinecone 콘솔에서 인덱스를 만들고, 차원 수가 사용 중인 임베딩 모델의 차원과 일치하는지 확인합니다.

import { Pinecone as PineconeClient } from "@pinecone-database/pinecone";
import { PineconeStore } from "@langchain/pinecone";
import { OpenAIEmbeddings } from "@langchain/openai";
import type { Document } from "@langchain/core/documents";

const pinecone = new PineconeClient({
  apiKey: process.env.PINECONE_API_KEY!,
});

async function buildPineconeStore(docs: Document[]) {
  const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" });

  const index = pinecone.Index("your-index-name");

  return await PineconeStore.fromDocuments(docs, embeddings, {
    pineconeIndex: index,
    // namespace: "optional-ns",
  });
}

인덱싱이 끝난 뒤에는 asRetriever 로 검색기를 만들어 체인에 연결합니다. k 값은 가져올 문서 개수로, 너무 크면 프롬프트가 길어져 토큰 한도에 걸리기 쉽습니다.


7. Vector Store: Supabase(pgvector)

Supabase는 Postgres 위에 pgvector 확장을 제공합니다. 애플리케이션 데이터와 같은 DB에 벡터를 두고 싶을 때 유리합니다. LangChain.js에서는 SupabaseVectorStore 를 사용합니다.

사전 준비: Supabase SQL 에디터에서 vector 확장과 테이블·함수를 문서대로 생성합니다. 임베딩 차원 컬럼(vector(1536) 등)은 모델에 맞춥니다.

import { createClient } from "@supabase/supabase-js";
import { OpenAIEmbeddings } from "@langchain/openai";
import { SupabaseVectorStore } from "@langchain/community/vectorstores/supabase";
import type { Document } from "@langchain/core/documents";

export async function buildSupabaseStore(docs: Document[]) {
  const client = createClient(
    process.env.SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY! // 서버 전용
  );

  const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" });

  return await SupabaseVectorStore.fromDocuments(docs, embeddings, {
    client,
    tableName: "documents",
    queryName: "match_documents",
  });
}

보안: SERVICE_ROLE 키는 RLS를 우회하므로 클라이언트에 노출하면 안 됩니다. Next.js에서는 API Route나 Server Action 등 서버 측에서만 사용합니다. 공개 앱이라면 Supabase RLS와 anon 키 기반의 제한된 쿼리 설계를 검토합니다.


8. RAG 패턴 구현

RAG는 검색(Retrieval)생성(Generation) 을 결합합니다. 흐름은 다음과 같습니다.

  1. 사용자 질문을 임베딩하거나, 질문 텍스트로 유사 문서를 검색
  2. 검색된 Document 목록을 프롬프트에 주입
  3. LLM이 주어진 문맥만 근거로 답변(필요 시 인용·불확실성 표시)

버전별로 createRetrievalChain 같은 헬퍼 위치가 달라질 수 있으므로, 여기서는 LCEL만으로 2단계 RAG를 구성합니다. RunnablePassthrough.assign으로 검색 결과를 context에 넣고, 같은 입력의 question을 프롬프트로 넘깁니다.

import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { RunnablePassthrough, RunnableSequence } from "@langchain/core/runnables";
import type { BaseRetriever } from "@langchain/core/retrievers";

function buildRagChain(retriever: BaseRetriever) {
  const llm = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 });

  const prompt = ChatPromptTemplate.fromMessages([
    [
      "system",
      "다음 컨텍스트만을 근거로 답하세요. 컨텍스트에 없으면 모른다고 말하세요.\n\n{context}",
    ],
    ["human", "{question}"],
  ]);

  const retrievalChain = RunnableSequence.from([
    RunnablePassthrough.assign({
      context: async (input: { question: string }) => {
        const docs = await retriever.invoke(input.question);
        return docs.map((d) => d.pageContent).join("\n\n---\n\n");
      },
    }),
    prompt,
    llm,
    new StringOutputParser(),
  ]);

  return retrievalChain;
}

// 사용 예: const rag = buildRagChain(vectorStore.asRetriever({ k: 4 }));
// await rag.invoke({ question: "사용자 질문" });

설계 포인트:

  • 컨텍스트만 답하게 시스템 지시를 명확히 하면 환각이 줄어듭니다.
  • k 와 청크 크기를 동시에 튜닝합니다.
  • 메타데이터 필터(제품 ID, 사용자 ID)가 필요하면 Retriever 단에서 필터를 지원하는지 확인합니다.

9. 스트리밍과 토큰 제한

9.1 스트리밍

챗봇 UX에서는 토큰이 생성되는 대로 화면에 붙이는 것이 일반적입니다. Runnable의 stream 을 사용하면 청크 단위로 스트림을 받을 수 있습니다.

import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  streaming: true,
});

const stream = await model.stream([
  { role: "user", content: "스트리밍 테스트로 짧은 시 한 수를 써 주세요." },
]);

for await (const chunk of stream) {
  process.stdout.write(chunk.content?.toString() ?? "");
}

Next.js에서는 이 이터레이터를 ReadableStream 으로 감싸 Response에 넣거나, Vercel AI SDK 등과 어댑터로 연결합니다. 중요한 것은 SSE/청크 인코딩 규약을 프론트엔드와 맞추는 것입니다.

9.2 토큰·컨텍스트 한도

모델마다 컨텍스트 길이출력 토큰 상한이 있습니다. ChatOpenAI 생성 시 maxTokens(또는 모델별 옵션)으로 출력 길이를 제한할 수 있습니다.

실무 팁:

  • RAG에서는 검색 문서 총합 + 질문 + 지시문이 컨텍스트 한도를 넘지 않게 합니다.
  • 긴 회화 이력은 요약 메모리 또는 최근 N턴만 유지로 잘라 냅니다.
  • 비용·지연을 줄이려면 작은 모델로 검색 쿼리 재작성, 큰 모델로 최종 답변만 생성하는 계층형 구성을 고려합니다.

10. Next.js(App Router)와 통합

10.1 서버에서만 LangChain 실행

LangChain과 API 키는 서버에서 실행하는 것이 원칙입니다. App Router에서는 app/api/.../route.tsPOST 핸들러에서 체인을 호출합니다.

// app/api/chat/route.ts (개념 예시)
import { NextResponse } from "next/server";
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage } from "@langchain/core/messages";

export const runtime = "nodejs"; // 일부 네이티브 의존성은 edge 대신 node 권장

export async function POST(req: Request) {
  const { message } = await req.json();

  const model = new ChatOpenAI({ model: "gpt-4o-mini" });
  const res = await model.invoke([new HumanMessage(message)]);

  return NextResponse.json({ reply: res.content });
}

10.2 스트리밍 응답

스트리밍 시에는 NextResponseReadableStream 을 넘기거나, 표준 Response를 반환합니다. 클라이언트는 fetch + ReadableStream 또는 이벤트 소스 패턴으로 읽습니다. CORS·버퍼링은 배포 환경(Vercel, Cloudflare 등)에 따라 다르므로 프로덕션에서 반드시 검증합니다.


11. 실전 챗봇: 구조 잡기

최소 실전 구성은 다음과 같습니다.

  1. API 레이어: 질문 수신 → (선택) RAG 검색 → LLM 호출 → (스트리밍) 응답
  2. 상태: 세션 ID별로 대화 이력 저장(서버 메모리, Redis, DB)
  3. 가드레일: 입력 길이 제한, 금지 주제, PII 마스킹

대화 이력을 LangChain에 넣을 때는 ChatPromptTemplateMessagesPlaceholder로 히스토리를 끼우거나, RunnableWithMessageHistory 패턴(버전별 제공 여부 확인)을 사용합니다. 장기 세션에서는 요약 + 최근 메시지 조합이 비용 대비 효과가 좋습니다.

import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, AIMessage } from "@langchain/core/messages";

const chatPrompt = ChatPromptTemplate.fromMessages([
  ["system", "친절한 고객 지원 봇입니다. 정책 위반 요청은 거절합니다."],
  new MessagesPlaceholder("history"),
  ["human", "{input}"],
]);

const model = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0.3 });

const chain = chatPrompt.pipe(model);

const history = [
  new HumanMessage("환불 기간이 어떻게 되나요?"),
  new AIMessage("구매일로부터 7일 이내 미사용 시 환불 가능합니다."),
];

const reply = await chain.invoke({
  history,
  input: "그럼 부분 환불은요?",
});

운영 단계에서는 LangSmith 등으로 트레이스를 남겨 프롬프트·검색 품질을 개선합니다.


12. 모범 사례와 흔한 실수

  • 버전 고정: package-lock.json으로 LangChain 패키지 버전을 묶고, 메이저 업 시 마이그레이션 가이드를 읽습니다.
  • 키 관리: 저장소에 키를 커밋하지 않고, CI/CD 시크릿을 사용합니다.
  • 임베딩 일치: Pinecone/Supabase에 넣을 때와 쿼리할 때 동일 임베딩 모델을 씁니다.
  • Edge 제약: 일부 벡터 클라이언트는 Node 전용입니다. Edge Runtime을 쓸 경우 의존성 호환을 확인합니다.
  • 테스트: 체인은 입력·출력 스키마를 정해 스냅샷 테스트하거나, 최소한 회귀용 프롬프트 세트를 둡니다.

13. 정리

LangChain.js는 TypeScript 친화적인 LLM 앱 구조를 제공하며, LCEL로 파이프라인을 명확히 나눌 수 있습니다. 프롬프트 템플릿과 체인으로 기본 QA를 만든 뒤, Pinecone 또는 Supabase에 벡터를 두고 RAG를 얹고, 스트리밍과 토큰 제한으로 운영 품질을 맞추면 Next.js와 결합한 실전 챗봇까지 한 경로로 확장할 수 있습니다.

다음 단계로는 도구 호출(Tool Calling), 에이전트, 구조화된 출력(JSON) , 평가(평가용 데이터셋) 를 공식 문서 순서대로 깊게 파는 것을 권합니다.


참고 자료