> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.onesignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Mobile Service Extensions

> 앱에서 iOS 및 Android 알림 Service Extension을 구현합니다.

Notification Service Extension을 사용하면 푸시 알림이 사용자에게 표시되기 전에 가로채고 수정할 수 있습니다.

<Note>
  [OSNotification 클래스](./osnotification-payload)를 통해 OneSignal에서 전송된 푸시 알림의 데이터에 액세스할 수 있습니다.
</Note>

***

## Android Notification Service Extension

사용자에게 표시되기 전에 알림을 처리할 수 있습니다. 일반적인 사용 사례는 다음과 같습니다:

* 알림 표시 여부와 관계없이 백그라운드에서 데이터 수신
* 사용자 지정 강조 색상, 진동 패턴 또는 기타 `NotificationCompat` 옵션과 같은 클라이언트 측 앱 로직에 따라 특정 알림 설정 재정의

자세한 내용은 [Android의 NotificationCompat 옵션 문서](https://developer.android.com/reference/androidx/core/app/NotificationCompat)를 참조하세요.

### 1단계: Service Extension용 클래스 생성

`INotificationServiceExtension`을 확장하는 클래스를 만들고 `onNotificationReceived` 메서드를 구현합니다.

`onNotificationReceived` 메서드 매개변수는 [INotificationReceivedEvent](https://github.com/OneSignal/OneSignal-Android-SDK/blob/25924dc3739fbe3ae64a73efc7b504449a18cdea/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/notifications/INotificationReceivedEvent.kt#L46) 유형의 `event`입니다.

<CodeGroup>
  ```Java Java theme={null}
  package your.package.name

  import androidx.annotation.Keep;
  import com.onesignal.notifications.IActionButton;
  import com.onesignal.notifications.IDisplayableMutableNotification;
  import com.onesignal.notifications.INotificationReceivedEvent;
  import com.onesignal.notifications.INotificationServiceExtension;

  @Keep // Keep is required to prevent minification from renaming or removing your class
  public class NotificationServiceExtension implements INotificationServiceExtension {

       @Override
       public void onNotificationReceived(INotificationReceivedEvent event) {
          IDisplayableMutableNotification notification = event.getNotification();

          if (notification.getActionButtons() != null) {
             for (IActionButton button : notification.getActionButtons()) {
                // you can modify your action buttons here
             }
          }

       /* Add customizations here. See examples below for additional methods to modify the notification*/
       }
  }

  ```

  ```kotlin Kotlin theme={null}
  package your.package.name

  import androidx.annotation.Keep
  import com.onesignal.notifications.INotificationReceivedEvent
  import com.onesignal.notifications.INotificationServiceExtension

  @Keep
  class NotificationServiceExtension : INotificationServiceExtension {
      override fun onNotificationReceived(event: INotificationReceivedEvent) {
          val notification = event.notification
          val context = event.context

          notification.actionButtons?.forEach { button ->
              // Modify action buttons here
          }
      }
  }
  ```
</CodeGroup>

<Note>
  `@Keep` 어노테이션은 ProGuard/R8이 축소 과정에서 `INotificationServiceExtension`을 구현하는 클래스의 이름을 변경하거나 제거하지 못하도록 방지합니다.
</Note>

### 2단계: 알림 사용자 지정

다음은 위의 템플릿 Notification Service Extension 클래스에서 구현할 수 있는 일반적인 예시입니다.

<Tabs>
  <Tab title="알림 표시 방지">
    `event.preventDefault()`를 호출하면 알림이 자동으로 표시되지 않으며, 수동으로 표시하거나 완전히 표시하지 않도록 결정할 수 있습니다.

    <CodeGroup>
      ```Java Java theme={null}
      event.preventDefault();

      //Do some async work, then decide to show or dismiss
      new Thread(() -> {
          try {
              Thread.sleep(1000);
          } catch (InterruptedException ignored) {}

          //Manually show the notification
          event.getNotification().display();
      }).start();
      ```

      ```Kotlin Kotlin theme={null}
      event.preventDefault()

      //Do some async work, then decide to show or dismiss
      Thread{
          try {
              Thread.sleep(1000)
          } catch (ingored: InterruptedException) {}

          //Manually show the notification
          event.notification.display()
      }.start()

      ```
    </CodeGroup>

    <Warning>
      `event.preventDefault()`를 호출한 후 `display()`를 호출하지 않으면 알림이 아무런 경고 없이 자동으로 삭제됩니다. 자세한 내용은 [중복 알림](./duplicated-notifications#android-notification-service-extension)을 참조하세요.
    </Warning>
  </Tab>

  <Tab title="사용자 지정 필드 추가">
    <CodeGroup>
      ```Java Java theme={null}
      String promoCode = notification.getAdditionalData() != null
          ? notification.getAdditionalData().optString("promo_code", null)
          : null;

      if (promoCode != null) {
          String updatedBody = notification.getBody() + " Use code: " + promoCode;
          notification.setExtender(builder -> {
              builder.setContentText(updatedBody);
          });
      }
      ```

      ```Kotlin Kotlin theme={null}
      val promoCode = notification.additionalData?.optString("promo_code", null)

      promoCode?.let {
          val updatedBody = "${notification.body}\nUse code: $promoCode"
          notification.setExtender { builder ->
              builder.setContentText(updatedBody)
          }
      }

      ```
    </CodeGroup>
  </Tab>

  <Tab title="알림 색상 및 아이콘 변경">
    <CodeGroup>
      ```Java Java theme={null}
      int iconResId = R.drawable.icon_default;
      String type = notitifcation.getAdditionalData() != null
          ? notification.getAdditionalData().optString("type", "")
          : "";

      switch (type) {
          case "sale":
              iconResId = R.drawable.icon_sale;
              break;
          case "reminder":
              iconResId = R.drawable.icon_reminder;
              break;
      }

      int finalIconResId = iconResId;
      notification.setExtender(builder -> {
          builder.setColor(0xFF0000FF).setSmallIcon(finalIconResId);
      });

      ```

      ```Kotlin Kotlin theme={null}
      val type = notification.additionalData?.optString("type", "") ?: ""

      val iconResId = when (type) {
      "sale" -> R.drawable.icon_sale
          "reminder" -> R.drawable.icon_reminder
          else -> R.drawable.icon_default
      }

      notification.setExtender { builder ->
          builder.setColor(0xFF0000FF).setSmallIcon(iconResId)
      }

      ```
    </CodeGroup>

    <Note>
      여기서 참조하는 아이콘은 앱의 `res/drawable` 디렉토리에 존재해야 합니다.
    </Note>
  </Tab>
</Tabs>

### 3단계: `AndroidManifest.xml`에 Service Extension 추가

`AndroidManifest.xml` 파일의 application 태그 내에 클래스 이름과 값을 `meta-data`로 추가합니다. "unused" 경고는 무시하세요.

```XML XML theme={null}
<application>
  <meta-data
    android:name="com.onesignal.NotificationServiceExtension"
    android:value="com.onesignal.example.NotificationServiceExtension" />
</application>
```

`com.onesignal.example.NotificationServiceExtension`을 클래스의 정규화된 이름으로 교체하세요.

***

## iOS Notification Service Extension

[UNNotificationServiceExtension](https://developer.apple.com/reference/usernotifications/unnotificationserviceextension)을 사용하면 푸시 알림이 사용자에게 표시되기 전에 콘텐츠를 수정할 수 있으며 다음과 같은 다른 중요한 기능에 필요합니다:

* [이미지 및 리치 미디어](./rich-media)
* [확인된 전달](./confirmed-delivery)
* [배지](./badges)
* [액션 버튼](./action-buttons)
* [Firebase Analytics를 사용한 영향을 받은 열람](./google-analytics-for-firebase)

앱에 대한 [Mobile SDK 설정](./mobile-sdk-setup) 지침을 따랐다면 Extension이 이미 구성되어 있을 것입니다. 이 섹션에서는 OneSignal 알림 페이로드 데이터에 액세스하는 방법과 발생할 수 있는 문제를 해결하는 방법을 설명합니다.

### iOS 푸시 페이로드 가져오기

`didReceive(_:withContentHandler:)` 오버라이드는 `OneSignalExtension.didReceiveNotificationExtensionRequest`를 호출합니다. 이 메서드가 호출되기 전에 `bestAttemptContent`를 읽거나 수정할 수 있습니다.

이 예시에서는 다음 데이터와 함께 알림을 보냅니다:

```json JSON theme={null}
{
  "app_id": "YOUR_APP_ID",
  "target_channel": "push",
  "headings": {"en": "The message title"},
  "contents": {"en": "The message contents"},
  "data":{
    "additional_data_key_1":"value_1",
    "additional_data_key_2":"value_2"
    },
  "include_subscription_ids": ["SUBSCRIPTION_ID_1"]
}
```

`userInfo`의 `custom` 딕셔너리 내 `a` 키를 통해 OneSignalNotificationServiceExtension 내에서 이 추가 `data`에 액세스합니다:

<CodeGroup>
  ```swift Swift theme={null}
  if let bestAttemptContent = bestAttemptContent {

      if let customData = bestAttemptContent.userInfo["custom"] as? [String: Any],
         let additionalData = customData["a"] as? [String: Any] {

          if let jsonData = try? JSONSerialization.data(withJSONObject: additionalData, options: .prettyPrinted),
             let jsonString = String(data: jsonData, encoding: .utf8) {
              print("The additionalData dictionary in JSON format:\n\(jsonString)")
          } else {
              print("Failed to convert additionalData to JSON format.")
          }
      }

      if let messageData = bestAttemptContent.userInfo["aps"] as? [String: Any],
         let apsData = messageData["alert"] as? [String: Any],
         let body = apsData["body"] as? String,
         let title = apsData["title"] as? String {
          print("The message contents is: \(body), message headings is: \(title)")
      } else {
          print("Unable to retrieve apsData")
      }

      OneSignalExtension.didReceiveNotificationExtensionRequest(self.receivedRequest,
                                                                with: bestAttemptContent,
                                                                withContentHandler: self.contentHandler)
  }
  ```

  ```objc Objective-C theme={null}
  if (bestAttemptContent) {
      NSDictionary *customData = bestAttemptContent.userInfo[@"custom"];
      if ([customData isKindOfClass:[NSDictionary class]]) {
          NSDictionary *additionalData = customData[@"a"];
          if ([additionalData isKindOfClass:[NSDictionary class]]) {
              NSError *error;
              NSData *jsonData = [NSJSONSerialization dataWithJSONObject:additionalData options:NSJSONWritingPrettyPrinted error:&error];
              if (jsonData) {
                  NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
                  NSLog(@"The additionalData dictionary in JSON format:\n%@", jsonString);
              } else {
                  NSLog(@"Failed to convert additionalData to JSON format: %@", error.localizedDescription);
              }
          }
      }

      NSDictionary *messageData = bestAttemptContent.userInfo[@"aps"];
      if ([messageData isKindOfClass:[NSDictionary class]]) {
          NSDictionary *apsData = messageData[@"alert"];
          if ([apsData isKindOfClass:[NSDictionary class]]) {
              NSString *body = apsData[@"body"];
              NSString *title = apsData[@"title"];
              if ([body isKindOfClass:[NSString class]] && [title isKindOfClass:[NSString class]]) {
                  NSLog(@"The message content is: %@, message heading is: %@", body, title);
              }
          } else {
              NSLog(@"Unable to retrieve apsData");
          }
      }

      [OneSignalExtension didReceiveNotificationExtensionRequest:self.receivedRequest
                                               withNotification:bestAttemptContent
                                                withContentHandler:self.contentHandler];
  }
  ```
</CodeGroup>

**콘솔 출력 예시:**

```
The additionalData dictionary in JSON format:
{
  "additional_data_key_1" : "value_1",
  "additional_data_key_2" : "value_2"
}
The message contents is: The message contents, message headings is: The message title
```

### iOS Notification Service Extension 문제 해결

이 가이드는 iOS 모바일 앱에서 이미지, 액션 버튼 또는 확인된 전달이 표시되지 않는 문제를 디버깅하기 위한 것입니다.

#### Xcode 설정 확인

**General > Targets**에서 **main app target**과 **OneSignalNotificationServiceExtension** 타겟이 동일하고 올바른지 확인하세요:

* **Supported Destinations**
* **Minimum Deployment**(iOS 14.5 이상)

<Warning>
  Cocoapods를 사용하는 경우 Podfile의 main target과 일치하는지 확인하여 빌드 오류를 방지하세요.
</Warning>

<Frame caption="Xcode의 Main App Target 예시">
  <img src="https://mintcdn.com/onesignal/RWA35uTjv8voG5iC/images/mobile/main-app-target-general-settings.png?fit=max&auto=format&n=RWA35uTjv8voG5iC&q=85&s=eb9dd4d9dbec5f2b9f351ff168087557" alt="Xcode General tab showing Supported Destinations and Minimum Deployment for the main app target" width="2988" height="1824" data-path="images/mobile/main-app-target-general-settings.png" />
</Frame>

<Frame caption="Xcode의 OneSignalNotificationServiceExtension Target 예시">
  <img src="https://mintcdn.com/onesignal/x4RdPY-EcasyyQ-o/images/mobile/onesignal-notification-service-extension-target-general-settings.png?fit=max&auto=format&n=x4RdPY-EcasyyQ-o&q=85&s=ea6f697fda7b4a338bb7e3a4dac3856f" alt="Xcode General tab showing Supported Destinations and Minimum Deployment for the notification service extension target" width="2988" height="1824" data-path="images/mobile/onesignal-notification-service-extension-target-general-settings.png" />
</Frame>

**OneSignalNotificationServiceExtension > Info** 탭을 계속 진행하여 `NSExtension` 키를 확장합니다. 다음이 표시되는지 확인하세요:

```XML XML theme={null}
 <dict>
   <key>NSExtensionPointIdentifier</key>
   <string>com.apple.usernotifications.service</string>
   <key>NSExtensionPrincipalClass</key>
   <string>$(PRODUCT_MODULE_NAME).NotificationService</string>
 </dict>
```

예시:

<Frame caption="Info 탭의 NSExtension 키 예시">
  <img src="https://mintcdn.com/onesignal/RWA35uTjv8voG5iC/images/mobile/onesignal-notification-service-extension-info-tab.png?fit=max&auto=format&n=RWA35uTjv8voG5iC&q=85&s=06e65f878427e94127ef7391903d583e" alt="Xcode Info tab showing the NSExtension dictionary with NSExtensionPointIdentifier and NSExtensionPrincipalClass keys" width="3282" height="1888" data-path="images/mobile/onesignal-notification-service-extension-info-tab.png" />
</Frame>

<Warning>
  Objective-C를 사용하는 경우 `$(PRODUCT_MODULE_NAME).NotificationService` 대신 `NotificationService`를 사용하세요.
</Warning>

#### "Copy only when installing" 끄기

**main app target > Build Phases > Embed App Extensions**를 선택합니다. "Copy only when installing"이 선택되지 않았는지 확인하세요. 선택되어 있으면 선택 해제하세요:

<Frame caption="Main app target 빌드 단계 설정">
  <img src="https://mintcdn.com/onesignal/RWA35uTjv8voG5iC/images/mobile/main-app-target-build-phases.png?fit=max&auto=format&n=RWA35uTjv8voG5iC&q=85&s=cd507862b8e5bb0934b6f6b082d0b26e" alt="Xcode Build Phases tab showing Embed App Extensions with Copy only when installing unchecked" width="3282" height="1888" data-path="images/mobile/main-app-target-build-phases.png" />
</Frame>

### iOS Notification Service Extension 디버깅

다음 단계에 따라 Notification Service Extension이 올바르게 설정되었는지 확인하세요.

#### 1. OneSignalNotificationServiceExtension 코드 업데이트

`NotificationService.m` 또는 `NotificationService.swift`를 열고 전체 파일 내용을 아래 코드로 바꾸세요. Extension이 실행 중인지 확인하는 데 도움이 되는 로깅을 추가합니다.

`YOUR_BUNDLE_ID`를 실제 Bundle ID로 바꾸세요.

<CodeGroup>
  ```Swift Swift theme={null}
  import UserNotifications
  import OneSignalExtension
  import os.log

  class NotificationService: UNNotificationServiceExtension {

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

      override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
          self.receivedRequest = request
          self.contentHandler = contentHandler
          self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)

          let userInfo = request.content.userInfo
          let custom = userInfo["custom"]
          print("Running NotificationServiceExtension: userInfo = \(userInfo.description)")
          print("Running NotificationServiceExtension: custom = \(custom.debugDescription)")
          os_log("%{public}@", log: OSLog(subsystem: "YOUR_BUNDLE_ID", category: "OneSignalNotificationServiceExtension"), type: OSLogType.debug, userInfo.debugDescription)

          if let bestAttemptContent = bestAttemptContent {
              print("Running NotificationServiceExtension")
              bestAttemptContent.body = "[Modified] " + bestAttemptContent.body

              OneSignalExtension.didReceiveNotificationExtensionRequest(self.receivedRequest, with: bestAttemptContent, withContentHandler: self.contentHandler)
          }
      }

      override func serviceExtensionTimeWillExpire() {
          if let contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {
              OneSignalExtension.serviceExtensionTimeWillExpireRequest(self.receivedRequest, with: self.bestAttemptContent)
              contentHandler(bestAttemptContent)
          }
      }

  }

  ```

  ```objc Objective-C theme={null}
  #import <OneSignalExtension/OneSignalExtension.h>

  #import "NotificationService.h"

  @interface NotificationService ()

  @property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
  @property (nonatomic, strong) UNNotificationRequest *receivedRequest;
  @property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;

  @end

  @implementation NotificationService

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

      NSUserDefaults *userDefault = [[NSUserDefaults alloc] initWithSuiteName:@"group.YOUR_BUNDLE_ID.onesignal"];
      NSLog(@"NSE player_id: %@", [userDefault stringForKey:@"GT_PLAYER_ID"]);
      NSLog(@"NSE app_id: %@", [userDefault stringForKey:@"GT_APP_ID"]);

      NSLog(@"Running NotificationServiceExtension");
      self.bestAttemptContent.body = [@"[Modified] " stringByAppendingString:self.bestAttemptContent.body];

      [OneSignal.Debug setLogLevel:ONE_S_LL_VERBOSE];

      [OneSignalExtension didReceiveNotificationExtensionRequest:self.receivedRequest
                                                withNotification:self.bestAttemptContent
                                              withContentHandler:self.contentHandler];
  }

  - (void)serviceExtensionTimeWillExpire {
      [OneSignalExtension serviceExtensionTimeWillExpireRequest:self.receivedRequest
                                               withNotification:self.bestAttemptContent];

      self.contentHandler(self.bestAttemptContent);
  }

  @end
  ```
</CodeGroup>

<Note>
  디버그 로그 유형은 Console에서 **Action > Include Debug Messages**를 통해 활성화해야 합니다.
</Note>

#### 2. Active Scheme 변경

Active Scheme을 `OneSignalNotificationServiceExtension`으로 설정합니다.

<Frame caption="Xcode Active Scheme 선택">
  <img src="https://mintcdn.com/onesignal/x4RdPY-EcasyyQ-o/images/mobile/xcode-active-scheme-selection.png?fit=max&auto=format&n=x4RdPY-EcasyyQ-o&q=85&s=27407135e39bc606a02e005efd3a56ff" alt="Xcode toolbar showing the active scheme dropdown set to OneSignalNotificationServiceExtension" width="3282" height="1888" data-path="images/mobile/xcode-active-scheme-selection.png" />
</Frame>

#### 3. 프로젝트 빌드 및 실행

Xcode에서 실제 장치에서 프로젝트를 빌드하고 실행합니다.

#### 4. Console 열기

Xcode에서 **Window > Devices and Simulators**를 선택합니다.

<Frame caption="Xcode Devices and Simulators 창">
  <img src="https://mintcdn.com/onesignal/x4RdPY-EcasyyQ-o/images/mobile/xcode-devices-and-simulators-selection.png?fit=max&auto=format&n=x4RdPY-EcasyyQ-o&q=85&s=2bc1305ee0fd45087f33a078a7aeaffb" alt="Xcode menu showing the Window dropdown with Devices and Simulators selected" width="2518" height="1374" data-path="images/mobile/xcode-devices-and-simulators-selection.png" />
</Frame>

연결된 장치가 표시되어야 합니다. **Open Console**을 선택합니다.

<Frame caption="장치 콘솔 액세스 버튼">
  <img src="https://mintcdn.com/onesignal/x4RdPY-EcasyyQ-o/images/mobile/xcode-device-console-access-button.png?fit=max&auto=format&n=x4RdPY-EcasyyQ-o&q=85&s=7ffa64afd28d452e200241ad4751c102" alt="Xcode Devices window showing the Open Console button for a connected device" width="2304" height="1624" data-path="images/mobile/xcode-device-console-access-button.png" />
</Frame>

#### 5. Console 확인

Console에서:

1. **Action > Include Debug Messages** 선택
2. CATEGORY로 `OneSignalNotificationServiceExtension` 검색
3. **Start** 선택

<Frame caption="Console 디버깅 구성">
  <img src="https://mintcdn.com/onesignal/x4RdPY-EcasyyQ-o/images/mobile/xcode-console-debugging-configuration.png?fit=max&auto=format&n=x4RdPY-EcasyyQ-o&q=85&s=82593a0287811e6b787cee686ab7196d" alt="macOS Console app showing the category filter and Start button for debugging the notification service extension" width="3368" height="1232" data-path="images/mobile/xcode-console-debugging-configuration.png" />
</Frame>

이 장치에 메시지가 포함된 알림을 보냅니다([푸시 알림](/reference/push-notification) API에서 보내는 경우 `contents` 속성 사용). 이 예시에서 페이로드는 다음과 같습니다:

```curl cURL theme={null}
curl --request POST \
 --url 'https://api.onesignal.com/notifications' \
 --header 'Authorization: Key YOUR_API_KEY' \
 --header 'accept: application/json' \
 --header 'content-type: application/json' \
 --data '
{
"app_id": "YOUR_APP_ID",
"target_channel": "push",
"headings": {"en": "The message title"},
"contents": {"en": "The message contents"},
"data":{"additional_data_key_1":"value_1","additional_data_key_2":"value_2"},
"include_subscription_ids": [
"SUBSCRIPTION_ID_1"
]
}'
```

앱이 실행 중이거나 실행 중이 아닐 때 로그된 메시지가 표시되어야 합니다.

<Frame caption="Console 디버그 출력 예시">
  <img src="https://mintcdn.com/onesignal/x4RdPY-EcasyyQ-o/images/mobile/xcode-console-debug-output.png?fit=max&auto=format&n=x4RdPY-EcasyyQ-o&q=85&s=d93e3fc36610941e1c01b83abc0234b3" alt="macOS Console app showing debug log output from the OneSignalNotificationServiceExtension" width="3604" height="1776" data-path="images/mobile/xcode-console-debug-output.png" />
</Frame>

메시지가 표시되지 않으면 앱에서 OneSignal을 제거하고 [Mobile SDK setup](./mobile-sdk-setup)을 다시 따라 통합을 확인하세요.

## FAQ

**iOS에서 Notification Service Extension이 실행되지 않는 이유는 무엇인가요?**

Extension은 `mutable-content`가 설정된 경우에만 실행됩니다. OneSignal은 첨부 파일이나 액션 버튼이 있는 알림에 대해 자동으로 이를 설정합니다. Xcode 설정이 [문제 해결 섹션](#troubleshooting-the-ios-notification-service-extension)의 요구 사항과 일치하는지 확인하세요.

**Android에서 알림 표시를 방지할 수 있나요?**

네, `event.preventDefault()`를 호출하여 알림을 억제할 수 있습니다. 나중에 `event.getNotification().display()`를 호출하여 표시하거나 호출하지 않고 자동으로 삭제할 수 있습니다. 자세한 내용은 [중복 알림](./duplicated-notifications#android-notification-service-extension)을 참조하세요.

**Android에서 @Keep 어노테이션이 필요한가요?**

네, `@Keep` 어노테이션은 축소 과정에서 ProGuard/R8이 `INotificationServiceExtension`을 구현하는 클래스의 이름을 변경하거나 제거하지 못하도록 방지합니다.

## 관련 페이지

<Columns cols={2}>
  <Card title="Mobile SDK 설정" icon="mobile" href="./mobile-sdk-setup">
    iOS 및 Android용 OneSignal SDK를 설치하고 구성합니다.
  </Card>

  <Card title="이미지 및 리치 미디어" icon="image" href="./rich-media">
    푸시 알림에 이미지, GIF 및 비디오를 첨부합니다.
  </Card>

  <Card title="확인된 전달" icon="check" href="./confirmed-delivery">
    장치에 대한 알림 전달 확인을 추적합니다.
  </Card>

  <Card title="중복 알림" icon="clone" href="./duplicated-notifications">
    모든 플랫폼에서 중복 푸시 알림 문제를 해결합니다.
  </Card>
</Columns>
