StaxXmlParser - 비동기 XML 파싱
StaxXmlParser - 비동기 XML 파싱
섹션 제목: “StaxXmlParser - 비동기 XML 파싱”StaxXmlParser
는 Java의 StAX(Streaming API for XML)에서 영감을 받은 JavaScript/TypeScript용 고성능 풀 기반 XML 파서입니다. 모든 파싱 작업이 완전히 비동기적으로 수행되어 메인 스레드를 차단하지 않고 대용량 XML 파일을 처리하기에 이상적입니다.
🔧 빠른 시작
섹션 제목: “🔧 빠른 시작”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" }// ]
Fetch를 사용한 원격 XML 파싱
섹션 제목: “Fetch를 사용한 원격 XML 파싱”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);}
네임스페이스 처리
섹션 제목: “네임스페이스 처리”// 네임스페이스가 있는 XMLconst 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';
타입 가드 사용의 이점
섹션 제목: “타입 가드 사용의 이점”- 타입 안전성: 특정 이벤트 타입에 존재하는 속성만 접근하도록 보장하여 런타임 오류를 방지
- IntelliSense 지원: 더 나은 IDE 자동완성 및 제안
- 깔끔한 코드:
event.type === XmlEventType.START_ELEMENT
와 같은 수동 타입 확인보다 더 읽기 쉬움 - 타입 좁히기: 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);});
비교: 타입 가드 사용 vs 미사용
섹션 제목: “비교: 타입 가드 사용 vs 미사용”타입 가드 미사용 (오류 가능성):
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 경고 없음 }}
📚 API 참조
섹션 제목: “📚 API 참조”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}