푸시

Prev Next

Classic/VPC 환경에서 이용 가능합니다.

개발자가 서버 또는 클라우드에서 모바일 장치로 효율적으로 메시지를 전송할 수 있게 합니다. 푸시를 사용하면 알림 메시지를 모바일 앱과 웹 앱에 전송할 수 있습니다. 푸시는 iOS, Android 애플리케이션을 포함한 다양한 플랫폼을 지원합니다.

대시보드 설정

푸시 서비스를 사용하려면 먼저 각 플랫폼의 키를 준비한 뒤 대시보드에 설정해야 합니다.

FCM 푸시 키 생성 방법

FCM 푸시 키를 발급하려면 아래 절차를 순서대로 진행해 주십시오.

  1. Firebase Console > 프로젝트 설정 > 클라우드 메시징 > 서비스 계정 관리를 클릭해 주십시오.
    ko-gamepot3-push01.png

  2. Google Cloud > 서비스 계정 > 생성되어 있는 이메일 계정을 선택해 주십시오.
    ko-gamepot3-push02.png

  3. > 키 추가 > 새 키 만들기 > 키 유형은 'JSON'으로 선택 후 [만들기] 버튼을 클릭해 주십시오.

    • 기존에 만들어 둔 키가 있다면, 해당 파일을 사용해도 됩니다.
      ko-gamepot3-push03.png

APNS 인증키 생성 방법

APNs 인증키는 iOS 푸시 발송을 위해 반드시 필요하므로 다음 절차에 따라 발급해 주십시오.

  1. 애플 개발자 사이트 로그인 > Certificates, IDs & Profiles > 키 선택 > [+] 버튼을 클릭하여 신규 키를 생성해 주십시오.

    • 푸시 인증키는 최대 2개까지만 생성할 수 있습니다.
      ko-gamepot3-push04.png
  2. Key Name을 입력하고, Apple Push notifications service (APNs)를 활성화해 주십시오.
    ko-gamepot3-push05.png

  3. [Register] 버튼을 누르고 인증키를 발급해 주십시오.
    ko-gamepot3-push06.png

  4. Key ID를 확인하고 다운로드해 주십시오.

    • 다운로드 된 파일은 확장자가 .p8 입니다.
    • Key 파일은 한번만 다운로드 가능합니다.
      ko-gamepot3-push07.png

대시보드 키 설정 방법

발급받은 키는 GAMEPOT 대시보드에 등록해야 정상적으로 푸시를 발송할 수 있습니다.

  • 프로젝트 설정 - 연동 - Google Android (FCM) Configuration

    • SenderID: Firebase 콘솔 > 프로젝트 설정 > 클라우드 메시징 > 발신자 ID
    • Private Key (json file): FCM 푸시 키 생성 방법에서 생성한 JSON 파일을 메모장 혹은 텍스트 편집기와 같은 프로그램으로 연 뒤 전체 내용을 복사하여 등록합니다.
  • 프로젝트 설정 - 연동 - Apple iOS (APNs) Configuration

    • Certificate: APNS 인증키 생성 방법 에서 생성한 .p8 파일을 메모장 혹은 텍스트 편집기와 같은 프로그램으로 연 뒤 전체 내용을 복사하여 등록합니다.
    • Key ID: APNS 인증키 생성 방법 에서 생성한 .p8 파일의 Key ID를 등록합니다.
    • Team ID: 애플 개발자 사이트 '멤버십 세부사항'의 팀 ID를 입력합니다.
    • Bundle ID: 앱 패키지의 Bundle Identifier 값을 입력합니다.

푸시 설정 방법

각 플랫폼별로 SDK 설정을 완료해야 푸시 알림을 정상적으로 수신할 수 있습니다.

AOS Native

GAMEPOT Android SDK를 사용하는 경우 아래 절차에 따라 푸시 연동을 구성해 주십시오.

Firebase Messaging 연동

Android 푸시 수신을 위해서는 FCM(Firebase Cloud Messaging) 연동이 필요합니다. FCM을 연동하려면 아래 절차를 따르세요.

  1. Firebase 프로젝트 생성 및 앱 등록

    • Firebase 콘솔에서 새 프로젝트를 생성합니다.
    • 프로젝트에 Android 앱을 추가하고, 패키지명을 입력합니다.
    • google-services.json 파일을 다운로드하여 앱 모듈의 루트 디렉터리에 추가합니다.
  2. build.gradle 설정

    • 모듈 appbuild.gradle.kts에 아래 플러그인과 의존성을 추가합니다.
    • Kotlin:
    plugins {
         id("com.google.gms.google-services")
    }
    dependencies {
         implementation("com.google.firebase:firebase-messaging-ktx:23.2.1")
    }
    
    • 프로젝트 루트의 build.gradle.kts에 아래 플러그인을 추가합니다.
    • Kotlin:
    plugins {
         id("com.google.gms.google-services") version "4.4.1" apply false
    }
    
  3. FCM 초기화 및 토큰 관리

    • 앱 실행 시 FCM이 자동으로 초기화되며, 디바이스 토큰이 발급됩니다.
    • 토큰은 서버에 등록하여 푸시 메시지 발송에 사용합니다.
  4. 문제 해결

    • Default FirebaseApp is not initialized 오류가 발생하면 gradle 설정을 다시 확인해 주십시오.
    • google-services.json 파일이 없을 경우 Firebase 콘솔에서 다시 다운로드한 뒤 올바른 위치에 추가해 주십시오.

푸시 알림 아이콘 설정

푸시 메시지를 수신했을 때 알림 바에 표시할 작은 아이콘을 설정할 수 있습니다. 별도로 설정하지 않을 경우 SDK에 포함된 기본 이미지를 사용하며, 게임에 맞는 아이콘을 직접 설정할 수 있습니다.

Android Asset Studio를 이용해 제작하면 자동으로 폴더별 이미지가 생성되어 편리합니다.

푸시 알림 아이콘을 설정하는 방법은 다음과 같습니다.

  1. 아래 경로에 res/drawable 폴더들을 각각 생성한 후 크기에 맞는 아이콘 이미지 파일을 추가합니다.
    • 아래 표는 해상도별 폴더와 권장 아이콘 크기를 안내합니다.
폴더명 크기
res/drawable-mdpi/ 24x24
res/drawable-hdpi/ 36x36
res/drawable-xhdpi/ 48x48
res/drawable-xxhdpi/ 72x72
res/drawable-xxxhdpi/ 96x96
  1. 이미지 파일의 이름을 ic_stat_nbase_small로 변경합니다.

iOS Native

GAMEPOT iOS SDK를 사용하는 경우 아래 절차에 따라 푸시 연동을 구성해 주십시오.

푸시 기능 초기화

푸시 기능을 활성화하려면 AppDelegate를 통해 알림 권한 요청과 토큰 등록 로직을 구성해 주십시오.

AppDelegate 설정 예제는 Swift와 Objective-C 각각 아래와 같습니다.

  • Swift:
import NBase

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    registerForRemoteNotifications()
    return true
}

func registerForRemoteNotifications() {
    let center = UNUserNotificationCenter.current()
    center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
        if granted {
            DispatchQueue.main.async {
                UIApplication.shared.registerForRemoteNotifications()
            }
        } else {
            print("The push notification permission has been denied")
        }
    }
}

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
    NBase.setPushToken(token: token)
}

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
    NBase.setPushToken(token: "")
}
  • Objective-C:
#import "[ProjectName]-Swift.h"

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
    center.delegate = self;

    [center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge)
                         completionHandler:^(BOOL granted, NSError * _Nullable error) {
        if (granted) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [[UIApplication sharedApplication] registerForRemoteNotifications];
            });
        } else {
            NSLog(@"Push permission denied");
        }
    }];

    return YES;
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    NSString *token = [self stringWithDeviceToken:deviceToken];
    [NBaseBridge.shared setPushToken:token];
}

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
    NSLog(@"Failed to register for remote notifications: %@", error);
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center
       willPresentNotification:(UNNotification *)notification
         withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
            
    completionHandler(UNNotificationPresentationOptionBanner | 
                     UNNotificationPresentationOptionSound | 
                     UNNotificationPresentationOptionBadge);
}

- (NSString *)stringWithDeviceToken:(NSData *)deviceToken {
    NSUInteger length = deviceToken.length;
    if (length == 0) {
        return nil;
    }
    const unsigned char *buffer = deviceToken.bytes;
    NSMutableString *hexString = [NSMutableString stringWithCapacity:(length * 2)];
    for (int i = 0; i < length; ++i) {
        [hexString appendFormat:@"%02x", buffer[i]];
    }
    return [hexString copy];
}

이미지 푸시

iOS 앱에서 알림 이미지를 수신하고 처리하기 위해 알림 서비스 확장을 추가해야 합니다.

  • Swift:
  1. Xcode 프로젝트의 TARGETS에서 Notification Service Extension을 추가합니다.
  2. 생성된 Notification Service Extension 모듈의 NotificationService.swift 파일을 아래와 같이 수정합니다.
import UserNotifications

class NotificationService: UNNotificationServiceExtension {

    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
        
        guard let bestAttemptContent = bestAttemptContent,
              let userInfo = request.content.userInfo as? [String: Any] else {
            contentHandler(request.content)
            return
        }

        let imageURL = userInfo["imageUrl"] as? String ??
                      userInfo["gcm.notification.image"] as? String

        guard let imageURL = imageURL, !imageURL.isEmpty else {
            contentHandler(bestAttemptContent)
            return
        }

        downloadAndAttachImage(urlString: imageURL) { attachment in
            if let attachment = attachment {
                bestAttemptContent.attachments = [attachment]
            }
            DispatchQueue.main.async {
                contentHandler(bestAttemptContent)
            }
        }
    }

    private func downloadAndAttachImage(urlString: String, completionHandler: @escaping (UNNotificationAttachment?) -> Void) {
        guard let url = URL(string: urlString.trimmingCharacters(in: .whitespaces)) else {
            completionHandler(nil)
            return
        }

        let session = URLSession(configuration: .ephemeral)
        let task = session.downloadTask(with: url) { (location, response, error) in
            if let error = error {
                completionHandler(nil)
                return
            }
            
            guard let location = location else {
                completionHandler(nil)
                return
            }

            let fileManager = FileManager.default
            let tmpDirectory = NSTemporaryDirectory()
            let tmpFile = "image_\(Date().timeIntervalSince1970).png"
            let tmpPath = (tmpDirectory as NSString).appendingPathComponent(tmpFile)
            let tmpURL = URL(fileURLWithPath: tmpPath)

            do {
                if fileManager.fileExists(atPath: tmpPath) {
                    try fileManager.removeItem(atPath: tmpPath)
                }
                try fileManager.moveItem(at: location, to: tmpURL)

                let attachment = try UNNotificationAttachment(
                    identifier: UUID().uuidString,
                    url: tmpURL,
                    options: [UNNotificationAttachmentOptionsTypeHintKey: "public.png"]
                )
                completionHandler(attachment)
            } catch {
                completionHandler(nil)
            }
        }

        task.resume()

        DispatchQueue.main.asyncAfter(deadline: .now() + 25) {
            task.cancel()
        }
    }

    override func serviceExtensionTimeWillExpire() {
        if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }
}
  • Objective-C:
  1. Xcode 프로젝트의 TARGETS에서 Notification Service Extension을 추가합니다.
  2. 생성된 Notification Service Extension 모듈의 NotificationService.m 파일을 아래와 같이 수정합니다.
#import "NotificationService.h"

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];

    NSDictionary *userInfo = request.content.userInfo;
    NSString *imageUrl = userInfo[@"imageUrl"];
    if (!imageUrl) {
        imageUrl = userInfo[@"gcm.notification.image"];
    }

    if (imageUrl && imageUrl.length > 0) {
        [self downloadAndAttachImage:imageUrl completionHandler:^(UNNotificationAttachment *attachment) {
            if (attachment) {
                self.bestAttemptContent.attachments = @[attachment];
            }
            self.contentHandler(self.bestAttemptContent);
        }];
    } else {
        self.contentHandler(self.bestAttemptContent);
    }
}

- (void)downloadAndAttachImage:(NSString *)urlString completionHandler:(void(^)(UNNotificationAttachment *attachment))completionHandler {
    NSURL *url = [NSURL URLWithString:[urlString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]];
    if (!url) {
        completionHandler(nil);
        return;
    }

    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]];
    [[session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
        if (error) {
            completionHandler(nil);
            return;
        }
        
        if (!location) {
            completionHandler(nil);
            return;
        }

        NSString *tmpDirectory = NSTemporaryDirectory();
        NSString *tmpFile = [NSString stringWithFormat:@"image_%f.png", [[NSDate date] timeIntervalSince1970]];
        NSString *tmpPath = [tmpDirectory stringByAppendingPathComponent:tmpFile];
        NSURL *tmpURL = [NSURL fileURLWithPath:tmpPath];

        NSFileManager *fileManager = [NSFileManager defaultManager];
        if ([fileManager fileExistsAtPath:tmpPath]) {
            [fileManager removeItemAtPath:tmpPath error:nil];
        }

        NSError *moveError = nil;
        [fileManager moveItemAtURL:location toURL:tmpURL error:&moveError];
        if (moveError) {
            completionHandler(nil);
            return;
        }

        NSError *attachmentError = nil;
        UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:[[NSUUID UUID] UUIDString]
                                                                                              URL:tmpURL
                                                                                          options:@{UNNotificationAttachmentOptionsTypeHintKey: @"public.png"}
                                                                                            error:&attachmentError];

        if (attachmentError) {
            completionHandler(nil);
            return;
        }

        completionHandler(attachment);
    }] resume];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [session invalidateAndCancel];
    });
}

- (void)serviceExtensionTimeWillExpire {
    if (self.contentHandler && self.bestAttemptContent) {
        self.contentHandler(self.bestAttemptContent);
    }
}

Unity

Unity 기반 프로젝트에서는 Android 및 iOS 빌드를 모두 고려하여 구성 파일과 의존성을 설정해야 합니다.

firebase 구성 파일 추가

Unity에서 Android 푸시 알림(Firebase Messaging)을 사용하려면 아래와 같이 firebase 구성 파일을 준비해 주십시오.

  1. Firebase 콘솔에서 Android 앱을 등록하고, google-services.json 파일을 다운로드합니다.
  2. 다운로드한 google-services.json 파일을 Unity 프로젝트의 ./Assets 폴더에 복사합니다.
    • 경로 예시: ./Assets/google-services.json

Unity FCM SDK 추가

Unity에서 FCM SDK를 연동하려면 아래와 같이 라이브러리를 추가해야 합니다.

  1. /Assets/NBaseSDK/Editor/NBaseSDKDependencies.xml 경로 내에 firebase-messaging-ktx 라이브러리를 추가합니다.
  • XML:
<dependencies>
    <androidPackages>
        <androidPackage spec="io.nbase:nbasesdk:3.0.xx"/>
        <androidPackage spec="com.google.firebase:firebase-messaging-ktx:23.2.1" />
    </androidPackages>
    <iosPods>
    </iosPods>
</dependencies>
  1. Assets > External Dependency Manager > Android Resolver > Force Resolve를 클릭하여 라이브러리 의존성을 강제로 적용합니다.

푸시 알림 아이콘 설정(유니티)

Unity 푸시 메시지를 수신했을 때 알림 바에 표시할 작은 아이콘을 설정할 수 있습니다. 별도로 설정하지 않으면 SDK에 포함된 기본 이미지를 사용하며, 게임에 맞는 아이콘을 직접 설정할 수 있습니다.

Android Asset Studio를 이용해서 제작하면 자동으로 폴더별로 이미지가 생성되어 편리합니다.

유니티 엔진 202x 이후 버전의 푸시 알림 아이콘을 설정하는 방법은 다음과 같습니다.

  1. 아래 경로에 res/drawable 폴더들을 각각 생성한 후 그 크기에 맞는 아이콘 이미지 파일을 추가합니다.
    • 아래 표는 리소스 경로와 해상도를 설명합니다.
폴더명 크기
/Assets/Plugins/Android/GamePotResources.androidlib/res/drawable-mdpi/ 24x24
/Assets/Plugins/Android/GamePotResources.androidlib/res/drawable-hdpi/ 36x36
/Assets/Plugins/Android/GamePotResources.androidlib/res/drawable-xhdpi/ 48x48
/Assets/Plugins/Android/GamePotResources.androidlib/res/drawable-xxhdpi/ 72x72
/Assets/Plugins/Android/GamePotResources.androidlib/res/drawable-xxxhdpi/ 96x96
  1. mainTemplate.gradle에 해당 라이브러리를 추가합니다.
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project('GamePotResources.androidlib') // 추가
    ...
}
  1. 이미지 파일의 이름을 ic_stat_nbase_small로 변경합니다.

푸시 상태

사용자가 푸시 수신 동의 여부를 직접 관리할 수 있도록 앱 내에서 상태 변경 기능을 구현해 주십시오.

푸시 상태 설정

푸시 수신을 위해서는 푸시 상태를 true로 설정해야 합니다. 푸시 상태 변경에 사용되는 각 파라미터의 의미는 아래와 같습니다.

파라미터 설명

  • push: 푸시 수신 동의 상태 (true/false)
  • night: 야간 푸시 동의 상태 (한국 시간 기준, true/false)
  • ad: 광고성 푸시 동의 상태 (true/false). 광고성 푸시 값이 false이면 일반/야간 푸시 설정과 관계없이 푸시가 발송되지 않습니다.
  • token: 푸시 토큰 값

푸시 수신 여부 설정을 변경하려면 로그인 이후 아래 코드를 호출해 주십시오.

  • Kotlin:
val pushToken = NBase.getPushToken()
val pushState = com.nbase.sdk.model.PushState(
    push = enable,
    night = night,
    ad = ad,
    token = pushToken
)
NBase.setPushState(pushState) { status, e ->
    if (e != null) {
        // failed.
    } else {
        // succeeded.
    }
}
  • Java:
String pushToken = _NBase.getPushToken();
com.nbase.sdk.model.PushState pushState = new com.nbase.sdk.model.PushState(
    Boolean.parseBoolean(enable),
    Boolean.parseBoolean(night),
    Boolean.parseBoolean(ad),
    pushToken
);
NBase nBase = NBase.INSTANCE;
nBase.setPushState(pushState, (status, e) -> {
    if (e != null) {
        // failed.
    } else {
        // succeeded.
    }
    return null;
});
  • Swift:
NBase.setPushState(enable: enable, ad: ad, night: night, token: NBase.getPushToken()) { result in
    switch result {
    case .success:
        // succeeded.
    case .failure:
        // failed.
    }
}
  • Objective-C:
BOOL enable = YES;
BOOL night = NO;
BOOL ad = YES;
NSString *token = [NBaseBridge.shared getPushToken];

[NBaseBridge.shared setPushState:enable night:night ad:ad token:token :^(NSDictionary * _Nullable result, NSError * _Nullable error) {
    if (error) {
        // failed.
    } else {
        // succeeded.
    }
}];
  • C#:
bool push = true;
bool ad = true;
bool night = false;
string token = ""; // 빈 값을 넣으면 기존 토큰 유지

NBaseSDK.NBase.setPushState(push, ad, night, token, (pushState, error) => {
    if (error != null)
    {
        // failed.
    }
    else
    {
        // succeeded.
    }
});

푸시 상태 확인

사용자의 현재 푸시 동의 상태를 확인하려면 아래 예제를 참고해 주십시오.

  • Kotlin:
NBase.getPushState() { state, e ->
    if (e != null) {
        // failed.
    } else {
        // succeeded.
    }
}
  • Java:
NBase nBase = NBase.INSTANCE;
nBase.getPushState((state, e) -> {
    if (e != null) {
        // failed.
    } else {
        // succeeded.
    }
    return null;
});
  • Swift:
NBase.getPushState() { result in
    switch result {
    case .success:
        // succeeded.
    case .failure:
        // failed.
    }
}
  • Objective-C:
[NBaseBridge.shared getPushState:^(NSDictionary * _Nullable result, NSError * _Nullable error) {
    if (error) {
        // failed.
    } else {
        // succeeded.
    }
}];
  • C#:
NBaseSDK.NBase.getPushState((pushState, error) => {
    if (error != null)
    {
        // failed.
    }
    else
    {
        // succeeded.
    }
});

푸시 테스트 방법(모바일)

모바일 환경에서 푸시 알림이 정상적으로 동작하는지 확인하려면 아래 테스트 절차를 참고해 주십시오.

  • 게임팟 대시보드: 대시보드 > 메시지 > 푸시 알림 메뉴에서 푸시 발송 여부를 확인할 수 있습니다.

  • Android (Firebase 콘솔) 테스트: Firebase console > 프로젝트 선택 > Messaging > 캠페인에서 새 캠페인 버튼을 클릭하여 테스트 메시지를 발송해 주십시오.
    ko-gamepot-push02.png

    ko-gamepot-push03.png

    앱에서 수집한 푸시 토큰을 입력한 뒤 테스트 버튼을 누르면 해당 토큰으로 발송 여부를 확인할 수 있습니다.

  • iOS (CloudKit Console) 테스트: CloudKit Console > Push Notifications 메뉴에서 새 Notifications를 생성해 주십시오.
    CloudKit_Push Notifications.png

    Device Token과 Payload를 입력한 뒤 발송 결과를 확인해 주십시오.

문제 해결

자주 발생하는 오류와 해결 방법은 다음과 같습니다.

Q. java.lang.IllegalStateException: Default FirebaseApp is not initialized in this process {패키지 이름}. Make sure to call FirebaseApp.initializeApp(Context) first.

A. 해당 오류는 gradle 파일에 푸시를 위한 설정이 누락된 경우입니다. 아래 사항을 확인해 주십시오.

프로젝트 최상단 build.gradle.kts를 확인해 주십시오.

  • Gradle (프로젝트 루트):
plugins {
    id("com.google.gms.google-services") version "4.4.1" apply false
}

앱 모듈의 build.gradle.kts를 확인해 주십시오.

  • Gradle (모듈: app):
plugins {
    id("com.google.gms.google-services")
}

Q. org.gradle.api.GradleException: File google-services.json is missing. The Google Services Plugin cannot function without it.

A. 해당 오류는 google-services.json 파일을 찾을 수 없는 경우 발생합니다. 해당 파일을 앱 모듈 루트 폴더에 올바르게 배치했는지 확인해 주십시오. (예시) ./project folder/app/google-services.json