개발자를 위한 JSON 데이터 처리 가이드
JSON(JavaScript Object Notation)은 현대 웹 개발의 표준 데이터 교환 형식입니다. API 통신, 설정 파일, 데이터 저장 등 모든 곳에서 사용되는 JSON을 제대로 다루는 것은 필수 개발 역량입니다.
1. JSON 기본 개념과 구조
JSON의 핵심 특징
경량성과 가독성
{
"user": {
"id": 12345,
"name": "김개발",
"email": "dev@example.com",
"preferences": {
"theme": "dark",
"language": "ko",
"notifications": true
},
"skills": ["JavaScript", "Python", "React"],
"lastLogin": "2025-07-31T10:30:00Z"
}
}
언어 독립적 표준
- JavaScript에서 시작되었지만 모든 주요 프로그래밍 언어 지원
- HTTP API의 사실상 표준 데이터 형식
- XML 대비 40-50% 적은 데이터 크기
- 파싱 속도가 XML 대비 3-5배 빠름
JSON 데이터 타입과 구조
기본 데이터 타입
const jsonDataTypes = {
// 문자열 (반드시 큰따옴표 사용)
string: "Hello World",
// 숫자 (정수, 소수, 지수 표기법)
number: 42,
decimal: 3.14159,
scientific: 1.23e-4,
// 불린
boolean: true,
// null (undefined는 JSON에 없음)
null_value: null,
// 배열
array: [1, 2, 3, "mixed", true, null],
// 객체 (중첩 가능)
object: {
"nested": {
"deeply": {
"value": "found"
}
}
}
};
JSON vs JavaScript 객체 차이점
// JavaScript 객체 (유효하지만 JSON이 아님)
const jsObject = {
name: 'John', // 키에 따옴표 없음
age: undefined, // undefined 사용
date: new Date(), // Date 객체
func: function() {}, // 함수
comment: 'this works' // 단일 따옴표
};
// 유효한 JSON
const validJSON = {
"name": "John", // 키에 큰따옴표 필수
"age": null, // undefined 대신 null
"date": "2025-07-31", // 문자열로 표현
"comment": "this works" // 큰따옴표만 허용
};
실제 JSON 구조 예시
REST API 응답
{
"status": "success",
"code": 200,
"message": "사용자 정보를 성공적으로 조회했습니다",
"data": {
"user": {
"id": "user_123",
"profile": {
"name": "김개발",
"avatar": "https://example.com/avatar.jpg",
"bio": "풀스택 개발자입니다",
"location": "서울, 대한민국"
},
"stats": {
"followers": 1250,
"following": 340,
"posts": 89,
"joinedAt": "2023-01-15T09:00:00.000Z"
},
"preferences": {
"privacy": {
"profilePublic": true,
"emailVisible": false,
"showOnlineStatus": true
},
"notifications": {
"email": ["mentions", "followers"],
"push": ["messages", "comments"],
"frequency": "daily"
}
}
}
},
"meta": {
"requestId": "req_abc123",
"timestamp": "2025-07-31T12:34:56.789Z",
"executionTime": "23ms",
"rateLimit": {
"remaining": 97,
"resetAt": "2025-07-31T13:00:00.000Z"
}
}
}
설정 파일 (package.json 스타일)
{
"name": "my-awesome-project",
"version": "1.2.3",
"description": "최고의 웹 애플리케이션",
"main": "index.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest",
"build": "webpack --mode production",
"lint": "eslint . --fix"
},
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.5.0",
"jsonwebtoken": "^9.0.2"
},
"devDependencies": {
"nodemon": "^3.0.1",
"jest": "^29.6.2",
"eslint": "^8.45.0"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/username/my-awesome-project"
},
"keywords": ["nodejs", "express", "api", "mongodb"],
"author": {
"name": "김개발",
"email": "dev@example.com",
"url": "https://kimdev.blog"
},
"license": "MIT"
}
2. 검증과 파싱 기법
JSON 유효성 검증
기본 파싱과 에러 처리
class JSONValidator {
static parse(jsonString) {
try {
const parsed = JSON.parse(jsonString);
return {
success: true,
data: parsed,
error: null
};
} catch (error) {
return {
success: false,
data: null,
error: this.categorizeError(error, jsonString)
};
}
}
static categorizeError(error, jsonString) {
const errorInfo = {
name: error.name,
message: error.message,
type: 'unknown',
position: null,
suggestion: null
};
// 구문 오류 분석
if (error.message.includes('position')) {
const match = error.message.match(/position (\d+)/);
if (match) {
errorInfo.position = parseInt(match[1]);
errorInfo.context = this.getContextAroundPosition(jsonString, errorInfo.position);
}
}
// 일반적인 오류 패턴 분석
if (error.message.includes('Unexpected token')) {
errorInfo.type = 'syntax_error';
errorInfo.suggestion = '문법 오류: 따옴표, 쉼표, 괄호를 확인하세요';
} else if (error.message.includes('Unexpected end')) {
errorInfo.type = 'incomplete_json';
errorInfo.suggestion = 'JSON이 완전하지 않습니다. 닫는 괄호를 확인하세요';
} else if (error.message.includes('Unexpected string')) {
errorInfo.type = 'quote_error';
errorInfo.suggestion = '키 이름에 큰따옴표가 필요합니다';
}
return errorInfo;
}
static getContextAroundPosition(str, position, radius = 20) {
const start = Math.max(0, position - radius);
const end = Math.min(str.length, position + radius);
const before = str.substring(start, position);
const after = str.substring(position, end);
return {
before,
after,
character: str[position] || 'EOF',
line: str.substring(0, position).split('\n').length,
column: position - str.lastIndexOf('\n', position - 1)
};
}
}
// 사용 예시
const invalidJSON = `{
"name": "John",
"age": 30,
"city": "Seoul" // 마지막 쉼표 누락
"country": "Korea"
}`;
const result = JSONValidator.parse(invalidJSON);
console.log(result);
// {
// success: false,
// error: {
// type: 'syntax_error',
// suggestion: '문법 오류: 따옴표, 쉼표, 괄호를 확인하세요',
// position: 45,
// context: { ... }
// }
// }
고급 JSON 스키마 검증
class JSONSchemaValidator {
constructor(schema) {
this.schema = schema;
}
validate(data) {
const errors = [];
this.validateObject(data, this.schema, '', errors);
return {
valid: errors.length === 0,
errors: errors,
data: data
};
}
validateObject(obj, schema, path, errors) {
// 타입 검증
if (schema.type && typeof obj !== schema.type) {
errors.push({
path: path,
message: `Expected ${schema.type}, got ${typeof obj}`,
value: obj
});
return;
}
// 필수 필드 검증
if (schema.required) {
schema.required.forEach(field => {
if (!(field in obj)) {
errors.push({
path: path + '.' + field,
message: `Required field missing: ${field}`,
value: undefined
});
}
});
}
// 속성별 검증
if (schema.properties) {
Object.keys(schema.properties).forEach(key => {
if (key in obj) {
const newPath = path ? `${path}.${key}` : key;
this.validateField(obj[key], schema.properties[key], newPath, errors);
}
});
}
// 추가 속성 검증
if (schema.additionalProperties === false) {
const allowedKeys = Object.keys(schema.properties || {});
Object.keys(obj).forEach(key => {
if (!allowedKeys.includes(key)) {
errors.push({
path: path + '.' + key,
message: `Additional property not allowed: ${key}`,
value: obj[key]
});
}
});
}
}
validateField(value, fieldSchema, path, errors) {
// 타입별 상세 검증
switch (fieldSchema.type) {
case 'string':
this.validateString(value, fieldSchema, path, errors);
break;
case 'number':
this.validateNumber(value, fieldSchema, path, errors);
break;
case 'array':
this.validateArray(value, fieldSchema, path, errors);
break;
case 'object':
this.validateObject(value, fieldSchema, path, errors);
break;
}
}
validateString(str, schema, path, errors) {
if (schema.minLength && str.length < schema.minLength) {
errors.push({
path: path,
message: `String too short. Minimum length: ${schema.minLength}`,
value: str
});
}
if (schema.maxLength && str.length > schema.maxLength) {
errors.push({
path: path,
message: `String too long. Maximum length: ${schema.maxLength}`,
value: str
});
}
if (schema.pattern && !new RegExp(schema.pattern).test(str)) {
errors.push({
path: path,
message: `String does not match pattern: ${schema.pattern}`,
value: str
});
}
if (schema.enum && !schema.enum.includes(str)) {
errors.push({
path: path,
message: `Value must be one of: ${schema.enum.join(', ')}`,
value: str
});
}
}
}
// 사용 예시
const userSchema = {
type: 'object',
required: ['name', 'email'],
properties: {
name: {
type: 'string',
minLength: 2,
maxLength: 50
},
email: {
type: 'string',
pattern: '^[^@]+@[^@]+\\.[^@]+$'
},
age: {
type: 'number',
minimum: 0,
maximum: 150
},
role: {
type: 'string',
enum: ['user', 'admin', 'moderator']
}
},
additionalProperties: false
};
const validator = new JSONSchemaValidator(userSchema);
const result = validator.validate({
name: 'Kim',
email: 'invalid-email',
age: 25,
role: 'user',
extra: 'not allowed'
});
console.log(result.errors);
// [
// {
// path: 'email',
// message: 'String does not match pattern: ^[^@]+@[^@]+\\.[^@]+$',
// value: 'invalid-email'
// },
// {
// path: 'extra',
// message: 'Additional property not allowed: extra',
// value: 'not allowed'
// }
// ]
안전한 JSON 파싱
보안 고려사항
class SecureJSONParser {
constructor(options = {}) {
this.maxDepth = options.maxDepth || 10;
this.maxKeys = options.maxKeys || 1000;
this.maxStringLength = options.maxStringLength || 1000000;
this.maxArrayLength = options.maxArrayLength || 10000;
}
parse(jsonString) {
// 크기 제한 검사
if (jsonString.length > this.maxStringLength) {
throw new Error(`JSON string too large: ${jsonString.length} > ${this.maxStringLength}`);
}
let parsed;
try {
parsed = JSON.parse(jsonString);
} catch (error) {
throw new Error(`Invalid JSON: ${error.message}`);
}
// 구조 검증
this.validateStructure(parsed, 0);
return parsed;
}
validateStructure(obj, depth) {
if (depth > this.maxDepth) {
throw new Error(`JSON nesting too deep: ${depth} > ${this.maxDepth}`);
}
if (Array.isArray(obj)) {
if (obj.length > this.maxArrayLength) {
throw new Error(`Array too large: ${obj.length} > ${this.maxArrayLength}`);
}
obj.forEach(item => this.validateStructure(item, depth + 1));
} else if (obj && typeof obj === 'object') {
const keys = Object.keys(obj);
if (keys.length > this.maxKeys) {
throw new Error(`Too many object keys: ${keys.length} > ${this.maxKeys}`);
}
keys.forEach(key => {
if (key.length > 100) { // 키 길이 제한
throw new Error(`Object key too long: ${key.length} > 100`);
}
this.validateStructure(obj[key], depth + 1);
});
}
}
// 안전한 stringify (순환 참조 처리)
stringify(obj, space = null) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular Reference]';
}
seen.add(value);
}
// 함수나 undefined 값 처리
if (typeof value === 'function') {
return '[Function]';
}
if (value === undefined) {
return '[Undefined]';
}
return value;
}, space);
}
}
// 사용 예시
const secureParser = new SecureJSONParser({
maxDepth: 5,
maxKeys: 100,
maxStringLength: 50000
});
try {
const data = secureParser.parse(dangerousJSON);
console.log('안전하게 파싱됨:', data);
} catch (error) {
console.error('파싱 실패:', error.message);
}
3. 고급 처리 기법
JSON 스트리밍 처리
대용량 JSON 파일 스트리밍
class JSONStreamProcessor {
constructor() {
this.buffer = '';
this.depth = 0;
this.inString = false;
this.escapeNext = false;
this.objectStack = [];
}
// 청크 단위로 JSON 처리
processChunk(chunk) {
this.buffer += chunk;
const results = [];
let i = 0;
while (i < this.buffer.length) {
const char = this.buffer[i];
// 문자열 내부 처리
if (this.inString) {
if (this.escapeNext) {
this.escapeNext = false;
} else if (char === '\\') {
this.escapeNext = true;
} else if (char === '"') {
this.inString = false;
}
} else {
// 문자열 시작
if (char === '"') {
this.inString = true;
}
// 객체/배열 시작
else if (char === '{' || char === '[') {
this.depth++;
this.objectStack.push({
type: char === '{' ? 'object' : 'array',
start: i
});
}
// 객체/배열 끝
else if (char === '}' || char === ']') {
this.depth--;
if (this.depth === 0 && this.objectStack.length > 0) {
// 완전한 객체 발견
const start = this.objectStack[0].start;
const jsonStr = this.buffer.substring(start, i + 1);
try {
const parsed = JSON.parse(jsonStr);
results.push(parsed);
} catch (error) {
console.error('JSON 파싱 오류:', error.message);
}
// 버퍼에서 처리된 부분 제거
this.buffer = this.buffer.substring(i + 1);
this.objectStack = [];
i = -1; // 루프 재시작
}
}
}
i++;
}
return results;
}
// 스트림 완료 처리
flush() {
if (this.buffer.trim()) {
console.warn('처리되지 않은 데이터가 남아있습니다:', this.buffer);
}
}
}
// Node.js 스트림과 함께 사용
const fs = require('fs');
const path = require('path');
async function processLargeJSONFile(filePath) {
const processor = new JSONStreamProcessor();
const stream = fs.createReadStream(filePath, { encoding: 'utf8', highWaterMark: 16 * 1024 });
return new Promise((resolve, reject) => {
const results = [];
stream.on('data', (chunk) => {
const objects = processor.processChunk(chunk);
results.push(...objects);
});
stream.on('end', () => {
processor.flush();
resolve(results);
});
stream.on('error', reject);
});
}
JSON 변환과 매핑
복잡한 데이터 변환
class JSONTransformer {
constructor(transformRules) {
this.rules = transformRules;
}
transform(data) {
return this.applyRules(data, this.rules);
}
applyRules(data, rules) {
if (Array.isArray(data)) {
return data.map(item => this.applyRules(item, rules));
}
if (data && typeof data === 'object') {
const result = {};
Object.entries(rules).forEach(([targetKey, rule]) => {
if (typeof rule === 'string') {
// 단순 키 매핑
result[targetKey] = this.getNestedValue(data, rule);
} else if (typeof rule === 'function') {
// 함수를 통한 변환
result[targetKey] = rule(data);
} else if (rule.source) {
// 복합 규칙
let value = this.getNestedValue(data, rule.source);
// 타입 변환
if (rule.type) {
value = this.convertType(value, rule.type);
}
// 기본값 설정
if (value === undefined && rule.default !== undefined) {
value = rule.default;
}
// 검증
if (rule.validate && !rule.validate(value)) {
throw new Error(`Validation failed for ${targetKey}: ${value}`);
}
result[targetKey] = value;
}
});
return result;
}
return data;
}
getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => {
return current && current[key] !== undefined ? current[key] : undefined;
}, obj);
}
convertType(value, type) {
switch (type) {
case 'string':
return String(value);
case 'number':
return Number(value);
case 'boolean':
return Boolean(value);
case 'date':
return new Date(value);
case 'array':
return Array.isArray(value) ? value : [value];
default:
return value;
}
}
}
// 사용 예시: API 응답을 프론트엔드 형식으로 변환
const apiResponse = {
user_info: {
first_name: "김",
last_name: "개발",
email_address: "dev@example.com",
created_date: "2023-01-15T09:00:00Z",
is_active: "true",
profile_data: {
avatar_url: "https://example.com/avatar.jpg",
bio_text: "풀스택 개발자"
}
}
};
const transformRules = {
id: 'user_info.id',
name: (data) => `${data.user_info.first_name}${data.user_info.last_name}`,
email: 'user_info.email_address',
avatar: 'user_info.profile_data.avatar_url',
bio: 'user_info.profile_data.bio_text',
isActive: {
source: 'user_info.is_active',
type: 'boolean'
},
joinedAt: {
source: 'user_info.created_date',
type: 'date'
},
displayName: {
source: 'user_info.display_name',
default: '사용자'
}
};
const transformer = new JSONTransformer(transformRules);
const frontendData = transformer.transform(apiResponse);
console.log(frontendData);
// {
// name: "김개발",
// email: "dev@example.com",
// avatar: "https://example.com/avatar.jpg",
// bio: "풀스택 개발자",
// isActive: true,
// joinedAt: Date object,
// displayName: "사용자"
// }
JSON 패치와 업데이트
RFC 6902 JSON Patch 구현
class JSONPatch {
static apply(document, patches) {
let result = JSON.parse(JSON.stringify(document)); // 깊은 복사
patches.forEach(patch => {
result = this.applyOperation(result, patch);
});
return result;
}
static applyOperation(document, operation) {
const { op, path, value, from } = operation;
switch (op) {
case 'add':
return this.add(document, path, value);
case 'remove':
return this.remove(document, path);
case 'replace':
return this.replace(document, path, value);
case 'move':
return this.move(document, from, path);
case 'copy':
return this.copy(document, from, path);
case 'test':
return this.test(document, path, value) ? document :
(() => { throw new Error(`Test failed at ${path}`) })();
default:
throw new Error(`Unknown operation: ${op}`);
}
}
static add(document, path, value) {
const segments = this.parsePath(path);
const target = this.navigateToParent(document, segments);
const lastSegment = segments[segments.length - 1];
if (Array.isArray(target)) {
const index = lastSegment === '-' ? target.length : parseInt(lastSegment);
target.splice(index, 0, value);
} else {
target[lastSegment] = value;
}
return document;
}
static remove(document, path) {
const segments = this.parsePath(path);
const target = this.navigateToParent(document, segments);
const lastSegment = segments[segments.length - 1];
if (Array.isArray(target)) {
target.splice(parseInt(lastSegment), 1);
} else {
delete target[lastSegment];
}
return document;
}
static replace(document, path, value) {
const segments = this.parsePath(path);
const target = this.navigateToParent(document, segments);
const lastSegment = segments[segments.length - 1];
target[lastSegment] = value;
return document;
}
static parsePath(path) {
if (path === '') return [];
return path.substring(1).split('/').map(segment =>
segment.replace(/~1/g, '/').replace(/~0/g, '~')
);
}
static navigateToParent(document, segments) {
let current = document;
for (let i = 0; i < segments.length - 1; i++) {
const segment = segments[i];
if (Array.isArray(current)) {
current = current[parseInt(segment)];
} else {
current = current[segment];
}
if (current === undefined) {
throw new Error(`Path not found: ${segments.slice(0, i + 1).join('/')}`);
}
}
return current;
}
// 두 JSON 객체의 차이점을 패치로 생성
static createPatch(original, modified) {
const patches = [];
this.generatePatches(original, modified, '', patches);
return patches;
}
static generatePatches(original, modified, path, patches) {
if (original === modified) return;
// 타입이 다르면 replace
if (typeof original !== typeof modified ||
Array.isArray(original) !== Array.isArray(modified)) {
patches.push({ op: 'replace', path, value: modified });
return;
}
if (Array.isArray(modified)) {
// 배열 처리
const maxLength = Math.max(original.length, modified.length);
for (let i = maxLength - 1; i >= 0; i--) {
const currentPath = `${path}/${i}`;
if (i >= modified.length) {
patches.push({ op: 'remove', path: currentPath });
} else if (i >= original.length) {
patches.push({ op: 'add', path: currentPath, value: modified[i] });
} else {
this.generatePatches(original[i], modified[i], currentPath, patches);
}
}
} else if (modified && typeof modified === 'object') {
// 객체 처리
const allKeys = new Set([...Object.keys(original || {}), ...Object.keys(modified)]);
allKeys.forEach(key => {
const currentPath = `${path}/${key.replace(/~/g, '~0').replace(/\//g, '~1')}`;
if (!(key in modified)) {
patches.push({ op: 'remove', path: currentPath });
} else if (!(key in (original || {}))) {
patches.push({ op: 'add', path: currentPath, value: modified[key] });
} else {
this.generatePatches(original[key], modified[key], currentPath, patches);
}
});
} else {
// 원시 값 변경
patches.push({ op: 'replace', path, value: modified });
}
}
}
// 사용 예시
const original = {
user: {
name: "김개발",
age: 30,
skills: ["JavaScript", "Python"]
}
};
const modified = {
user: {
name: "김개발",
age: 31,
skills: ["JavaScript", "Python", "React"],
location: "Seoul"
}
};
// 패치 생성
const patches = JSONPatch.createPatch(original, modified);
console.log(patches);
// [
// { op: 'replace', path: '/user/age', value: 31 },
// { op: 'add', path: '/user/skills/2', value: 'React' },
// { op: 'add', path: '/user/location', value: 'Seoul' }
// ]
// 패치 적용
const result = JSONPatch.apply(original, patches);
console.log(result); // modified와 동일한 결과