jq로 커맨드라인에서 JSON 다루기

터미널에서 API 응답이나 로그의 JSON을 다루다 보면 grep/sed는 중첩된 배열/객체 구조 앞에서 금방 한계에 부딪힌다. 이 프로젝트의 output/post_index.json도 400개 넘는 포스트 정보가 { "159": {...}, "348": {...} } 형태로 중첩돼 있어, “draft 상태인 글만” 같은 질의를 텍스트 매칭으로 처리하기 어렵다. jq.field 접근과 select() 필터링, map() 변환을 파이프(|)로 이어붙여 이런 질의를 처리하는 커맨드라인 JSON 프로세서다. 이 글에서는 jq의 핵심 필터 문법을 정리하고, 실제 이 프로젝트의 JSON 파일을 대상으로 직접 실행한 결과를 담는다.

jq 필터 문법

자주 쓰는 필터와 옵션만 정리하면 아래 표 정도로 충분하다. 나머지는 이 필터들을 파이프로 조합해서 해결된다.

문법설명예시
.field객체에서 특정 키 값 추출jq '.last_synced'
.a.b / .["key"]중첩 객체 접근 (키에 특수문자·숫자만 있으면 대괄호 표기)jq '.posts["159"]'
.[]배열/객체의 모든 값을 스트림으로 펼쳐서 출력jq '.posts[]'
select(cond)조건이 참인 항목만 통과시키는 필터select(.status == "draft")
map(expr)배열의 각 항목을 expr로 변환한 새 배열 생성map(.title)
|파이프. 앞 필터의 출력을 다음 필터 입력으로 연결.posts | to_entries | map(.value.title)
to_entries / from_entries객체 ↔ {key, value} 배열 상호 변환.posts | to_entries
length배열/객체/문자열의 길이(개수).posts | length
sort_by(expr) / group_by(expr)expr 기준 정렬 / 그룹핑group_by(.status)
-r결과를 따옴표 없는 raw 텍스트로 출력 (문자열일 때)jq -r '.title'
-c결과를 줄바꿈 없이 한 줄로 압축 출력jq -c '.'
--arg name value셸 변수를 jq 필터 안에서 $name으로 사용jq --arg kw "Python" '...'

실전 코드

아래 예시는 전부 이 프로젝트의 실제 파일 output/post_index.json(post_index.py가 관리하는 WordPress 글 로컬 인덱스)을 대상으로 그대로 실행한 결과다.

필드 접근과 순회

$ jq '.last_synced' output/post_index.json
$ jq '.posts["159"]' output/post_index.json
"2026-07-21T13:04:45"
{
  "title": "candidate",
  "status": "draft",
  "modified": "2023-07-24T17:24:33"
}

.posts는 키가 post ID인 객체다. .posts[]로 값만 스트림으로 펼친 뒤 -r로 raw 문자열을 뽑으면 제목 목록을 바로 얻는다.

$ jq -r '.posts[] | .title' output/post_index.json | head -3
candidate
Linux Kernel 에서 System call 처리 1
파이썬 기초 문법

조건 필터링: select()

status가 draft인 글만 골라 개수를 세고, -r로 제목만 뽑아본다.

$ jq '[.posts[] | select(.status == "draft")] | length' output/post_index.json
$ jq -r '.posts[] | select(.status == "draft") | .title' output/post_index.json | head -5
67
candidate
Linux Kernel 에서 System call 처리 1
Ubuntu 서버에서 네트워크 구성
Ubuntu 서버에서 DNS 설정하기
리눅스에서 Visual Studio Code 사용

변환: map()과 to_entries

.posts는 객체라서 map()을 바로 쓸 수 없다. to_entries{key, value} 배열로 바꾼 뒤 map()으로 원하는 형태로 재구성한다.

$ jq '[.posts | to_entries[] | {id: .key, title: .value.title}] | .[0:3]' output/post_index.json
[
  {
    "id": "159",
    "title": "candidate"
  },
  {
    "id": "348",
    "title": "Linux Kernel 에서 System call 처리 1"
  },
  {
    "id": "529",
    "title": "파이썬 기초 문법"
  }
]

변수 주입: –arg

셸 변수나 스크립트 인자를 jq 필터 안에 안전하게 넣고 싶을 때 --arg를 쓴다. 필터 문자열 안에 값을 직접 이어붙이면 따옴표·특수문자 이스케이프 문제가 생기기 쉬운데, --arg$이름 변수로 주입해주므로 그런 문제가 없다.

$ jq --arg kw "Python" '[.posts[] | select(.title | contains($kw))] | length' output/post_index.json
20

파이프라인 조합과 집계

필터를 |로 계속 이어붙이면 “draft 중 최근 수정된 5건만, id/수정일/제목만” 같은 다단계 질의도 한 줄로 처리된다.

$ jq -r '.posts | to_entries
  | map(select(.value.status == "draft"))
  | sort_by(.value.modified) | reverse
  | .[0:5][]
  | "\(.key)\t\(.value.modified)\t\(.value.title)"' output/post_index.json
3571	2026-07-12T15:36:29	F2FS 파일시스템 압축 적용에 대한 고찰
3420	2026-07-05T07:25:12	성심당 줄 길 땐? 에디터가 직접 먹은 대전 추천 빵집 5곳
742	2026-07-01T17:04:47	클라우드웨이즈로 워드프레스 설치 및 도메인 설정 방법
1825	2026-07-01T17:00:00	Ubuntu 22.04 LXC 컨테이너 사용하기
2747	2026-07-01T16:59:37	Ubuntu 25.04 Desktop 설치 가이드

group_bysort_by를 조합하면 상태별 개수 같은 간단한 집계도 바로 나온다.

$ jq '[.posts[].status] | group_by(.) | map({status: .[0], count: length}) | sort_by(-.count)' output/post_index.json
[
  {
    "status": "publish",
    "count": 337
  },
  {
    "status": "draft",
    "count": 67
  },
  {
    "status": "trash",
    "count": 3
  }
]

주의사항

  • 기본 출력은 JSON 문자열: -r 없이 .title만 실행하면 값이 "candidate"처럼 따옴표로 감싸진 채 나온다. 셸 스크립트에서 변수에 담아 쓰려면 -r이 필수다.
  • 필드가 없는 경우 에러: .posts.foo처럼 존재하지 않는 키를 파고들면 배열/객체가 아닌 값에서 접근 시 에러가 날 수 있다. 존재 여부가 불확실하면 .foo?처럼 물음표를 붙이거나 // "기본값"으로 폴백을 준다.
  • 필터 안 따옴표 충돌: 필터를 작은따옴표('...')로 감싸는 게 일반적인데, 필터 내부에서 문자열 리터럴에 큰따옴표를 쓰면("Python") 셸 인용과 안 겹친다. 값이 외부(셸 변수)에서 온다면 문자열을 직접 이어붙이지 말고 --arg로 주입하는 편이 안전하다.
  • root 권한이 없는 환경: sudo apt-get install -y jq가 안 되면 jq 공식 릴리스의 정적 바이너리를 받아 ~/.local/bin 같은 PATH 안 디렉터리에 두고 실행 권한만 주면 된다.
  • 대용량 파일: jq는 기본적으로 전체 JSON을 메모리에 로드한다. 수 GB짜리 로그라면 --stream 옵션이나 애초에 파일을 줄 단위(JSON Lines)로 분리하는 방식을 고려한다.

마무리

jq는 .field, .[], select(), map() 네 가지와 파이프만 익혀도 실무 JSON 질의 대부분을 처리할 수 있다. 여기에 raw 출력용 -r, 외부 값을 안전하게 넣는 --arg까지 더하면 curl ... | jq ...처럼 API 응답을 바로 셸 파이프라인에 태우는 패턴도 무리 없이 짤 수 있다.

참고

답글 남기기