재활용 수거함에 ‘페트병 전용’이라는 라벨을 붙여 놓았는데, 누군가 종이 조각을 집어넣어도 아무런 항의 없이 묵묵히 받아들이는 모습을 상상해 보세요.
이것이 바로 개발자가 SQLite 타입 시스템을 처음 만났을 때 겪게 되는 등골 오싹한 경험입니다.
타입이 맞지 않으면 즉시 입국을 거부하는 PostgreSQL 같은 엄격한 세관 스타일에 익숙하다면, SQLite 소탈함(또는 느슨함)에 인생을 의심하게 될지도 모릅니다.
더 끔찍한 것은, 테이블 구조를 변경하려고 할 때 그것이 당신에게 이렇게 말한다는 것입니다:
“테이블을 직접 수정할 수 없습니다. 새 집을 짓고, 가구를 옮긴 다음, 오래된 집을 폭파하세요.”
SQLite의 내부에는 5가지 스토리지 클래스만 존재한다
CREATE TABLE에서 아무리 화려한 타입 이름(VARCHAR(255), BIGINT, DECIMAL)을 선언하더라도, SQLite 내부적으로는 다음 5가지 스토리지 클래스만 인식합니다:
| 스토리지 클래스 | 설명 |
|---|---|
| NULL | 빈 값 |
| INTEGER | 정수 (값의 크기에 따라 자동으로 1~8 바이트 점유) |
| REAL | 부동 소수점 수 (고정 8 바이트) |
| TEXT | 문자열 (기본 UTF-8 인코딩) |
| BLOB | 바이너리 데이터 (입력된 상태 그대로 저장) |
컬럼에 설정한 타입은 SQLite에게 단지 ‘권장 사항’일 뿐이며, ‘강제 규칙’이 아닙니다.
이를 **“타입 어피니티(Type Affinity)”**라고 부릅니다.
SQLite는 데이터를 권장 타입으로 변환하려고 시도하지만, 변환할 수 없는 경우에는 에러 없이 원본 데이터를 그대로 삽입합니다.
SQLite에서 age INTEGER 컬럼을 선언하고 문자열 '영원한 18세'를 삽입해도 아주 기쁘게 받아들여집니다.
가장 쉽게 빠지기 쉬운 3가지 타입 함정
함정 1: 네이티브 Boolean 부재
SQLite에는 Boolean 타입이 없습니다. True와 False는 정수 1과 0으로만 표현할 수 있습니다.
Node.js를 사용해 SQLite에서 데이터를 조회할 때, true나 false가 아니라 숫자 1 또는 0이 반환됩니다.
if (user.is_admin === true)와 같은 조건식을 직접 사용하면 절대 참이 되지 않습니다.
함정 2: Date/Time 타입 부재
SQLite에는 날짜 시간 타입이 없습니다. 시간은 다음과 같이 저장할 수밖에 없습니다:
| 저장 방식 | 예시 | 장단점 |
|---|---|---|
| TEXT (ISO-8601 문자열) | '2026-05-19T18:00:00Z' |
가장 추천. 가독성이 높고 향후 PostgreSQL 마이그레이션 시 매끄럽게 변환 가능 |
| INTEGER (Unix 타임스탬프) | 1747656000 |
공간을 적게 차지하지만 사람이 읽을 수 없음 |
날짜를 2026/5/19 오후 6시와 같이 자신만의 커스텀 형식으로 저장하지 마세요. 향후 데이터 마이그레이션 시 재앙이 일어날 것입니다.
함정 3: INTEGER 컬럼에 문자열을 삽입해도 에러가 나지 않음
PostgreSQL에서는 INTEGER 컬럼에 문자열을 삽입하면 즉시 에러가 발생합니다. 하지만 SQLite는 묵묵히 변환을 시도하고, 실패하면 원본 그대로 수락합니다.
이는 오염된 데이터가 조용히 데이터베이스에 스며들 수 있음을 의미합니다. 어느 날 프로그램이 예상치 못한 타입을 받아 크래시가 나기 전까지는 문제를 발견할 수 없습니다.
방어적 프로그래밍: 관대한 데이터베이스를 엄격한 태도로 다루기
SQLite의 느슨함에 직면했을 때, Node.js 개발에서는 엄격한 방어 메커니즘을 구축해야 합니다:
| 방어 단계 | 도구 | 역할 |
|---|---|---|
| 컴파일 타임 가드 | TypeScript | 코드 작성 단계에서 잘못된 타입을 캐치함 |
| API 입구 검증 | Zod | 입력되는 데이터를 엄격하게 검증함 (age가 반드시 숫자임을 보장) |
| 내부 타입 변환 | Prisma / Drizzle ORM | SQLite와 PostgreSQL 간의 타입 차이를 자동으로 처리함 |
**‘데이터 검증의 문지기’**를 데이터베이스 계층에서
Node.js애플리케이션 계층으로 이동시키는 것이 SQLite의 개발 속도를 누리면서 향후 확장성을 확보하기 위한 핵심 전략입니다.
ORM을 사용할 경우 코드 내에서 type: 'boolean'으로 선언해 두면, ORM이 SQLite 저장 시 자동으로 1/0으로 변환하고, 조회 시 true/false로 자동 변환해 주어 기저의 타입 차이를 완벽히 가려줍니다.
ALTER TABLE은 미완성: 수정할 수 있는 것과 없는 것
SQLite의 테이블 구조 수정 지원은 매우 제한적입니다:
| 작업 | 지원 여부 |
|---|---|
컬럼 추가 (ADD COLUMN) |
예 |
컬럼 이름 변경 (RENAME COLUMN) |
예 |
컬럼 삭제 (DROP COLUMN) |
예 (최신 버전에서 지원) |
테이블 이름 변경 (RENAME TO) |
예 |
| 컬럼 타입 변경 | 아니오 |
UNIQUE, NOT NULL 제약 조건 추가/제거 |
아니오 |
| 기본키(Primary Key) 수정 | 아니오 |
| 외래키(Foreign Key) 수정 | 아니오 |
‘지원되지 않는’ 수정을 수행해야 할 때,
SQLite는 ‘재생성 및 마이그레이션’ 전략을 실행할 것을 요구합니다.
재생성 및 마이그레이션 4단계: SQLite식 테이블 업그레이드 방법
직접 변경이 불가능하므로 공식 문서가 권장하는 표준 방법은 새 집을 짓고, 가구를 옮기고, 이전 집을 날려버리고, 새 문패를 다는 것입니다:
| 단계 | 액션 | 설명 |
|---|---|---|
| 1 | 새 테이블 생성 | 올바른 구조로 CREATE TABLE users_new (...) 생성 |
| 2 | 데이터 복사 | INSERT INTO users_new SELECT ... FROM users |
| 3 | 이전 테이블 삭제 | DROP TABLE users |
| 4 | 테이블 이름 변경 | ALTER TABLE users_new RENAME TO users |
이 4개 단계는 일사천리로 실행되어야 합니다. 중간에 전원이 끊기거나 프로그램이 크래시 나면 데이터 유실로 이어집니다.
업그레이드 시 데이터 유실 방지: 두 가지 안전 방어선
방어선 1: 물리적 방어, 파일 직접 복사
SQLite는 본질적으로 하나의 파일입니다. 스키마 변경을 실행하기 전에 .db 파일을 복사해 백업본을 만드는 것으로 충분합니다.
const fs = require('fs');
fs.copyFileSync('my_project.db', 'my_project_backup.db');
만약 문제가 발생하더라도 파일을 덮어쓰는 것만으로 눈 깜짝할 사이에 복구할 수 있습니다. 이는 다른 대형 데이터베이스에서는 제공하지 않는 큰 장점입니다.
방어선 2: 트랜잭션 래퍼, 데이터베이스의 타임머신
이사 단계를 하나의 Transaction으로 감싸줍니다. 단계 중 하나라도 실패하면 전체 프로세스가 자동으로 롤백되어 아무 일도 없었던 것처럼 복원됩니다.
const Database = require('better-sqlite3');
const db = new Database('my_project.db');
const migrateData = db.transaction(() => {
db.prepare(`
CREATE TABLE users_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER NOT NULL DEFAULT 18
)
`).run();
db.prepare(`
INSERT INTO users_new (id, name, age)
SELECT id, name, COALESCE(age, 18) FROM users
`).run();
db.prepare('DROP TABLE users').run();
db.prepare('ALTER TABLE users_new RENAME TO users').run();
});
try {
migrateData();
console.log('테이블 업그레이드 완료');
} catch (error) {
console.error('업그레이드 실패, 데이터가 안전하게 복원되었습니다:', error.message);
}
**“백업 파일 + 트랜잭션 바인딩”**이 데이터베이스 마이그레이션의 에어백 역할을 합니다.
엄격한 태도로 소탈한 SQLite 제어하기
소탈한
SQLite를 엄격한 애플리케이션 계층 아키텍처로 다룸으로써, 향후 기술 부채를 방지하면서 극도로 빠른 개발 속도의 장점을 안심하고 누릴 수 있습니다.
SQLite의 타입 시스템은 매우 유연하며, ALTER TABLE 역시 제약 조건이 많습니다.
하지만 Node.js 단에서 TypeScript 타입 검사 + Zod 검증 + ORM 추상화를 착실히 적용하고, 물리적 백업 + 트랜잭션 안전 전략을 결합하면 SQLite가 제공하는 뛰어난 개발 효율성을 마음껏 누릴 수 있으며 동시에 향후 PostgreSQL로의 순탄한 이전 경로를 닦아둘 수 있습니다.