API 응답이나 설정 파일을 열었더니 JSON이 한 줄로 쭉 이어져 있어서 어디가 어딘지 알 수 없을 때가 있습니다. 보통 jq를 쓰라고 하지만, 회사 PC처럼 프로그램을 마음대로 설치할 수 없는 환경도 많습니다. 파이썬이 깔려 있다면 명령줄 JSON 보기는 설치 없이 python -m json.tool 한 줄이면 됩니다. 이 글에서는 이 명령의 옵션을 직접 돌려 보고, json.tool에 없는 “값만 골라내기”는 50줄짜리 표준 라이브러리 스크립트로 채웁니다. 윈도우 11에서 해 보니 결과를 파일로 넘기는 순간 이모지 하나 때문에 cp949 오류가 났고, 파이프로 값을 넘길 때도 한글이 깨지는 문제를 두 번 만났습니다. 33.8MB짜리 JSON을 펼치는 데는 약 8.3초가 걸렸습니다. 환경은 윈도우 11, Python 3.12.10, Node 24.19.0입니다.
테스트용 JSON — API 응답처럼 한 줄로
블로그 글 목록 API 응답을 흉내 내서 posts.json(485바이트)을 만들었습니다. 한글 제목, 이모지 하나(✅), 빈 배열, null이 들어 있고 공백 없이 한 줄입니다.
{"status":"ok","count":3,"items":[{"id":489,"title":"마크다운 표 CSV 변환","category":"AI 도구 활용","tags":["python","csv"],"stats":{"views":132,"comments":2},"draft":false},{"id":485,"title":"개인정보 마스킹 파이썬", ... },{"id":471,"title":"폴더 동기화 스크립트 ✅", ... ,"draft":null}]}
기본 사용법 — 펼치기는 되는데 한글이 \uXXXX로
가장 기본 형태는 파일 이름만 주는 것입니다.
$ python -m json.tool posts.json
{
"status": "ok",
"count": 3,
"items": [
{
"id": 489,
"title": "\ub9c8\ud06c\ub2e4\uc6b4 \ud45c CSV \ubcc0\ud658",
"category": "AI \ub3c4\uad6c \ud65c\uc6a9",
"tags": [
"python",
"csv"
],
...
구조는 4칸 들여쓰기로 보기 좋게 펼쳐졌지만 한글이 전부 \ub9c8\ud06c... 같은 이스케이프로 나왔습니다. 파이썬 json 모듈은 기본값이 “ASCII 문자만 출력”이기 때문입니다. 한글을 그대로 보려면 --no-ensure-ascii 옵션을 붙입니다.
윈도우에서 파일로 저장하면 cp949 오류
펼친 결과를 파일로 남기려고 >로 리다이렉트했더니 바로 실패했습니다.
$ python -m json.tool --no-ensure-ascii posts.json > pretty.json
'cp949' codec can't encode character '\u2705' in position 13: illegal multibyte sequence
윈도우에서 출력을 파일이나 파이프로 넘기면 파이썬은 표준 출력을 한글 윈도우 기본 인코딩인 cp949로 씁니다. 한글은 cp949에 들어 있어서 문제가 없지만, 이모지 ✅(U+2705)는 cp949에 없어서 거기서 멈췄습니다. 테스트 데이터에 이모지를 하나 넣어 둔 덕분에 발견한 셈인데, 실제 API 응답에도 이모지나 특수문자는 흔히 섞여 있습니다. 해결 방법 두 가지를 모두 확인했습니다.
# 방법 1: 출력 파일을 두 번째 인자로 준다
python -m json.tool --no-ensure-ascii posts.json out.json
# 방법 2: 파이썬을 UTF-8 모드로 실행한다
python -X utf8 -m json.tool --no-ensure-ascii posts.json > pretty.json
두 방법 모두 오류 없이 끝났고, 결과 파일은 둘 다 1,084바이트에 한글이 그대로 들어 있었습니다. 첫 번째 방법은 json.tool이 출력 파일을 직접 UTF-8로 열기 때문에 되고, 두 번째는 파이썬 전체를 UTF-8 모드로 실행하는 방식입니다. 참고로 윈도우에서 만든 결과 파일은 줄바꿈이 CRLF(\r\n)였습니다.
자주 쓰는 옵션 — 들여쓰기, 키 정렬, 한 줄로 압축
--indent 2로 들여쓰기를 줄이고, --sort-keys로 키를 알파벳순으로 정렬해 봤습니다. 키가 많은 객체 두 개를 눈으로 비교할 때 특히 편합니다.
$ python -X utf8 -m json.tool --no-ensure-ascii --indent 2 --sort-keys posts.json
{
"count": 3,
"items": [
{
"category": "AI 도구 활용",
"draft": false,
"id": 489,
"stats": {
"comments": 2,
"views": 132
},
...
],
"status": "ok"
}
원래 맨 위에 있던 status가 맨 아래로 내려갔습니다. 반대로 펼쳐진 파일을 다시 한 줄로 만들 때는 --compact를 씁니다. 펼친 pretty.json에 돌리니 원본과 같은 공백 없는 한 줄이 나왔습니다. 제 노트북의 Python 3.12에서 쓸 수 있는 옵션 전체는 사용법 안내에 이렇게 나옵니다(없는 파일 이름을 줬을 때 출력된 내용입니다).
usage: python -m json.tool [-h] [--sort-keys] [--no-ensure-ascii]
[--json-lines]
[--indent INDENT | --tab | --no-indent | --compact]
[infile] [outfile]
| 옵션 | 하는 일 | 이럴 때 |
|---|---|---|
| –no-ensure-ascii | 한글을 \uXXXX 대신 그대로 출력 | 한글 데이터는 거의 항상 |
| –indent N / –tab | 들여쓰기 칸 수 / 탭으로 | 깊은 구조를 좁은 화면에서 |
| –sort-keys | 키를 알파벳순 정렬 | 두 JSON을 눈이나 diff로 비교 |
| –compact | 공백 없이 한 줄로 | 파일 크기 줄이기, 다시 붙여 넣기 |
| –json-lines | 한 줄에 JSON 하나씩 있는 파일 처리 | 로그(.jsonl) 보기 |
깨진 JSON 찾기 — 오류는 한 번에 하나씩
json.tool은 JSON 검사기로도 쓸 수 있습니다. 손으로 고친 설정 파일에서 자주 나오는 실수인 “끝 쉼표”를 일부러 두 군데 넣은 broken.json을 만들었습니다. 5번째 줄 객체 끝의 ,}와, 그다음 배열 끝의 }, 뒤 ]입니다.
{
"status": "ok",
"items": [
{"id": 489, "title": "마크다운 표 CSV 변환"},
{"id": 485, "title": "개인정보 마스킹 파이썬",},
]
}
$ python -X utf8 -m json.tool broken.json
Expecting property name enclosed in double quotes: line 5 column 41 (char 116)
# 5번째 줄 끝 쉼표를 지우고 다시
$ python -X utf8 -m json.tool broken.json
Expecting value: line 6 column 3 (char 120)
첫 실행에서는 5번째 줄 41번째 칸, 즉 끝 쉼표 바로 다음의 }를 정확히 짚었습니다. 그런데 두 번째 실수는 첫 번째를 고치고 다시 돌려야 보고됐습니다. json 모듈은 처음 만난 오류에서 멈추기 때문에, 실수가 여러 개면 고치고 돌리기를 반복해야 합니다. 실패할 때 종료 코드가 1이라서 스크립트에서 “이 파일이 올바른 JSON인지” 확인하는 용도로도 쓸 수 있습니다.
한 줄에 JSON이 하나씩 들어 있는 JSON Lines 형식 로그도 시험해 봤습니다. 예시 로그 3줄짜리 lines.jsonl을 직접 만들어 그냥 넣었더니 “Extra data” 오류가 났고, --json-lines를 붙이자 줄마다 따로 펼쳐졌습니다.
$ python -X utf8 -m json.tool lines.jsonl
Extra data: line 2 column 1 (char 65)
$ python -X utf8 -m json.tool --json-lines --no-ensure-ascii --indent 2 lines.jsonl
{
"time": "2026-11-02T10:00:01",
"level": "INFO",
"msg": "발행 시작"
}
{
"time": "2026-11-02T10:00:09",
"level": "WARN",
"msg": "썸네일 재시도",
"retry": 1
}
...
값만 골라내기 — jpick.py 50줄
json.tool은 펼치기와 검사만 하고, jq처럼 “모든 글의 제목만” 뽑는 기능은 없습니다. 그래서 점으로 경로를 적으면 해당 값만 출력하는 작은 스크립트를 만들었습니다. 숫자는 배열 번호, *는 “전부”라는 뜻입니다. 표준 라이브러리만 씁니다.
"""jq 없이 JSON에서 값 골라내기 (표준 라이브러리만)
사용법:
python jpick.py 경로 [파일] 파일을 안 주면 표준 입력에서 읽음
경로 예: items.0.title items.*.title items.*.stats.views
"""
import json
import sys
def pick(node, keys):
if not keys:
yield node
return
key, rest = keys[0], keys[1:]
if key == '*':
children = node.values() if isinstance(node, dict) else node
for child in children:
yield from pick(child, rest)
elif isinstance(node, list):
yield from pick(node[int(key)], rest)
else:
yield from pick(node[key], rest)
def main():
if len(sys.argv) < 2:
sys.exit(__doc__)
path = sys.argv[1]
if len(sys.argv) > 2:
with open(sys.argv[2], encoding='utf-8') as f:
data = json.load(f)
else:
# 윈도우는 파이프 입력을 cp949로 읽는다 → 바이트로 받아 json이 UTF-8로 해석하게
data = json.loads(sys.stdin.buffer.read())
keys = path.split('.') if path else []
try:
for value in pick(data, keys):
if isinstance(value, str):
print(value) # 문자열은 따옴표 없이 (셸에서 바로 쓰기 좋게)
else:
print(json.dumps(value, ensure_ascii=False))
except (KeyError, IndexError, ValueError, TypeError) as e:
sys.exit('경로를 찾을 수 없음: ' + path + ' (' + type(e).__name__ + ': ' + str(e) + ')')
if __name__ == '__main__':
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
main()
$ python jpick.py items.*.title posts.json
마크다운 표 CSV 변환
개인정보 마스킹 파이썬
폴더 동기화 스크립트 ✅
$ python jpick.py items.0.stats posts.json
{"views": 132, "comments": 2}
$ python jpick.py items.*.stats.views posts.json
132
98
251
$ python jpick.py items.0.titel posts.json
경로를 찾을 수 없음: items.0.titel (KeyError: 'titel')
문자열은 따옴표 없이 한 줄에 하나씩 나오게 해서, 출력을 다른 명령에 바로 넘기기 좋게 했습니다. 이 모양이 되기까지 오류를 두 번 고쳤습니다.
첫 번째는 오류 메시지 깨짐입니다. 처음엔 표준 출력만 UTF-8로 바꿔 두었는데, 오타 난 키를 넣어 보니 메시지가 이렇게 나왔습니다.
$ python jpick.py items.0.titel posts.json
��θ� ã�� �� ����: items.0.titel (KeyError: 'titel')
sys.exit()에 넘긴 메시지는 표준 출력이 아니라 표준 오류(stderr)로 나갑니다. stderr는 cp949 그대로였던 겁니다. sys.stderr.reconfigure(encoding='utf-8') 한 줄을 추가해 고쳤습니다.
두 번째는 파이프 입력입니다. json.tool 결과를 파이프로 넘기자 이번엔 이런 오류가 났습니다.
$ python -X utf8 -m json.tool --no-ensure-ascii posts.json | python jpick.py items.*.title
경로를 찾을 수 없음: items.*.title (UnicodeEncodeError: 'utf-8' codec can't encode character '\udceb' in position 3: surrogates not allowed)
$ python -c "import sys;print(sys.stdin.encoding, sys.stdin.errors)" < posts.json
cp949 surrogateescape
윈도우에서는 파이프나 리다이렉트로 들어오는 표준 입력도 cp949로 읽힙니다. UTF-8 한글이 엉뚱한 문자로 들어왔다가 출력할 때 터진 것입니다. 게다가 메시지가 “경로를 찾을 수 없음”으로 나와서 처음엔 경로가 틀린 줄 알았습니다. UnicodeEncodeError가 ValueError의 하위 클래스라서, 경로 오류용으로 잡아 둔 except에 함께 걸린 것입니다. 입력을 sys.stdin.buffer.read()로 바이트째 받아 json.loads()에 넘기자 json 모듈이 UTF-8로 해석해서 세 제목이 정상으로 나왔습니다.
큰 파일에서는 얼마나 걸릴까 — 33.8MB 실측
글 30만 건짜리 한 줄 JSON(33.8MB)을 만들어 시간을 쟀습니다. 비교용으로 node의 JSON.stringify(data, null, 4)로 같은 일을 하는 스크립트도 돌렸습니다.
// node로 같은 일: node pretty_node.js 입력.json 출력.json
const fs = require('fs');
const t = Date.now();
const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
fs.writeFileSync(process.argv[3], JSON.stringify(data, null, 4));
console.log('node 소요 ' + ((Date.now() - t) / 1000).toFixed(2) + '초');
| 작업 | 소요 시간 | 비고 |
|---|---|---|
| json.tool 펼치기 (4칸) | 8.28초 / 8.38초 | 결과 83.7MB, 3,500,005줄 |
| json.tool –compact | 5.47초 | 한 줄로 다시 쓰기 |
| jpick.py로 조회수 30만 개 추출 | 1.91초 | 읽기만 하고 출력 |
| node 펼치기 (스크립트 내부 시간) | 1.23초 / 2.29초 / 1.44초 | node 시작 시간 제외 |
두 결과를 바이트 단위로 비교해 보니, 파이썬 결과의 CRLF를 LF로 바꾸고 맨 끝 줄바꿈 하나를 빼면 node 결과와 완전히 같았습니다. 파이썬 쪽 파일이 약 3.5MB 큰 건 350만 줄의 줄바꿈이 CRLF라서입니다.
json.tool이 node보다 몇 배 느린 이유가 궁금해서 설치된 파이썬의 json/encoder.py를 열어 봤습니다. C로 만든 빠른 인코더는 json.dumps()로 한 번에 문자열을 만들면서 들여쓰기가 없을 때만 쓰이고, json.tool처럼 json.dump()로 파일에 조금씩 쓰는 경우에는 순수 파이썬 코드가 돕니다. 그래서 --compact도 5초가 넘게 걸렸습니다. 다만 수십 MB 정도는 몇 초면 되니, 가끔 보는 용도라면 충분합니다.
주의할 점
- 윈도우에서
>로 저장할 땐-X utf8이나 출력 파일 인자를 쓰세요. 이모지 하나에 cp949 오류로 멈췄습니다. - 파이프로 받는 파이썬 스크립트는 바이트로 읽으세요. 윈도우 표준 입력은 기본이 cp949였습니다.
- 예외를 넓게 잡으면 원인이 가려집니다. 인코딩 오류가 “경로를 찾을 수 없음”으로 보였습니다. 이상한 메시지가 나오면 괄호 안의 예외 이름부터 보세요.
- 깨진 JSON 오류는 하나씩 나옵니다. 하나 고쳤다고 끝이 아니니, 성공할 때까지 다시 돌려 확인합니다.
- 로그 파일(.jsonl)은
--json-lines를 붙입니다. 안 붙이면 “Extra data” 오류가 납니다. - 윈도우에서 만든 결과는 줄바꿈이 CRLF입니다. 리눅스 서버로 옮겨 diff를 돌리면 모든 줄이 다르다고 나올 수 있습니다.
마무리
보기만 할 땐 python -X utf8 -m json.tool –no-ensure-ascii, 값을 뽑을 땐 jpick.py 같은 작은 스크립트 하나면 jq 없이도 충분했습니다.
가장 작은 첫걸음은 이렇습니다.
- 지금 보고 있는 JSON 파일에
python -X utf8 -m json.tool --no-ensure-ascii 파일.json을 실행해 구조를 펼쳐 봅니다. - 자주 꺼내 보는 값이 있다면 위
jpick.py를 저장하고python jpick.py items.*.title 파일.json처럼 경로를 적어 봅니다. - 설정 파일을 손으로 고친 뒤에는 json.tool을 한 번 돌려 종료 코드가 0인지 확인하는 습관을 들입니다.
다음 글에서는 AI에게 같은 질문을 두 번 보내지 않도록, 답변을 SQLite에 저장해 두고 재사용하는 캐시를 직접 만들어 보겠습니다.