숏컷 코드
{"id":1,"event":"login"}
{"id":2,"event":"purchase"}
{"id":3,"event":"logout"}기본 구조
한 줄에 JSON 값 하나만 둔다
JSON Lines와 NDJSON은 파일 전체가 하나의 JSON document인 형식이 아니라, 각 줄이 독립적인 JSON 값인 형식입니다. 보통 각 줄에는 object를 두지만 JSON 문법상 string, number, array도 한 줄의 값이 될 수 있습니다. 실무에서는 로그, 이벤트, 배치 export처럼 레코드가 계속 추가되는 데이터에 주로 object 한 줄씩을 씁니다.
{"level":"info","message":"started"}
{"level":"warn","message":"retrying"}
{"level":"error","message":"failed"}각 줄이 독립적이므로 파일 끝에 새로운 줄을 append하기 쉽고, 전체 파일을 메모리에 올리지 않아도 line by line으로 처리할 수 있습니다. 이 점이 큰 JSON 배열과 가장 큰 차이입니다.
줄바꿈은 레코드 경계다
pretty printed JSON처럼 object 내부를 여러 줄로 나누면 JSON Lines 형식이 깨집니다. 줄바꿈 하나가 곧 다음 레코드 시작을 뜻하기 때문입니다. 사람이 읽기 좋게 들여쓰기한 JSON 배열과 달리, JSON Lines는 기계가 안정적으로 스트리밍 처리하기 위한 형식입니다.
// 좋은 예
{"id":1,"name":"Kim"}
{"id":2,"name":"Lee"}
// 나쁜 예: 한 레코드가 여러 줄로 쪼개짐
{
"id": 1,
"name": "Kim"
}문자열 안의 줄바꿈은 실제 줄바꿈 문자로 넣지 말고 \n escape sequence로 직렬화해야 합니다. 그렇지 않으면 parser는 문자열 일부를 다음 레코드로 오해합니다.
JSON 배열이 아니라서 JSON.parse() 한 번으로 끝나지 않는다
JSON Lines 파일 전체를 그대로 JSON.parse()에 넣으면 실패합니다. 첫 번째 줄만 놓고 보면 유효한 JSON이지만, 파일 전체는 쉼표와 배열 괄호 없이 JSON 값이 여러 개 이어진 형태이기 때문입니다. 처리 흐름은 "파일 전체 parse"가 아니라 "줄 분리 후 각 줄 parse"입니다.
const rows = text
.split(/\r?\n/)
.filter(Boolean)
.map((line) => JSON.parse(line));대용량 파일에서는 위처럼 전체 문자열을 나눈 뒤 map으로 처리하기보다 스트림 reader를 사용해 한 줄씩 읽는 편이 안전합니다. 특히 로그 파일, message queue export, 데이터 파이프라인에서는 레코드 하나가 실패해도 전체 처리를 중단할지, 실패 줄만 격리할지 정책을 따로 정해야 합니다.
배열과 비교
| 상황 | JSON 배열 | JSON Lines/NDJSON |
|---|---|---|
| 작은 설정 파일 | 적합 | 과함 |
| 레코드 append | 마지막 ] 처리 필요 | 줄 하나 추가 |
| streaming 처리 | 전체 구조 관리 필요 | 한 줄씩 처리 |
| 사람이 읽는 문서 | pretty print 가능 | 한 줄 레코드 중심 |
| 레코드별 실패 격리 | 별도 로직 필요 | 줄 번호 기준 격리 쉬움 |
주의할 점
JSON Lines와 NDJSON은 "JSON 파일을 여러 줄로 예쁘게 쓴 형식"이 아닙니다. 각 줄이 독립적인 JSON 값이어야 하며, 레코드 내부 줄바꿈은 반드시 escape해야 합니다. 파일 전체를 JSON.parse()로 처리하려는 코드도 형식과 맞지 않습니다.
빈 줄 처리도 도구마다 차이가 있습니다. 많은 파서가 빈 줄을 무시하지만, 엄격한 파이프라인에서는 빈 줄을 잘못된 레코드로 볼 수 있습니다. 생산자가 빈 줄을 만들지 않도록 하고, 소비자는 필요하면 .filter(Boolean) 같은 정책을 명시적으로 둡니다.
참고 링크
2 sources