반응형
해당 글에서는 iOS 환경에서 Xcode를 기반으로 앱 빌드를 하였을 때 발생하는 오류에 대해 이를 해결하는 방법에 대해서 알아봅니다.
1) expo-sqlite 문제점
💡 expo-sqlite 문제점
- 구성한 소스코드를 App Store 내에 배포하기 위한 과정에서 아래와 같은 에러가 발생하였습니다.
- 이는 버전과 빌드 버전에 대해에 명시하지 않았다는 오류로 판단이 되었습니다.
- The bundle 'Payload/.app/Frameworks/crsqlite.framework' is missing plist key. The Info.plist file is missing the required key: CFBundleShortVersionString. Please find more information about CFBundleShortVersionString at https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring
- This bundle Payload/.app/Frameworks/crsqlite.framework is invalid. The Info.plist file is missing the required key: CFBundleVersion. Please find more information about CFBundleVersion at https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleversion
💡 아래와 같이 동일한 문제를 발생한 경우에 대해서 접근을 하였습니다.
2) expo-sqlite 문제점 해결 시도
1. Version, Build 버전 지정
💡Version, Build 버전 지정
- 현재 애플리케이션의 버전을 확인하기 위해서 Xcode 내에서 Identity 탭 내에 Bundle Identifer의 Version과 Build를 확인해 보았을 때, 값이 지정됨을 확인하였습니다.
- 그렇기에 해당 부분에 대해서 문제점을 발견하지 못하였습니다.
💡 또한 기존의 Info.plist 내에 MARKETING_VERSION, CURRENT_PROJECT_VERSION 부분에 대해서 인식하지 못하는 것과 같다는 점에서 명시적으로 CFBundleShortVersionString로 명시적으로 2.8.1로 지정하거나 CFBundleVersion로 13을 지정하였으나 동일한 문제가 반복되었습니다.
// 기존의 버전
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
// 변경된 버전
<key>CFBundleShortVersionString</key>
<string>2.8.1</string>
<key>CFBundleVersion</key>
<string>13</string>
2. expo-sqlite 버전 체크
💡 expo-sqlite 버전 체크
- 현재 expo 49, expo-sqlite 11.8.0 버전을 이용 중에 있습니다.
- expo 49에서 지원해 주는 버전의 최대 버전이 11.8.0 버전이기에 최대 버전을 이용 중에 있고, 더 이상의 버전을 사용하려면 expo 버전을 올려야 하는 상황이 되어서 해당 부분에 해결이 되지는 않았습니다.
"dependencies": {
"expo": "49.0.0",
"expo-sqlite": "11.8.0",
}
3. Xcode 다운그레이드
💡 Xcode 다운그레이드
- 아래의 글에서 착안하여 Xcode 버전에 따라서 앱 배포 시 문제점이 발생할 수 있다는 문제점을 착안하였습니다.
- 기존의 XCode 14.2 버전에서 앱을 배포하였으나, 최근 XCode 15.4 버전으로 배포하였기에 해당 부분에 의심으로 테스트를 진행하였습니다.
iOS "Asset validation failed" with Native Plugin
💡 아래와 같이 이전에 사용 중이 14.2 버전을 통해서 빌드를 수행하였습니다.
💡 Xcode 14.2 버전을 통해 수행하였을 때, 아래와 같은 문제가 발생하였습니다
- SDK version issue. This app was built with the iOS 16.2 SDK. All iOS and iPadOS apps must be built] with the iOS 17 SDK or later, included in Xcode 15 or later, in order to be uploaded to App Store Connect or submitted for distribution. (ID: b882e90e-570d-4f5d-bbd6-2dd4af120d4d)
- 해당 오류는 앱스토어 배포 요구서항과 관련된 SDK 버전 문제가 발생하였습니다.
- 현재 앱이 iOS 16.2 SDK로 빌드되었습니다 하지만 앱스토어는 iOS 17 SDK 이상의 버전으로 빌드된 앱만 허용합니다.
- 이를 해결하기 위해서는 Xcode 15 이상의 버전을 사용하여 앱을 빌드해야 합니다
- 따라서 이전 버전의 Xcode로 다운그레이드하는 것은 해결책이 될 수 없으며, 오히려 Xcode 15 이상의 최신 버전을 사용해야 앱스토어 배포가 가능합니다.
- 결론적으로, 이전 배포 때, Xcode 15 이상에서 배포가 되었기에 다시 다운그레이드로 Xcode 14 버전으로 빌드를 수행할 수 없다는 오류를 확인하였습니다.
3) Another Solved
💡 Another Solved
- 위에 대한 해결책을 찾을 수 없어서 expo-sqlite를 사용하는 것에 대해서 react-native-sqlite-storage 라이브러리로 대체하는 것을 결정하였습니다.
- expo 49 버전을 expo 50 이상 버전으로 업데이트를 할 수도 있지만 이에 따른 다른 라이브러리의 문제가 발생할 수 있기에 다른 라이브러리로 대체하였습니다.
1. react-native-sqlite-storage
💡 react-native-sqlite-storage
- expo-sqlite와 동일하게 react-native 환경에서 sqlite를 활용하기 위한 라이브러리이며, 설치나 설정이 필요하지 않은 내장형 파일 기반의 관계형 데이터베이스(RDBMS)입니다.
- 외부에 데이터베이스를 두지 않고 모든 휴대폰 및 대부분의 컴퓨터에 내장되어 있어서 로컬/클라이언트 저장소를 위한 임베디드 DB입니다.
2. expo-sqlite -> react-native-sqlite-storage Migration
💡 expo-sqlite -> react-native-sqlite-storage Migration
- expo-sqlite 라이브러리를 사용하여 sqlite를 이용하는 방식에서 react-native-sqlite-storage를 이용하여 sqlite을 이용하는 방식으로 변경을 수행하였습니다.
2.1. expo-sqlite의 sqlite Instance 생성 및 executeQuery 수행 방법
💡 expo-sqlite의 sqlite Instance 생성 및 executeQuery 수행 방법
- 기존의 구성하였던 expo-sqlite 설정 파일입니다.
1. createDBInstance()
- 데이터베이스 인스턴스를 생성하고 반환하는 메서드입니다
- 웹 플랫폼의 경우 지원하지 않으므로 빈 트랜잭션을 반환합니다
- 데이터베이스를 열고, 외래키 제약조건, WAL 모드, 타임아웃 설정을 초기화합니다
2. closeDatabase()
- 데이터베이스 연결을 종료하고 인스턴스를 null로 설정하는 메서드입니다
3. enableForeignKeys()
- 외래키 제약조건을 활성화하여 테이블 간의 관계와 데이터 무결성을 보장합니다
4. enableWAL()
- Write-Ahead Logging 모드를 활성화하여 데이터베이스 쓰기 성능과 동시성을 개선합니다
5. setBusyTimeout()
- 데이터베이스가 잠겨있을 때 대기할 최대 시간을 설정합니다
기본값은 5000밀리 초(5초)입니다
6. executeQuery()
- SQL 쿼리를 실행하는 메인 메서드입니다
- 쿼리와 선택적 매개변수를 받아 실행합니다.
- 트랜잭션을 통해 쿼리를 실행하며, 성공 시 COMMIT, 실패 시 ROLLBACK을 수행합니다.
- 쿼리 실행 후 데이터베이스 연결을 자동으로 종료합니다
import * as SQLite from 'expo-sqlite';
import { Platform } from 'react-native';
/**
* 공통 데이터베이스 인스턴스와 테이블 실행 구문을 관리합니다.
*/
class SqliteConfig {
private DATABASE_NAME = "test.db";
private dbInstance: SQLite.SQLiteDatabase | null = null; // createDBInstance 생성 이후에 전역으로 사용되는 인스턴스
/**
* 데이터베이스 인스턴스를 생성하고 이를 반환합니다.
* @returns {Promise<SQLite.SQLiteDatabase | { transaction: () => { executeSql: () => void } }>}
*/
private async createDBInstance(): Promise<SQLite.SQLiteDatabase> {
// Web의 경우에는 이를 사용할 수 없습니다.
if (Platform.OS === "web") {
return {
transaction: () => ({
executeSql: () => { },
}),
} as any;
}
// 데이터베이스 인스턴스가 존재하지 않는 경우 이를 생성하고, 제약조건을 추가합니다.
const dbInstance = SQLite.openDatabase(this.DATABASE_NAME); // 데이터베이스를 열어주고 값을 대입합니다.
await this.enableForeignKeys(dbInstance); // 왜래키 제약조건을 활성화 합니다.
await this.enableWAL(dbInstance); // WAL(Write-Ahead Logging) 모드를 활성화합니다.
await this.setBusyTimeout(dbInstance); // 데이터베이스 락 타임아웃을 설정합니다.
return dbInstance;
}
/**
* 데이터베이스 인스턴스를 닫아줍니다.
* @returns {Promise<void>}
*/
private async closeDatabase(dbInstance): Promise<void> {
await dbInstance.closeAsync();
dbInstance = null;
}
/**
* 왜래키 제약조건을 활성화 합니다.
* - 왜래 키 제약 조건이 활성화되면, 테이블 간의 관계를 유지하고 데이터 무결성을 보장하는 데 도움이 됩니다.
* @param {SQLite.SQLiteDatabase} dbInstance
* @returns {Promise<void> }
*/
private async enableForeignKeys(dbInstance: SQLite.SQLiteDatabase): Promise<void> {
return new Promise((resolve, reject) => {
dbInstance.exec([{ sql: 'PRAGMA foreign_keys = ON;', args: [] }], false, (error) => {
if (error) reject(error);
else resolve();
});
});
}
/**
* WAL(Write-Ahead Logging) 모드를 활성화합니다.
* - WAL 모드는 데이터베이스 쓰기 성능을 향상시키고 동시성을 개선합니다.
* @param {SQLite.SQLiteDatabase} dbInstance
* @returns {Promise<void>}
*/
private async enableWAL(dbInstance: SQLite.SQLiteDatabase): Promise<void> {
return new Promise((resolve, reject) => {
dbInstance.exec([{ sql: 'PRAGMA journal_mode = WAL;', args: [] }], false, (error) => {
if (error) reject(error);
else resolve();
});
});
}
/**
* 데이터베이스 락 타임아웃을 설정합니다.
* - 데이터베이스가 잠겨있을 때 대기할 최대 시간(밀리초)을 설정합니다.
* @param {SQLite.SQLiteDatabase} dbInstance
* @param {number} timeout - 타임아웃 시간(밀리초)
* @returns {Promise<void>}
*/
private async setBusyTimeout(dbInstance: SQLite.SQLiteDatabase, timeout: number = 5000): Promise<void> {
return new Promise((resolve, reject) => {
dbInstance.exec([{ sql: `PRAGMA busy_timeout = ${timeout};`, args: [] }], false, (error) => {
if (error) reject(error);
else resolve();
});
});
}
/**
* 데이터베이스 명령어를 전달받은 파라미터 SQL을 기반으로 수행합니다
* - params는 Optional하게 존재 할 수도 있고 존재하지 않을 수도 있습니다.
* @param {string} query
* @param {any[]} params
* @returns
*/
async executeQuery(query: string, params?: any[]): Promise<any> {
if (params && params.length === 0) params = [];
try {
this.dbInstance = await this.createDBInstance() as SQLite.SQLiteDatabase;
const execute = new Promise(async (resolve, reject) => {
// 해당 트랜잭션을 수행하면 성공 시 COMMIT / 실패 시 ROLLBACK이 수행됩니다.
this.dbInstance!.transaction(async (tx) => {
tx.executeSql(query, params,
// SQL 실행 성공
(_, result) => {
resolve(result.rows._array)
},
// SQL 실행 실패
(_, error) => {
reject(error);
return false;
},
)
},
// errorCallback: 트랜잭션 실패 시 수행
(error) => {
console.log("[-] 트랜잭션 실패 :: ", error);
reject(error);
},
// successCallback: 트랜잭션 성공 시 수행
() => {
// 트랜잭션이 성공적으로 완료된 후에만 데이터베이스를 닫습니다
this.closeDatabase(this.dbInstance)
.then(() => console.log("[+] 데이터베이스 연결 종료"))
.catch(error => console.log("[-] 데이터베이스 연결 종료 실패 :: ", error));
});
});
return execute;
} catch (error) {
throw new Error(`쿼리 실행중에 오류가 발생하였습니다 :: ${error}`,)
}
}
}
export default new SqliteConfig();
💡 [참고] 해당사항에 대해서는 아래의 글에서 확인이 가능합니다.
2.2. react native sqlite의 sqlite Instance 생성 및 executeQuery 수행 방법
💡 react native sqlite의 sqlite Instance 생성 및 executeQuery 수행 방법
- 기존의 expo-sqlite에서 react native sqlite storage를 활용한 Migration 방법입니다.
1. constructor()
- Promise 기반 API를 활성화하여 비동기 작업을 더 쉽게 처리할 수 있게 합니다
2. createDBInstance()
- 데이터베이스 인스턴스를 생성하고 초기 설정을 수행합니다
- 웹 플랫폼에서는 지원하지 않으며, 데이터베이스 이름과 위치를 지정하여 열기를 수행합니다
3. closeDatabase()
- 데이터베이스 연결을 안전하게 종료하는 역할을 합니다
4. enableForeignKeys()
- 외래키 제약조건을 활성화하여 데이터 무결성을 보장합니다
5. enableWAL()
- Write-Ahead Logging 모드를 활성화하여 데이터베이스 성능을 개선합니다
6. setBusyTimeout()
- 데이터베이스 락 상태에서의 대기 시간을 설정합니다 (기본값 5000ms)
7. executeQuery()
- SQL 쿼리를 실행하고 결과를 반환하는 메인 메서드입니다
- 쿼리 실행 후 결과를 배열로 변환하고 데이터베이스 연결을 자동으로 종료합니다
import SQLite from 'react-native-sqlite-storage';
import { Platform } from 'react-native';
/**
* 공통 데이터베이스 인스턴스와 테이블 실행 구문을 관리합니다.
*/
class SqliteConfig {
private DATABASE_NAME = "test.db";
constructor() {
SQLite.enablePromise(true); // Promise 기반 API 활성화
}
/**
* 데이터베이스 인스턴스를 생성하고 이를 반환합니다.
*/
private async createDBInstance(): Promise<SQLite.SQLiteDatabase> {
if (Platform.OS === "web") {
throw new Error("Web platform is not supported");
}
try {
const dbInstance = await SQLite.openDatabase({
name: this.DATABASE_NAME,
location: 'default'
});
await this.enableForeignKeys(dbInstance);
await this.enableWAL(dbInstance);
await this.setBusyTimeout(dbInstance);
return dbInstance;
} catch (error) {
throw new Error(`Failed to open database: ${error}`);
}
}
/**
* 데이터베이스 인스턴스를 닫아줍니다.
*/
private async closeDatabase(db: SQLite.SQLiteDatabase): Promise<void> {
try {
await db.close();
} catch (error) {
throw new Error(`Failed to close database: ${error}`);
}
}
/**
* 외래키 제약조건을 활성화 합니다.
*/
private async enableForeignKeys(db: SQLite.SQLiteDatabase): Promise<void> {
try {
await db.executeSql('PRAGMA foreign_keys = ON;');
} catch (error) {
throw new Error(`Failed to enable foreign keys: ${error}`);
}
}
/**
* WAL 모드를 활성화합니다.
*/
private async enableWAL(db: SQLite.SQLiteDatabase): Promise<void> {
try {
await db.executeSql('PRAGMA journal_mode = WAL;');
} catch (error) {
throw new Error(`Failed to enable WAL mode: ${error}`);
}
}
/**
* 데이터베이스 락 타임아웃을 설정합니다.
*/
private async setBusyTimeout(db: SQLite.SQLiteDatabase, timeout: number = 5000): Promise<void> {
try {
await db.executeSql(`PRAGMA busy_timeout = ${timeout};`);
} catch (error) {
throw new Error(`Failed to set busy timeout: ${error}`);
}
}
/**
* SQL 쿼리를 실행합니다.
*/
async executeQuery(query: string, params: any[] = []): Promise<any> {
let dbInstance: SQLite.SQLiteDatabase | null = null;
try {
dbInstance = await this.createDBInstance();
const [results] = await dbInstance.executeSql(query, params);
// SQLite 결과를 배열로 변환 SQLite.ResultSet
const rows: SQLite.ResultSet[] = [];
for (let i = 0; i < results.rows.length; i++) {
rows.push(results.rows.item(i));
}
return rows;
} catch (error) {
throw new Error(`Query execution failed: ${error}`);
} finally {
if (dbInstance) {
await this.closeDatabase(dbInstance);
console.log("[+] Database connection closed");
}
}
}
}
export default new SqliteConfig();
오늘도 감사합니다. 😀
반응형