> ## 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.

# 중복 알림

> SDK 충돌, 서버 측 재시도, 다중 앱 환경 및 플랫폼별 버그로 인한 중복 푸시 알림 문제를 해결합니다.

중복 알림은 동일한 기기가 동일한 메시지 내용을 두 번 이상 수신할 때 발생합니다. 이 가이드는 가장 일반적인 원인과 해결 방법을 다룹니다.

동일한 사용자가 여러 기기(휴대폰, 태블릿, 데스크톱)에서 알림을 받는 경우, 이는 타겟팅 구성(세그먼트, 외부 ID 등)에 기반한 예상된 동작입니다. 중복 인앱 메시지의 경우 [인앱 메시지 문제 해결](./in-app-message-troubleshooting#duplicated-in-app-messages) 가이드를 참조하세요.

<Info>
  Apple은 중복을 유발한 iOS 17의 버그를 인정했습니다. 이는 iOS 17.3에서 수정되었습니다. [자세히 보기](https://forums.developer.apple.com/forums/thread/747044).
</Info>

## 여기서 시작하세요

상황에 가장 적합한 섹션을 선택하세요:

* 서버가 실패한 API 호출을 재시도하거나 백엔드 파이프라인이 이중 전송할 수 있습니다 → [동일한 메시지가 여러 번 전송됨](#same-message-sent-multiple-times)
* 앱이 OneSignal과 함께 Firebase 또는 다른 푸시 SDK를 사용합니다 → [서드파티 알림 핸들러](#third-party-notification-handlers)
* Android 앱이 코드에서 알림을 커스터마이즈합니다(서비스 확장 또는 포그라운드 리스너) → [Android 알림 핸들러](#android-notification-handlers)
* iOS 앱이 자체 `UNUserNotificationCenterDelegate`를 구현합니다 → [iOS 포그라운드 핸들러](#ios-foreground-handler)
* 개발 빌드 또는 여러 번 설치했을 때만 중복이 나타납니다 → [다중 앱 인스턴스](#multiple-app-instances)

## 동일한 메시지가 여러 번 전송됨

가장 일반적인 원인은 OneSignal API를 통해 동일한 알림 페이로드를 두 번 이상 전송하는 것입니다. 일반적인 이유:

* 서버가 첫 번째 요청이 성공했는지 확인하지 않고 요청을 재시도합니다.
* 백엔드 파이프라인의 로직이 동일한 알림을 두 번 전송합니다.
* OneSignal로 마이그레이션했지만 이전 제공업체에서 여전히 알림을 보내고 있습니다. 두 시스템에서 동시에 보내는 것을 피하세요.

<Card title="멱등 API 요청" icon="rotate" href="/reference/idempotent-notification-requests">
  재시도 시 멱등 키를 재사용하여 중복 메시지를 방지합니다.
</Card>

## 서드파티 알림 핸들러

앱의 다른 코드가 OneSignal의 푸시 페이로드를 처리하고 OneSignal 외에도 자체 알림을 표시할 때 중복이 발생할 수 있습니다. 여기에는 다음이 포함됩니다:

* 동일한 페이로드를 수신하는 다른 원격 푸시 SDK(예: Firebase Cloud Messaging).
* Android의 `FirebaseMessagingService` 또는 iOS의 `UNUserNotificationCenterDelegate`와 같이 수신 페이로드를 읽고 로컬 알림을 구성하는 커스텀 메시지 핸들러.

OneSignal SDK는 유일한 핸들러일 때 OneSignal 페이로드를 자동으로 필터링합니다. 서드파티 코드는 명시적으로 알려주지 않는 한 필터링하는 방법을 모릅니다.

### OneSignal 알림 식별

모든 OneSignal 알림에는 OneSignal 알림 UUID를 포함하는 `i` 키가 있는 `custom` 객체가 포함됩니다:

```json JSON theme={null}
{
  "custom": {
    "i": "the-notification-id"
  }
}
```

전체 페이로드 참조는 [커스텀 OneSignal 페이로드 구조](./osnotification-payload#custom-onesignal-payload-structure)를 참조하세요.

### 서드파티 핸들러에서 OneSignal 알림 필터링

OneSignal 이외의 모든 핸들러에서 `custom.i` 키를 확인하고 해당 키가 있으면 조기 반환합니다. 그러면 OneSignal은 자체 페이로드를 처리하고 다른 코드는 자체 페이로드를 계속 처리합니다.

<Tabs>
  <Tab title="Android">
    커스텀 `FirebaseMessagingService` 또는 다른 원격 메시지 핸들러에서:

    ```kotlin Kotlin theme={null}
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        val customJson = remoteMessage.data["custom"]
        if (customJson != null) {
            try {
                val custom = JSONObject(customJson)
                if (custom.has("i")) {
                    // OneSignal 알림——OneSignal SDK가 처리하도록 합니다
                    return
                }
            } catch (e: JSONException) {
                // OneSignal 페이로드가 아니므로 계속 진행합니다
            }
        }

        // 여기서 자체 알림을 처리합니다
    }
    ```
  </Tab>

  <Tab title="iOS">
    `UNUserNotificationCenterDelegate` 또는 커스텀 알림 핸들러에서:

    ```swift Swift theme={null}
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        let userInfo = notification.request.content.userInfo

        if let custom = userInfo["custom"] as? [String: Any], custom["i"] != nil {
            // OneSignal 알림——OneSignal SDK가 처리하도록 합니다
            completionHandler([])
            return
        }

        // 여기서 자체 알림을 처리합니다
    }
    ```
  </Tab>
</Tabs>

<Tip>
  다른 제공업체에서 전송되는 페이로드 구조를 제어할 수 있다면, 고유한 마커 키(예: `my_app_notification: true`)를 추가하고 양방향으로 필터링하세요——자체 코드에서는 마커가 포함된 알림만 처리하고, `custom.i` 키가 포함된 알림은 OneSignal이 처리하도록 합니다.
</Tip>

## Android 알림 핸들러

OneSignal은 알림이 표시되기 전에 가로채고 커스터마이즈할 수 있는 두 가지 Android 콜백을 제공합니다:

* [알림 서비스 확장](./service-extensions)의 `onNotificationReceived`——앱이 백그라운드에 있을 때를 포함하여 모든 푸시에서 실행됩니다.
* [포그라운드 생명주기 리스너](./mobile-sdk-reference#addforegroundlifecyclelistener-push)의 `onWillDisplay`——앱이 포그라운드에 있을 때만 실행됩니다.

이러한 콜백이 OneSignal의 자동 표시 외에도 알림을 표시할 때 중복이 발생합니다.

### `preventDefault()` 및 `display()` 규칙

OneSignal은 `event.preventDefault()`로 억제하지 않는 한 각 알림을 자동으로 표시합니다. 두 가지 일반적인 실수가 중복을 유발합니다:

* 먼저 `event.preventDefault()`를 호출하지 않고 `event.getNotification().display()`를 호출하면——알림이 두 번 표시됩니다.
* OneSignal이 원본을 표시하는 동안 [`NotificationManagerCompat.notify()`](https://developer.android.com/reference/androidx/core/app/NotificationManagerCompat)로 별도의 알림을 게시하는 경우.

하나의 표시 경로만 활성화되어야 합니다——OneSignal의 `display()` 또는 자체 `NotificationManagerCompat.notify()` 중 하나이며, 둘 다 사용하면 안 됩니다.

<CodeGroup>
  ```kotlin Kotlin theme={null}
  override fun onNotificationReceived(event: INotificationReceivedEvent) {
      event.preventDefault()

      val notification = event.notification
      notification.setExtender { builder ->
          builder.setColor(0xFF0000FF.toInt())
      }
      notification.display()
  }
  ```

  ```java Java theme={null}
  @Override
  public void onNotificationReceived(INotificationReceivedEvent event) {
      event.preventDefault();

      IDisplayableMutableNotification notification = event.getNotification();
      notification.setExtender(builder ->
          builder.setColor(0xFF0000FF)
      );
      notification.display();
  }
  ```
</CodeGroup>

같은 규칙이 포그라운드 생명주기 리스너의 `onWillDisplay`에도 적용됩니다:

```kotlin Kotlin theme={null}
OneSignal.Notifications.addForegroundLifecycleListener(object : INotificationLifecycleListener {
    override fun onWillDisplay(event: INotificationWillDisplayEvent) {
        event.preventDefault()

        // 자체 UI를 렌더링하거나 event.notification.display()를 호출하여 OneSignal 알림을 표시합니다
    }
})
```

<Warning>
  `preventDefault()`를 호출했지만 `display()`를 호출하지 않으면 알림이 조용히 삭제되어 사용자에게 표시되지 않습니다. 모든 코드 경로가 `display()`를 정확히 한 번 호출하거나 의도적으로 알림을 억제하도록 하세요.
</Warning>

<Warning>
  알림 서비스 확장과 포그라운드 생명주기 리스너를 모두 사용하는 경우, 특정 앱 상태에 대해 하나의 핸들러만 알림을 표시하도록 하세요. 양쪽에서 `display()`를 호출하면 중복이 발생합니다.
</Warning>

### 콜백 내 비동기 작업

표시 전에 백그라운드 작업(네트워크 호출, 데이터베이스 조회)을 수행할 때는 콜백에서 동기적으로 `preventDefault()`를 호출하고 비동기 작업이 완료된 후에만 `display()`를 호출하세요. `preventDefault()` 없이 콜백에서 반환하면 OneSignal이 알림을 표시하고, 이후 `display()` 호출이 중복을 생성합니다.

## iOS 포그라운드 핸들러

OneSignal은 SDK 초기화 중에 자신을 `UNUserNotificationCenterDelegate`로 설정합니다. 앱이 포그라운드 알림 처리도 구현하는 경우, 알림이 두 번 표시될 수 있습니다——OneSignal에서 한 번, 코드에서 한 번.

중복을 유발하는 일반적인 패턴:

* OneSignal로 호출을 전달하지 않고 자체 `UNUserNotificationCenterDelegate`를 구현한 후, 동일한 메서드에서 `completionHandler([.banner, .sound])`를 호출하고 커스텀 인앱 알림을 표시하는 경우.
* OneSignal이 표시하는 수신 푸시에 응답하여 로컬 알림(`UNUserNotificationCenter.current().add(...)`)을 예약하는 경우.

중복을 유발하지 않고 포그라운드 표시를 제어하려면 OneSignal의 [포그라운드 생명주기 리스너](./mobile-sdk-reference#addforegroundlifecyclelistener-push)를 사용하고 직접 알림을 렌더링하려는 경우 `event.preventDefault()`를 호출하세요:

```swift Swift theme={null}
OneSignal.Notifications.addForegroundLifecycleListener(self)

func onWillDisplay(event: OSNotificationWillDisplayEvent) {
    event.preventDefault()

    // 자체 UI를 렌더링하거나 event.notification.display()를 호출하여 OneSignal 알림을 표시합니다
}
```

## 다중 앱 인스턴스

<Tabs>
  <Tab title="Android">
    프로덕션 및 개발 버전의 앱이 모두 설치되어 있을 때 중복이 발생할 수 있습니다. 각 앱에는 고유한 패키지 이름이 있고 자체 푸시 토큰을 받습니다.

    알림을 길게 눌러 어느 앱 인스턴스에서 전송했는지 확인하세요.
  </Tab>

  <Tab title="iOS">
    OneSignal의 두 기기 레코드가 동일한 푸시 토큰을 공유하는 경우 기기가 두 개의 알림을 받을 수 있습니다. 이는 동일한 토큰을 여러 번 가져오거나 등록할 때 발생할 수 있습니다. OneSignal은 이를 방지하기 위한 검사를 포함하지만 엣지 케이스는 이를 우회할 수 있습니다.
  </Tab>

  <Tab title="Web">
    웹 푸시 중복은 동일한 OneSignal 앱 ID를 사용하여 여러 오리진을 구독하여 발생합니다. [오리진](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy)은 브라우저가 푸시 구독의 범위를 지정하는 데 사용하는 스킴, 호스트 및 포트의 조합입니다.

    예를 들어, `https://example.com` 및 `https://sub.example.com`을 구독한 사용자는 두 개의 구독으로 계산되며, 동일한 앱 ID를 사용한 전송은 두 곳 모두에 도달합니다.

    해결 방법:

    1. 오리진별로 [브라우저 데이터 및 푸시 권한을 재설정](./troubleshooting-web-push)하세요.
    2. 많은 사용자에게 영향을 미치는 경우 새 OneSignal 앱을 만들고 사이트의 init 코드에서 새 앱 ID를 사용하세요.
    3. 사용자가 사이트를 다시 방문하여 다시 구독하도록 하세요.

    여러 브라우저 또는 브라우저 프로필에서 구독하면 여러 알림이 표시됩니다.
  </Tab>
</Tabs>

## 진단 팁

중복 문제를 더 빠르게 디버그하려면 다음을 수집하여 OneSignal 지원팀에 보내주세요:

* 중복을 수신한 기기의 OneSignal 구독 ID
* OneSignal 메시지 ID 또는 대시보드의 메시지 링크
* 앱의 다른 라이브러리 또는 플러그인 목록
* [문제를 재현하는 디버그 로그](./capturing-a-debug-log)
* 자세한 재현 단계

## FAQ

### 앱에 두 개의 알림 SDK가 있으면 어떻게 되나요?

두 SDK가 모두 동일한 알림을 독립적으로 처리하고 표시할 수 있습니다. OneSignal은 자체 알림을 자동으로 필터링하지만 다른 SDK는 그렇지 않습니다. `custom.i` 키를 확인하여 다른 SDK의 핸들러에서 OneSignal 페이로드를 필터링하세요. 코드 예시는 [서드파티 알림 핸들러](#third-party-notification-handlers)를 참조하세요.

### 이전 제공업체와 OneSignal에서 동시에 푸시를 보내는 방법은 무엇인가요?

점진적으로 전환할 수 있지만 두 제공업체에서 동일한 메시지를 보내는 것을 피하세요.

* **Android**: OneSignal을 통합하고 앱을 출시할 때 이전 SDK 알림 처리 코드를 제거하세요. 사용자가 업데이트하면 이전 제공업체에서 푸시를 받지 않게 됩니다.
* **iOS**: 사용자가 업데이트하는 동안 일시적으로 이전 제공업체에서 계속 보낼 수 있습니다. 완전히 전환되면 중복을 피하기 위해 OneSignal에서만 보내세요.

### 빠르게 변하는 업데이트에 대한 여러 알림을 방지하는 방법은 무엇인가요?

[알림 생성](/reference/push-notification) API에서 [`collapse_id`](/reference/push-notification#body-parameters)를 사용하여 알림을 쌓는 대신 이전 알림을 교체하세요. 여러 알림이 동일한 `collapse_id`를 공유하면 각 새 알림이 트레이의 이전 알림을 교체합니다. 이는 주가 업데이트, 라이브 스코어, 배달 예정 시간 및 유사한 빈번한 업데이트에 유용합니다.

## 관련 페이지

<Columns cols={2}>
  <Card title="모바일 SDK 문제 해결" icon="mobile" href="./mobile-troubleshooting">
    iOS 및 Android에서 푸시 알림 전달의 일반적인 문제를 해결합니다.
  </Card>

  <Card title="웹 푸시 문제 해결" icon="browser" href="./troubleshooting-web-push">
    웹 푸시 구독, 서비스 워커 및 전달 문제를 디버그합니다.
  </Card>

  <Card title="인앱 메시지 문제 해결" icon="message" href="./in-app-message-troubleshooting">
    인앱 메시지 표시, 트리거 및 중복 문제를 수정합니다.
  </Card>

  <Card title="모바일 서비스 확장" icon="gear" href="./service-extensions">
    iOS 및 Android에서 표시 전에 푸시 알림을 가로채고 커스터마이즈합니다.
  </Card>

  <Card title="표시되지 않는 알림(웹)" icon="bell-slash" href="./notifications-not-shown-web-push">
    전송되었지만 표시되지 않는 웹 푸시 알림 문제를 해결합니다.
  </Card>
</Columns>
