콘텐츠로 이동

Converter - 핵심 개념

이 가이드는 StAX-XML Converter를 효과적으로 사용하기 위해 이해해야 할 기본 개념을 다룹니다.

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);

모든 스키마 메서드는 새 스키마 인스턴스를 반환하여 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); // undefined
console.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는 XML 문서에서 요소를 선택하는 주요 방법입니다.

// 루트에서 절대 경로
x.string().xpath('/root/element/child')
// 하위 요소 검색 (문서 어디서나)
x.string().xpath('//element')
// 속성 접근
x.string().xpath('/@id')
x.string().xpath('//@href')
// 결합
x.string().xpath('/root/item/@name')

String 및 Number 스키마:

const title = x.string().xpath('/book/title');
const price = x.number().xpath('/book/price');

Object 스키마:

// 개별 필드의 XPath
const 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는 파싱이 실패할 때 상세한 에러 정보를 제공합니다:

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 // 중첩된 객체 스키마
});
// 동기 - 작은 문서에 약간 더 빠름
const result = schema.parseSync(smallXml);
// 비동기 - 큰 문서에 더 좋음
const result = await schema.parse(largeXml);
// ❌ 느림 - 전체 문서를 여러 번 검색
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 필드, transformx.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);
}