AI 맞춤형 문서 생성 구축하기 (React 버전)
요약
LLM의 불확실성을 극복하고 디자인 템플릿을 엄격하게 준수하기 위해, AI가 직접 문서를 생성하는 대신 데이터를 맞춤화하고 코드로 렌더링하는 구조를 제안합니다. 데이터에서 템플릿을 거쳐 최종 형식으로 이어지는 파이프라인을 구축함으로써 출력의 안정성을 확보하고 토큰 비용을 절감할 수 있습니다.
핵심 포인트
- LLM에게 직접 문서 생성을 맡기기보다 결정론적 도구(Deterministic tools)를 호출하도록 설계하여 출력 안정성 확보
- 프롬프트 엔지니어링만으로는 디자인 준수 및 사실적 출력의 일관성을 유지하기 어려움
- 멀티 포맷 렌더링 시 변환 도구 대신 대상 형식을 직접 생성하는 방식이 시각적 오류를 줄이는 데 효과적
- 정적 및 다중 대상 문서 생성에는 추상 구문 트리(AST) 방식이 LLM 친화적이며 렌더링 독립성이 높음
서론
AI 어시스턴트를 사용하여 문서를 생성해야 하지만 데이터의 변동성을 제한해야 하고, 디자인 템플릿을 엄격하게 따라야 하며, "고객을 위한 제안서 PDF를 만들어줘, 군더더기 없이 부탁해"와 같은 프롬프트만으로는 충분하지 않다면, 제 사례에서 효과적이었고 여러분에게도 도움이 될 수 있는 방법을 소개합니다. 하지만 그전에, 제가 다뤄야 했던 구체적인 제약 사항들은 다음과 같습니다:
- 디자인 템플릿 준수
- 다양한 형식 지원 (우선 PDF 및 HTML)
- 다양한 환경에서 렌더링: 브라우저(browser), 서버(server), 이메일(email)
- 정의된 사실과 숫자로 작동
핵심 전제: 생성 구조는 코드로 처리되어야 합니다. LLM의 역할은 사용자 입력을 분석하고 결정론적 도구(deterministic tools, 메서드)를 호출하여 주어진 케이스에 맞춰 문서를 미세 조정하는 것입니다. 이를 통해 출력의 안정성을 유지하고 문서 본문 생성에 토큰을 낭비하는 것을 방지할 수 있습니다.
왜 단순히 프롬프트를 더 잘 작성하는 것만으로는 안 될까요?
대규모 언어 모델(Large language models)은 긴 대화 중에, 특히 요약(summarization) 단계 이후에 흐트러지는 경향이 있어 엄격한 지침조차 무시될 수 있습니다. 요컨대, 사실적 출력의 안정성이 없습니다. 게다가 AI로 템플릿화된 문서를 생성하는 것은 비용 효율적이지 않습니다. 이미 코드베이스에 존재하는 디자인을 복제하려고 시도하는 데 상당한 양의 토큰이 소비될 것입니다.
기본 사용 사례
사용자가 문서를 선택하고 고객별 세부 정보를 제공하면, AI 어시스턴트가 콘텐츠를 맞춤화합니다. 그런 다음 사용자는 이를 이메일로 보내거나 PDF로 다운로드할 수 있습니다. 문서가 웹페이지에 삽입되어야 하고 AEO/GEO/SEO에 최적화되어야 하는 다른 시나리오들도 있습니다. 이를 염두에 두겠지만, 지금은 기본 사례에 집중하겠습니다.
해결책
이 과제의 가장 까다로운 부분 중 하나는 멀티 포맷 렌더링(multi-format rendering)입니다. PDF를 HTML이나 React로, 또는 그 반대로 변환할 수 있는 훌륭한 도구들이 많지만, 변환 과정에서 시각적 아티팩트(visual artifacts)가 발생하거나 크기와 레이아웃이 깨지는 비용이 따릅니다. 더 신뢰할 수 있는 접근 방식은 중간 매개체 없이 대상 형식을 직접 생성하는 것입니다.
파이프라인은 다음과 같습니다: 데이터(Data) -> AI 맞춤화(AI Tailoring) -> 템플릿(Template) -> 형식 렌더링(Format Rendering) 템플릿 옵션의 양은 압도적입니다. 하지만 높은 수준에서 보면, 여러분은 추상 구문 트리(AST), 가상 DOM (Virtual DOM), 또는 템플릿 엔진 (Template engine) 중 하나를 선택하게 됩니다.
| 구분 | 추상 구문 트리 (AST) | 가상 DOM 유사 방식 (예: snabbdom) | 템플릿 엔진 (예: awesome-te) |
|---|---|---|---|
| 출력 (Output) | 일반 데이터 트리 (Plain data tree) | 차분화 가능한 노드 트리 (Diffable node tree) | 렌더링된 문자열 (Rendered string) |
| 렌더링 독립성 | 예, 하나의 트리로 여러 렌더러 사용 가능 | 차분화 런타임(diffing runtime)에 종속됨 | 하나의 출력 형식에 종속됨 |
| LLM 친화성 | 검증 및 생성 용이 | 프레임워크 기본 요소(primitives)가 필요하여 어려움 | 느슨함, 문자열은 검증하기 어려움 |
| 동적 UI | 목표가 아님, 정적 문서에는 적합 | 이를 위해 구축됨 | 제한적, 보통 전체 문자열을 다시 렌더링함 |
| 번들 크기 | 최소화, 객체일 뿐임 | 런타임이 더 무거움 | 런타임에서 가벼움 |
| 최적 용도 | 정적, 다중 대상 문서 | 대화형 앱 (Interactive apps) | 단일 대상 HTML/이메일 |
동적 템플릿(상호작용, 조건부 렌더링 등)에 대한 요구 사항이 없다면, AST는 훌륭한 후보입니다. 가벼우면서도 동시에 렌더링 독립적이기 때문입니다. 실제로 저는 AST 노드를 생성하는 간단한 함수 목록을 가지고 있으며, 따라서 템플릿은 다음과 같은 형태를 띱니다:
const template = page([
h1(' Hi there, this is a DSL template '),
p(' Lightweight, simple and somewhat readable '),
// ...
]);
이것은 내부적으로 다음과 같은 일반 객체(plain object)로 해결됩니다:
const template = {
type: "Page",
elements: [
{
type: "Text",
style: styles.h1,
elements: " Hi there, this is a DSL template "
},
{
type: "Text",
style: styles.p,
elements: " Lightweight, simple and somewhat readable "
},
// ...
]
};
렌더링 (Rendering)
React를 사용하면 CSR(클라이언트 사이드 렌더링), SSR(서버 사이드 렌더링), 그리고 이메일을 위한 자연스러운 렌더링 엔진이 됩니다. PDF의 경우, 저는 결국 React-PDF를 사용하게 되었습니다. 이는 JSX와 유사한 구문을 사용하여 PDF 문서를 구성하고 렌더링할 수 있게 해주며, PDF에 대해 React 컴포넌트와 동일한 사고 모델(mental model)을 가질 수 있어 개발자 경험(DX)이 눈에 띄게 좋아집니다.
const MyDocument = () => (
<Document>
<Page size="A4" style={styles.page}>
<View style={styles.
section } > < Text > Section #1 </ Text > </ View > < View style = { styles . section } > < Text > Section #2 </ Text > </ View > </ Page > </ Document > ); 또한 <PDFDownloadLink /> 컴포넌트도 함께 제공되므로, 브라우저 메모리에서 문서를 직접 다운로드할 수 있어 스토리지를 완전히 선택 사항으로 만들 수 있습니다. 스타일링 (Styling) PDF/Word 문서의 덜 동적인 특성 때문에 사용 가능한 스타일 세트는 상당히 제한적입니다. 적어도 display: flex 를 사용할 수 있는데, 이는 이미 기대 이상의 기능입니다 (비록 그 부분집합이긴 하지만). 재미있는 사실은, @react-pdf/renderer는 rem은 지원하지만 em은 지원하지 않으며, 기본 폰트 크기가 브라우저에서 사용하는 16px보다 상당히 크다는 점입니다. 따라서 PDF에서는 괜찮아 보이는 글자들이 React 버전에서는 거의 보이지 않을 수도 있습니다. 상대 단위 (Relative units)를 기술적으로 사용할 수는 있지만, 픽셀 (pixels)을 사용하는 것이 확실히 더 안전합니다. 종합하기 (Putting It Together) const LeafletDocument = ({ data }) => { // 데이터로 템플릿 채우기 const templateJSON = buildTemplate ( data ); // AST 요소를 컴포넌트로 매핑 const ReactElementsMap = { Image : ( props ) => < img { ... props } />, h1 : ( props ) => < h1 { ... props } />, // ... }; // 템플릿 구축 const WebDocument = ( < DocumentBuilder template = { templateJSON } elements = { ReactElementsMap } /> ); // 또는 PDF를 위해 매핑을 교체: // const PDFDocument = <DocumentBuilder template={templateJSON} elements={PDFElementsMap} />; // 또는 https://github.com/nitin42/redocx 를 통한 Word: // const WordDocument = <DocumentBuilder template={templateJSON} elements={WordElementsMap} />; return WebDocument ; }; DocumentBuilder 컴포넌트는 템플릿을 재귀적으로 렌더링합니다: const Elements = ({ elements , components }) => { return ( <> { elements ?. map (( itemProps , index ) => typeof itemProps === " string " ? ( < Fragment key = { index } > { parseHTMLTags ( itemProps , parseHTMLTagsOptions ( components )) || "" } </ Fragment > ) : ( < Element key = { index } { ... itemProps } components = { components } /> ) ) } </> ); }; const Element = ({ elements , components , ...
props }) => { const ElementComponent = components [ props . type || DocumentElementType . View ]; return ( < ElementComponent { ... props } > < Elements elements = { elements } components = { components } /> </ ElementComponent > ); }; export const DocumentBuilder = ({ data , components }) => { return ( < components . Document style = { { fontSize : 8 } } > < Elements elements = { data ?. elements } components = { components } /> </ components . Document > ); }; AI Tailor 귀하의 AI 전략(LLM 채팅 또는 MCP)에 관계없이 높은 수준의 접근 방식은 동일합니다. 다음 기능을 수행하는 도구를 구축해야 합니다: AI가 의존할 수 있는 가능한 값의 데이터셋 제공 프롬프트를 사용하여 맞춤화 입력과 기존 데이터를 일치시킴 AI 응답 검증 여기 TanStack AI를 사용한 간소화된 예제가 있습니다: import { toolDefinition } from " @tanstack/ai " ; import type { JSONSchema } from " @tanstack/ai " ; const inputSchema : JSONSchema = { type : " object " , properties : { prospectDetails : { type : " string " , description : " 잠재 고객의 회사 세부 정보 (이름, 산업, 규모 등) " , }, }, required : [ " prospectDetails " ], }; const outputSchema : JSONSchema = { type : " object " , properties : { differentiators : { type : " array " , items : { type : " object " , properties : { id : { type : " string " }, headline : { type : " string " }, proofPoint : { type : " string " }, matchedPriority : { type : " string " }, }, required : [ " id " , " headline " , " proofPoint " , " matchedPriority " ], }, }, }, required : [ " differentiators " ], }; const tailorLeafletDef = toolDefinition ({ name : " tailor_leaflet " , description : " 잠재 고객의 회사 세부 정보에 맞게 전단지를 맞춤화합니다 " , inputSchema , outputSchema , }); const tailorLeaflet = tailorLeafletDef . server ( async ({ differentiators }, context ) => { const genai = new GoogleGenAI ({ ... }); const leafletOptions = await getLeafletOptions (); // 참고: 저희는 leafletOptions를 프롬프트에 직접 주입합니다.
// 만약 객체가 너무 커진다면, 컨텍스트 (context)가 비대해지는 것을 방지하기 위해 RAG (Retrieval-Augmented Generation) 방식을 고려하십시오.
const prompt = 당신은 판매용 리플렛 (sales leaflet)을 작성하고 있습니다. 다음의 리플렛 옵션 (leaflet options)과 고객 차별화 요소 (client differentiators)를 바탕으로, 맞춤형 리플렛 콘텐츠를 JSON 형식으로 생성하십시오. <LeafletOptions> ${ leafletOptions } </LeafletOptions> <Differentiators> ${ differentiators } </Differentiators> 다음의 구조와 정확히 일치하는 유효한 JSON 객체로 응답하십시오: <Schema> ${ JSON . stringify ( leafletJSONSchema )} </Schema> 마크다운 (markdown)이나 설명 없이 오직 JSON만 반환하십시오. ;
const response = await genai . models . generateContent ({ model : CHAT_MODEL , contents : prompt , });
try {
const parsed = validateResponse ( response . text );
return parsed ;
} catch ( error ) {
console . error ( " Error parsing response: " , error );
return null ;
}
} );
맞춤형 데이터를 확보하고 나면, 클라이언트 (client)나 필요한 곳 어디에서든 이를 렌더링할 수 있습니다.
응답 검증 (Validating the Response)
프롬프트 (prompt)가 아무리 정교하더라도, 응답을 보호하는 안전장치를 마련할 것을 강력히 권장합니다. 앞서 언급한 것과 같은 이유로, 저희는 단순히 프롬프트를 더 잘 작성하는 것에 그치지 않았습니다. 모델이 지시사항과 정확히 일치하게 응답할 것이라는 보장은 없기 때문입니다. 저의 경우, AI가 제안한 옵션들을 원본 데이터 (예: leafletOptions)와 대조하여 검증하는 커스텀 체크 (custom check) 로직을 작성했습니다. 더 많은 유연성이 필요하다면, LLM을 판사로 사용하는 'LLM as a judge' 방식을 추가하는 것도 확실한 대안입니다. 그리고 어떤 방식이든, 문서가 어디로 전달되기 전에 사람이 검토하도록 하는 것 (human-in-the-loop)은 항상 좋은 아이디어입니다.
다음 단계 (Next steps)
저의 경우에는 이 도구를 MCP 앱으로도 제공하는 것이 효과적이었습니다. 이를 통해 리플렛/제안 문서 (leaflets/offer documents)를 LLM 채팅에서 직접 맞춤화하고 미리 볼 수 있어, 동료들이 더 쉽게 접근할 수 있게 되었습니다.
요약 (Summary)
여기서 설명한 접근 방식은 생성된 문서가 안정적이고 사양에 맞게 유지되도록 하며, 과정 중에 토큰 (tokens)을 절약할 수도 있습니다. 물론 모든 것은 트레이드오프 (trade-offs) 관계에 있습니다. 이 솔루션은 어느 정도의 개발 오버헤드 (development overhead)가 발생하며 지속적인 지원이 필요하겠지만, 생성하는 문서가 많아질수록 투자 대비 효과 (ROI)는 더욱 강력해집니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기