[RN/오류노트] Another Solved - The bundle 'Payload/.app/Frameworks/crsqlite.framework' is missing plist key. , This bundle Payload/.app/Frameworks/crsqlite.framework is invalid.
- 구성한 소스코드를 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
- 현재 애플리케이션의 버전을 확인하기 위해서 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>
- 현재 expo 49, expo-sqlite 11.8.0 버전을 이용 중에 있습니다. - expo 49에서 지원해 주는 버전의 최대 버전이 11.8.0 버전이기에 최대 버전을 이용 중에 있고, 더 이상의 버전을 사용하려면 expo 버전을 올려야 하는 상황이 되어서 해당 부분에 해결이 되지는 않았습니다.
- 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 버전으로 빌드를 수행할 수 없다는 오류를 확인하였습니다.
- 위에 대한 해결책을 찾을 수 없어서 expo-sqlite를 사용하는 것에 대해서 react-native-sqlite-storage 라이브러리로 대체하는 것을 결정하였습니다. - expo 49 버전을 expo 50 이상 버전으로 업데이트를 할 수도 있지만 이에 따른 다른 라이브러리의 문제가 발생할 수 있기에 다른 라이브러리로 대체하였습니다.
- expo-sqlite와 동일하게 react-native 환경에서 sqlite를 활용하기 위한 라이브러리이며, 설치나 설정이 필요하지 않은 내장형 파일 기반의 관계형 데이터베이스(RDBMS)입니다. - 외부에 데이터베이스를 두지 않고 모든 휴대폰 및 대부분의 컴퓨터에 내장되어 있어서 로컬/클라이언트 저장소를 위한 임베디드 DB입니다.
- 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';
/**
* 공통 데이터베이스 인스턴스와 테이블 실행 구문을 관리합니다.
*/classSqliteConfig{
private DATABASE_NAME = "test.db";
private dbInstance: SQLite.SQLiteDatabase | null = null; // createDBInstance 생성 이후에 전역으로 사용되는 인스턴스 /**
* 데이터베이스 인스턴스를 생성하고 이를 반환합니다.
* @returns {Promise<SQLite.SQLiteDatabase | { transaction: () => { executeSql: () => void }}>}
*/privateasync createDBInstance(): Promise<SQLite.SQLiteDatabase> {
// Web의 경우에는 이를 사용할 수 없습니다.if (Platform.OS === "web") {
return {
transaction: () => ({
executeSql: () => { },
}),
} asany;
}
// 데이터베이스 인스턴스가 존재하지 않는 경우 이를 생성하고, 제약조건을 추가합니다.const dbInstance = SQLite.openDatabase(this.DATABASE_NAME); // 데이터베이스를 열어주고 값을 대입합니다.awaitthis.enableForeignKeys(dbInstance); // 왜래키 제약조건을 활성화 합니다.awaitthis.enableWAL(dbInstance); // WAL(Write-Ahead Logging) 모드를 활성화합니다.awaitthis.setBusyTimeout(dbInstance); // 데이터베이스 락 타임아웃을 설정합니다.return dbInstance;
}
/**
* 데이터베이스 인스턴스를 닫아줍니다.
* @returns {Promise<void>}
*/privateasync closeDatabase(dbInstance): Promise<void> {
await dbInstance.closeAsync();
dbInstance = null;
}
/**
* 왜래키 제약조건을 활성화 합니다.
* - 왜래 키 제약 조건이 활성화되면, 테이블 간의 관계를 유지하고 데이터 무결성을 보장하는 데 도움이 됩니다.
* @param {SQLite.SQLiteDatabase}dbInstance
* @returns {Promise<void> }
*/privateasync enableForeignKeys(dbInstance: SQLite.SQLiteDatabase): Promise<void> {
returnnewPromise((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>}
*/privateasync enableWAL(dbInstance: SQLite.SQLiteDatabase): Promise<void> {
returnnewPromise((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>}
*/privateasync setBusyTimeout(dbInstance: SQLite.SQLiteDatabase, timeout: number = 5000): Promise<void> {
returnnewPromise((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 = awaitthis.createDBInstance() as SQLite.SQLiteDatabase;
const execute = newPromise(async (resolve, reject) => {
// 해당 트랜잭션을 수행하면 성공 시 COMMIT / 실패 시 ROLLBACK이 수행됩니다.this.dbInstance!.transaction(async (tx) => {
tx.executeSql(query, params,
// SQL 실행 성공(_, result) => {
resolve(result.rows._array)
},
// SQL 실행 실패 (_, error) => {
reject(error);
returnfalse;
},
)
},
// errorCallback: 트랜잭션 실패 시 수행(error) => {
console.log("[-] 트랜잭션 실패 :: ", error);
reject(error);
},
// successCallback: 트랜잭션 성공 시 수행() => {
// 트랜잭션이 성공적으로 완료된 후에만 데이터베이스를 닫습니다this.closeDatabase(this.dbInstance)
.then(() =>console.log("[+] 데이터베이스 연결 종료"))
.catch(error =>console.log("[-] 데이터베이스 연결 종료 실패 :: ", error));
});
});
return execute;
} catch (error) {
thrownewError(`쿼리 실행중에 오류가 발생하였습니다 :: ${error}`,)
}
}
}
exportdefaultnew SqliteConfig();