본문 바로가기

파일 형식 백과사전

XML로 받은 데이터를 JSON으로 바꾸고 싶을 때, 표준 라이브러리만으로 해결하기

API나 설정 파일을 XML로 받았는데, 쓰려는 라이브러리나 프런트엔드는 JSON만 받는 상황을 만나신 적이 있을 것입니다. 둘 다 데이터를 담는 형식인데 모양이 달라서, 어떻게 옮겨야 깔끔한지 막연하게 느껴집니다.

변환 자체는 파이썬 표준 라이브러리만으로 충분히 됩니다. 다만 XML에는 JSON에 없는 개념이 두 가지 있어서, 그 부분을 어떻게 처리하느냐가 결과 품질을 가릅니다. 한글이 섞인 실제 XML을 직접 변환해 가며 짚어 보겠습니다.

 

먼저 알아야 할 두 가지 걸림돌

XML과 JSON은 닮았지만 다음 두 지점에서 1대1로 떨어지지 않습니다. 이 둘만 정리하면 나머지는 단순 매핑입니다.

XML의 개념JSON에는?이 글의 처리 방식
속성 (attribute), 예: id="b1"없음키 앞에 @를 붙여 보존
속성과 본문 텍스트가 한 태그에 같이 있음없음본문은 #text 키로 분리
같은 태그 이름 반복, 예: book이 여러 개배열로 표현2개 이상이면 리스트로 묶음

속성은 태그 안에 이름="값"으로 들어가는 추가 정보입니다. JSON 객체에는 이런 자리가 없어서, 그냥 키로 끌어내리되 일반 자식 요소와 헷갈리지 않게 표시를 달아 둡니다.


변환할 샘플 XML

속성, 중첩 요소, 한글, 반복 태그를 모두 담은 작은 서점 데이터를 준비했습니다. book이 두 개 들어 있는 점을 눈여겨봐 주세요.

<store name="한빛문고" city="서울">
  <book id="b1" lang="ko">
    <title>파이썬 입문</title>
    <price currency="KRW">18000</price>
  </book>
  <book id="b2" lang="en">
    <title>Clean Code</title>
    <price currency="USD">42</price>
  </book>
  <notice>재고는 매장에 따라 다릅니다</notice>
</store>

변환 함수와 실행 순서

아래 함수는 추가 설치 없이 표준 라이브러리 xml.etree.ElementTreejson만 씁니다. 측정 환경은 윈도우 11, 파이썬 3.14.3입니다.

동작은 세 단계입니다. 첫째 속성을 @ 키로 옮기고, 둘째 자식 요소를 재귀로 변환하며, 셋째 같은 태그가 두 번째로 나오면 그때 리스트로 바꿉니다.

import json
import xml.etree.ElementTree as ET

def elem_to_dict(elem):
    node = {}
    # 1) 속성은 @ 접두사로 보존
    for k, v in elem.attrib.items():
        node["@" + k] = v
    children = list(elem)
    if children:
        for child in children:
            data = elem_to_dict(child)
            tag = child.tag
            if tag in node:
                # 3) 같은 태그가 또 나오면 리스트로 전환
                if not isinstance(node[tag], list):
                    node[tag] = [node[tag]]
                node[tag].append(data)
            else:
                node[tag] = data
    else:
        text = (elem.text or "").strip()
        # 2) 속성이 있으면 본문은 #text 키로 분리
        if node:
            if text:
                node["#text"] = text
        else:
            return text   # 속성도 자식도 없으면 값 그대로
    return node

root = ET.fromstring(xml_text)  # xml_text는 위 샘플 문자열
result = {root.tag: elem_to_dict(root)}
print(json.dumps(result, ensure_ascii=False, indent=2))

json.dumpsensure_ascii=False를 넣은 이유는, 빼면 한글이 \uXXXX 형태로 도망가기 때문입니다. 한글을 한글 그대로 남기려면 이 옵션이 필요합니다.


실제로 나온 JSON

위 코드를 그대로 돌려 얻은 출력입니다. 윈도우 콘솔은 기본 인코딩 탓에 한글이 깨져 보일 수 있어, 출력은 UTF-8로 받아 확인했습니다.

{
  "store": {
    "@name": "한빛문고",
    "@city": "서울",
    "book": [
      {
        "@id": "b1",
        "@lang": "ko",
        "title": "파이썬 입문",
        "price": {
          "@currency": "KRW",
          "#text": "18000"
        }
      },
      {
        "@id": "b2",
        "@lang": "en",
        "title": "Clean Code",
        "price": {
          "@currency": "USD",
          "#text": "42"
        }
      }
    ],
    "notice": "재고는 매장에 따라 다릅니다"
  }
}

store의 name과 city는 @name, @city로 들어가 자식 요소와 구분됩니다. price는 통화 속성과 본문 숫자를 함께 가지므로 @currency#text로 나뉘었습니다. notice처럼 속성도 자식도 없는 태그는 군더더기 없이 값만 남습니다.

꼭 짚어야 할 함정: book이 하나면 배열이 아니다

위에서 book은 두 개라 리스트로 나왔습니다. 그런데 같은 코드에 book을 하나만 넣으면 결과가 이렇게 달라집니다.

{
  "store": {
    "@name": "한빛문고",
    "book": {
      "@id": "b1",
      "title": "파이썬 입문"
    }
  }
}

같은 스키마인데 book이 한 번은 리스트, 한 번은 객체로 나옵니다. 이걸 받는 쪽 코드가 for b in data["store"]["book"]처럼 항상 리스트로 가정하면, book이 하나일 때 객체의 키를 돌면서 엉뚱하게 동작합니다. XML을 JSON으로 옮길 때 가장 많이 터지는 지점이 바로 이 개수에 따라 모양이 바뀌는 문제입니다.

해결은 간단합니다. 반복될 수 있는 태그 이름을 미리 정해 두고, 그 태그는 개수와 무관하게 항상 리스트로 강제하면 됩니다. 위 함수에서 force_list = {"book"} 같은 집합을 받아, 해당 태그면 처음부터 리스트에 담도록 한 줄만 고치면 일관성이 생깁니다.

자주 묻는 질문

Q. 변환했더니 18000이 문자열 "18000"으로 들어갑니다.
XML 본문은 전부 텍스트라서 숫자도 문자열로 옵니다. 숫자가 필요하면 변환 후 해당 키만 int나 float로 바꿔 주어야 합니다. 자동 판별을 넣으면 우편번호 앞자리 0이 사라지는 부작용이 있어, 필요한 키만 손보는 편이 안전합니다.

Q. lxml을 써야 하나요?
이 정도 변환은 표준 라이브러리만으로 충분합니다. 네임스페이스가 복잡하거나 주석, CDATA까지 다뤄야 하면 lxml 같은 외부 도구가 편할 수 있습니다.

 

속성은 @, 본문은 #text로 분리하고, 반복될 태그는 개수와 상관없이 항상 리스트로 고정하면 변환이 안정됩니다.