Swift

XMLParser

GREEN.1229 2024. 5. 20. 18:55

안녕하세요. 그린입니다 🍏

이번 포스팅에서는 XMLParser에 대해 알아보겠습니다 🙋🏻

 

네트워크 통신을 하고 응답값으로 대부분 JSON 형식으로 처리를 하는것에 익숙할거에요.

그런데, 간혹 어떤 공공 API들은 XML 형식으로 응답을 주는 경우도 있습니다.

정말 대부분이 JSON으로 주긴하지만, 그래도 XML로 준다해서 포기할 수 없으니 XML 응답값을 어떻게 파싱하고 뷰에 나타낼 수 있는지 한번 XMLParser를 통해서 알아봅시다 😄

 

그럼 바로 레츠고! 🚀

 


XMLParser

우선 XMLParser에 대해 개념을 알아본 다음에 실제로 이용해볼께요.

XMLParser는 DTD 선언을 포함해 XML 도큐먼트의 이벤트를 다루는 파서라고 보시면 됩니다.

즉 쉽게 말해서 XML 형식을 우리 Swift 모델로 파싱해주는 역할을 하는 객체라고 볼 수 있어요.

 

class XMLParser : NSObject

 

NSObject를 상속받는 간단한 객체에요.

 

XMLParser는 XML 문서를 처리할 때 발견되는 항목들에 대해 딜리게이트를 사용하여 알려줍니다.

분석하는것과 분석에 대한 오류를 알려주는것 외에는 다른 작업은 수행하지 않아요.

흔히 파서 객체라고도 불리고 콜백에서 사용되지 않는 한, XMLParser는 주어진 인스턴스가 하나의 스레드에서만 사용되는 한 스레드로부터 안전한 클래스입니다.

 

JSON과 마찬가지로, 해당 파서 객체 초기화 시 URL이나 Data, Stream 모두를 이용할 수 있습니다.

 

 

중요한건 파싱을 위해 커스텀한 파서 객체를 만들때, XMLParserDelegate를 채택하여 이용해야 합니다.

 

XMLParserDelegate는 XML 파서가 파싱된 문서의 내용에 대해 대리자에게 알리기 위해 사용되는 인터페이스입니다.

 

unowned(unsafe) var delegate: (any XMLParserDelegate)? { get set }

protocol XMLParserDelegate

 

즉, XMLParser에는 delegate가 존재하고 여기에 XMLParserDelegate를 채택한 객체를 넣어주는거이죠.

그렇게 하게됨으로, 다양한 XML 처리 관련 기능들을 XMLParserDelegate의 메서드들에서 하도록 해주죠.

 

 

정말 다양하죠!?

 

딜리게이트까지 파서 객체에 넣어줬다면 이제 파싱을 시작하면 됩니다.

 

func parse() -> Bool

 

XMLParser에 존재하는 이 parse 메서드를 호출하여 이벤트 기반 구문 분석을 시작하는것이죠.

간단히 파싱하는 메서드라고 보면됩니다 😃

 

여기까지가 핵심적인 XMLParser의 역할, 기능과 XMLParserDelegate의 역할입니다.

 

사실 이렇게 개념만 나열하면 정신없는데, 실제로 코드를 보면서 구현해보면 아주 간단합니다!

 

바로 코드로 실습해볼까요?


XMLParser를 이용해 XML 문서 파싱하기

우선 공공 API를 이용하는건 파악하기 더 불편하니, 간단히 XML 데이터 응답 문서를 하나 아래와 같은 형식으로 만들어보고 프로젝트에 넣어 사용해볼께요.

 

<animals>
    <animal>
        <name>momo</name>
        <owner>green</owner>
        <age>10</age>
    </animal>
    <animal>
        <name>bbobbi</name>
        <owner>blue</owner>
        <age>3</age>
    </animal>   
    <animal>
        <name>chorong</name>
        <owner>yellow</owner>
        <age>15</age>
    </animal>
</animals>

 

이렇게 동물들의 모델 값을 가지는 동물 리스트 응답 값이 있습니다.

그런 다음 해당 파일은 프로젝트에 위치시킵니다.

 

다음으로 Swift 모델을 정의해야겠죠?

 

import Foundation

struct Animal: Identifiable {
  let id = UUID()
  let name: String
  let owner: String
  let age: String
}

 

이렇게 응답 필드값에 맞춰 늘 먹던것처럼 모델을 정의해줍니다.

 

그리고 이제 중요한 부분인 XMLParser를 이용해 커스텀한 파서 객체를 만듭니다.

 

import Foundation

class AnimalXMLParser: NSObject, XMLParserDelegate {
  private var animals: [Animal] = []
  private var currentElement = ""
  private var currentName = ""
  private var currentOwner = ""
  private var currentAge = ""
  
  func parse(data: Data) -> [Animal] {
    let parser = XMLParser(data: data)
    parser.delegate = self
    parser.parse()
    return animals
  }
  
  // MARK: - XMLParserDelegate
  func parser(
    _ parser: XMLParser,
    didStartElement elementName: String,
    namespaceURI: String?,
    qualifiedName qName: String?,
    attributes attributeDict: [String : String] = [:]
  ) {
    currentElement = elementName
    if currentElement == "animal" {
      currentName = ""
      currentOwner = ""
      currentAge = ""
    }
  }
  
  func parser(_ parser: XMLParser, foundCharacters string: String) {
    switch currentElement {
    case "name":
      currentName += string
    case "owner":
      currentOwner += string
    case "age":
      currentAge += string
    default:
      break
    }
  }
  
  func parser(
    _ parser: XMLParser,
    didEndElement elementName: String,
    namespaceURI: String?,
    qualifiedName qName: String?
  ) {
    if elementName == "animal" {
      let animal = Animal(name: currentName, owner: currentOwner, age: currentAge)
      animals.append(animal)
    }
  }
}

 

변수들은 각 현재 XML 문서를 읽을때 현재 읽고있는 필드들과 반환할 동물 리스트인 Animal 배열 타입이 필요합니다.

 

그리고 parse 메서드를 만들어서 우리는 URL대신 Data를 넣어줌으로 인자로 받고 Animal 배열을 반환 받도록 메서드를 만듭니다.

그 안에서 이제 XMLParser 객체를 data를 넣어서 초기화 시키고 딜리게이트를 설정합니다.

그리고 파싱을 시작하기 위해 내장된 parse 메서드를 호출한 후 마지막으로 값을 반환하면 됩니다.

 

이제 해당 객체 생성 시 XMLParserDelegate를 채택했으니, 필수적으로 파싱에 필요한 기능들을 가진 메서드들에서 구현을 해줍니다.

 

우선 XML 문서를 읽기 시작하면서 읽는 요소가 animal일 시 초기화를 시키죠.

즉, animal에 담긴 이름부터 나이 필드까지 추가해야함으로 초기화를 시키는것입니다.

 

그리고 두번째 메서드에서, 문자를 찾아서 값을 넣어주는데요.

여기서는 XML 문서를 읽으며 name 필드를 찾으면 현재 이름 변수에 그 값을 넣어주는식으로 모든 필드들을 매칭시켜둡니다.

 

그리고 마지막으로 해당 요소가 끝이나면 현재 이름부터 나이까지 저장된 변수 값들로 Animal 인스턴스 객체를 만들어서 Animal 배열에 추가하는것이죠.

 

이렇게 전체 XML 문서를 읽으며 파싱하는 반복작업을 통하는것입니다.

 

그럼 실제로 SwiftUI 뷰에서 확인해볼까요?

 

import SwiftUI

struct ContentView: View {
  @State private var animals: [Animal] = []
  
  var body: some View {
    NavigationView {
      List(animals) { animal in
        VStack(alignment: .leading) {
          Text(animal.name)
          
          Text(animal.owner)
          
          Text(animal.age)
        }
      }
      .navigationTitle("Animals")
      .task {
        loadXMLData()
      }
    }
  }
  
  private func loadXMLData() {
    if let url = Bundle.main.url(forResource: "animals", withExtension: "xml") {
      if let data = try? Data(contentsOf: url) {
        let parser = AnimalXMLParser()
        animals = parser.parse(data: data)
      }
    }
  }
}

 

간단히 리스트로 동물들을 모두 가져와서 이름부터 나이까지 표현해주는 코드입니다.

loadXMLData 메서드를 뷰가 나타날때 호출하여 가져오고 animals 상태 변수에 넣어주죠.

 

그럼 정상적으로 XML 문서를 파싱하고 표현해주는지 확인해볼까요~?

 

 

아주 정상적으로 잘 파싱하고 나타내주는걸 확인할 수 있습니다.

 

이렇게 XML 문서도 쉽게 파싱해볼 수 있어요.

다만 JSON보다 좀 번거롭긴하지만요ㅎㅎ

 


마무리

XML 문서를 파싱하는 경우가 있을까 싶었는데, 생각보다 아직도 많은 공공 API들은 XML로 응답을 주는 경우가 있더라구요.

처음 알았습니다 🥲

해당 예제 코드는 아래 제 깃헙 레포에서 편하게 확인 및 사용해보셔도 좋습니다!

 

playground/xmlParser at main · GREENOVER/playground

학습을 하며 간단한 예제들을 만들어 보는 작고 소중한 놀이터. Contribute to GREENOVER/playground development by creating an account on GitHub.

github.com

 


레퍼런스

 

XMLParser | Apple Developer Documentation

An event driven parser of XML documents (including DTD declarations).

developer.apple.com

 

XMLParserDelegate | Apple Developer Documentation

The interface an XML parser uses to inform its delegate about the content of the parsed document.

developer.apple.com

 

parse() | Apple Developer Documentation

Starts the event-driven parsing operation.

developer.apple.com