React & React Native/오류노트

[RN/오류노트] Another Solved - The bundle 'Payload/.app/Frameworks/crsqlite.framework' is missing plist key. , This bundle Payload/.app/Frameworks/crsqlite.framework is invalid.

adjh54 2024. 12. 18. 20:00
반응형
해당 글에서는 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

 

💡 아래와 같이 동일한 문제를 발생한 경우에 대해서 접근을 하였습니다.

Xcode 15 distribute issue

 

 

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",
}

https://www.npmjs.com/package/expo-sqlite?activeTab=versions

 

expo-sqlite

Provides access to a database using SQLite (https://www.sqlite.org/). The database is persisted across restarts of your app.. Latest version: 15.0.3, last published: a month ago. Start using expo-sqlite in your project by running `npm i expo-sqlite`. There

www.npmjs.com

 

 

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입니다.
 

react-native-sqlite-storage

SQLite3 bindings for React Native (Android & iOS). Latest version: 6.0.1, last published: 3 years ago. Start using react-native-sqlite-storage in your project by running `npm i react-native-sqlite-storage`. There are 67 other projects in the npm registry u

www.npmjs.com

 

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();

 

💡 [참고] 해당사항에 대해서는 아래의 글에서 확인이 가능합니다.
 

[RN] React Native expo-sqlite 이해 및 설정 방법 -2 : 활용 방법 및 데이터 확인 방법

해당 글에서는 React Native에서 expo-sqlite를 이용하는 활용 방법 및 데이터베이스 데이터를 GUI 툴을 이용하여 확인하는 방법에 대해 알아봅니다 💡 [참고] 이전에 작성한 글을 참고하시면 도움이

adjh54.tistory.com

 

 

 

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();

 

 

 

 

오늘도 감사합니다. 😀

 

반응형