Skip to content
블로그로 돌아가기
튜토리얼

JSONPath 문법 가이드: 예제로 배우는 JSON 쿼리와 필터

복사해 쓰는 예제로 JSONPath 문법을 배우세요 — 루트, 재귀 하강, 와일드카드, 슬라이스, 필터 표현식. 모든 쿼리를 온라인에서 바로 테스트하세요.

11 분 소요

JSONPath 문법 가이드: 예제로 배우는 JSON 쿼리와 필터

JSONPath는 JSON을 위한 쿼리 언어로, XPath가 XML의 쿼리 언어인 것과 같습니다. 경로 표현식을 작성하면 평가기가 일치하는 모든 값을 반환합니다. 서점 문서에서 모든 저자 이름을 가져오려면 $.store.book[*].author라고 쓰면 됩니다. 그러면 순회 코드를 한 줄도 짜지 않고도 저자 목록이 그대로 나옵니다.

이 가이드는 JSONPath 문법의 모든 선택자를 읽으면서 바로 실행하는 복사용 예제와 함께 다룹니다. 방언(dialect)은 두 가지입니다. 2007년 Goessner 방언은 사실상의 고전 방언이고, RFC 9535는 2024년 2월에 나온 IETF 공식 표준입니다. 두 방언은 흔히 쓰는 경로에서는 일치하지만 엣지 케이스에서 갈라지므로, 차이가 나오는 지점마다 짚어 드립니다. 아래 모든 표현식은 JSONPath 테스터에서 직접 실행하고 두 엔진을 오가며 비교합니다.

아래는 선택자 치트시트입니다. 글의 나머지에서는 각 행을 공유 JSON 문서 하나에 대해 실제 예제로 풀어 설명합니다.

선택자의미예제
$문서의 루트$
@현재 요소(필터 안에서)[?(@.price < 10)]
.name / ['name']자식 멤버$.store.book
..재귀 하강$..author
*모든 요소 / 멤버$.store.book[*]
[0]배열 인덱스$.store.book[0]
[start:end:step]배열 슬라이스(반열림)$.store.book[0:2]
[a,b]이름 / 인덱스의 합집합$.store.book[0,2]
[?()]필터 표현식$.store.book[?(@.price < 10)]
length() count() match() search() value()RFC 9535 함수(필터 전용)[?length(@.title) > 15]

JSONPath란 무엇인가?

JSONPath는 JSON 문서에서 노드를 선택하는 선언형 쿼리 언어입니다. 객체와 배열을 훑는 반복문을 작성하는 대신, 원하는 위치를 경로로 기술하면 평가기가 일치하는 값을 반환합니다. XPath가 XML에 하는 일과 같다고 생각하면 됩니다. 구조를 단계적으로 짚어 가는 선택자들로 이루어진 경로입니다.

JSONPath는 개발자가 JSON을 다루는 곳이라면 어디에나 등장합니다. API 응답에서 필드를 뽑아낼 때, 통합 테스트에서 값을 단언할 때, Kubernetes·AWS Step Functions·Azure Logic Apps의 파이프라인 설정에서 필드를 지정할 때, 그리고 순회 로직을 손으로 짜지 않고 크고 불규칙한 JSON에서 데이터를 추출할 때 사용합니다.

방언이 갈린 데는 짧은 역사가 있습니다. Stefan Goessner는 2007년에 JSONPath를 제안했습니다. 빠르게 퍼져 사실상의 표준이 되었지만, 정식으로 명세화된 적이 없어서 구현마다 세부 사항이 조금씩 달라졌습니다. IETF는 2024년 2월 RFC 9535, 즉 최초의 공식 JSONPath 명세로 그 간극을 메웠습니다. 두 방언은 오늘날 모두 살아 있어서, 같은 표현식이라도 어떤 라이브러리가 실행하느냐에 따라 다르게 동작합니다.

쿼리를 시작하기 전에 구조를 먼저 읽어 두면 도움이 됩니다. 지저분한 입력은 JSON 포맷터로 보기 좋게 정리해 중첩 구조가 한눈에 들어오게 하세요.

예제 문서

아래 모든 예제는 고전적인 Goessner 서점 JSON에 대해 실행됩니다. 한 번 붙여 넣고 계속 재사용하세요.

{
  "store": {
    "book": [
      { "title": "Sayings of the Century", "author": "Nigel Rees", "price": 8.95 },
      { "title": "Sword of Honour", "author": "Evelyn Waugh", "price": 12.99 },
      { "title": "Moby Dick", "author": "Herman Melville", "price": 8.99 },
      { "title": "The Lord of the Rings", "author": "J. R. R. Tolkien", "price": 22.99 }
    ],
    "bicycle": { "color": "red", "price": 19.95 }
  }
}

제목·저자·가격을 가진 책 네 권과 자전거 하나입니다. 가격은 8.95, 12.99, 8.99, 22.99라는 점을 기억해 두세요. 이 값들이 뒤에 나올 필터 결과를 좌우합니다.

루트, 자식, 재귀 하강 ($ . ..)

모든 표현식은 $로 표기하는 루트에서 시작합니다. 거기서부터 점(dot)이나 대괄호 표기로 자식에 들어갑니다. 두 방식은 동등합니다.

$.store.book          → the book array
$['store']['book']    → identical result

대괄호 표기는 키에 공백, 점, 그 밖의 특수 문자가 들어 있을 때 필요합니다. $['first name']은 동작하지만 $.first name은 동작하지 않습니다.

.. 연산자는 재귀 하강입니다. 직접 자식만이 아니라 문서의 모든 단계를 탐색하며, 뒤따르는 선택자에 일치하는 것을 깊이에 상관없이 전부 모읍니다.

$..author
→ ["Nigel Rees", "Evelyn Waugh", "Herman Melville", "J. R. R. Tolkien"]

..와 전체 경로 중 무엇을 쓸 것인가

재귀 하강은 편리하지만 뭉툭합니다. $..price는 트리 어디에 있든 모든 가격에 일치합니다. 의도하지 않았을 수 있는 store.bicycle.price까지 포함합니다. 구조를 알고 있다면 경로를 명시해 쿼리를 정확하게 유지하세요.

$..price                  → [8.95, 12.99, 8.99, 22.99, 19.95]  (includes the bicycle)
$.store.book[*].price     → [8.95, 12.99, 8.99, 22.99]         (only books)

..는 정말로 불규칙하거나 알 수 없는 구조에만 아껴 쓰세요. 편의를 얻는 대신 제어를 내주는 셈입니다. 데이터를 잘 알수록 명시적 경로 쪽이 낫습니다.

와일드카드, 인덱스, 배열 슬라이스 (* [0] [start:end:step])

와일드카드 *는 배열의 모든 요소나 객체의 모든 멤버를 선택합니다.

$.store.book[*].title
→ ["Sayings of the Century", "Sword of Honour", "Moby Dick", "The Lord of the Rings"]

배열 인덱스는 0부터 시작하며, 음수 인덱스는 끝에서부터 셉니다.

$.store.book[0].title     → ["Sayings of the Century"]
$.store.book[-1].title    → ["The Lord of the Rings"]

슬라이스는 Python·JavaScript와 같은 반열림 [start:end:step] 규칙을 따릅니다. start는 포함되고 end는 제외됩니다.

$.store.book[0:2].title   → ["Sayings of the Century", "Sword of Honour"]

이 표현식은 세 권이 아니라 권을 반환합니다. 인덱스 0과 1을 가져오고 인덱스 2 앞에서 멈춥니다. 끝 경계를 제외하는 이 규칙은 가장 흔한 JSONPath 버그이므로, 머릿속에 새겨 둘 만합니다.

[0:2]   → first TWO elements (indices 0, 1)   ← correct
[0:3]   → first THREE elements (indices 0, 1, 2)

경계를 생략하면 끝까지 가고, step을 더하면 N번째마다 하나씩 가져옵니다.

$.store.book[2:].title    → ["Moby Dick", "The Lord of the Rings"]
$.store.book[:3].title    → first three titles
$.store.book[::2].title   → ["Sayings of the Century", "Moby Dick"]  (every other)

필터 표현식 [?()]

필터 [?()]는 술어(predicate)가 참인 요소만 남기며, 필터 안에서 @는 검사 중인 현재 요소를 가리킵니다.

가격이 10 미만인 책을 선택하려면 이렇게 합니다.

$.store.book[?(@.price < 10)].title
→ ["Sayings of the Century", "Moby Dick"]

서점 가격(8.95, 12.99, 8.99, 22.99)에 대해 두 권이 조건을 만족합니다. 필터 술어를 단계별로 만드는 방법은 다음과 같습니다.

  1. 리터럴과 비교하세요. ==, !=, <, <=, >, >=를 사용합니다. 예를 들어 @.price > 10.
  2. 문자열을 일치시키세요. 문자열 리터럴은 작은따옴표를 씁니다. @.author == 'Nigel Rees'.
  3. 존재 여부를 검사하세요. 멤버 참조만 적으면 그 멤버를 가진 요소를 선택합니다. [?(@.isbn)]isbn이 있는 책만 남깁니다.
  4. 조건을 결합하세요. &&||로 술어를 잇습니다. [?(@.price < 10 && @.author == 'Herman Melville')].

가장 잦은 필터 실수는 @의 범위입니다. 술어 안에서 현재 요소는 @이지 $가 아닙니다. $.price라고 쓰면 검사 중인 책이 아니라 문서 루트를 가리킵니다.

$.store.book[?($.price < 10)]   → wrong scope, matches nothing useful
$.store.book[?(@.price < 10)]   → correct: each book's own price

필터에서의 RFC 9535 vs Classic

두 방언은 공백과 따옴표에서 갈라집니다. Classic은 관대해서 공백이 없는 [?(@.price<10)]도 문제없이 파싱됩니다. RFC 9535는 문법을 정확히 따르며 필터 작성 방식에 더 엄격합니다. 다른 곳에서는 동작하던 필터가 실패한다면 공백과 엔진을 확인하세요. 필터를 깔끔하게(연산자 주위에 공백, 문자열은 작은따옴표) 유지하면, 결국 어떤 라이브러리가 실행하든 동일하게 평가됩니다.

합집합 선택자 — 여러 키를 한 번에 선택 ([a,b])

합집합 선택자는 대괄호 하나 안에 여러 이름이나 인덱스를 나열해 그 모두를 모읍니다.

$.store.book[0]['title','author']
→ ["Sayings of the Century", "Nigel Rees"]

합집합은 인덱스로도 동작하며, 다른 선택자와 섞어 고정된 투영(projection)을 만들 수 있습니다.

$.store.book[0,2].title          → ["Sayings of the Century", "Moby Dick"]
$.store.book[*]['title','price']  → title and price of every book

합집합은 객체 전체나 와일드카드 훑기 대신 특정 필드 몇 개만 원할 때 알맞은 도구입니다.

RFC 9535 함수: length, count, match, search, value

RFC 9535는 다섯 개의 표준 함수 확장을 정의합니다. 여기에는 많은 사람이 걸려 넘어지고 다른 가이드들이 자주 틀리는 규칙이 하나 있습니다.

이 함수들은 필터 [?...] 안에서만 호출할 수 있으며, 독립적인 경로 세그먼트로는 절대 호출할 수 없습니다.

$.store.book.length()라고 쓰는 것은 유효한 RFC 9535가 아니며, 표준 문법이 이를 거부합니다. 그 세그먼트 호출 형태는 jsonpath-plus 확장이지 명세의 일부가 아닙니다. 길이로 필터링하려면 술어 안에서 함수를 호출하세요.

$.store.book[?length(@.title) > 15]
→ [
    { "title": "Sayings of the Century", "author": "Nigel Rees", "price": 8.95 },
    { "title": "The Lord of the Rings", "author": "J. R. R. Tolkien", "price": 22.99 }
  ]

선택된 두 제목은 모두 15자보다 깁니다. “Moby Dick”(9)과 “Sword of Honour”(15로, 15 초과가 아님)는 제외됩니다.

각 함수가 필터 안에서 하는 일은 다음과 같습니다.

  • length() — 문자열, 배열, 객체의 길이: [?length(@.title) > 15]
  • count() — 노드 리스트의 노드 개수: [?(count(@.authors) > 1)]
  • match() — 문자열 전체에 대한 정규식 검사(I-Regexp 패턴): [?match(@.author, 'J.*')]
  • search() — 부분 문자열 정규식 검사: [?search(@.title, 'the')]
  • value() — 단일 노드 노드 리스트를 비교를 위해 그 값으로 변환

다섯 개 모두 RFC 9535의 기능입니다. Classic(Goessner) 방언은 이들을 구현하지 않으므로, 함수 기반 표현식이 실패한다면 필터 안에서 호출하고 있는지 그리고 엔진이 RFC 9535로 설정되어 있는지 확인하세요.

RFC 9535 vs Classic Goessner — 같은 표현식이 다른 이유

JSONPath 표현식이 두 도구에서 다른 결과를 반환한다면 보통 방언이 원인입니다. 두 방언을 비교하면 다음과 같습니다.

항목Classic Goessner (2007)RFC 9535 (2024)
표준화사실상의 표준, 정식 명세 없음최초의 공식 IETF 명세
필터 공백/따옴표관대함 ([?(@.price<10)] 허용)엄격함, 문법을 정확히 따름
누락 멤버 비교구현마다 다름잘 정의됨, 예외를 던지지 않음
표준 함수방언에 포함되지 않음length count match search value
정규화 경로표준 형식 없음표준화된 작은따옴표 대괄호 형식
합집합 순서라이브러리마다 다름명세화됨

실무에서는 이렇게 고릅니다. 다운스트림 시스템이 RFC 9535 준수를 표방한다면 표준 엔진에 맞춰 작성하고 검증하세요. jsonpath.com, jsonpath-plus, 또는 Jayway 기반 서비스에서 복사한 표현식을 유지보수한다면, 결과가 재현되도록 Classic을 쓰세요. JSONPath 테스터는 토글 하나로 두 엔진을 모두 실행하므로, 표현식을 한 번 붙여 넣고 각 방언이 어떻게 처리하는지 나란히 봅니다. 차이의 원인은 이 이중 엔진 비교로 가장 빨리 진단됩니다.

JSONPath vs XPath vs jq — 무엇을 쓸까

이 셋은 자주 혼동됩니다.

  • JSONPath는 JSON을 위한 선언형 경로 쿼리입니다. 코드를 짜지 않고 위치만 지정하고 싶은 설정과 테스트 단언에 끼워 넣기 좋습니다.
  • XPath는 XML 쪽 대응물입니다. JSONPath는 그 표기 일부(*, .., [])를 빌려 왔고 그래서 비유가 성립하지만, 두 언어는 서로 호환되지 않고 함수 집합도 다릅니다.
  • jq는 명령줄 JSON 프로세서입니다. 경로 선택을 넘어 변환·집계·재구성까지 하며, 셸 파이프라인 안에서 동작합니다.

선택은 대개 명확합니다. 끼워 넣는 단언이나 파이프라인 설정 필드에는 JSONPath를 쓰세요. 셸 기반 변환과 데이터 가공에는 jq를 쓰세요. jq 치트시트가 그 워크플로를 깊이 있게 다룹니다. 그리고 필드가 어디 있느냐가 아니라 페이로드가 기대한 형태에 부합하는가가 문제라면, JSON 스키마 검사기와 그 완전한 유효성 검사 가이드로 검증하세요.

흔한 JSONPath 실수 7가지

  1. 루트 $를 빠뜨림. store.book은 대부분의 엔진이 거부합니다. 모든 표현식은 $에서 시작합니다.
  2. 슬라이스 off-by-one. [0:2]는 세 개가 아니라 두 개입니다. 끝 경계는 제외됩니다.
  3. 잘못된 방언. Classic 표현식을 RFC 9535에서(또는 그 반대로) 실행하면 파싱 오류가 나거나 다른 노드에 일치할 수 있습니다. 엔진을 맞게 전환하세요.
  4. 함수를 독립 세그먼트로 사용. $.store.book.length()는 유효한 RFC 9535가 아닙니다. length()는 필터 안에서 호출하세요.
  5. 필터에서 @를 빠뜨림. [?($.price < 10)]은 루트를 가리킵니다. [?(@.price < 10)]을 쓰세요.
  6. 잘못된 대괄호 따옴표. $[store]는 오류입니다. 키에 따옴표를 붙이세요. $['store'].
  7. ..가 첫 단계에서 멈춘다고 가정. 재귀 하강은 직접 자식만이 아니라 모든 깊이에서 일치합니다.

JSONPath를 온라인에서, 비공개로 테스트하기

JSONPath 문법은 직접 실행해 보면서 익히는 게 가장 빠릅니다. JSONPath 테스터는 이 가이드의 모든 표현식을 실시간으로 평가합니다. RFC 9535와 Classic 두 엔진, Values / Paths / Both 결과 보기, 디버깅용 정규화 경로를 지원하고, 100% 브라우저 안에서 실행합니다. 업로드도 가입도 eval도 없어서 독점 페이로드도 그대로 넣어 쓸 수 있습니다. 여기서 경로를 만들어 원하는 노드를 정확히 선택하는지 확인한 뒤, 검증된 표현식을 코드·테스트·파이프라인에 곧바로 붙여 넣으세요.

JSON 워크플로의 나머지로는, 샘플 응답을 타입이 있는 인터페이스로 바꾸려면 JSON to TypeScript를, 두 문서를 필드 단위로 비교하려면 JSON Diff를 사용하세요.

자주 묻는 질문

JSONPath는 어디에 쓰나요?

JSONPath는 명령형 코드 없이 JSON을 쿼리합니다. 개발자들은 API 응답에서 필드를 뽑아낼 때, 통합 테스트에서 값을 단언할 때, 그리고 Kubernetes·AWS Step Functions·Azure Logic Apps 설정에서 필드를 지정할 때 사용합니다. 순회를 손으로 짜기 번거로운 크고 불규칙한 구조에서 데이터를 추출할 때 특히 쓸모가 있습니다.

RFC 9535와 고전 JSONPath의 차이는 무엇인가요?

Classic은 Stefan Goessner의 2007년 사실상의 방언입니다. 널리 구현되었지만 정식 명세가 없어서 라이브러리마다 갈라졌습니다. RFC 9535는 IETF의 2024년 2월 공식 명세로, 정밀한 문법, 결과를 위한 정규화 경로, 그리고 다섯 개의 표준 함수를 정의합니다. 두 방언은 필터, 합집합, 누락 멤버 비교의 경계에서 차이가 납니다.

JSONPath 필터 표현식은 어떻게 동작하나요?

필터 [?()]는 술어가 참인 요소만 남기며, @는 현재 요소입니다. 예를 들어 $.store.book[?(@.price < 10)]은 가격이 10 미만인 책을 선택합니다. &&||로 조건을 결합하고, 멤버 존재 여부를 검사하며, 문자열이나 숫자 리터럴과 비교할 수 있습니다.

length()를 $.store.book.length()처럼 쓸 수 있나요?

아닙니다. RFC 9535에서 length()와 나머지 네 함수는 $.store.book[?length(@.title) > 15]처럼 필터 안에서만 호출할 수 있습니다. 독립 세그먼트 형태인 $.store.book.length()는 jsonpath-plus 확장이지 표준 JSONPath가 아니며, RFC 9535 문법이 이를 거부합니다.

JSONPath는 XPath와 같은가요?

아니지만 발상은 비슷합니다. XPath는 XML을, JSONPath는 JSON을 쿼리하며, 둘 다 경로 선택자로 노드를 찾습니다. JSONPath는 XPath 표기 일부(*, .., [])를 의도적으로 빌려 와 비유가 유용하지만, 문법과 의미, 함수 집합은 서로 다르고 호환되지 않습니다.

JSONPath에서 재귀 하강(..)은 무엇을 하나요?

.. 연산자는 직접 자식만이 아니라 문서의 모든 단계를 탐색합니다. $..author는 어느 중첩 깊이에 나타나든 모든 author 멤버를 모읍니다. 깊이 중첩되거나 불규칙한 구조에서 단일 필드를 뽑아내는 가장 빠른 방법이지만, 예상보다 훨씬 많은 노드에 일치할 수 있으니 가능하면 범위를 좁히세요.

태그: jsonpath json query-language rfc-9535 filter-expression developer-tools