콘텐츠로 이동

StaxXmlParser - 비동기 XML 파싱

StaxXmlParser는 Java의 StAX(Streaming API for XML)에서 영감을 받은 JavaScript/TypeScript용 고성능 풀 기반 XML 파서입니다. 모든 파싱 작업이 완전히 비동기적으로 수행되어 메인 스레드를 차단하지 않고 대용량 XML 파일을 처리하기에 이상적입니다.

import { StaxXmlParser, XmlEventType } from 'stax-xml';
// XML 문자열로부터 ReadableStream 생성
const xmlContent = `
<books>
<book id="1">
<title>The Great Gatsby</title>
<author>F. Scott Fitzgerald</author>
</book>
<book id="2">
<title>To Kill a Mockingbird</title>
<author>Harper Lee</author>
</book>
</books>
`;
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(xmlContent));
controller.close();
}
});
// 풀 기반 방식으로 XML 파싱
const parser = new StaxXmlParser(stream);
const books = [];
let currentBook = null;
let currentText = '';
for await (const event of parser) {
switch (event.type) {
case XmlEventType.START_ELEMENT:
if (event.name === 'book') {
currentBook = { id: event.attributes?.id || '', title: '', author: '' };
}
currentText = '';
break;
case XmlEventType.CHARACTERS:
currentText += event.value;
break;
case XmlEventType.END_ELEMENT:
if (currentBook) {
if (event.name === 'title') {
currentBook.title = currentText.trim();
} else if (event.name === 'author') {
currentBook.author = currentText.trim();
} else if (event.name === 'book') {
books.push(currentBook);
currentBook = null;
}
}
break;
}
}
console.log(books);
// 출력: [
// { id: "1", title: "The Great Gatsby", author: "F. Scott Fitzgerald" },
// { id: "2", title: "To Kill a Mockingbird", author: "Harper Lee" }
// ]

더 구조화된 방식으로 XML 문자열 파싱

섹션 제목: “더 구조화된 방식으로 XML 문자열 파싱”
import { StaxXmlParser, isCharacters, isEndDocument, isEndElement, isStartElement } from 'stax-xml';
// XML 문자열에서 ReadableStream 을 만드는 섹션
const xmlContent = `
<books>
<book id="1">
<title>The Great Gatsby</title>
<author>F. Scott Fitzgerald</author>
</book>
<book id="2">
<title>To Kill a Mockingbird</title>
<author>Harper Lee</author>
</book>
</books>
`;
interface Book {
id: string
title: string
author: string
}
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(xmlContent));
controller.close();
}
});
// pull 방식으로 xml 파싱 시작
const parser = new StaxXmlParser(stream);
const books: Book[] = [];
for await (const event of parser) {
if (isEndDocument(event)) {
break;
}
if (isStartElement(event) && event.name === 'book') {
books.push(await parseBook(event.attributes?.id || '', parser));
}
}
/**
* 개별 book 파싱
*/
async function parseBook(id: string, parser: StaxXmlParser): Promise<Book> {
const currentBook = {
id: id,
title: '',
author: ''
};
for await (const event of parser) {
if (isEndElement(event) && event.name === 'book') {
break;
}
else if (isStartElement(event)) {
const charEvent = (await parser.next()).value;
if (isCharacters(charEvent) && event.name === 'title') {
currentBook.title = charEvent.value;
}
else if (isCharacters(charEvent) && event.name === 'author') {
currentBook.author = charEvent.value;
}
}
}
return currentBook;
}
console.log(books);
// Output: [
// { id: "1", title: "The Great Gatsby", author: "F. Scott Fitzgerald" },
// { id: "2", title: "To Kill a Mockingbird", author: "Harper Lee" }
// ]
import { StaxXmlParser, XmlEventType } from 'stax-xml';
async function parseRemoteXml(url: string) {
try {
// 원격 URL에서 XML 가져오기
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 응답 본문을 ReadableStream으로 가져오기
const xmlStream = response.body;
if (!xmlStream) {
throw new Error('No response body');
}
// XML 스트림을 직접 파싱
const parser = new StaxXmlParser(xmlStream);
const results = [];
let currentItem = {};
let currentText = '';
for await (const event of parser) {
switch (event.type) {
case XmlEventType.START_ELEMENT:
if (event.name === 'item') {
currentItem = {};
}
currentText = '';
break;
case XmlEventType.CHARACTERS:
currentText += event.value;
break;
case XmlEventType.END_ELEMENT:
if (event.name === 'title' || event.name === 'description') {
currentItem[event.name] = currentText.trim();
} else if (event.name === 'item') {
results.push(currentItem);
}
break;
}
}
return results;
} catch (error) {
console.error('원격 XML 파싱 오류:', error);
throw error;
}
}
// 사용 예제
const rssUrl = 'https://example.com/feed.xml';
const xmlApiUrl = 'https://api.example.com/data.xml';
// RSS 피드 파싱
parseRemoteXml(rssUrl)
.then(items => {
console.log('RSS 항목:', items);
})
.catch(error => {
console.error('RSS 파싱 실패:', error);
});
// API 응답 파싱
parseRemoteXml(xmlApiUrl)
.then(data => {
console.log('API 데이터:', data);
})
.catch(error => {
console.error('API 응답 파싱 실패:', error);
});
const parser = new StaxXmlParser(stream, {
addEntities: [
{ entity: 'custom', value: 'Custom Value' },
{ entity: 'special', value: '' }
]
});

While 기반 반복자 패턴 (StAX와 유사)

섹션 제목: “While 기반 반복자 패턴 (StAX와 유사)”
import { StaxXmlParser, XmlEventType, isStartElement, isEndElement } from 'stax-xml';
// 중첩 구조를 가진 XML 데이터
const xmlContent = `
<catalog>
<products>
<product id="1" category="electronics">
<name>Laptop</name>
<price currency="USD">999.99</price>
<specifications>
<cpu>Intel i7</cpu>
<memory>16GB</memory>
<storage>512GB SSD</storage>
</specifications>
</product>
<product id="2" category="books">
<name>JavaScript Guide</name>
<price currency="USD">29.99</price>
<author>John Doe</author>
</product>
</products>
</catalog>
`;
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(xmlContent));
controller.close();
}
});
// while 기반 반복을 사용한 메인 파싱 함수
async function parseCatalog(xmlStream: ReadableStream<Uint8Array>) {
const parser = new StaxXmlParser(xmlStream);
const catalog = { products: [] };
const iterator = parser[Symbol.asyncIterator]();
let result = await iterator.next();
while (!result.done) {
const event = result.value;
if (isStartElement(event) && event.name === 'products') {
await parseProducts(iterator, catalog);
}
result = await iterator.next();
}
return catalog;
}
// 제품들을 위한 별도 파싱 함수
async function parseProducts(iterator: AsyncIterator<any>, catalog: any) {
let result = await iterator.next();
while (!result.done) {
const event = result.value;
if (isStartElement(event) && event.name === 'product') {
const product = await parseProduct(iterator, event);
catalog.products.push(product);
} else if (isEndElement(event) && event.name === 'products') {
break;
}
result = await iterator.next();
}
}
// 개별 제품을 위한 별도 파싱 함수
async function parseProduct(iterator: AsyncIterator<any>, startEvent: any) {
const product = {
id: startEvent.attributes?.id || '',
category: startEvent.attributes?.category || '',
name: '',
price: { amount: '', currency: '' },
specifications: {},
author: ''
};
let result = await iterator.next();
let currentText = '';
while (!result.done) {
const event = result.value;
switch (event.type) {
case XmlEventType.START_ELEMENT:
currentText = '';
if (event.name === 'price') {
product.price.currency = event.attributes?.currency || '';
} else if (event.name === 'specifications') {
await parseSpecifications(iterator, product);
}
break;
case XmlEventType.CHARACTERS:
currentText += event.value;
break;
case XmlEventType.END_ELEMENT:
if (event.name === 'name') {
product.name = currentText.trim();
} else if (event.name === 'price') {
product.price.amount = currentText.trim();
} else if (event.name === 'author') {
product.author = currentText.trim();
} else if (event.name === 'product') {
return product;
}
break;
}
result = await iterator.next();
}
return product;
}
// 사양을 위한 별도 파싱 함수
async function parseSpecifications(iterator: AsyncIterator<any>, product: any) {
let result = await iterator.next();
let currentText = '';
while (!result.done) {
const event = result.value;
switch (event.type) {
case XmlEventType.START_ELEMENT:
currentText = '';
break;
case XmlEventType.CHARACTERS:
currentText += event.value;
break;
case XmlEventType.END_ELEMENT:
if (event.name === 'specifications') {
return;
} else if (event.name === 'cpu' || event.name === 'memory' || event.name === 'storage') {
product.specifications[event.name] = currentText.trim();
}
break;
}
result = await iterator.next();
}
}
// 사용법
parseCatalog(stream).then(result => {
console.log(JSON.stringify(result, null, 2));
// 출력:
// {
// "products": [
// {
// "id": "1",
// "category": "electronics",
// "name": "Laptop",
// "price": { "amount": "999.99", "currency": "USD" },
// "specifications": {
// "cpu": "Intel i7",
// "memory": "16GB",
// "storage": "512GB SSD"
// },
// "author": ""
// },
// {
// "id": "2",
// "category": "books",
// "name": "JavaScript Guide",
// "price": { "amount": "29.99", "currency": "USD" },
// "specifications": {},
// "author": "John Doe"
// }
// ]
// }
});
// 대용량 XML 파일의 효율적인 처리
const parser = new StaxXmlParser(largeXmlStream, {
maxBufferSize: 128 * 1024, // 128KB 버퍼
enableBufferCompaction: true
});
// 전체 파일을 메모리에 로드하지 않고 이벤트가 발생할 때마다 처리
for await (const event of parser) {
// 각 이벤트를 개별적으로 처리
processEvent(event);
}
// 네임스페이스가 있는 XML
const xmlWithNamespaces = `
<root xmlns:ns="http://example.com/namespace">
<ns:element>Content</ns:element>
</root>
`;
for await (const event of parser) {
if (event.type === XmlEventType.START_ELEMENT) {
console.log('엘리먼트:', event.name);
console.log('로컬 이름:', event.localName);
console.log('네임스페이스 URI:', event.uri);
console.log('접두사:', event.prefix);
}
}
  • START_DOCUMENT: XML 문서의 시작
  • END_DOCUMENT: XML 문서의 끝
  • START_ELEMENT: XML 여는 태그
  • END_ELEMENT: XML 닫는 태그
  • CHARACTERS: 태그 사이의 텍스트 내용
  • CDATA: CDATA 섹션 내용
  • ERROR: 파싱 오류 발생

타입 가드 함수는 XML 이벤트에 대한 런타임 타입 확인과 TypeScript 타입 좁히기를 제공합니다. 이러한 함수들은 XML 이벤트와 함께 작업할 때 타입 안전한 코드를 작성하는 데 필수적이며, TypeScript가 특정 이벤트 타입을 적절히 추론하고 타입별 속성에 접근할 수 있게 해줍니다.

타입 가드는 값의 타입을 결정하기 위해 런타임 확인을 수행하는 함수이며, 동시에 TypeScript에 타입 정보를 제공합니다. stax-xml의 맥락에서, 타입 가드는 타입 오류 없이 이벤트별 속성에 안전하게 접근할 수 있도록 도와줍니다.

import {
isStartDocument,
isEndDocument,
isStartElement,
isEndElement,
isCharacters,
isCdata,
isError
} from 'stax-xml';
  1. 타입 안전성: 특정 이벤트 타입에 존재하는 속성만 접근하도록 보장하여 런타임 오류를 방지
  2. IntelliSense 지원: 더 나은 IDE 자동완성 및 제안
  3. 깔끔한 코드: event.type === XmlEventType.START_ELEMENT와 같은 수동 타입 확인보다 더 읽기 쉬움
  4. 타입 좁히기: TypeScript가 자동으로 타입을 좁혀주어 타입별 속성에 접근 가능
import { StaxXmlParser, isStartElement, isEndElement, isCharacters } from 'stax-xml';
const xmlContent = `
<book id="123">
<title>TypeScript Guide</title>
<author>John Doe</author>
</book>
`;
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(xmlContent));
controller.close();
}
});
const parser = new StaxXmlParser(stream);
for await (const event of parser) {
// 타입 가드가 타입 안전성과 좁히기를 제공
if (isStartElement(event)) {
// TypeScript가 이것이 StartElementEvent임을 인식
console.log('엘리먼트:', event.name);
console.log('속성:', event.attributes);
// event.attributes를 여기서 안전하게 접근 가능
} else if (isCharacters(event)) {
// TypeScript가 이것이 CharactersEvent임을 인식
console.log('텍스트 내용:', event.value);
// event.value를 여기서 안전하게 접근 가능
} else if (isEndElement(event)) {
// TypeScript가 이것이 EndElementEvent임을 인식
console.log('닫는 엘리먼트:', event.name);
// event.name을 여기서 안전하게 접근 가능
}
}

오류 처리와 함께하는 고급 사용법

섹션 제목: “오류 처리와 함께하는 고급 사용법”
import { StaxXmlParser, isStartElement, isCharacters, isError } from 'stax-xml';
async function parseWithErrorHandling(xmlStream: ReadableStream<Uint8Array>) {
const parser = new StaxXmlParser(xmlStream);
const result = { elements: [], errors: [] };
for await (const event of parser) {
if (isError(event)) {
// 파싱 오류를 안전하게 처리
console.error('파싱 오류:', event.error.message);
result.errors.push(event.error);
break; // 오류 시 파싱 중단
} else if (isStartElement(event)) {
result.elements.push({
name: event.name,
attributes: event.attributes
});
}
}
return result;
}
함수목적참을 반환하는 경우사용 가능한 속성
isStartDocument(event)문서 시작START_DOCUMENT 이벤트type
isEndDocument(event)문서 끝END_DOCUMENT 이벤트type
isStartElement(event)여는 태그START_ELEMENT 이벤트type, name, localName, prefix, uri, attributes, attributesWithPrefix
isEndElement(event)닫는 태그END_ELEMENT 이벤트type, name, localName, prefix, uri
isCharacters(event)텍스트 내용CHARACTERS 이벤트type, value
isCdata(event)CDATA 섹션CDATA 이벤트type, value
isError(event)파싱 오류ERROR 이벤트type, error

여러 타입 가드를 사용한 복잡한 파싱 예제

섹션 제목: “여러 타입 가드를 사용한 복잡한 파싱 예제”
import {
StaxXmlParser,
isStartDocument,
isEndDocument,
isStartElement,
isEndElement,
isCharacters,
isCdata,
isError
} from 'stax-xml';
interface Article {
title: string;
content: string;
author: string;
publishDate: string;
}
async function parseArticles(xmlStream: ReadableStream<Uint8Array>): Promise<Article[]> {
const parser = new StaxXmlParser(xmlStream);
const articles: Article[] = [];
let currentArticle: Partial<Article> | null = null;
let currentElement = '';
let textBuffer = '';
for await (const event of parser) {
if (isStartDocument(event)) {
console.log('문서 파싱 시작...');
} else if (isEndDocument(event)) {
console.log('문서 파싱 완료');
break;
} else if (isError(event)) {
throw new Error(`파싱 실패: ${event.error.message}`);
} else if (isStartElement(event)) {
currentElement = event.name;
textBuffer = '';
if (event.name === 'article') {
currentArticle = {
title: '',
content: '',
author: '',
publishDate: event.attributes?.publishDate || ''
};
}
} else if (isCharacters(event) || isCdata(event)) {
// CHARACTERS와 CDATA 이벤트 모두 'value' 속성을 가짐
textBuffer += event.value;
} else if (isEndElement(event)) {
const trimmedText = textBuffer.trim();
if (currentArticle && event.name !== 'article') {
switch (event.name) {
case 'title':
currentArticle.title = trimmedText;
break;
case 'content':
currentArticle.content = trimmedText;
break;
case 'author':
currentArticle.author = trimmedText;
break;
}
} else if (event.name === 'article' && currentArticle) {
// 모든 필수 필드가 존재하는지 확인
if (currentArticle.title && currentArticle.content && currentArticle.author) {
articles.push(currentArticle as Article);
}
currentArticle = null;
}
textBuffer = '';
currentElement = '';
}
}
return articles;
}
// 사용 예제
const articleXml = `
<articles>
<article publishDate="2024-01-15">
<title>타입 가드 이해하기</title>
<author>김철수</author>
<content><![CDATA[타입 가드는 타입 안전한 TypeScript 개발에 필수적입니다...]]></content>
</article>
<article publishDate="2024-01-20">
<title>XML 파싱 모범 사례</title>
<author>이영희</author>
<content>XML을 파싱할 때는 항상 오류를 우아하게 처리해야 합니다...</content>
</article>
</articles>
`;
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(articleXml));
controller.close();
}
});
parseArticles(stream).then(articles => {
console.log('파싱된 기사:', articles);
}).catch(error => {
console.error('파싱 실패:', error);
});

타입 가드 미사용 (오류 가능성):

for await (const event of parser) {
if (event.type === XmlEventType.START_ELEMENT) {
// TypeScript가 event에 'attributes' 속성이 있는지 모름
// 런타임 오류를 발생시킬 수 있음
console.log(event.attributes?.id); // TypeScript 경고
}
}

타입 가드 사용 (타입 안전):

for await (const event of parser) {
if (isStartElement(event)) {
// TypeScript가 event가 StartElementEvent임을 인식
// 완전한 IntelliSense 지원과 타입 안전성
console.log(event.attributes.id); // TypeScript 경고 없음
}
}
class StaxXmlParser {
constructor(
xmlStream: ReadableStream<Uint8Array>,
options?: StaxXmlParserOptions
)
}
interface StaxXmlParserOptions {
encoding?: string; // 기본값: 'utf-8'
addEntities?: { entity: string, value: string }[];
autoDecodeEntities?: boolean; // 기본값: true
maxBufferSize?: number; // 기본값: 64KB
enableBufferCompaction?: boolean; // 기본값: true
}