깊이 중첩된 API 응답을 보며 "저 안에 파묻힌 딱 그 필드만 꺼내면 되는데"라고 생각해 본 적 있다면 — JSONPath가 바로 그 도구입니다. XML에서 XPath가 하는 일과 비슷한 방식으로 JSON을 조회하는 쿼리 언어입니다. 경로 표현식을 작성하면 일치하는 값이 반환됩니다. 루프도 없고, 수동 순회도 없습니다. Stefan Goessner가 2007년에 소개했고, 여러 구현체가 서로 조금씩 호환되지 않는 채로 떠돌다가 2024년에 RFC 9535로 공식 표준화되었습니다. JSON Path 도구에서 JSONPath 표현식을 직접 사용해볼 수도 있습니다.
예제 전체에서 사용할 데이터셋
추상적인 예제 대신, 현실적인 데이터를 사용해 봅시다 — 이커머스 주문 응답입니다. 주문 관리 API에서 반환될 법한 JSON입니다:
{
"order": {
"id": "ORD-9182",
"status": "shipped",
"customer": {
"id": "CUST-441",
"name": "Maria Chen",
"email": "[email protected]"
},
"shippingAddress": {
"street": "14 Maple Avenue",
"city": "Portland",
"state": "OR",
"zip": "97201",
"country": "US"
},
"lineItems": [
{
"sku": "WH-1042",
"name": "Wireless Headphones",
"qty": 1,
"unitPrice": 89.99,
"inStock": true
},
{
"sku": "CB-USB-C",
"name": "USB-C Charging Cable",
"qty": 2,
"unitPrice": 12.49,
"inStock": true
},
{
"sku": "SC-PRO-7",
"name": "Phone Stand Pro",
"qty": 1,
"unitPrice": 34.00,
"inStock": false
}
],
"totals": {
"subtotal": 148.97,
"shipping": 5.99,
"tax": 11.92,
"total": 166.88
}
}
}루트 연산자 $와 기본 탐색
모든 JSONPath 표현식은 문서의 루트를 나타내는 $로 시작합니다.
그 다음은 점(.)으로 탐색합니다. 주문 상태가 필요하다면 간단합니다:
$.order.status
// → "shipped"
$.order.customer.name
// → "Maria Chen"
$.order.totals.total
// → 166.88
$.order.shippingAddress.city
// → "Portland"키에 공백이나 특수문자가 포함되어 있거나 인덱스로 배열 요소에 접근할 때 유용한 대괄호 표기법도 사용할 수 있습니다. 두 스타일 모두 RFC 9535 문법 규칙에 따라 유효합니다:
// 점 표기법
$.order.customer.email
// 대괄호 표기법 — 동일한 결과
$['order']['customer']['email']
// 배열 인덱스 — 첫 번째 항목
$.order.lineItems[0].name
// → "Wireless Headphones"
// 마지막 항목 (음수 인덱스 — RFC 9535에서 지원)
$.order.lineItems[-1].name
// → "Phone Stand Pro"$[-1:](슬라이스 문법)과
$.array[-1](직접 음수 인덱스) 모두 RFC 9535에서 유효하지만, 일부 오래된
구현체에서는 지원하지 않습니다. 음수 인덱스를 사용할 경우 특정 라이브러리에서 테스트해 보세요.와일드카드 — 모든 자식 요소 한 번에 조회
* 와일드카드는 특정 레벨의 모든 요소와 일치합니다. lineItems[0],
lineItems[1] 등을 따로 요청하는 대신 한 번에 모두 가져올 수 있습니다:
// 모든 항목 이름
$.order.lineItems[*].name
// → ["Wireless Headphones", "USB-C Charging Cable", "Phone Stand Pro"]
// 모든 항목 단가
$.order.lineItems[*].unitPrice
// → [89.99, 12.49, 34.00]
// 모든 항목 SKU
$.order.lineItems[*].sku
// → ["WH-1042", "CB-USB-C", "SC-PRO-7"]API 응답의 배열을 다룰 때 가장 자주 사용하는 패턴입니다. 애플리케이션 코드에서 배열을 순회하는 대신, 비즈니스 로직에 도달하기 전에 필요한 필드를 정확히 추출할 수 있습니다.
재귀 탐색 — 어떤 깊이에서든 값 찾기
.. 연산자는 전체 문서 트리를 재귀적으로 탐색합니다.
키 이름이 일치하는 모든 노드를 위치에 관계없이 수집하는 깊이 우선 탐색과 같습니다:
// 문서 어디서든 모든 "name" 필드 찾기
$..name
// → ["Maria Chen", "Wireless Headphones", "USB-C Charging Cable", "Phone Stand Pro"]
// 어디서든 모든 "city" 필드 찾기
$..city
// → ["Portland"]
// 어떤 깊이에서든 "id" 필드 모두 찾기
$..id
// → ["ORD-9182", "CUST-441"]$..name이 고객 이름과 모든 상품 이름을 찾는 것을 주목하세요 —
깊이와 무관합니다. 구조를 모르는 낯선 JSON 덩어리를 받았을 때 특정 키의 모든 값을 찾고 싶을 때
스키마 탐색에 강력하게 활용할 수 있습니다.
JSON 자체를 먼저 확인하고 싶다면 JSON 검증기와
JSON 포매터가 유용한 출발점입니다.
배열 슬라이싱
JSONPath는 배열에 파이썬 스타일의 슬라이스 표기법을 차용합니다. 형식은
[start:end:step]이며 각 부분을 생략할 수 있습니다.
RFC 9535 § 2.3.5에 문서화되어 있습니다:
// 처음 두 항목
$.order.lineItems[0:2]
// → [{ sku: "WH-1042", ... }, { sku: "CB-USB-C", ... }]
// 인덱스 1부터 끝까지
$.order.lineItems[1:]
// → [{ sku: "CB-USB-C", ... }, { sku: "SC-PRO-7", ... }]
// 마지막 항목만 (슬라이스 문법 — 널리 지원됨)
$.order.lineItems[-1:]
// → [{ sku: "SC-PRO-7", ... }]
// 한 칸씩 건너뛰기 (step 2)
$.order.lineItems[0::2]
// → [{ sku: "WH-1042", ... }, { sku: "SC-PRO-7", ... }]필터 표현식 — 조건으로 조회
JSONPath가 진가를 발휘하는 부분입니다. 필터 문법 [?(...)]을 사용하면
필드 값으로 배열 항목을 조회할 수 있습니다. @ 기호는 현재 검사 중인 항목을 나타냅니다.
Stefan Goessner의
2007년 원본 글에서
이 문법을 소개했고 RFC 9535가 공식화했습니다:
// $50 미만의 항목
$.order.lineItems[?(@.unitPrice < 50)]
// → [{ sku: "CB-USB-C", unitPrice: 12.49, ... }, { sku: "SC-PRO-7", unitPrice: 34.00, ... }]
// 현재 재고 있는 항목만
$.order.lineItems[?(@.inStock == true)]
// → [{ sku: "WH-1042", ... }, { sku: "CB-USB-C", ... }]
// 수량이 1보다 많은 항목
$.order.lineItems[?(@.qty > 1)]
// → [{ sku: "CB-USB-C", qty: 2, ... }]
// 재고 없는 항목
$.order.lineItems[?(@.inStock == false)].name
// → ["Phone Stand Pro"]조건을 결합할 수도 있습니다. 대부분의 구현체는 필터 표현식 내에서 &&와 ||를
지원하며, [?(@.inStock == true && @.unitPrice < 30)]과 같이 작성할 수 있습니다.
해당 라이브러리 문서를 확인하세요 — 연산자 지원에 관한 동작 방식은 RFC 이전 구현체 간의 주요 불일치 중 하나로,
RFC 9535 표준화가 이를 해결하려 했습니다.
[?(@.fieldName)]. 예를 들어, 일부 항목에만
discountCode 필드가 있다면, $.order.lineItems[?(@.discountCode)]는
할인이 적용된 항목만 반환합니다.JavaScript에서 JSONPath 사용
JavaScript에는 아직 JSONPath 내장 지원이 없습니다(XML의 document.evaluate()와 달리).
라이브러리가 필요합니다. 가장 많이 사용되는 두 가지는
jsonpath와
jsonpath-plus입니다.
jsonpath-plus는 더 활발히 유지 관리되며 RFC 9535와 더 잘 정렬되어 있습니다:
npm install jsonpath-plusimport { JSONPath } from 'jsonpath-plus';
const order = {
order: {
id: 'ORD-9182',
status: 'shipped',
customer: { name: 'Maria Chen', email: '[email protected]' },
lineItems: [
{ sku: 'WH-1042', name: 'Wireless Headphones', qty: 1, unitPrice: 89.99, inStock: true },
{ sku: 'CB-USB-C', name: 'USB-C Charging Cable', qty: 2, unitPrice: 12.49, inStock: true },
{ sku: 'SC-PRO-7', name: 'Phone Stand Pro', qty: 1, unitPrice: 34.00, inStock: false }
],
totals: { subtotal: 148.97, shipping: 5.99, tax: 11.92, total: 166.88 }
}
};
// 모든 상품 이름 가져오기
const names = JSONPath({ path: '$.order.lineItems[*].name', json: order });
console.log(names);
// [ 'Wireless Headphones', 'USB-C Charging Cable', 'Phone Stand Pro' ]
// $50 미만의 재고 있는 항목
const affordable = JSONPath({ path: '$.order.lineItems[?(@.inStock == true && @.unitPrice < 50)]', json: order });
console.log(affordable.map(item => item.name));
// [ 'USB-C Charging Cable' ]
// 주문 합계
const total = JSONPath({ path: '$.order.totals.total', json: order });
console.log(total[0]); // 166.88JSONPath()는 단일 값을 조회하더라도 항상 배열을 반환합니다.
이는 의도된 동작입니다: 경로 표현식은 선택자이며, 선택자는 0개, 1개 또는 여러 개의 노드와 일치할 수 있습니다.
따라서 고유한 값을 조회할 때는 항상 result[0]으로 가져오세요.
Python에서 JSONPath 사용
Python에서는
jsonpath-ng
라이브러리가 가장 완전한 옵션입니다. 핵심 스펙과 대부분의 필터 표현식 문법을 지원합니다:
pip install jsonpath-ngfrom jsonpath_ng import parse
order = {
"order": {
"id": "ORD-9182",
"status": "shipped",
"customer": {"name": "Maria Chen", "email": "[email protected]"},
"lineItems": [
{"sku": "WH-1042", "name": "Wireless Headphones", "qty": 1, "unitPrice": 89.99, "inStock": True},
{"sku": "CB-USB-C", "name": "USB-C Charging Cable", "qty": 2, "unitPrice": 12.49, "inStock": True},
{"sku": "SC-PRO-7", "name": "Phone Stand Pro", "qty": 1, "unitPrice": 34.00, "inStock": False}
],
"totals": {"subtotal": 148.97, "shipping": 5.99, "tax": 11.92, "total": 166.88}
}
}
# 표현식을 한 번 파싱하고 재사용 (더 효율적)
expr = parse("$.order.lineItems[*].name")
names = [match.value for match in expr.find(order)]
print(names)
# ['Wireless Headphones', 'USB-C Charging Cable', 'Phone Stand Pro']
# 모든 항목을 가져와 재고 확인
expr2 = parse("$.order.lineItems[*]")
for item in [m.value for m in expr2.find(order)]:
status = "in stock" if item["inStock"] else "OUT OF STOCK"
print(f"{item['name']} (${item['unitPrice']}) — {status}")
# 출력:
# Wireless Headphones ($89.99) — in stock
# USB-C Charging Cable ($12.49) — in stock
# Phone Stand Pro ($34.0) — OUT OF STOCKparse() + find() 패턴은 프로덕션 사용에 적합한 방식입니다 —
경로 문자열을 매번 다시 파싱하는 대신 표현식을 한 번 컴파일하고 여러 문서에 적용하세요.
GitHub의 jsonpath-ng 문서에는
확장 필터 문법과 일치하는 노드를 수정하는 update() 메서드에 대한 더 자세한 내용이 있습니다.
JSONPath vs jq — 어떤 것을 선택해야 할까?
JSONPath와 jq를 혼동하는 경우가 있습니다 — 둘 다 "JSON을 조회"하지만 서로 다른 문제를 해결합니다. 실용적인 비교는 다음과 같습니다:
- JSONPath는 애플리케이션에 내장되도록 설계된 경로 표현 언어입니다. 값을 조회하고 추출합니다. JavaScript, Python, Java 코드 또는 Kubernetes 선택자 같은 설정 파일 내에서 사용합니다.
- jq는 자체 프로그래밍 언어를 갖춘 완전한 커맨드라인 프로세서입니다. 조회, 변환, 재구성, 필터링, 새 값 계산이 가능합니다. 셸 스크립트와 터미널에서 일회성 데이터 처리에 적합한 도구입니다.
- JSONPath 사용: 특히 라이브러리가 내장 가능하고 경량이어야 할 때 JSON에서 프로그래밍 방식으로 필드를 추출해야 하는 애플리케이션 코드를 작성할 때.
- jq 사용: 커맨드라인에서 작업할 때, 셸 스크립트를 작성할 때, API 응답을 디버깅할 때, 또는 단순 추출을 넘어서는 변환(필드 이름 변경, 집계, 새 구조 생성)이 필요할 때.
- jq 문법이 더 강력하지만 복잡하기도 합니다 —
jq '.order.lineItems[] | select(.inStock) | .name'vs JSONPath의$.order.lineItems[?(@.inStock)].name. 순수 추출만 할 때는 JSONPath가 더 읽기 쉽습니다.
중간 지점도 있습니다: JavaScript로 이미 작업 중이고 가끔만 변환이 필요하다면,
Lodash의 _.get()이
의존성 없이 간단한 경로 접근을 커버합니다. 하지만 와일드카드, 재귀, 필터 표현식이 관련된 경우라면
적절한 JSONPath 라이브러리를 사용할 가치가 있습니다.
마무리
JSONPath는 "for 루프에서 이 객체를 수동으로 탐색하기"와 "jq 프로세스 실행하기" 사이의 실질적인 공백을 채웁니다.
$, ., [*],
.., [?(filter)]에 익숙해지면, 특히 일부 필드만 필요한 대용량 API 페이로드를 다룰 때
자연스럽게 손이 가게 됩니다.
RFC 9535 표준화 덕분에
문법이 이제 안정적이어서 오늘 작성하는 표현식이 호환 라이브러리에서 일관되게 동작할 것입니다.
JSON Path 도구에서 표현식을 직접 테스트해 보세요 —
JSON을 붙여넣고, 경로를 작성하면 결과를 즉시 확인할 수 있습니다. JSON을 먼저 정리해야 한다면
JSON 포매터와 JSON 검증기도 바로 옆에 있습니다.