Logo

자바스크립트에서 SQLite 데이터베이스 사용하기

이번 포스팅에서는 자바스크립트에서 경량화 데이터베이스 SQLite를 사용하는 기본적인 방법에 대해서 알아보겠습니다.

파이썬에서 SQLite 데이터베이스를 사용하는 방법에 대해서는 관련 게시물을 참고 바랍니다.

SQLite란?

SQLite는 전 세계에서 가장 많이 사용되는 경량화 데이터베이스입니다. 알게 모르게 우리가 쓰고 있는 대부분의 전자기기에서 SQLite가 사용되고 있기 때문이죠.

SQLite는 다른 데이터베이스와는 달리 클라이언트와 서버 간에 데이터를 송수신하지 않고, 모든 데이터를 로컬 컴퓨터에 저장하는데요. SQLite는 매우 가벼워서 하드웨어 리소스가 제한되고 특히 네트워크 통신이 어려운 환경에서 유용하게 활용됩니다. 뿐만 아니라, SQLite는 왠만한 중소규모 애플리케이션을 개발하기에도 부족하지 않는 놀라운 성능을 자랑합니다.

SQLite는 모든 데이터를 하나의 파일에 저장하며 이 파일만 복제하면 쉽게 데이터를 이전(migration)할 수 있습니다. 이러한 띄어난 이식성(portability)도 다른 데이터베이스에서는 찾기 힘든 SQLite의 특징입니다.

자바스크립트 라이브러리

자바스크립트에서 SQLite를 사용할 수 있도록 npm 저장소에는 여러 종류의 라이브러리가 올라와 있습니다.

예전에는 sqlite3sqlite, 이 2개의 라이브러리를 조합해서 사용하는 경우가 많았는데요. 최근에는 사용성을 개선한 better-sqlite3라는 라이브러리가 더 많이 사용되는 추세입니다.

본 포스팅에서는 better-sqlite3를 사용하여 SQLite 데이터에비스에 접속하고 다양한 쿼리를 실행해보겠습니다.

우선 better-sqlite3 패키지를 설치해야하고 라이브러리를 불러와야하는데요.

패키지 매니저로 npm을 사용하는 프로젝트에서는 터미널에서 npm add 명령어로 패키지를 설치합니다.

$ npm add better-sqlite3

Bun을 사용하는 프로젝트에서는 bun add 명령어로 패키지를 설치합니다.

$ bun add better-sqlite3

모듈 시스템으로 ES Modules를 사용하는 프로젝트에서는 import 문으로 라이브러리를 불러옵니다.

// ESM
import Database from "better-sqlite3";

CommonJS를 사용하는 프로젝트에서는 require 문으로 라이브러리를 불러옵니다.

// CJS
const Database = require("better-sqlite3");

데이터베이스 접속

데이터베이스 접속은 better-sqlite3 패키지로 부터 불러온 Database() 생성자를 호출하는 과정을 통해 이루어집니다.

SQLite는 기본적으로 데이터를 파일에 저장하기 때문에 데이터베이스에 접속할 때 데이터를 저장할 파일의 경로를 명시해야 합니다. 예를 들어, 현재 경로에 있는 file.db 파일에 데이터를 저장하고 싶다면, Database() 생성자의 첫 번째 인자로 파일 경로를 넘기면 됩니다.

const db = new Database("./file.db");

SQLite를 데이터를 영속적으로 저장하지 않는 인메모리(in-memory) 데이터베이스로 사용하고 싶다면 Database() 생성자의 인자로 파일 경로 대신에 :memory:를 사용합니다.

const db = new Database(":memory:");

Database() 생성자는 두 번째 인자로 다양한 옵션을 받습니다. 자주 쓰이는 옵션으로 verbose를 들 수 있는데요. SQL 구문이 실행될 때 마다 호출 될 함수를 verbose 옵션을 통해 지정해줄 수 있습니다.

const db = new Database(":memory:", { verbose: console.log });

참고로 다음과 같이 WAL 프라그마(pragma)도 활성화해주는 것이 성능 측면에서 강력하게 권장되고 있습니다.

db.pragma("journal_mode = WAL");

테이블 생성

SQLite는 관계형 데이터베이스이므로 테이블(table)에 데이터를 저장합니다. 새로운 테이블을 생성하려면 DDL(Data Definition Language)인 CREATE TABLE 쿼리을 실행해야하는데요. 위에서 데이터베이스 접속할 때 만든 Database 객체의 exec() 함수에 SQL 구문을 인자로 넘기면 됩니다.

예를 들어, idcode, name 칼럼(column)으로 이루어진 countries 테이블을 생성해보겠습니다.

db.exec(
  "CREATE TABLE countries (id INTEGER PRIMARY KEY, code TEXT, name TEXT)"
);

아래에서 code 칼럼을 기준으로 데이터 조회하거나 정렬 할 것이기 때문에 code 칼럼에 인덱스도 추가해주는 것이 성능 측면에서 좋겠죠?

db.exec("CREATE INDEX idx_code ON countries (code)");

데이터 삽입

INSERT 쿼리와 같은 DML(Data Manipulation Language)은 exec() 함수를 호출하여 바로 실행하는 것보다는 Prepared Statement를 통해서 실행하는 것이 권장됩니다. Prepared Statement를 사용하면 SQL 구문으로 부터 사용자 입력값을 분리할 수 있기 때문에 SQL Injection 공격으로 부터 애플리케이션을 보호할 수 있기 때문입니다.

다음과 같은 5개의 국가 데이터를 담은 자바스크립트 배열을 countries 테이블에 삽입하려고 합니다.

const countries = [
  { code: "KR", name: "Korea" },
  { code: "CA", name: "Canada" },
  { code: "US", name: "United States" },
  { code: "GB", name: "United Kingdom" },
  { code: "CN", name: "China" },
];

Prepared Statement는 Database 객체의 prepare() 함수에 파라미터화(parameterized)된 SQL 구문을 넘기면 만들어집니다. 예를 들어, INSERT 쿼리 내에 $code$name을 파라미터로 정의해 보겠습니다.

const insert = db.prepare(
  "INSERT INTO countries (code, name) VALUES ($code, $name)"
);

마지막으로 국가 배열을 루프 돌면서 Statement 객체의 run() 함수를 호출하면 쿼리가 실행이 됩니다. 각 국가 객체가 codename 속성으로 이루어져 있기 때문에, 파라미터화 되어 있는 INSERT 쿼리문에 바인딩(binding)될 수 있습니다.

for (const country of countries) insert.run(country);

데이터 갱신

데이트를 갱신하거나 삭제할 때는 UPDATE 쿼리를 인자로 넘겨 Statement 객체를 생성합니다. 그 다음, 생성한 Statement 객체의 run() 함수를 호출하는데요. 마찬가지로 UPDATE 구문에 바인딩할 파라미터를 담을 객체를 인자로 넘겨야 합니다.

const update = db.prepare(
  "UPDATE countries SET name = $name WHERE code = $code"
);
update.run({ code: "US", name: "미국" });

데이터 삭제

데이트를 갱신하거나 삭제할 때는 DELETE 쿼리를 인자로 넘겨 생성한 Statement 객체의 run() 함수를 호출합니다.

const remove = db.prepare("DELETE FROM countries WHERE code = ?");
remove.run("CN");

이처럼 파라미터의 개수가 적은 경우 ? 기호를 이용하여 SQL 구문 내의 파라미터 자리를 표시할 수 있습니다. 이 경우, run() 함수에 바인딩 파라미터를 넘길 때도 그냥 문자열이나 문자열을 담은 배열을 넘기면 됩니다.

데이터 조회

데이터를 조회할 때는 SELECT 쿼리를 인자로 넘겨서 Statement 객체를 생성해야 합니다. SELECT 쿼리는 조회된 데이터가 결과로 나오므로 run() 함수 대신에 get()이나 all(), iterate() 함수를 호출해야 합니다.

결과 데이터가 단 건인 경우, get() 함수에 SELECT 쿼리문에 바인딩할 파라미터를 넘깁니다.

const selectOne = db.prepare("SELECT * FROM countries WHERE code = ?");
const oneCountry = selectOne.get("KR");
console.log(oneCountry);
$ node index.js
{ id: 1, code: 'KR', name: 'Korea' }

SELECT 구문의 실행 결과로 여러 건의 데이터가 나오는 경우에는 all() 함수나 iterate() 함수를 사용해야 합니다.

all() 함수는 쿼리 결과를 배열에 담아 반환합니다.

const selectMany = db.prepare("SELECT * FROM countries ORDER BY code");
const manyCountries = selectMany.all();
console.log(manyCountries);
$ node index.js
[
  { id: 2, code: 'CA', name: 'Canada' },
  { id: 5, code: 'CN', name: 'China' },
  { id: 4, code: 'GB', name: 'United Kingdom' },
  { id: 1, code: 'KR', name: 'Korea' },
  { id: 3, code: 'US', name: 'United States' }
]

반면에 iterate() 함수는 루프를 돌리기 용이하도록 열거자(iterator) 객체를 반환합니다.

const selectMany = db.prepare("SELECT * FROM countries ORDER BY code");
for (const country of selectMany.iterate()) console.log(country);
$ node index.js
{ id: 2, code: 'CA', name: 'Canada' }
{ id: 5, code: 'CN', name: 'China' }
{ id: 4, code: 'GB', name: 'United Kingdom' }
{ id: 1, code: 'KR', name: 'Korea' }
{ id: 3, code: 'US', name: 'United States' }

데이터베이스 닫기

SQLite 데이터베이스와 연결을 종료하려면 Database 객체의 close() 함수를 호출합니다. close() 함수가 호출되면 Statement 객체를 생성하거나 실행할 수 없습니다.

db.close();

마치면서

지금까지 예제를 통해서 자바스크립트에서 SQLite 데이터베이스를 어떻게 접속하고 쿼리를 실행하는지 알아보았습니다. 실제 애플리케이션을 개발할 때는 여러 테이블을 조인(join)하기도 하고, 여러 쿼리를 하나의 트랜잭션(transaction)으로 처리하는 등 SQLite 데이터베이스를 상대로 좀 더 복잡한 작업을 수행하게 될 텐데요. better-sqlite3의 공식 API 문서를 통해서 이러한 고급 사용법을 추가로 학습하실 수 있으니 참고 바랍니다.