Converter - 핵심 개념
이 가이드는 StAX-XML Converter를 효과적으로 사용하기 위해 이해해야 할 기본 개념을 다룹니다.
스키마 빌더 (x)
섹션 제목: “스키마 빌더 (x)”x 객체는 XML 스키마를 생성하는 주요 인터페이스입니다:
import { x } from 'stax-xml/converter';
// 스키마 생성const stringSchema = x.string();const numberSchema = x.number();const objectSchema = x.object({...});const arraySchema = x.array(elementSchema, xpath);Fluent API 패턴
섹션 제목: “Fluent API 패턴”모든 스키마 메서드는 새 스키마 인스턴스를 반환하여 API를 불변하고 체이닝 가능하게 만듭니다:
const schema = x.number() .xpath('//price') .min(0) .max(1000);
// 각 메서드는 새 스키마를 반환const baseNumber = x.number();const withXPath = baseNumber.xpath('//value'); // 새 인스턴스const withMin = withXPath.min(0); // 새 인스턴스
// 원본은 변경되지 않음console.log(baseNumber.options.xpath); // undefinedconsole.log(withXPath.options.xpath); // '//value'파싱 모드
섹션 제목: “파싱 모드”Converter는 다양한 사용 사례를 위한 여러 파싱 메서드를 제공합니다:
동기 파싱
섹션 제목: “동기 파싱”동기, 블로킹 파싱을 위해 parseSync() 사용:
const schema = x.string().xpath('//title');const result = schema.parseSync(xmlString);// 반환: string사용 시기:
- XML이 이미 문자열로 메모리에 있을 때
- 동기 컨텍스트에 있을 때
- 성능이 중요할 때 (비동기보다 약간 빠름)
비동기 파싱
섹션 제목: “비동기 파싱”비동기 파싱을 위해 parse() 사용:
const schema = x.object({ name: x.string().xpath('//name'), value: x.number().xpath('//value')});
const result = await schema.parse(xmlString);// 반환: Promise<{ name: string; value: number; }>안전한 파싱
섹션 제목: “안전한 파싱”안전한 파싱 메서드는 예외를 던지는 대신 결과 객체를 반환합니다:
const schema = x.number().xpath('//age').min(0).max(120);
// 안전한 동기 파싱const result = schema.safeParseSync('<age>150</age>');
if (result.success) { console.log(result.data); // number} else { console.log(result.issues); // 에러 객체 배열}타입 추론
섹션 제목: “타입 추론”Converter는 Infer 유틸리티 타입을 사용하여 자동 TypeScript 타입 추론을 제공합니다:
import { x, type Infer } from 'stax-xml/converter';
const userSchema = x.object({ id: x.number().xpath('//id'), username: x.string().xpath('//username'), email: x.string().xpath('//email'), active: x.string().xpath('//active').transform(v => v === 'true')});
// 자동으로 타입 추론type User = Infer<typeof userSchema>;// {// id: number;// username: string;// email: string;// active: boolean; // 주의: 변환된 타입!// }
// 추론된 타입 사용const users: User[] = [];복잡한 타입 추론
섹션 제목: “복잡한 타입 추론”타입 추론은 중첩 구조 및 변환과 함께 작동합니다:
const productSchema = x.object({ name: x.string().xpath('//name'), price: x.number().xpath('//price'), tags: x.array(x.string(), '//tag'), specs: x.object({ weight: x.number().xpath('./weight'), dimensions: x.string().xpath('./dimensions') }).xpath('//specs').optional()});
type Product = Infer<typeof productSchema>;// {// name: string;// price: number;// tags: string[];// specs: { weight: number; dimensions: string; } | undefined;// }XPath 통합
섹션 제목: “XPath 통합”XPath는 XML 문서에서 요소를 선택하는 주요 방법입니다.
XPath 기초
섹션 제목: “XPath 기초”// 루트에서 절대 경로x.string().xpath('/root/element/child')
// 하위 요소 검색 (문서 어디서나)x.string().xpath('//element')
// 속성 접근x.string().xpath('/@id')x.string().xpath('//@href')
// 결합x.string().xpath('/root/item/@name')다양한 스키마의 XPath
섹션 제목: “다양한 스키마의 XPath”String 및 Number 스키마:
const title = x.string().xpath('/book/title');const price = x.number().xpath('/book/price');Object 스키마:
// 개별 필드의 XPathconst book = x.object({ title: x.string().xpath('/book/title'), author: x.string().xpath('/book/author')});
// 전체 객체의 XPath (자식 XPath 범위 지정)const book = x.object({ title: x.string().xpath('./title'), // /book에 상대적 author: x.string().xpath('./author') // /book에 상대적}).xpath('/book');Array 스키마:
// 배열에는 XPath 필수const items = x.array( x.string(), '//item' // 모든 <item> 요소 선택);에러 처리
섹션 제목: “에러 처리”Converter는 파싱이 실패할 때 상세한 에러 정보를 제공합니다:
XmlParseError
섹션 제목: “XmlParseError”import { XmlParseError } from 'stax-xml/converter';
try { const result = schema.parseSync(invalidXml);} catch (error) { if (error instanceof XmlParseError) { console.log(error.message); // 사람이 읽을 수 있는 메시지 console.log(error.issues); // 구체적인 문제 배열 }}에러 복구 전략
섹션 제목: “에러 복구 전략”1. Optional 사용
const schema = x.object({ id: x.number().xpath('//id'), optional: x.string().xpath('//optional').optional()});// 반환: { id: number; optional: string | undefined }2. 안전한 파싱 사용
const result = schema.safeParseSync(xml);if (!result.success) { // 에러를 우아하게 처리 return defaultValue;}return result.data;3. 기본값을 위한 Transform 사용
const schema = x.string() .xpath('//value') .transform(v => v || 'default');스키마 구성
섹션 제목: “스키마 구성”스키마는 구성하고 재사용할 수 있습니다:
스키마 재사용
섹션 제목: “스키마 재사용”// 재사용 가능한 스키마 정의const priceSchema = x.number().min(0);const idSchema = x.number().int().min(1);
// 객체에서 구성const productSchema = x.object({ id: idSchema.xpath('//id'), price: priceSchema.xpath('//price'), salePrice: priceSchema.xpath('//salePrice').optional()});중첩 객체
섹션 제목: “중첩 객체”const addressSchema = x.object({ street: x.string().xpath('./street'), city: x.string().xpath('./city'), zip: x.string().xpath('./zip')}).xpath('/address');
const personSchema = x.object({ name: x.string().xpath('/person/name'), address: addressSchema // 중첩된 객체 스키마});성능 고려사항
섹션 제목: “성능 고려사항”동기 vs 비동기
섹션 제목: “동기 vs 비동기”// 동기 - 작은 문서에 약간 더 빠름const result = schema.parseSync(smallXml);
// 비동기 - 큰 문서에 더 좋음const result = await schema.parse(largeXml);XPath 최적화
섹션 제목: “XPath 최적화”// ❌ 느림 - 전체 문서를 여러 번 검색const schema = x.object({ a: x.string().xpath('//a'), b: x.string().xpath('//b'), c: x.string().xpath('//c')});
// ✅ 더 좋음 - 단일 루트 XPath, 상대 자식const schema = x.object({ a: x.string().xpath('./a'), b: x.string().xpath('./b'), c: x.string().xpath('./c')}).xpath('/root');컴파일된 스키마
섹션 제목: “컴파일된 스키마”같은 스키마로 여러 문서를 반복 파싱할 때는 compile()을 사용하세요:
const personSchema = x.object({ id: x.number().xpath('./@id').int(), name: x.string().xpath('./name'), age: x.number().xpath('./age').int(), birthday: x.string().xpath('./birthday'), married: x.string() .xpath('./married') .transform(value => value === 'true'), firstTime: x.string().xpath('./married/@firstTime'), nickname: x.string().xpath('./nickname').optional(), address: x.object({ city: x.string().xpath('./city'), zip: x.string().xpath('./zip/text()') }).xpath('./address'), aliases: x.array(x.string(), './alias'), contacts: x.array( x.object({ type: x.string().xpath('./@type'), value: x.string().xpath('./value/text()') }), './contact' )});
const compiledSchema = x.object({ datasetId: x.string().xpath('/dataset/@id'), title: x.string().xpath('/dataset/title/text()'), metadata: x.object({ source: x.string().xpath('./source'), generatedAt: x.string().xpath('./generatedAt') }).xpath('/dataset/metadata'), labels: x.array(x.string(), '/dataset/labels/label'), people: x.array(personSchema, '//person')}).compile();
const result = compiledSchema.parseSync(xml);compile()은 동일한 공개 API를 유지하지만, 가장 빠른 경로는 고정된 XML 이벤트 dispatch로 낮출 수 있는 스키마 형태에서만 사용됩니다.
위 스키마는 주요 fast-path shape를 하나의 compiled schema 안에 조합한 예시입니다. 절대 element/attribute selector, 직접 text() selector, //person descendant 배열 경계, person item 내부의 상대 selector, 중첩 객체, scalar 배열, 객체 배열, optional 필드, transform을 모두 포함합니다. .int() 같은 숫자 검증은 텍스트 추출 후 그대로 적용됩니다.
fast path에 적합한 형태:
| 형태 | 예시 |
|---|---|
| 절대 요소 경로 | /catalog/book/title |
| descendant 요소 경로 | //book |
| 절대 또는 상대 속성 | /catalog/book/@id, ./@id |
| 객체 또는 배열 item 안의 상대 필드 | ./title, ./author/name |
| 직접 텍스트 선택 | /message/text(), ./title/text() |
| scalar 필드를 가진 객체 | x.object({ title: x.string().xpath('./title') }).xpath('/book') |
| scalar 또는 객체 배열 | x.array(x.string(), '/tags/tag'), x.array(bookSchema, '/catalog/book') |
| 중첩 객체, optional 필드, transform | x.object({...}).optional().transform(...) |
일반 converter 경로로 fallback 되는 형태:
| 형태 | 예시 |
|---|---|
| wildcard | /catalog/* |
| predicate | //book[@id="1"], //book[1] |
./가 없는 모호한 상대 경로 | title |
| 중첩 배열 | x.array(x.array(x.string(), './value'), '/group') |
| 배열 XPath와 element XPath를 동시에 지정한 배열 | x.array(x.string().xpath('./title'), '/book') |
| custom 또는 미지원 스키마 wrapper | 사용자 정의 schema subclass |
fallback은 동작 호환성을 유지하므로 이런 스키마도 compile() 후 정상 파싱됩니다. 다만 dispatch fast path의 성능 이점은 받지 않습니다.
스키마 재사용
섹션 제목: “스키마 재사용”// ❌ 매번 새 스키마 생성function parseUser(xml: string) { const schema = x.object({...}); return schema.parseSync(xml);}
// ✅ 스키마를 한 번 정의하고 재사용const userSchema = x.object({...});
function parseUser(xml: string) { return userSchema.parseSync(xml);}