jq 명령어 사용법: 터미널에서 JSON 데이터 자유자재로 다루기

jq 명령어 사용법: 터미널에서 JSON 데이터 자유자재로 다루기

curl로 API를 호출하고 나서 JSON 응답이 한 줄로 쭉 이어져서 나올 때 난감했던 경험, 한 번쯤 있지 않으신가요? 아니면 수백 줄짜리 JSON에서 딱 필요한 필드 하나만 뽑아내고 싶은데 눈으로 훑어보다 포기한 적은요?

이럴 때 빛을 발하는 게 바로 jq입니다. jq는 터미널에서 JSON 데이터를 자유롭게 조회하고 가공할 수 있게 해주는 커맨드라인 도구인데요. sed나 awk가 텍스트를 다루듯이 jq는 JSON을 다루는 전용 도구라고 보시면 됩니다.

이번 글에서는 jq의 설치부터 기본 문법과 실무에서 자주 쓰는 패턴까지 쭉 살펴보겠습니다.

설치

jq는 대부분의 운영체제에서 패키지 매니저로 간단하게 설치할 수 있습니다.

macOS에서는 Homebrew로 설치하면 됩니다.

brew install jq

Linux(Debian/Ubuntu)에서는 apt를 사용합니다.

sudo apt install jq

Windows에서는 scoop이나 chocolatey를 쓰면 편합니다.

scoop install jq

설치가 끝나면 버전을 확인해볼까요?

jq --version
결과
jq-1.7.1

기본 사용법

jq의 가장 기본적인 사용법은 . 필터입니다. 입력받은 JSON을 들여쓰기와 색상 하이라이팅이 적용된 상태로 예쁘게 출력해줍니다. 이것만으로도 꽤 쓸 만하죠.

echo '{"name":"홍길동","age":25}' | jq '.'
결과
{
  "name": "홍길동",
  "age": 25
}

API 응답을 읽기 좋게 만들 때 이 방법을 가장 많이 씁니다. curl과 조합하면 이런 식이에요.

curl -s https://jsonplaceholder.typicode.com/posts/1 | jq '.'
결과
{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae ..."
}

여기서 curl의 -s(silent) 옵션은 진행률 표시를 숨겨서 jq의 출력만 깔끔하게 보이게 해줍니다.

필드 접근

JSON 객체에서 특정 필드를 꺼내려면 .필드명을 사용합니다.

echo '{"name":"홍길동","age":25,"city":"서울"}' | jq '.name'
결과
"홍길동"

따옴표 없이 순수한 문자열만 필요하다면 -r (raw output) 옵션을 주면 됩니다.

echo '{"name":"홍길동","age":25,"city":"서울"}' | jq -r '.name'
결과
홍길동

중첩된 객체의 필드는 .을 이어 붙여서 접근합니다.

echo '{"user":{"name":"홍길동","address":{"city":"서울"}}}' | jq '.user.address.city'
결과
"서울"

여러 필드를 한꺼번에 꺼내고 싶으면 쉼표로 구분하면 됩니다.

echo '{"name":"홍길동","age":25,"city":"서울"}' | jq '.name, .age'
결과
"홍길동"
25

배열 다루기

JSON 배열을 다루는 법도 알아보겠습니다. 배열의 특정 인덱스에 접근하려면 .[인덱스]를 사용합니다.

echo '["사과","바나나","체리"]' | jq '.[0]'
결과
"사과"

배열의 모든 요소를 하나씩 꺼내려면 .[]를 씁니다.

echo '["사과","바나나","체리"]' | jq '.[]'
결과
"사과"
"바나나"
"체리"

슬라이싱도 됩니다. .[시작:끝] 형태로 범위를 지정할 수 있어요.

echo '[1,2,3,4,5]' | jq '.[1:3]'
결과
[2, 3]

배열의 길이를 알고 싶으면 length를 사용합니다.

echo '[1,2,3,4,5]' | jq 'length'
결과
5

객체 배열에서 필드 추출

실제로 API 응답을 다룰 때 가장 자주 하는 작업이 객체 배열에서 특정 필드만 뽑아내는 건데요. .[]로 배열을 풀고 .필드명으로 접근하면 됩니다.

curl -s https://jsonplaceholder.typicode.com/users | jq '.[].name'
결과
"Leanne Graham"
"Ervin Howell"
"Clementine Bauch"
"Patricia Lebsack"
"Chelsey Dietrich"
"Mrs. Dennis Schulist"
"Kurtis Weissnat"
"Nicholas Runolfsdottir V"
"Glenna Reichert"
"Clementina DuBuque"

각 사용자의 이름과 이메일을 함께 보고 싶다면 새로운 객체를 만들면 됩니다.

curl -s https://jsonplaceholder.typicode.com/users \
  | jq '.[] | {name: .name, email: .email}'
결과
{
  "name": "Leanne Graham",
  "email": "Sincere@april.biz"
}
{
  "name": "Ervin Howell",
  "email": "Shanna@melissa.tv"
}
...

결과를 배열로 감싸고 싶으면 전체를 [...]로 묶습니다.

curl -s https://jsonplaceholder.typicode.com/users \
  | jq '[.[] | {name: .name, email: .email}]'

이렇게 하면 유효한 JSON 배열이 출력되니까 다른 도구에 넘기거나 파일로 저장할 때 편리합니다.

파이프라인

jq의 강력한 점 중 하나는 셸의 파이프(|)처럼 필터를 연결할 수 있다는 거예요. 앞 필터의 출력이 뒤 필터의 입력으로 들어가는 구조인데요.

echo '{"users":[{"name":"홍길동","age":25},{"name":"김철수","age":30}]}' \
  | jq '.users | .[] | .name'
결과
"홍길동"
"김철수"

위 예제를 단계별로 보면 .users가 배열을 꺼내고, .[]가 각 요소를 풀고, .name이 이름을 추출하는 식입니다. 이런 파이프라인 방식 덕분에 복잡한 JSON 구조도 단계별로 접근할 수 있어요.

조건 필터링: select

select() 함수를 쓰면 조건에 맞는 요소만 걸러낼 수 있습니다.

예를 들어, JSONPlaceholder의 글 목록에서 특정 사용자의 글만 가져와 보겠습니다.

curl -s https://jsonplaceholder.typicode.com/posts \
  | jq '.[] | select(.userId == 1) | .title'
결과
"sunt aut facere repellat provident occaecati excepturi optio reprehenderit"
"qui est esse"
"ea molestias quasi exercitationem repellat qui ipsa sit aut"
"eum et est occaecati"
...

비교 연산자(==, !=, >, <, >=, <=)와 논리 연산자(and, or, not)를 조합해서 쓸 수도 있습니다.

curl -s https://jsonplaceholder.typicode.com/posts \
  | jq '.[] | select(.userId == 1 and .id <= 3) | {id, title}'
결과
{ "id": 1, "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit" }
{ "id": 2, "title": "qui est esse" }
{ "id": 3, "title": "ea molestias quasi exercitationem repellat qui ipsa sit aut" }

문자열이 특정 패턴을 포함하는지 확인할 때는 contains()test()를 씁니다.

curl -s https://jsonplaceholder.typicode.com/posts \
  | jq '.[] | select(.title | contains("qui")) | {id, title}'

test()는 정규표현식을 지원하니 더 정교한 매칭이 필요할 때 유용합니다.

curl -s https://jsonplaceholder.typicode.com/posts \
  | jq '.[] | select(.title | test("^e")) | {id, title}'

데이터 변환: map과 sort

map()은 배열의 각 요소에 변환을 적용합니다. .[] | ...과 비슷하지만 결과를 자동으로 배열에 담아준다는 차이가 있어요.

curl -s https://jsonplaceholder.typicode.com/users \
  | jq 'map({name, email})'
결과
[
  { "name": "Leanne Graham", "email": "Sincere@april.biz" },
  { "name": "Ervin Howell", "email": "Shanna@melissa.tv" },
  ...
]

map()select()를 같이 쓰면 필터링과 변환을 한 번에 할 수 있습니다.

curl -s https://jsonplaceholder.typicode.com/posts \
  | jq 'map(select(.userId == 1)) | length'
결과
10

userId가 1인 글이 총 10개라는 걸 바로 알 수 있죠.

정렬은 sort_by()를 사용합니다.

curl -s https://jsonplaceholder.typicode.com/users \
  | jq 'sort_by(.name) | .[0:3] | .[].name'
결과
"Chelsey Dietrich"
"Clementina DuBuque"
"Clementine Bauch"

역순 정렬은 reverse를 뒤에 붙이면 됩니다.

curl -s https://jsonplaceholder.typicode.com/users \
  | jq 'sort_by(.name) | reverse | .[0:3] | .[].name'

집계: group_by와 unique

데이터를 분석할 때 그룹핑은 빠질 수 없겠죠. group_by()로 특정 필드 기준으로 묶을 수 있습니다.

curl -s https://jsonplaceholder.typicode.com/posts \
  | jq 'group_by(.userId) | map({userId: .[0].userId, count: length})'
결과
[
  { "userId": 1, "count": 10 },
  { "userId": 2, "count": 10 },
  { "userId": 3, "count": 10 },
  ...
]

각 사용자가 몇 개의 글을 썼는지 한눈에 보이네요.

unique_by()는 중복을 제거할 때 씁니다.

curl -s https://jsonplaceholder.typicode.com/posts \
  | jq '[.[].userId] | unique'
결과
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

문자열 다루기

jq에는 문자열을 가공하는 내장 함수도 있습니다.

\() 문법을 사용한 문자열 보간(interpolation)은 셸 스크립트에서 정말 유용해요.

curl -s https://jsonplaceholder.typicode.com/users \
  | jq -r '.[] | "\(.name) <\(.email)>"'
결과
Leanne Graham <Sincere@april.biz>
Ervin Howell <Shanna@melissa.tv>
Clementine Bauch <Nathan@yesenia.net>
...

-r 옵션을 쓰면 따옴표 없이 출력되니까 셸 스크립트에서 바로 활용하기 좋습니다. 이걸 셸 리다이렉션으로 파일에 저장하면 간단한 데이터 추출 파이프라인이 완성되죠.

다른 문자열 함수들도 몇 가지 살펴볼게요.

# 대소문자 변환
echo '"Hello World"' | jq 'ascii_downcase'
# "hello world"

# 문자열 분할
echo '"apple,banana,cherry"' | jq 'split(",")'
# ["apple", "banana", "cherry"]

# 문자열 결합
echo '["apple","banana","cherry"]' | jq 'join(" + ")'
# "apple + banana + cherry"

# 문자열 길이
echo '"hello"' | jq 'length'
# 5

새로운 JSON 만들기

기존 데이터에서 원하는 형태로 새 JSON을 만들어내는 것도 jq가 잘합니다.

curl -s https://jsonplaceholder.typicode.com/users/1 \
  | jq '{
    displayName: .name,
    contact: {
      email: .email,
      phone: .phone
    },
    location: .address.city
  }'
결과
{
  "displayName": "Leanne Graham",
  "contact": {
    "email": "Sincere@april.biz",
    "phone": "1-770-736-8031 x56442"
  },
  "location": "Gwenborough"
}

이런 식으로 API 응답을 원하는 구조로 변환하면 JavaScript의 JSON.parse()파이썬의 json 모듈로 처리하기 전에 필요한 데이터만 미리 정리할 수 있어요.

조건문과 기본값

jq에서도 if-then-else를 쓸 수 있습니다.

curl -s https://jsonplaceholder.typicode.com/users \
  | jq '.[] | {name, tier: (if .id <= 3 then "gold" elif .id <= 6 then "silver" else "bronze" end)}'

// 연산자는 값이 null이거나 false일 때 기본값을 지정합니다. 데이터가 불완전할 수 있는 상황에서 유용해요.

echo '{"name":"홍길동","nickname":null}' \
  | jq '{name, nickname: (.nickname // "없음")}'
결과
{
  "name": "홍길동",
  "nickname": "없음"
}

실전 예제: curl + jq 조합

실무에서 가장 많이 쓰는 패턴은 역시 curl로 API를 호출한 뒤 jq로 응답을 가공하는 거예요.

특정 사용자의 글 제목만 추출하는 경우를 볼까요?

curl -s https://jsonplaceholder.typicode.com/posts \
  | jq -r '.[] | select(.userId == 1) | "\(.id). \(.title)"'
결과
1. sunt aut facere repellat provident occaecati excepturi optio reprehenderit
2. qui est esse
3. ea molestias quasi exercitationem repellat qui ipsa sit aut
4. eum et est occaecati
5. nesciunt quas odio
...

글과 댓글 수를 함께 보고 싶다면 여러 API를 호출해서 조합할 수도 있습니다.

# 각 글의 댓글 수 확인
for id in 1 2 3; do
  title=$(curl -s "https://jsonplaceholder.typicode.com/posts/$id" | jq -r '.title')
  count=$(curl -s "https://jsonplaceholder.typicode.com/posts/$id/comments" | jq 'length')
  echo "$id. $title (댓글 ${count}개)"
done
결과
1. sunt aut facere repellat provident occaecati excepturi optio reprehenderit (댓글 5개)
2. qui est esse (댓글 5개)
3. ea molestias quasi exercitationem repellat qui ipsa sit aut (댓글 5개)

실전 예제: GitHub CLI + jq

GitHub CLI(gh)도 jq와 궁합이 잘 맞습니다. gh--json 옵션으로 JSON 출력을 받고 --jq 옵션으로 바로 jq 표현식을 적용할 수 있거든요.

# 열린 이슈의 제목과 라벨
gh issue list --json number,title,labels \
  --jq '.[] | "\(.number): \(.title)"'

# 내가 만든 PR 중 병합된 것만
gh pr list --author @me --state merged --json title,mergedAt \
  --jq 'sort_by(.mergedAt) | reverse | .[0:5] | .[] | "\(.mergedAt[0:10]) \(.title)"'

gh api와 조합하면 더 다양한 작업이 가능합니다.

# 저장소의 스타 수 상위 5개 (특정 사용자)
gh api users/DaleSeo/repos --jq 'sort_by(.stargazers_count) | reverse | .[0:5] | .[] | "\(.name): ⭐ \(.stargazers_count)"'

파일 입출력

지금까지는 파이프로 데이터를 넘겼는데 jq에 직접 파일을 전달할 수도 있습니다.

# 파일에서 읽기
jq '.name' data.json

# 결과를 파일로 저장
curl -s https://jsonplaceholder.typicode.com/users \
  | jq 'map({name, email})' > contacts.json

여러 JSON 파일을 합칠 때는 --slurp(-s) 옵션이 유용합니다.

# 여러 파일의 내용을 하나의 배열로 합치기
jq -s '.' file1.json file2.json

자주 쓰는 패턴 정리

마지막으로 실무에서 자주 쓰게 되는 패턴을 정리해볼게요.

# 1. JSON 예쁘게 출력
cat data.json | jq '.'

# 2. 특정 필드만 추출
cat data.json | jq '.[] | .name'

# 3. 조건 필터링
cat data.json | jq '.[] | select(.age > 20)'

# 4. 필드 개수 세기
cat data.json | jq 'length'

# 5. 키 목록 확인
echo '{"a":1,"b":2,"c":3}' | jq 'keys'
# ["a", "b", "c"]

# 6. 값만 추출
echo '{"a":1,"b":2,"c":3}' | jq '[.[] ]'
# [1, 2, 3]

# 7. 특정 필드 존재 여부 확인
echo '{"name":"홍길동"}' | jq 'has("name")'
# true

# 8. null 제거
echo '[1,null,2,null,3]' | jq 'map(select(. != null))'
# [1, 2, 3]

# 9. 객체 병합
echo '{"a":1}' | jq '. + {"b":2}'
# {"a": 1, "b": 2}

# 10. TSV/CSV 변환
curl -s https://jsonplaceholder.typicode.com/users \
  | jq -r '.[] | [.id, .name, .email] | @tsv'

10번의 @tsv는 탭으로 구분된 형식으로 변환해줍니다. @csv를 쓰면 CSV 형식이 되고요. 스프레드시트로 가져가야 할 때 요긴합니다.

마치며

jq는 한번 익혀두면 터미널에서 JSON을 다루는 게 훨씬 수월해집니다. 특히 curl로 API를 호출한 결과를 분석하거나 GitHub CLI의 JSON 출력을 가공할 때 진가를 발휘하죠.

처음에는 . 하나로 예쁘게 출력하는 것부터 시작해서 필드 추출이나 조건 필터링, 데이터 변환 순서로 하나씩 익혀가면 금방 손에 익을 거예요. JSON을 프로그래밍 언어(JavaScriptPython)로 처리하기 전에 jq로 먼저 데이터를 탐색하고 구조를 파악하는 습관을 들이면 개발 속도가 꽤 빨라집니다.

더 자세한 문법과 내장 함수는 jq 공식 매뉴얼에서 확인하실 수 있습니다. 온라인에서 바로 jq를 실행해볼 수 있는 jq play도 학습할 때 유용하니 한번 사용해보세요.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord