끄적이는 개발노트

React Native - SQLite 본문

React Native

React Native - SQLite

크런키스틱 2025. 3. 16. 21:48
728x90

어플을 개발하면 필연적으로 따라오는 고민 중 하나로 DB가 있다.

정말 간단한 데이터와 같은 경우 async-storage와 같이 간단한 storage를 활용해도 용량과 속도에 있어 문제될 부분은 없다. 하지만, 저장해야 될 데이터가 많아질 경우 Cloud platform이나 Local Storage 서비스를 이용해야만 한다. 이를 결정짓는 요소로는 본인이 제작하는 어플의 요소들을 고려해야 한다. 본인 역시 이 부분에서 매번 고민이 많았고 다양한 DB의 특징이나 사용법들을 조회하고 사용했었다. 그 중, 본인이 가장 많이 고민했었던 선택 사항을 정리해보자면 아래와 같다.

 

1. Firebase (cloud firestore, real-time database)

  • 소규모 어플을 이용할 때 용이함.(트래픽이 적을 때)
  • 온라인 동기화
  • NoSQL
  • 상대적으로 적은 러닝커브

2. SQLite (최종 선택)

  • 관계형 DB
  • 완전 무료 및 오픈 소스
  • SQL 방식

3. Realm

  • 빠른 속도
  • NoSQL

 

0. 선택과정

본인의 선택과정은 생각보다 다사다난했다.

첫 번째 선택은 Realm으로 이유는 무료+같은 LocalStorage 서비스인 SQLite 보다 빠른 속도+MongoDB 경험 이었다. 하지만, 문서를 찾다보니 Realm의 SDK 지원이 중단된다는 말이 있어(Sync 기능, Realm 자체는 커뮤니티화로 남음) 괜시리 불안한 마음에 Firebase로 전환했다.

 

두 번째로 선택한 Firebase의 real-time과 firestore는 정말 쉽고 간단하게 사용할 수 있었다. 하지만 개발을 하다보니 간단한만큼 데이터 타입에 있어 제약적인 부분이 존재해 본인이 원하는 구조의 DB 형태를 짜기에는 다소 무리가 있었다. 그렇다고 Firestore를 통해 원하는 형태로 데이터를 저장하려다보니 당시 개발하는 어플이 우리나라 지역과 매핑되기 때문에 데이터 자체의 크기는 작아도 잦은 CRUD가 이루어질 것 같았다. 그렇게 되면 트래픽이 올라갈 수록 가격이 비싸지는 Firebase 특성상 요금에 대한 문제가 발생하기 때문에 다시 개발을 멈추고 고민을 하기 시작했다. 이 때 위에 정리하지 않은 watermelon DB, react-native-mmkv 등 다양한 DB와 storage가 존재하는구나를 알게 되었다.

 

마지막으로 다양한 자료를 찾아보고 고민하다가 Local Storage를 활용하는 것이 어플과 더 잘 어울릴 것이라는 생각과 함께 SQLite로 선택하게 되었다. 또한, RN에서 제공하는 아주 편리한 프레임워크인 expo에서도 자체적으로 expo-sqlite를 제공하는 점이 Cli를 통해 이번 어플을 개발하고 있던 나에게도 왠지 모를 끌림으로 다가왔다. 다만, NoSQL로만 DB는 다뤄봤기 때문에 SQL과 ORM에 익숙하지 않아 개발을 할 때 속도가 조금 더딘 점은 있었지만 굉장히 재밌고 유익한 시간이었다.

 

 

1. 설치

npm i react-native-sqlite-storage
#or
yarn add react-native-sqlite-storage

 

 

2. 설정

react-native.config.js

module.exports = {
  ...,
  dependencies: {
  	...,
    'react-native-sqlite-storage': {
      platforms: {
        android: {
          sourceDir:
            '../node_modules/react-native-sqlite-storage/platforms/android-native',
          packageImportPath: 'import io.liteglue.SQLitePluginPackage;',
          packageInstance: 'new SQLitePluginPackage()',
        },
      },
    },
    ...
  },
  ...
};

 

 

3. DB 생성

우선적으로 db를 생성해야 한다.

DB Browser를 통해 db를 생성하고 해당 파일을 복사하는 것을 추천한다.

https://sqlitebrowser.org/

 

DB Browser for SQLite

DB Browser for SQLite DB Browser for SQLite (DB4S) is a high quality, visual, open source tool designed for people who want to create, search, and edit SQLite or SQLCipher database files. DB4S gives a familiar spreadsheet-like interface on the database in

sqlitebrowser.org

해당 프로그램을 다운로드받고 실행한 뒤에 새 데이터베이스를 클릭하면 된다.

이 때, 원하는 이름으로 DB 파일을 생성하고 해당 파일을 android/app/src/main/assets/www/test.db 와 같이 복사하면 된다. (www 폴더는 직접 생성해야 한다.)

 

4. DB 연결 및 테이블 생성

  1. SQLite.enablePromise(true) 를 실행
  2. SQLite.openDatabase() 을 통해 db 연결
    • name : 불러올 DB 파일명
    • location : DB 위치 (www 폴더에 담았을 경우, default)
    • createFromLocation : DB 파일 경로
  3. db.executeSql(query, []) 를 통해 원하는 SQL 쿼리문 실행
// App.tsx

import './global.css';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import Navigation from 'navigation';
import {RecoilRoot} from 'recoil';
import Toast from 'react-native-toast-message';
import {toastConfig} from '@/config/toast.config';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
import SQLite from 'react-native-sqlite-storage';
import {useEffect} from 'react';

// Enable the SQLite
SQLite.enablePromise(true);

const App = () => {
  const getDBConnection = async () => {
    return SQLite.openDatabase({
      name: 'test.db',
      location: 'default',
      createFromLocation: '~www/test.db',
    });
  };

  // Create table
  const createTable = async (db: SQLite.SQLiteDatabase) => {
    const query = `CREATE TABLE IF NOT EXISTS test(id TEXT UNIQUE PRIMARY KEY, title TEXT, contents TEXT, count INTEGER);`;

    return await db.executeSql(query, []);
  };

  // Create SQLite Table (If exists, no create)
  const createSqlTable = async () => {
    const db = await getDBConnection()
      .then(res => {
        console.log('성공', res);
        return res;
      })
      .catch(err => console.error(err));

    if (db) await createTable(db);
  };

  useEffect(() => {
    createSqlTable();
  }, []);

  return (
    <RecoilRoot>
      <GestureHandlerRootView style={{flex: 1}}>
        <SafeAreaProvider>
          <Navigation />
          <Toast config={toastConfig} />
        </SafeAreaProvider>
      </GestureHandlerRootView>
    </RecoilRoot>
  );
};

export default App;

 

openDatabase의 경우, db를 연결해서 사용하고자 하는 위치에서 실행해주면 된다.

테이블 생성은 SQL문에서 `CREATE TABLE IF NOT EXISTS`를 활용하여 존재하지 않을 경우 테이블이 생성되게끔 설정했다.

 

 

5. 간단한 CRUD

아래는 정말 간단한 CRUD 예시 코드로 다양한 쿼리는 필요에 따라 SQL문을 참고하면서 작성하면 된다. 본인 역시 SQL을 다뤄보는 것은 이번 어플을 개발하면서 처음이었기 때문에 다양한 쿼리문을 검색하고 공부하면서 개발을 진행했다.

// test.tsx

import CustomText from '@/components/text';
import {TestProps} from '@/types/stack';
import {useState} from 'react';
import {Button, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import SQLite from 'react-native-sqlite-storage';

const TestScreen = ({navigation}: TestProps) => {
  const [data, setData] = useState<{
    id: string;
    title: string;
    contents: string;
    count: number;
  }>({
    id: '',
    title: '',
    contents: '',
    count: 0,
  });

  // Connect DB
  const getDBConnection = async () => {
    return SQLite.openDatabase({
      name: 'test.db',
      location: 'default',
      createFromLocation: '~www/test.db',
    });
  };

  // Create Table
  const saveTable = async (tableName: string, data: any) => {
    const db = await getDBConnection();
    const query = `INSERT OR REPLACE INTO ${tableName}(id, title, contents, count) VALUES(?, ?, ?, ?)`;

    return await db.executeSql(query, [
      data.id,
      data.title,
      data.contents,
      data.count,
    ]);
  };

  // Read Table
  const readTable = async (tableName: string) => {
    const db = await getDBConnection();
    const query = `SELECT * FROM ${tableName}`;

    return await db.executeSql(query, []);
  };

  // Update Table
  const updateTable = async (tableName: string, data: any) => {
    const db = await getDBConnection();
    const query = `UPDATE ${tableName} SET (id, title, contents, count) = (?, ?, ?, ?) WHERE id = '${data.id}'`;

    return await db.executeSql(query, [
      data.id,
      data.title,
      data.contents,
      data.count,
    ]);
  };

  // Delete Table
  const deleteTable = async (tableName: string) => {
    const db = await getDBConnection();
    const query = `DELETE from ${tableName}`;
    return await db.executeSql(query);
  };

  return (
    <SafeAreaView className="flex-1">
      <Button
        title="저장"
        onPress={async () => {
          const data = {
            id: 'testId',
            title: 'test',
            contents: 'hi',
            count: 1,
          };
          await saveTable('test', data);
        }}
      />
      <Button
        title="읽기"
        onPress={async () => {
          await readTable('test').then(res => setData(res[0].rows.item(0)));
        }}
      />
      <Button
        title="업데이트"
        onPress={async () => {
          const data = {
            id: 'testId',
            title: 'test2',
            contents: 'change',
            count: 2,
          };
          await updateTable('test', data);
        }}
      />
      <Button
        title="삭제"
        onPress={async () => {
          await deleteTable('test');
          setData({
            id: '',
            title: '',
            contents: '',
            count: 0,
          });
        }}
      />
      <View>
        <CustomText>{data.id}</CustomText>
        <CustomText>{data.title}</CustomText>
        <CustomText>{data.contents}</CustomText>
        <CustomText>{data.count ? data.count : ''}</CustomText>
      </View>
    </SafeAreaView>
  );
};

export default TestScreen;

 

 

6. 유의할 점

개인적으로 개발하면서 느낀 유의할 점으로는,

  • SQLite는 json 형태로 데이터가 저장되지 않기 때문에 json형태의 값을 저장하고자싶다면 TEXT 타입과 JSON.stringify를 통해 string 형태로 바꿔주고서 저장해야 한다.
  • 위에 언급했듯이 json 형태 text로 저장한 후에 extract나 제공하는 몇가지 메소드를 통해 가공할 수 있지만, json의 형태가 복잡할 수록 한계가 있어 가급적이면 피하는 것이 좋다.
  • data를 전달할 때, 위와 같이 column과 데이터 배열이 틀리지 않게 잘 전달해주어야 한다.
  • update 값을 바로 반환받는 RETURNING * 이 SQLite에서는 작동하지 않아, 값이 필요할 경우 별도로 한번 조회를 해주어야 한다.
  • read를 통해 받아온 데이터를 꺼내려면 반환값의 형태가 바로 데이터가 아니기 때문에 res[0].rows.item(0) 이나 res[0].rows.item(0)['필드']와 같이 접근해야 한다.

 

7. 실행

실행해보면 아래와 같이 데이터의 간단한 CRUD가 잘 작동하는 것을 확인할 수 있다.

728x90