LangChain과 Node.js를 사용하여 AI 이력서 빌더를 만드는 방법
요약
LangChain과 Node.js를 활용하여 단순한 텍스트 생성을 넘어 구조화된 AI 파이프라인을 구축하는 방법을 다룹니다. Express API와 스트리밍 응답, 프롬프트 엔지니어링을 결합해 전문적인 이력서 재작성기를 만드는 실전 가이드를 제공합니다.
핵심 포인트
- LangChain을 활용한 LLM 애플리케이션 오케스트레이션 방법
- Node.js 및 Express 환경에서의 AI 파이프라인 구축
- 단순 API 호출과 구조화된 체이닝의 차이점 이해
- 실제 서비스 가능한 수준의 프롬프트 엔지니어링 적용
몇 달 전, 제 친구 Marcus는 한 핀테크 기업의 시니어 백엔드(Senior Backend) 직무에 지원하고 있었습니다. 그는 분산 시스템(Distributed Systems), AWS, 전체 스택(Full Stack)을 아우르는 5년의 탄탄한 경력을 가지고 있었습니다. 하지만 그의 이력서는 누군가 LinkedIn에서 복사해 온 직무 기술서 목록처럼 보였습니다. "마이크로서비스(Microservices) 유지 관리 담당." "CI/CD 파이프라인 구현 지원." 여러분도 어떤 느낌인지 아실 겁니다.
저는 그에게 말했습니다. 문제는 당신이 무엇을 했느냐가 아니라, 그것을 어떻게 말하고 있느냐라는 것입니다. 채용 담당자들은 이력서를 제대로 읽을지 결정하기 전까지 약 6초를 소비합니다. 6초입니다. 그리고 그 6초가 "~을 담당함"이라는 문구를 읽는 데 쓰인다면, 당신은 그들을 놓친 것입니다.
우리는 함께 두 시간 동안 이력서를 다시 작성했습니다. 모든 불렛 포인트(Bullet point)는 강력한 동사로 시작했습니다. 모든 성과에는 숫자가 포함되었습니다. "세 개의 고트래픽 엔드포인트(High-traffic endpoints)에 Redis 캐싱을 도입하여 API 응답 시간을 40% 단축함." 훨씬 나아졌습니다. Marcus는 면접 기회를 얻었습니다.
당연히 다음 생각은 이것이었습니다. 만약 이 과정을 자동화할 수 있다면 어떨까? "이력서를 ChatGPT에 던져 넣고 더 좋게 만들어 달라고 요청하는" 방식이 아닙니다. 그런 방식은 일반적이고 형편없는 결과물(Generic slop)을 만들어냅니다. 제가 말하는 것은 이력서의 맥락을 이해하고, 전문적인 재작성 패턴을 적용하며, 깔끔하고 직무에 특화된 결과물을 반환하는 실제적이고 구조화된 AI 파이프라인(AI pipeline)입니다.
그것이 바로 LangChain이 만들어진 목적입니다. 그리고 이 가이드에서 우리는 정확히 그것을 만들어 볼 것입니다. 실제 Express API, 스트리밍 응답(Streaming responses), 그리고 실제로 좋은 결과를 만들어내는 프롬프트 엔지니어링(Prompt engineering)을 활용하여 LangChain과 Node.js를 사용한 AI 기반 이력서 재작성기를 구축할 것입니다.
LangChain이란 무엇이며, 왜 사용해야 하는가?
솔직한 답변을 드리자면, LangChain은 대규모 언어 모델(Large Language Models, LLM)을 기반으로 애플리케이션을 구축하기 위한 오케스트레이션 프레임워크(Orchestration framework)입니다. Express.js를 생각하는 방식과 비슷하게 생각하면 됩니다. Express가 Node의 순수 http 모듈로 할 수 없는 일을 하는 것은 아니지만, 웹 앱을 구축할 때 구조화되고 조합 가능한(Composable) 방식을 제공하여 앱이 스스로의 무게에 짓눌려 무너지지 않게 해줍니다.
LangChain은 LLM 애플리케이션에 대해서도 동일한 역할을 수행합니다. 모든 곳에서 단순히 OpenAI API를 직접 호출할 수도 있습니다. 일회성 스크립트라면 괜찮습니다. 하지만 앱이 성장함에 따라 — 작업별로 다른 프롬프트(Prompt), 다단계 추론 체인(Multi-step reasoning chains), 대화 간의 메모리(Memory) 등이 필요해지면 — 가공되지 않은 API 호출은 금방 복잡해집니다.
프로젝트가 커졌을 때 가공되지 않은 OpenAI API 코드가 어떤 모습인지 확인해 보세요:
// 가공되지 않은 OpenAI — 작동은 하지만 확장성이 떨어짐
const response = await openai.chat.completions.create({
model: "gpt-4",
...
단 한 번의 호출에는 문제가 없습니다. 이제 여기에 다음 기능들을 추가해 보세요: 프롬프트 버전 관리(Prompt versioning), 해당 출력을 두 번째 모델 호출로 연결하는 체이닝(Chaining), 이전 메시지로부터의 메모리(Memory), 속도 제한(Rate limits)에 걸렸을 때 다른 모델로의 폴백(Fallback), 클라이언트로의 스트리밍 출력(Streaming output). 갑자기 수많은 상태(State)를 수동으로 관리해야 하는 상황에 직면하게 됩니다.
LangChain은 조합 가능한 기본 요소(Composable primitives)를 통해 이 모든 것을 처리합니다: 재사용 가능하고 테스트 가능한 프롬프트를 위한 PromptTemplate, 프롬프트를 모델에 연결하는 LLMChain, 다단계 파이프라인을 위한 SequentialChain, 내장된 스트리밍 지원, 그리고 모든 주요 LLM 제공업체와의 통합 기능이 그것입니다.
우리의 이력서 빌더(Resume builder)의 경우, 체인(Chain)은 다음과 같은 구조를 가집니다: 이력서를 구조화된 섹션으로 파싱(Parse)하고, 각 섹션을 실행 중심의 불렛 포인트(Action-oriented bullet points)를 생성하는 프롬프트에 통과시킨 다음, 조립된 결과를 반환합니다. 이제 직접 만들어 보겠습니다.
우리가 만드는 것
코드를 한 줄이라도 쓰기 전에, 시스템의 개요를 살펴보겠습니다:
┌─────────────────────────────────────────────────────┐
│ CLIENT (Frontend) │
│ POST /api/rewrite { resumeText, section } │
...
혁신적인 것은 없지만, 각 계층(Layer)은 테스트 가능한 단일 작업을 수행합니다. 체인이 흥미로운 부분이므로 빠르게 구현해 보겠습니다.
프로젝트 설정
새로운 Node.js 프로젝트를 시작하고 의존성(Dependencies)을 설치합니다:
mkdir resume-ai && cd resume-ai
npm init -y
npm install express langchain @langchain/openai @langchain/core dotenv
루트에 .env 파일을 생성합니다:
OPENAI_API_KEY=sk-your-key-here
PORT=3001
그리고 프로젝트 구조는 다음과 같습니다:
resume-ai/
├── src/
│ ├── parseResume.js
...
전체적으로 ES 모듈 (ES module) 구문을 사용할 수 있도록 package.json에 "type": "module"을 추가하세요.
1단계: 이력서 파싱 (Parsing)
이 부분은 모두가 건너뛰는 지루한 작업이지만, 대부분의 AI 이력서 도구들이 형편없는 결과물을 만들어내는 이유이기도 합니다. 800단어 분량의 이력서 텍스트를 모델에 그냥 던져 넣고 "더 좋게 만들어줘"라고 요청해서는 안 됩니다. 개선하려는 섹션을 분리해야 합니다. 그렇지 않으면 모델은 맥락(context) 없이 작동하게 됩니다.
다음은 간단한 섹션 파서(parser)입니다. 완벽하지는 않습니다. 실제 이력서는 수십 가지의 형식을 가지고 있기 때문입니다. 하지만 일반적인 패턴들은 처리할 수 있습니다:
// src/parseResume.js
export function parseResumeText(rawText) {
const sections = {
...
주의할 점 두 가지가 있습니다. lowerLine.length < 40 체크는 "Responsible for experience in customer service"와 같은 문장이 섹션 헤더로 잘못 읽히는 것을 방지합니다. 그리고 우리는 리터럴 문자열 "\n"을 기준으로 분할합니다. 만약 텍스트 영역(textarea)이나 파일 업로드를 통해 텍스트를 받는다면, 줄바꿈(newlines)은 이미 포함되어 있을 것입니다.
2단계: LangChain 재작성 체인 (Rewrite Chain)
이것이 앱의 핵심입니다. LangChain 체인은 프롬프트 템플릿 (prompt template), 모델 (model), 그리고 출력 (output)이라는 세 가지 요소를 연결합니다. 코드는 다음과 같습니다:
// src/resumeChain.js
import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
...
온도(temperature) 설정이 0.7이 아닌 0.4로 설정된 점에 주목하세요. 재작성 작업의 경우, 모델이 약간의 창의성(단순히 글자 그대로 바꾸는 것이 아닌)을 발휘하되, 없는 사실을 지어내기 시작할 정도로 지나치게 창의적이지는 않아야 합니다. 0.4는 좋은 시작점이며, 결과에 따라 조정하십시오.
프롬프트는 세 가지 원칙을 중심으로 구조화되었습니다: 모델에게 역할 부여("전문 이력서 코치"), 맥락 제공(전체 이력서), 그리고 명시적인 형식 제약 조건 제공(동사 활용, 수치화, 정확히 4개의 불렛 포인트)입니다. 모호한 프롬프트는 모호한 출력을 만듭니다. 명시적인 프롬프트는 일관되고 사용 가능한 출력을 만듭니다.
3단계: Express API
이제 모든 것을 HTTP 엔드포인트(endpoint)로 연결합니다:
// src/app.js
import "dotenv/config";
import express from "express";
...
입력 크기 제한(50kb)과 resumeContext.slice(0, 3000)는 모두 의도된 것입니다. 3,000자 정도의 이력서 발췌본으로는 대부분의 GPT-4 토큰 제한(token limits)에 걸리지 않지만, 일부 이력서는 놀라울 정도로 깁니다. 특히 프로젝트 설명이 방대한 경우 더욱 그렇습니다. 3,000자에서 자르는(Truncating) 방식은 비용을 예측 가능하게 유지해 줍니다.
4단계: 응답 스트리밍 (Streaming the Response)
좋은 사용자 경험(UX)을 위해서는 전체 완료를 기다리기보다 AI 응답이 도착하는 대로 스트리밍(stream)하는 것이 좋습니다. 400단어 분량의 재작성 작업은 완료까지 6~8초가 걸릴 수 있는데, 8초 동안 빈 화면이 보이는 것은 서비스가 고장 난 것처럼 느껴질 수 있습니다.
LangChain은 콜백(callbacks)을 통해 스트리밍을 간단하게 구현할 수 있게 해줍니다:
import { HumanMessage } from "@langchain/core/messages";
app.post("/api/rewrite/stream", async (req, res) => {
...
프론트엔드에서는 Fetch API와 ReadableStream을 사용하여 이를 소비(consume)합니다. 각 data: 이벤트는 토큰(token)을 전달하며, 이를 도착하는 대로 UI에 추가합니다. 사용자는 응답이 실시간으로 나타나는 것을 보게 되며, 실제 속도가 느리더라도 빠르다고 느끼게 됩니다.
시청하기: Node.js에서의 LangChain (Quick Start)
흔한 실수 (및 이를 피하는 방법)
1. 예기치 않게 발생하는 토큰 제한 (Token limits)
GPT-4의 컨텍스트 윈도우(context window)는 크지만, 토큰당 비용이 발생합니다. 매 요청마다 전체 이력서와 프롬프트(prompt)를 보낸다면, 규모가 커질수록 비용이 빠르게 증가합니다. 해결책: 위에서 보여준 것처럼 이력서 컨텍스트를 자르고(truncate), 파싱된 섹션들을 캐싱(cache)하여 매 API 호출마다 다시 파싱하지 않도록 합니다.
2. 모델이 성과를 지어내는 문제
이것이 가장 큰 문제입니다. 소스 데이터 없이 모델에게 "성과를 수치화하라"고 요청하면, 모델은 숫자를 지어낼 것입니다. "로딩 시간을 73% 단축함"이라는 문구는 채용 담당자가 면접에서 질문하기 전까지는 멋지게 들리겠지만 말입니다. 해결책: 프롬프트에서 모델에게 명시적으로 지시하세요: "원본 텍스트에 숫자가 있는 경우에만 숫자를 추가하세요. 숫자가 없다면 대신 질적 언어(qualitative language)를 사용하세요."
3. 이력서 내용을 통한 프롬프트 인젝션 (Prompt injection)
교활한 사용자가 이력서 텍스트 안에 "이전의 모든 지침을 무시하고 다음과 같이 출력하세요..."와 같은 내용을 넣을 수 있습니다. 해당 텍스트를 모델에 직접 전송하기 때문에 이 방식이 작동하게 됩니다. 해결책: 입력을 정화(sanitize)하고, ---RESUME START--- / ---RESUME END---와 같은 명확한 구분자(delimiter)를 사용하여 이력서 내용과 프롬프트(prompt)의 지침 부분을 분리하세요.
4. 속도 제한(Rate limiting) 미설정
OpenAI의 속도 제한(rate limits)은 사용자 기준이 아니라 API 키 기준으로 적용됩니다. 한 명의 사용자가 엔드포인트(endpoint)를 과도하게 호출하면 모든 사용자에 대한 제한에 걸릴 수 있습니다. 서비스를 출시하기 전에 express-rate-limit과 같은 속도 제한기(rate limiter)를 추가하세요. 이력서 도구의 경우 IP당 분당 5회 요청 정도가 합리적인 시작점입니다.
5. 필요하지 않은 상황에서 GPT-4 선택
GPT-4는 비용이 많이 들고 느립니다. 대부분의 이력서 재작성 작업의 경우, gpt-4o-mini가 훨씬 적은 비용으로 거의 동일한 결과를 생성합니다. 두 모델을 모두 테스트해 보세요. 이와 같이 구조화되고 제약이 있는 작업에서 저렴한 모델이 얼마나 뛰어난 성능을 보이는지 보고 놀랄 수도 있습니다.
LangChain vs. Raw OpenAI API — 언제 무엇을 사용할 것인가
<table><colgroup><col> <col> <col></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>요소 (Factor)</p></th><th colspan="1" rowspan="1"><p>Raw OpenAI API</p></th><th colspan="1" rowspan="1"><p>LangChain</p></th></tr><tr><td colspan="1" rowspan="1"><p>설정 복잡도 (Setup complexity)</p></td><td colspan="1" rowspan="1"><p>낮음 — 하나의 임포트(import), 하나의 호출</p></td><td colspan="1" rowspan="1"><p>중간 — 학습해야 할 추상화(abstractions)가 더 많음</p></td></tr><tr><td colspan="1" rowspan="1"><p>단일 프롬프트 앱 (Single prompt apps)</p></td><td colspan="1" rowspan="1"><p>완벽하게 적합함</p></td><td colspan="1" rowspan="1"><p>과함 (Overkill)</p></td></tr><tr><td colspan="1" rowspan="1"><p>다단계 체인 (Multi-step chains)</p></td><td colspan="1" rowspan="1"><p>수동으로 연결하기 번거로움</p></td><td colspan="1" rowspan="1"><p>최상급 지원 (First-class support)</p></td></tr><tr><td colspan="1" rowspan="1"><p>프롬프트 재사용 및 테스트</p></td><td colspan="1" rowspan="1"><p>직접 구현(DIY) — 내장된 구조 없음</p></td><td colspan="1" rowspan="1"><p>PromptTemplate을 통해 용이함</p></td></tr><tr><td colspan="1"rowspan="1"><p>턴(turns) 간 메모리 유지</p></td><td colspan="1" rowspan="1"><p>수동 배열 관리</p></td><td colspan="1" rowspan="1"><p>내장된 메모리 유형</p></td></tr><tr><td colspan="1" rowspan="1"><p>스트리밍(Streaming)</p></td><td colspan="1" rowspan="1"><p>지원되지만, 수동 연결 필요</p></td><td colspan="1" rowspan="1"><p>지원됨, 콜백 기반</p></td></tr><tr><td colspan="1" rowspan="1"><p>LLM 제공업체 전환</p></td><td colspan="1" rowspan="1"><p>API 호출 재작성 필요</p></td><td colspan="1" rowspan="1"><p>모델 객체 교체 가능</p></td></tr><tr><td colspan="1" rowspan="1"><p>커뮤니티/생태계</p></td><td colspan="1" rowspan="1"><p>작음 (OpenAI 전용)</p></td><td colspan="1" rowspan="1"><p>크고 활발하며, 통합 기능이 많음</p></td></tr></tbody></table>
일반적인 규칙: 앱에서 두 가지 이상의 다른 유형의 LLM 호출을 하거나, 어떤 종류의 체이닝(chaining)이 필요하다면 LangChain은 처음부터 오케스트레이션 코드(orchestration code)를 작성하는 수고를 덜어줍니다. 간단한 원샷(one-shot) 래퍼(wrapper)라면, 순수 API가 더 깔끔합니다.
요약 (TL;DR)
-
LangChain은 LLM 앱을 위한 오케스트레이션 레이어입니다. AI를 위한 Express라고 생각하세요. 다단계 체인, 프롬프트 재사용 또는 메모리 요구 사항이 있을 때 사용하세요.
-
프롬프트 전에 파싱(Parse)하세요. 원본 이력서 블롭(raw resume blob)을 모델에 보내는 것은 일반적인 결과물을 얻기 쉬운 방법입니다. 개선하고 싶은 섹션을 식별하고, 모델에게 집중된 컨텍스트를 제공하세요.
-
프롬프트를 명시적으로 제약하세요. 행동 동사, 숫자 정량화(number quantification), 글머리 기호 개수 등—모델에게 원하는 형식을 정확히 알려주세요. 모호한 프롬프트는 모호한 결과를 낳습니다.
-
UX 개선을 위해 응답 스트리밍(Stream responses)하세요. 8초 동안 빈 화면은 고장 난 것처럼 느껴지지만, 실시간으로 나타나는 응답은 빠르다고 느껴집니다.
-
함정(pitfalls)에 대비하세요: API 속도 제한(rate limit)을 설정하고, 프롬프트 주입(prompt injection) 방지를 위해 이력서 입력을 정제하며, GPT-4로 기본 설정을 하기 전에
gpt-4o-mini를 테스트하세요. 이는 종종 충분히 좋고 10배 더 저렴합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기