@comneed/textby
v0.0.3
Published
Transform JSON data into LLM-friendly markdown with LiquidJS templates. Supports .md, .mdx, .liquid formats with powerful template composition.
Downloads
33
Maintainers
Readme
@comneed/textby
JSON 데이터를 LLM-friendly 마크다운으로 변환하는 템플릿 기반 생성기
TextBy는 구조화된 JSON 데이터를 LLM이 이해하기 쉬운 마크다운 형식으로 변환하는 라이브러리입니다. llmstxt.org 표준을 따르며, Next.js, React, Vue, NestJS 등 모든 JavaScript/TypeScript 환경에서 사용할 수 있습니다.
LiquidJS 템플릿 엔진을 기반으로 하여 강력한 템플릿 문법을 제공합니다.
✨ 핵심 기능
- 📝 다양한 템플릿 형식:
.md,.liquid파일 지원 - 🔧 간단한 API: 직관적이고 사용하기 쉬운 인터페이스
- 🚀 TypeScript 완벽 지원: 완전한 타입 안전성
- 📦 범용적: 브라우저(SPA), Node.js, Next.js, Express 등 모든 환경
- 🎨 LiquidJS 템플릿 문법: 변수, 반복문, 조건문, 필터, 커스텀 필터/태그
- 📂 JSON 파일 자동 로드: 파일 경로만 전달하면 자동으로 데이터 로드 (Node.js)
📦 설치
pnpm add @comneed/textby
# or
npm install @comneed/textby
# or
yarn add @comneed/textby🚀 빠른 시작
SPA (React, Vue, Svelte 등)에서 사용하기
브라우저 환경에서는 인라인 템플릿 문자열과 객체 데이터를 사용합니다:
import { TextBy } from '@comneed/textby';
const textby = new TextBy();
// 인라인 템플릿 + 객체 데이터
const markdown = await textby.render({
template: `
# {{ name }}
> {{ description }}
## Skills
{% for skill in skills %}
- {{ skill }}
{% endfor %}
`,
data: {
name: "John Doe",
description: "Full Stack Developer",
skills: ["React", "Next.js", "TypeScript"]
}
});
console.log(markdown);서버사이드 (Node.js, Next.js, Express)에서 사용하기
서버 환경에서는 파일 경로를 사용할 수 있습니다:
1. 템플릿 파일 작성 (templates/portfolio.md)
# {{ name }} ({{ fullName }})
> {{ description }}
## Skills
{% for category in skillCategories %}
### {{ category.category }}
{{ category.skills | join: ", " }}
{% endfor %}2. 데이터 파일 작성 (data/portfolio.json)
{
"name": "John Doe",
"fullName": "John Doe",
"description": "Full Stack Developer",
"skillCategories": [
{
"category": "Frontend",
"skills": ["React", "Next.js", "TypeScript"]
}
]
}3. 마크다운 생성
import { TextBy } from '@comneed/textby';
const textby = new TextBy();
// 방법 1: 파일 경로 모두 사용 (가장 간단!)
const markdown = await textby.render({
template: './templates/portfolio.md',
data: './data/portfolio.json' // JSON 파일 자동 로드
});
// 방법 2: 템플릿 파일 + 객체 데이터
const markdown2 = await textby.render({
template: './templates/portfolio.md',
data: {
name: "John Doe",
// ... 데이터
}
});
// 방법 3: 파일로 저장
await textby.renderToFile({
template: './templates/portfolio.md',
data: './data/portfolio.json',
output: './public/portfolio.txt'
});📚 API 문서
TextBy 생성자
new TextBy(options?: TextByOptions)
TextBy 인스턴스를 생성합니다.
Options:
interface TextByOptions {
// LiquidJS 템플릿 조합 설정
root?: string | string[]; // 메인 템플릿 디렉토리 (Node.js)
partials?: string | string[]; // Partials 디렉토리 (Node.js)
layouts?: string | string[]; // Layouts 디렉토리 (Node.js)
templates?: Record<string, string>; // 인메모리 템플릿 (브라우저)
globals?: Record<string, any>; // 전역 변수
extname?: string; // 파일 확장자 (기본값: '')
}예시:
// Node.js: 파일 기반
const textby = new TextBy({
root: './templates',
partials: './templates/partials',
extname: '.liquid'
});
// 브라우저: 인메모리
const textby = new TextBy({
templates: {
'header': '# {{ title }}',
'footer': '---'
}
});render(options: RenderOptions): Promise<string>
템플릿과 데이터를 받아 문자열을 생성합니다.
Parameters:
interface RenderOptions {
template: string; // 템플릿 문자열 또는 파일 경로
data: Record<string, any> | string; // 데이터 객체 또는 JSON 파일 경로
isFilePath?: boolean; // 파일 경로 여부 (기본값: 자동 감지)
}자동 감지 규칙:
- 브라우저: 항상 인라인 템플릿으로 처리 (
isFilePath: true면 에러) - Node.js:
.md,.liquid확장자 또는./,../,/시작 시 파일로 인식
예시:
// 인라인 템플릿 (브라우저/Node.js 모두)
const result = await textby.render({
template: '# {{ name }}',
data: { name: 'John' }
});
// 파일 템플릿 (Node.js만)
const result = await textby.render({
template: './templates/portfolio.md',
data: { name: 'John' }
});
// JSON 파일 자동 로드 (Node.js만)
const result = await textby.render({
template: './templates/portfolio.md',
data: './data/portfolio.json'
});renderToFile(options: RenderToFileOptions): Promise<void>
템플릿을 렌더링하고 파일로 저장합니다. (Node.js 전용)
Parameters:
interface RenderToFileOptions extends RenderOptions {
output: string; // 출력 파일 경로 (디렉토리 자동 생성)
}예시:
await textby.renderToFile({
template: './templates/portfolio.md',
data: portfolioData,
output: './public/llms.txt'
});registerFilter(name: string, filter: Function): void
커스텀 Liquid 필터를 등록합니다.
예시:
textby.registerFilter('uppercase', (value: string) => {
return value.toUpperCase();
});
// 템플릿에서 사용: {{ name | uppercase }}registerTag(name: string, tag: any): void
커스텀 Liquid 태그를 등록합니다.
예시:
textby.registerTag('highlight', {
parse(token) {
this.content = token.args;
},
render(ctx) {
return `**${this.content}**`;
}
});
// 템플릿에서 사용: {% highlight Important %}🎯 사용 사례
1. 서버사이드: 포트폴리오 llms.txt 생성
import { TextBy } from '@comneed/textby';
const textby = new TextBy();
// JSON 파일 사용 (권장)
await textby.renderToFile({
template: './templates/portfolio.md',
data: './data/portfolio.json', // 자동 로드
output: './public/llms.txt'
});
// 또는 객체 직접 전달
await textby.renderToFile({
template: './templates/portfolio.md',
data: {
name: 'Your Name',
skills: ['JavaScript', 'TypeScript', 'React'],
projects: [/* ... */]
},
output: './public/llms.txt'
});2. SPA: 동적 마크다운 생성
import { TextBy } from '@comneed/textby';
function generateReadme(userData) {
const textby = new TextBy();
// 인라인 템플릿 사용
const markdown = await textby.render({
template: `
# {{ name }}
## 프로필
- Email: {{ email }}
- GitHub: {{ github }}
## 스킬
{% for skill in skills %}
- {{ skill }}
{% endfor %}
`,
data: userData
});
return markdown;
}3. API 문서 생성
await textby.renderToFile({
template: './templates/api-docs.md',
data: './data/api-spec.json', // OpenAPI 스펙 등
output: './docs/api.txt'
});📖 템플릿 문법
TextBy는 강력한 템플릿 문법을 지원합니다. 주요 문법:
변수 출력
{{ variableName }}
{{ object.property }}
{{ array[0] }}반복문
{% for item in items %}
{{ item.name }}
{% endfor %}
{% for item in items limit:3 %}
제한된 반복
{% endfor %}조건문
{% if condition %}
내용
{% elsif otherCondition %}
다른 내용
{% else %}
기본 내용
{% endif %}필터
{{ text | upcase }}
{{ text | downcase }}
{{ array | join: ", " }}
{{ text | append: "suffix" }}
{{ text | prepend: "prefix" }}
{{ number | plus: 5 }}
{{ text | replace: "old", "new" }}더 많은 필터와 문법은 LiquidJS 공식 문서를 참고하세요.
🧩 템플릿 조합 (LiquidJS 기능)
TextBy는 LiquidJS 템플릿 엔진을 사용하므로, LiquidJS의 모든 템플릿 조합 기능을 그대로 사용할 수 있습니다:
{% include %}- 부모 스코프를 공유하는 partial 포함{% render %}- 격리된 스코프의 partial 렌더링{% layout %}- 레이아웃 상속 및 블록 오버라이드
템플릿 조합 설정
Node.js 환경 (파일 기반)
const textby = new TextBy({
root: './templates', // 메인 템플릿 디렉토리
partials: './templates/partials', // partial 템플릿 디렉토리
layouts: './templates/layouts', // layout 템플릿 디렉토리
extname: '.liquid' // 자동 확장자 추가
});브라우저 환경 (인메모리)
const textby = new TextBy({
templates: {
'header': `# {{ name }}`,
'footer': `---\n*Generated with TextBy*`
}
});템플릿 조합 간단 예제
// Partial 템플릿 사용
const result = await textby.render({
template: `
{% include 'header' %}
Content goes here
{% include 'footer' %}
`,
data: { name: 'John Doe' }
});더 자세한 템플릿 조합 문법은 LiquidJS 공식 문서를 참고하세요.
🔧 개발
# 의존성 설치
pnpm install
# 빌드
pnpm build
# 개발 모드
pnpm dev
# 테스트
pnpm test
# 린트
pnpm lint📄 라이선스
MIT
🌐 Node.js 서버 통합
Express, Fastify 등 Node.js 서버에서도 사용 가능합니다.
import express from 'express';
import { TextBy } from '@comneed/textby';
const app = express();
const textby = new TextBy();
app.get('/llms.txt', async (req, res) => {
const markdown = await textby.render({
template: './templates/profile.md',
data: profileData
});
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.send(markdown);
});
app.listen(3000);📦 실제 사용 예시
모노레포의 apps/docs 앱에서 TextBy를 실제로 사용하는 완전한 예시를 확인할 수 있습니다:
cd apps/docs
pnpm dev
# http://localhost:3002/llms.txt 접속⚡ Next.js 통합
App Router (Next.js 13+)
// app/llms.txt/route.ts
import { TextBy } from '@comneed/textby';
export const dynamic = 'force-static'; // 정적 생성
export async function GET() {
const textby = new TextBy();
const markdown = await textby.render({
template: './templates/profile.md', // 또는 .mdx
data: profileData
});
return new Response(markdown, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'public, s-maxage=3600',
},
});
}Pages Router
// pages/api/llms.txt.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { TextBy } from '@comneed/textby';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const textby = new TextBy();
const markdown = await textby.render({
template: './templates/profile.mdx',
data: profileData
});
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.status(200).send(markdown);
}실제 작동 예시: apps/docs 앱에서 완전한 Next.js 통합을 확인할 수 있습니다.
📋 지원하는 템플릿 형식
.md- 마크다운 파일 (Liquid 문법 포함).liquid- Liquid 템플릿 파일- 인라인 템플릿 - 파일 없이 문자열로 직접 전달
모든 형식에서 동일한 LiquidJS 템플릿 문법을 사용할 수 있습니다.
🔍 Troubleshooting
템플릿 파일을 찾을 수 없어요
증상: Error: ENOENT: no such file or directory
해결방법:
// 절대 경로 사용
import { join } from 'path';
const templatePath = join(process.cwd(), 'templates', 'docs.md');
const result = await textby.render({
template: templatePath,
data: myData
});브라우저에서 파일 경로가 작동하지 않아요
원인: 브라우저 환경에서는 파일 시스템에 접근할 수 없습니다.
해결방법: 인라인 템플릿이나 인메모리 템플릿을 사용하세요.
// ❌ 브라우저에서는 작동하지 않음
const result = await textby.render({
template: './templates/docs.md',
data: myData
});
// ✅ 인라인 템플릿 사용
const result = await textby.render({
template: `# {{ title }}\n{{ content }}`,
data: myData
});Liquid 문법 에러가 발생해요
증상: LiquidError: unexpected token
해결방법:
- 태그는
{% %}, 변수는{{ }}를 사용하세요 - 태그는 반드시 닫아야 합니다 (
{% for %}...{% endfor %}) - 주석은
{% comment %}...{% endcomment %}또는{% # 주석 %}를 사용하세요
Next.js에서 빌드 시 에러가 나요
증상: Module not found: Can't resolve 'fs'
원인: 클라이언트 컴포넌트에서 파일 시스템을 사용하려고 시도
해결방법:
// app/llms.txt/route.ts (Server Component)
import { TextBy } from '@comneed/textby';
export async function GET() {
const textby = new TextBy();
// 서버에서만 파일 시스템 사용 가능
const result = await textby.render({
template: './templates/docs.md',
data: './data/info.json'
});
return new Response(result);
}한글이 깨져서 나와요
해결방법: 올바른 Content-Type 헤더를 설정하세요.
// Express
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
// Next.js
return new Response(markdown, {
headers: {
'Content-Type': 'text/plain; charset=utf-8'
}
});⚡ 성능 최적화
1. 템플릿 캐싱
자주 사용하는 템플릿은 미리 컴파일하여 재사용하세요:
// 싱글톤 패턴으로 TextBy 인스턴스 재사용
class MarkdownGenerator {
private static textby: TextBy;
static getInstance() {
if (!this.textby) {
this.textby = new TextBy({
// 파일 기반 템플릿은 자동으로 캐싱됨
root: './templates',
partials: './templates/partials',
});
}
return this.textby;
}
}
// 사용
const textby = MarkdownGenerator.getInstance();
const result = await textby.render({
template: './templates/docs.md',
data: myData
});2. 데이터 최적화
큰 JSON 파일은 필요한 부분만 로드하세요:
// ❌ 전체 데이터 로드 (느림)
const allData = await loadEntireDatabase();
// ✅ 필요한 데이터만 로드
const pageData = await loadPageData(pageId);
const result = await textby.render({
template: templateString,
data: pageData // 작은 데이터셋
});3. Next.js 정적 생성
빌드 시 한 번만 생성하도록 설정:
// app/llms.txt/route.ts
export const dynamic = 'force-static';
export const revalidate = 3600; // 1시간마다 재생성 (ISR)
export async function GET() {
// 빌드 시 한 번만 실행됨
const result = await textby.render({
template: './templates/docs.md',
data: './data/info.json'
});
return new Response(result, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'public, max-age=3600, s-maxage=3600'
}
});
}4. 스트리밍 (대용량 데이터)
대용량 마크다운 생성 시 스트리밍 사용:
import { Readable } from 'stream';
// Express에서 스트리밍
app.get('/large-doc', async (req, res) => {
const markdown = await textby.render({
template: './templates/large-doc.md',
data: hugeDataset
});
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
const stream = Readable.from([markdown]);
stream.pipe(res);
});🎓 고급 사용법
동적 템플릿 선택
조건에 따라 다른 템플릿 사용:
const getTemplate = (type: string) => {
const templates = {
blog: './templates/blog-post.md',
doc: './templates/documentation.md',
api: './templates/api-reference.md'
};
return templates[type] || templates.doc;
};
const markdown = await textby.render({
template: getTemplate(contentType),
data: contentData
});다국어 지원
언어별 템플릿 관리:
const textby = new TextBy({
root: './templates',
partials: './templates/partials'
});
const locale = 'ko'; // 또는 'en', 'ja' 등
const markdown = await textby.render({
template: `./templates/${locale}/docs.md`,
data: {
...contentData,
locale: locale
}
});조건부 섹션
특정 조건에서만 섹션 표시:
{% if premium %}
## 프리미엄 기능
{% for feature in premiumFeatures %}
- {{ feature.name }}: {{ feature.description }}
{% endfor %}
{% endif %}
{% if hasProjects and projects.size > 0 %}
## 프로젝트
{% for project in projects %}
### {{ project.title }}
{{ project.description }}
{% endfor %}
{% endif %}중첩 데이터 처리
깊게 중첩된 객체 다루기:
const complexData = {
user: {
profile: {
personal: {
name: "John Doe",
contact: {
email: "[email protected]"
}
}
}
}
};
const template = `
# {{ user.profile.personal.name }}
Email: {{ user.profile.personal.contact.email }}
`;📊 실전 예제 모음
1. 블로그 포스트 생성
const blogPost = await textby.render({
template: `
# {{ title }}
**작성자**: {{ author.name }} | **날짜**: {{ publishedAt }}
{{ content }}
## 태그
{% for tag in tags %}
- #{{ tag }}
{% endfor %}
## 관련 글
{% for related in relatedPosts %}
- [{{ related.title }}]({{ related.url }})
{% endfor %}
`,
data: {
title: "TextBy 시작하기",
author: { name: "Developer" },
publishedAt: "2024-10-23",
content: "TextBy는 강력한 템플릿 엔진입니다...",
tags: ["tutorial", "javascript", "markdown"],
relatedPosts: [
{ title: "고급 기능", url: "/advanced" }
]
}
});2. 팀 소개 페이지
const teamPage = await textby.render({
template: `
# {{ teamName }}
> {{ tagline }}
## 팀원
{% for member in members %}
### {{ member.name }} - {{ member.role }}
{{ member.bio }}
**전문 분야**: {{ member.skills | join: ", " }}
{% if member.github %}🔗 [GitHub]({{ member.github }}){% endif %}
{% if member.twitter %}🐦 [Twitter]({{ member.twitter }}){% endif %}
---
{% endfor %}
`,
data: {
teamName: "Development Team",
tagline: "Building the future, together",
members: [
{
name: "Alice",
role: "Lead Developer",
bio: "10년 경력의 풀스택 개발자",
skills: ["React", "Node.js", "AWS"],
github: "https://github.com/alice"
}
]
}
});3. 제품 카탈로그
const catalog = await textby.render({
template: `
# {{ storeName }} 제품 카탈로그
{% for category in categories %}
## {{ category.name }}
{{ category.description }}
{% for product in category.products %}
### {{ product.name }}
**가격**: {{ product.price | append: '원' }}
{% if product.discount %}
~~원가: {{ product.originalPrice }}원~~ **{{ product.discount }}% 할인!**
{% endif %}
{{ product.description }}
**재고**: {% if product.inStock %}✅ 있음{% else %}❌ 없음{% endif %}
---
{% endfor %}
{% endfor %}
`,
data: {
storeName: "Tech Store",
categories: [
{
name: "노트북",
description: "최신 노트북 제품",
products: [
{
name: "MacBook Pro",
price: 2500000,
originalPrice: 3000000,
discount: 16,
description: "강력한 M3 칩 탑재",
inStock: true
}
]
}
]
}
});4. 이력서 생성
const resume = await textby.render({
template: `
# {{ name }}
{{ title }} | {{ location }}
📧 {{ email }} | 📱 {{ phone }}
{% if linkedin %}🔗 [LinkedIn]({{ linkedin }}){% endif %}
{% if github %}💻 [GitHub]({{ github }}){% endif %}
## 소개
{{ summary }}
## 경력
{% for job in experience %}
### {{ job.company }} - {{ job.position }}
*{{ job.startDate }} - {{ job.endDate | default: "현재" }}*
{{ job.description }}
**주요 성과**:
{% for achievement in job.achievements %}
- {{ achievement }}
{% endfor %}
{% endfor %}
## 기술 스택
{% for category in skills %}
### {{ category.category }}
{{ category.items | join: " • " }}
{% endfor %}
## 학력
{% for edu in education %}
- **{{ edu.degree }}**, {{ edu.school }} ({{ edu.year }})
{% endfor %}
`,
data: {
name: "김개발",
title: "Senior Full Stack Developer",
location: "서울, 대한민국",
email: "[email protected]",
phone: "010-1234-5678",
linkedin: "https://linkedin.com/in/developer",
github: "https://github.com/developer",
summary: "10년 이상의 웹 개발 경험을 가진 풀스택 개발자입니다.",
experience: [
{
company: "Tech Corp",
position: "Senior Developer",
startDate: "2020.01",
endDate: null,
description: "대규모 웹 애플리케이션 개발 리드",
achievements: [
"트래픽 50% 증가 처리",
"API 응답 속도 40% 개선"
]
}
],
skills: [
{
category: "Frontend",
items: ["React", "Vue", "TypeScript", "Next.js"]
},
{
category: "Backend",
items: ["Node.js", "Python", "PostgreSQL", "Redis"]
}
],
education: [
{
degree: "컴퓨터공학 학사",
school: "한국대학교",
year: "2014"
}
]
}
});🤝 커뮤니티 및 지원
문제 제보
버그를 발견하셨나요? GitHub Issues에 제보해주세요.
기능 제안
새로운 기능을 제안하고 싶으신가요? GitHub Discussions에서 논의해주세요.
📚 추가 자료
- LiquidJS 문서 - 템플릿 문법 상세 가이드
- llmstxt.org - LLM-friendly 문서 표준
- GitHub 저장소
🙏 크레딧
이 라이브러리는 다음 오픈소스 프로젝트를 기반으로 합니다:
- LiquidJS - 강력한 템플릿 엔진
- llmstxt.org - LLM-friendly 문서 표준
Made with ❤️ by comneed
