728x90
반응형
JSONB 타입 이해하기
PostgreSQL의 JSONB 타입은 JSON 데이터를 바이너리 형태로 저장하는 데이터 타입입니다. 이 타입을 사용할 때 가장 많이 하는 실수 중 하나가 일반 문자열을 그대로 저장하려고 하는 것입니다.
핵심 포인트: JSONB 컬럼에는 반드시 유효한 JSON 형식의 데이터만 저장할 수 있습니다.
기본 규칙
✅ 저장 가능한 데이터
- JSON 객체: {"key": "value"}
- JSON 배열: ["item1", "item2"]
- JSON 문자열: "hello world"
- JSON 숫자: 123
- JSON 불린: true, false
- JSON null: null
❌ 저장 불가능한 데이터
- 일반 문자열 (따옴표 없이): hello world
- 쉼표로 구분된 문자열: WORD,BULL,KEY
- 유효하지 않은 JSON 형식
JSON 형식 문자열 저장
이미 JSON 형식으로 변환된 문자열은 그대로 PostgreSQL JSONB 컬럼에 저장할 수 있습니다.
예제 1: JSON 배열 문자열
// 이미 JSON 형식의 문자열
const jsonString = '["WORD","BULL","KEY"]';
// PostgreSQL에 그대로 저장 가능
await query('INSERT INTO users (tags) VALUES ($1)', [jsonString]);
// 조회 후 사용할 때
const result = await query('SELECT tags FROM users WHERE id = $1', [userId]);
const tagsArray = JSON.parse(result.rows[0].tags); // '["WORD","BULL","KEY"]'
예제 2: JSON 객체 문자열
// 이미 JSON 형식의 문자열
const jsonString = '{"name":"홍길동","age":30}';
// PostgreSQL에 그대로 저장 가능
await query('INSERT INTO users (profile) VALUES ($1)', [jsonString]);
// 조회 후 사용할 때
const result = await query('SELECT profile FROM users WHERE id = $1', [userId]);
const profileObj = JSON.parse(result.rows[0].profile); // {name: "홍길동", age: 30}
일반 문자열 저장
일반 문자열을 JSONB 컬럼에 저장하려면 반드시 JSON.stringify()를 사용하여 JSON 형식으로 변환해야 합니다.
예제 1: 쉼표로 구분된 문자열을 배열로 저장
// 일반 문자열
const tagsString = "WORD,BULL,KEY";
// 1. 배열로 변환
const tagsArray = tagsString.split(',');
// 2. JSON 문자열로 변환
const jsonString = JSON.stringify(tagsArray);
// 3. PostgreSQL에 저장
await query('INSERT INTO users (tags) VALUES ($1)', [jsonString]);
// 조회 후 사용할 때
const result = await query('SELECT tags FROM users WHERE id = $1', [userId]);
const tagsArray = JSON.parse(result.rows[0].tags); // ["WORD","BULL","KEY"]
예제 2: 단순 문자열을 JSON 문자열로 저장
// 일반 문자열
const message = "안녕하세요";
// JSON 문자열로 변환 (따옴표가 추가됨)
const jsonString = JSON.stringify(message); // "안녕하세요"
// PostgreSQL에 저장
await query('INSERT INTO users (message) VALUES ($1)', [jsonString]);
// 조회 후 사용할 때
const result = await query('SELECT message FROM users WHERE id = $1', [userId]);
const messageString = JSON.parse(result.rows[0].message); // "안녕하세요"
실무 예제
Node.js에서 실제 사용 예제
| 상황 | 입력 데이터 | 처리 방법 | 저장될 값 |
| 이미 JSON 문자열 | '["a","b","c"]' | 그대로 저장 | ["a","b","c"] |
| JavaScript 배열 | ["a","b","c"] | JSON.stringify() 적용 | ["a","b","c"] |
| 쉼표 구분 문자열 | "a,b,c" | split() → JSON.stringify() | ["a","b","c"] |
| 단순 문자열 | "hello" | JSON.stringify() 적용 | "hello" |
완전한 CRUD 예제
// 사용자 태그 관리 예제
class UserTagService {
// 태그 저장 (여러 형태의 입력 처리)
async saveTags(userId, tags) {
let jsonString;
if (typeof tags === 'string') {
if (tags.startsWith('[') && tags.endsWith(']')) {
// 이미 JSON 배열 문자열
jsonString = tags;
} else {
// 쉼표로 구분된 문자열
const tagsArray = tags.split(',').map(tag => tag.trim());
jsonString = JSON.stringify(tagsArray);
}
} else if (Array.isArray(tags)) {
// JavaScript 배열
jsonString = JSON.stringify(tags);
} else {
throw new Error('유효하지 않은 태그 형식입니다.');
}
await query('UPDATE users SET tags = $1 WHERE id = $2', [jsonString, userId]);
}
// 태그 조회
async getTags(userId) {
const result = await query('SELECT tags FROM users WHERE id = $1', [userId]);
return JSON.parse(result.rows[0].tags);
}
// 태그 추가
async addTag(userId, newTag) {
const currentTags = await this.getTags(userId);
if (!currentTags.includes(newTag)) {
currentTags.push(newTag);
await this.saveTags(userId, currentTags);
}
}
}
베스트 프랙티스
1. 타입 검증 함수 만들기
function ensureJsonString(data) {
if (typeof data === 'string') {
try {
JSON.parse(data); // 유효한 JSON인지 확인
return data;
} catch (e) {
// 유효하지 않은 JSON이면 문자열로 처리
return JSON.stringify(data);
}
}
return JSON.stringify(data);
}
2. 에러 처리
try {
await query('INSERT INTO users (tags) VALUES ($1)', [invalidJsonString]);
} catch (error) {
if (error.code === '22P02') { // invalid input syntax for type json
console.error('유효하지 않은 JSON 형식입니다:', invalidJsonString);
// 적절한 변환 후 재시도
const validJson = JSON.stringify(invalidJsonString);
await query('INSERT INTO users (tags) VALUES ($1)', [validJson]);
}
}
3. 성능 최적화
팁: JSONB는 인덱싱이 가능하므로 자주 검색하는 필드에 대해서는 GIN 인덱스를 생성하는 것이 좋습니다.
-- JSONB 컬럼에 GIN 인덱스 생성
CREATE INDEX idx_user_tags ON users USING GIN (tags);
-- 특정 태그 검색 시 인덱스 활용
SELECT * FROM users WHERE tags @> '["WORD"]';
정리
핵심 요약
- JSON 형식 문자열: 그대로 저장 가능
- 일반 문자열: JSON.stringify() 후 저장
- 조회 후 사용: JSON.parse()로 변환
- 에러 방지: 저장 전 항상 JSON 유효성 검증
PostgreSQL의 JSONB 타입을 올바르게 사용하면 NoSQL의 유연성과 SQL의 강력함을 동시에 활용할 수 있습니다. 위의 가이드를 참고하여 안전하고 효율적인 데이터 저장을 구현해보세요!
728x90
'Tech Notes' 카테고리의 다른 글
| Next.js App Router 실행 원리 완전 가이드 (1) | 2025.08.15 |
|---|---|
| SQL 쿼리 작성 방식 비교 - 파라미터 바인딩 vs 템플릿 리터럴 (0) | 2025.08.12 |
| 📚TypeORM 핵심 개념 정리 (1) | 2025.08.11 |
| TaskMaster는 Cursor Pro, Claude Pro와 상관이 없네? (1) | 2025.08.02 |
| Shrimp Task Manager : 워크플로우/프로세스 관리 MCP (2) | 2025.08.02 |