

# IVS 챗 클라이언트 메시징 SDK
<a name="chat-sdk"></a>

Amazon Interactive Video Service(IVS) Chat Client Messaging SDK는 Amazon IVS로 애플리케이션을 구축하는 개발자를 위한 것입니다. 이 SDK는 Amazon IVS 아키텍처를 활용하도록 설계되었으며 Amazon IVS Chat과 함께 업데이트가 제공되고 있습니다. 이 기본 SDK는 애플리케이션 및 사용자가 애플리케이션에 액세스하는 데 사용하는 디바이스에 미치는 성능 영향을 최소화하도록 설계되었습니다.

## 플랫폼 요구 사항
<a name="chat-sdk-platform-requirements"></a>

### 데스크톱 브라우저
<a name="chat-desktop-browsers"></a>


| 브라우저 | 지원되는 버전 | 
| --- | --- | 
| Chrome | 두 가지 주요 버전(현재 및 최신 이전 버전) | 
| Edge | 두 가지 주요 버전(현재 및 최신 이전 버전) | 
| Firefox | 두 가지 주요 버전(현재 및 최신 이전 버전) | 
| Opera | 두 가지 주요 버전(현재 및 최신 이전 버전) | 
| Safari | 두 가지 주요 버전(현재 및 최신 이전 버전) | 

### 모바일 브라우저
<a name="chat-mobile-browsers"></a>


| 브라우저 | 지원되는 버전 | 
| --- | --- | 
| Android용 Chrome | 두 가지 주요 버전(현재 및 최신 이전 버전) | 
| Android용 Firefox | 두 가지 주요 버전(현재 및 최신 이전 버전) | 
| Android용 Opera | 두 가지 주요 버전(현재 및 최신 이전 버전) | 
| WebView Android | 두 가지 주요 버전(현재 및 최신 이전 버전) | 
| 삼성 인터넷 | 두 가지 주요 버전(현재 및 최신 이전 버전) | 
| iOS용 Safari | 두 가지 주요 버전(현재 및 최신 이전 버전) | 

### 기본 플랫폼
<a name="chat-native-platforms"></a>


| 플랫폼 | 지원되는 버전 | 
| --- | --- | 
| Android | 5.0 이상 | 
| iOS |  13.0 이상  | 

## 지원
<a name="chat-sdk-support"></a>

채팅 룸에서 오류나 기타 문제가 발생하면 IVS 챗 API를 통해 고유한 채팅 룸 식별자를 확인합니다([ListRooms](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/API_ListRooms.html) 참조).

이 채팅룸 식별자를 AWS Support와 공유합니다. 이를 통해 문제를 해결하는 데 도움이 되는 정보를 얻을 수 있습니다.

**참고:** [Amazon IVS Chat 릴리스 정보](release-notes.md)를 참조하여 사용 가능한 버전 및 해결된 문제를 확인하세요. 해당하는 경우 Support에 문의하기 전에 SDK 버전을 업데이트하고 문제가 해결되는지 확인합니다.

### 버저닝
<a name="chat-sdk-support-versioning"></a>

Amazon IVS Chat Client Messaging SDK는 [유의적 버전 관리](https://semver.org/)를 사용합니다.

이를 설명하기 위해 다음을 가정합니다.
+ 최신 릴리스는 버전 4.1.3입니다.
+ 이전 주요 버전의 최신 릴리스는 3.2.4입니다.
+ 버전 1.x의 최신 릴리스는 1.5.6입니다.

이전 버전과 호환되는 새 기능은 최신 버전의 마이너 릴리스로 추가됩니다. 이 경우 새 기능의 다음 집합이 버전 4.2.0으로 추가됩니다.

이전 버전과 호환되는 마이너 버그 수정은 최신 버전의 패치 릴리스로 추가됩니다. 여기서 마이너 버그의 다음 수정 집합은 버전 4.1.4로 추가됩니다.

이전 버전과 호환되는 메이저 버그 수정은 다르게 처리됩니다. 이러한 버그 수정은 다음과 같이 여러 버전에 추가됩니다.
+ 최신 버전의 패치 릴리스에 추가되는 경우. 이 경우 버전 4.1.4입니다.
+ 이전 마이너 버전의 패치 릴리스에 추가되는 경우. 이 경우 버전 3.2.5입니다.
+ 최신 버전 1.x 릴리스의 패치 릴리스에 추가되는 경우. 이 경우 버전 1.5.7입니다.

메이저 버그 수정은 Amazon IVS 제품 팀에서 정의합니다. 일반적인 예로는 중요한 보안 업데이트와 고객에게 필요한 기타 수정이 있습니다.

**참고:** 위의 예에서 릴리스된 버전은 숫자가 차례대로 높아집니다(예: 4.1.3에서 4.1.4). 실제로는 하나 이상의 패치 번호가 내부에 남고 릴리스되지 않을 수 있으므로, 예를 들어 릴리스된 버전은 4.1.3에서 4.1.6으로 증가할 수 있습니다.

또한, 버전 1.x는 2023년 말까지 또는 3.x가 릴리스될 때까지 중 더 나중에 도래하는 시점에 지원됩니다.

## Amazon IVS Chat API
<a name="chat-sdk-chat-apis"></a>

서버 측(SDK에서 관리하지 않음)에는 각각 고유한 책임이 있는 두 가지 API가 있습니다.
+ **데이터 영역** - [IVS 챗 메시징 API](https://docs.aws.amazon.com//ivs/latest/chatmsgapireference/welcome.html)는 토큰 기반 인증 체계를 통해 구동되는 프런트엔드 애플리케이션(iOS, Android, macOS 등)에서 사용하도록 설계된 WebSocket API입니다. 이전에 생성된 채팅 토큰을 사용하여 이 API를 사용하는 기존 채팅 룸에 연결할 수 있습니다.

  *Amazon IVS Chat Client Messaging SDK는 데이터 영역에만 관련됩니다. SDK에서는 사용자가 이미 백엔드를 통해 채팅 토큰을 생성하고 있다고 가정합니다. 이러한 토큰의 검색은 SDK가 아닌 프런트엔드 애플리케이션에서 관리하는 것으로 간주됩니다.*
+ **컨트롤 플레인** - [IVS 챗 Control Plane API](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/Welcome.html)는 고유한 *백엔드 애플리케이션*에 대한 인터페이스를 제공하여 채팅 룸과 채팅 룸에 참여하는 사용자를 관리하고 만들 수 있습니다. *자체 백엔드*에서 관리하는 앱의 채팅 환경에 대한 관리자 패널로 생각하면 됩니다. 데이터 영역이 채팅 룸을 인증하는 데 필요로 하는 *채팅 토큰* 생성을 담당하는 컨트롤 플레인 작업이 있습니다.

  **중요:** *IVS 챗 Client Messaging SDK는 컨트롤 플레인 작업을 호출하지 않습니다. 채팅 토큰을 만들려면 백엔드를 설정해야 합니다. 이 채팅 토큰을 검색하려면 프런트엔드 애플리케이션이 백엔드와 통신해야 합니다.*

# IVS Chat Client SDK: Android 설명서
<a name="chat-sdk-android"></a>

Amazon Interactive Video(IVS) Chat Client Messaging Android SDK는 Android를 사용하는 플랫폼에 [IVS 챗 메시징 API](https://docs.aws.amazon.com//ivs/latest/chatmsgapireference/welcome.html)를 쉽게 통합할 수 있는 인터페이스를 제공합니다.

`com.amazonaws:ivs-chat-messaging` 패키지는 본 문서에서 설명하는 인터페이스를 구현합니다.

**최신 버전의 IVS 챗 클라이언트 메시징 Android SDK:** 1.1.0([릴리스 정보](https://docs.aws.amazon.com//ivs/latest/ChatUserGuide/release-notes.html#jan31-23))

**참조 문서:** Amazon IVS Chat Client Messaging Android SDK에서 사용할 수 있는 가장 중요한 메서드에 대한 자세한 내용은 [https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.1.0/](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.1.0/)의 참조 문서를 확인하세요.

**샘플 코드:** GitHub의 [https://github.com/aws-samples/amazon-ivs-chat-for-android-demo](https://github.com/aws-samples/amazon-ivs-chat-for-android-demo)에서 Android 샘플 리포지토리를 참조하세요.

**플랫폼 요구 사항:** 개발 환경에는 Android 5.0(API 레벨 21) 이상이 필요합니다.

# IVS Chat Client Messaging Android SDK 시작하기
<a name="chat-android-getting-started"></a>

시작하기 전에 [Amazon IVS Chat 시작하기](getting-started-chat.md)의 내용을 숙지해야 합니다.

## 패키지 추가
<a name="chat-android-add-package"></a>

`com.amazonaws:ivs-chat-messaging`을 `build.gradle` 종속성에 추가합니다.

```
dependencies {
   implementation 'com.amazonaws:ivs-chat-messaging'
}
```

## Proguard 규칙 추가
<a name="chat-android-proguard-rules"></a>

R8/Proguard 규칙 파일(`proguard-rules.pro`)에 다음 항목을 추가합니다.

```
-keep public class com.amazonaws.ivs.chat.messaging.** { *; }
-keep public interface com.amazonaws.ivs.chat.messaging.** { *; }
```

## 백엔드 설정
<a name="chat-android-setup-backend"></a>

이 통합에는 [Amazon IVS API](https://docs.aws.amazon.com//ivs/latest/LowLatencyAPIReference/Welcome.html)와 통신하는 서버의 엔드포인트가 필요합니다. [공식 AWS 라이브러리](https://aws.amazon.com/developer/tools/)를 사용하여 서버에서 Amazon IVS API에 액세스합니다. 공개 패키지(예: node.js 및 Java)의 여러 언어로 액세스할 수 있습니다.

다음으로 [Amazon IVS Chat API](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/Welcome.html)와 통신하는 서버 엔드포인트를 생성하고 토큰을 생성합니다.

## 서버 연결 설정
<a name="chat-android-setup-server"></a>

`ChatTokenCallback`을 파라미터로 사용하는 메서드를 생성하고 백엔드에서 채팅 토큰을 가져옵니다. 해당 토큰을 콜백의 `onSuccess` 메서드로 전달합니다. 오류가 발생한 경우 예외를 콜백의 `onError` 메서드로 전달합니다. 이는 다음 단계에서 기본 `ChatRoom` 엔터티를 인스턴스화하는 데 필요합니다.

아래에서는 `Retrofit` 호출을 사용하여 위 사항을 구현하는 샘플 코드를 확인할 수 있습니다.

```
// ...

private fun fetchChatToken(callback: ChatTokenCallback) {
    apiService.createChatToken(userId, roomId).enqueue(object : Callback<ChatToken> {
        override fun onResponse(call: Call<ExampleResponse>, response: Response<ExampleResponse>) {
            val body = response.body()
            val token = ChatToken(
                body.token,
                body.sessionExpirationTime,
                body.tokenExpirationTime
            )
            callback.onSuccess(token)
        }

        override fun onFailure(call: Call<ChatToken>, throwable: Throwable) {
            callback.onError(throwable)
        }
    })
}
// ...
```

# IVS Chat Client Messaging Android SDK 사용
<a name="chat-android-using-sdk"></a>

이 문서에서는 Amazon IVS Chat Client Messaging Android SDK 사용과 관련된 단계를 안내합니다.

## 채팅 룸 인스턴스 초기화
<a name="chat-android-initialize-room"></a>

`ChatRoom` 클래스의 인스턴스를 만듭니다. 채팅 룸이 호스팅된 AWS 리전인 `regionOrUrl`, 그리고 이전 단계에서 생성된 토큰 가져오기 메서드인 `tokenProvider`의 전달이 필요합니다.

```
val room = ChatRoom(
    regionOrUrl = "us-west-2",
    tokenProvider = ::fetchChatToken
)
```

다음으로 채팅 관련 이벤트의 핸들러를 구현할 리스너 객체를 만들고, 이를 `room.listener` 속성에 할당합니다.

```
private val roomListener = object : ChatRoomListener {
    override fun onConnecting(room: ChatRoom) {
      // Called when room is establishing the initial connection or reestablishing connection after socket failure/token expiration/etc
    }

    override fun onConnected(room: ChatRoom) {
        // Called when connection has been established
    }

    override fun onDisconnected(room: ChatRoom, reason: DisconnectReason) {
        // Called when a room has been disconnected
    }

    override fun onMessageReceived(room: ChatRoom, message: ChatMessage) {
        // Called when chat message has been received
    }

    override fun onEventReceived(room: ChatRoom, event: ChatEvent) {
        // Called when chat event has been received
    }

    override fun onDeleteMessage(room: ChatRoom, event: DeleteMessageEvent) {
       // Called when DELETE_MESSAGE event has been received
    }
}

val room = ChatRoom(
    region = "us-west-2",
    tokenProvider = ::fetchChatToken
)

room.listener = roomListener // <- add this line

// ...
```

기본 초기화의 마지막 단계는 WebSocket 연결을 설정하여 특정 룸에 연결하는 것입니다. 이를 위해 룸 인스턴스 내에서 `connect()` 메서드를 호출합니다. 앱이 백그라운드에서 재개되는 경우에도 연결이 유지되도록 `onResume() ` 수명 주기 메서드에서 이를 수행하는 것이 좋습니다.

```
room.connect()
```

SDK는 서버에서 받은 채팅 토큰으로 인코딩된 채팅 룸과의 연결을 설정하려고 시도합니다. 실패하면 룸 인스턴스에 지정된 횟수만큼 다시 연결을 시도합니다.

## 채팅 룸에서 작업 수행
<a name="chat-android-room-actions"></a>

`ChatRoom` 클래스에는 메시지를 보내고 삭제하고 다른 사용자의 연결을 끊는 작업이 있습니다. 이러한 작업은 요청 확인 또는 거부 알림을 받을 수 있는 선택적 콜백 파라미터를 허용합니다.

### 메시지 전송
<a name="chat-android-room-actions-send-message"></a>

이 요청의 경우 채팅 토큰에 `SEND_MESSAGE` 기능이 인코딩되어 있어야 합니다.

메시지 전송 요청 트리거:

```
val request = SendMessageRequest("Test Echo")
room.sendMessage(request)
```

요청의 확인/거부를 받으려면 콜백을 두 번째 파라미터로 제공:

```
room.sendMessage(request, object : SendMessageCallback {
   override fun onConfirmed(request: SendMessageRequest, response: ChatMessage) {
      // Message was successfully sent to the chat room.
   }
   override fun onRejected(request: SendMessageRequest, error: ChatError) {
      // Send-message request was rejected. Inspect the `error` parameter for details.
   }
})
```

### 메시지 삭제
<a name="chat-android-room-actions-delete-message"></a>

이 요청의 경우 채팅 토큰에 DELETE\$1MESSAGE 기능이 인코딩되어 있어야 합니다.

메시지 삭제 요청 트리거:

```
val request = DeleteMessageRequest(messageId, "Some delete reason")
room.deleteMessage(request)
```

요청의 확인/거부를 받으려면 콜백을 두 번째 파라미터로 제공:

```
room.deleteMessage(request, object : DeleteMessageCallback {
   override fun onConfirmed(request: DeleteMessageRequest, response: DeleteMessageEvent) {
      // Message was successfully deleted from the chat room.
   }
   override fun onRejected(request: DeleteMessageRequest, error: ChatError) {
      // Delete-message request was rejected. Inspect the `error` parameter for details.
   }
})
```

### 다른 사용자 연결 해제
<a name="chat-android-room-actions-disconnect-user"></a>

이 요청의 경우 채팅 토큰에 `DISCONNECT_USER` 기능이 인코딩되어 있어야 합니다.

조정 목적으로 다른 사용자의 연결 해제:

```
val request = DisconnectUserRequest(userId, "Reason for disconnecting user")
room.disconnectUser(request)
```

요청의 확인/거부를 받으려면 콜백을 두 번째 파라미터로 제공:

```
room.disconnectUser(request, object : DisconnectUserCallback {
   override fun onConfirmed(request: SendMessageRequest, response: ChatMessage) {
      // User was disconnected from the chat room.
   }
   override fun onRejected(request: SendMessageRequest, error: ChatError) {
      // Disconnect-user request was rejected. Inspect the `error` parameter for details.
   }
})
```

## 채팅 룸 연결 해제
<a name="chat-android-disconnect-room"></a>

채팅 룸과의 연결을 해제하려면 룸 인스턴스에서 `disconnect()` 메서드를 호출합니다.

```
room.disconnect()
```

애플리케이션이 백그라운드 상태일 때 잠시 후 WebSocket 연결의 작동이 중지되므로 백그라운드 상태로 또는 백그라운드 상태에서 전환할 때 수동으로 연결하거나 연결을 해제하는 것이 좋습니다. 이를 위해 Android `room.connect()` 또는 `onResume()`에서 `Activity` 수명 주기 메서드의 `Fragment` 호출과 `room.disconnect()` 수명 주기 메서드의 `onPause()` 호출을 일치시킵니다.

# IVS 챗 클라이언트 메시징 SDK: Android 자습서 1부: 채팅룸
<a name="chat-sdk-android-tutorial-chat-rooms"></a>

본 문서는 두 파트로 구성된 자습서 중 첫 번째 파트에 해당하는 자습서입니다. [Kotlin](https://kotlinlang.org/) 프로그래밍 언어를 사용하여 완전한 기능을 갖춘 Android 앱을 구축하여 Amazon IVS Chat Messaging SDK로 작업하기 위한 필수 사항을 알아봅니다. 여기에서 지칭하는 앱은 *Chatterbox*라고 합니다.

모듈을 시작하기 전에 몇 분 정도 시간을 내어 사전 조건, 채팅 토큰의 주요 개념, 채팅룸 생성에 필요한 백엔드 서버를 숙지해 두세요.

이 자습서는 IVS 챗 메시징 SDK를 처음 사용하는 숙련된 Android 개발자를 위해 만들어졌습니다. Kotlin 프로그래밍 언어와 Android 플랫폼에서 UI를 만드는 데 익숙해야 합니다.

이 자습서의 첫 번째 부분은 여러 섹션으로 나뉩니다.

1. [로컬 인증/권한 부여 서버 설정](#chat-android-rooms-auth-server)

1. [Chatterbox 프로젝트 생성](#chat-android-rooms-chatterbox)

1. [채팅룸에 연결 및 연결 업데이트 관찰](#chat-android-rooms-connect-state)

1. [토큰 공급자 구축](#chat-android-rooms-token-provider)

1. [다음 단계](#chat-android-rooms-next-steps)

전체 SDK 설명서를 보려면 우선 [Amazon IVS Chat Client Messaging SDK](chat-sdk.md)(Amazon IVS Chat 사용 설명서**에서 참조) 및 [Chat Client Messaging: SDK for Android Reference](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/)(GitHub)를 참조하세요.

## 사전 조건
<a name="chat-android-rooms-prerequisites"></a>
+ Kotlin과 Android 플랫폼에서 애플리케이션을 만드는 데 익숙해야 합니다. Android용 애플리케이션을 만드는 데 익숙하지 않은 경우 Android 개발자를 위한 [첫 앱 빌드](https://developer.android.com/codelabs/basic-android-kotlin-compose-first-app#0) 가이드에서 기본 사항을 배워 보세요.
+ [Amazon IVS Chat 시작하기](getting-started-chat.md)를 철저하게 읽고 이해합니다.
+ 기존 IAM 정책에 정의된 `CreateChatToken` 및 `CreateRoom` 기능을 사용하여 AWS IAM 사용자를 생성합니다. ([Amazon IVS Chat 시작하기](getting-started-chat.md)를 참조하세요.)
+ 이 사용자의 비밀/액세스 키가 AWS 보안 인증 파일에 저장되어 있는지 확인합니다. 지침은 [AWS CLI 사용 설명서](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html)(특히 [구성 및 보안 인증 파일 설정](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html))를 참조합니다.
+ 채팅룸을 생성하고 ARN을 저장합니다. [Amazon IVS Chat 시작하기](getting-started-chat.md)을(를) 참조하세요. (ARN을 저장하지 않은 경우 나중에 콘솔이나 Chat API를 사용하여 조회할 수 있습니다.)

## 로컬 인증/권한 부여 서버 설정
<a name="chat-android-rooms-auth-server"></a>

백엔드 서버는 채팅룸을 생성하고 IVS 챗 Android SDK가 채팅룸의 클라이언트를 인증하고 권한을 부여하는 데 필요한 채팅 토큰을 생성하는 일을 맡습니다.

Amazon IVS 채팅 시작하기에서 [채팅 토큰 생성](getting-started-chat-auth.md)을 참조하세요.** 플로우차트에서 볼 수 있듯이 서버 측 코드는 채팅 토큰 생성을 담당합니다 즉, 앱은 서버 측 애플리케이션에서 채팅 토큰을 요청하여 채팅 토큰을 생성하는 자체 수단을 제공해야 합니다.

저희는 [Ktor](https://ktor.io/) 프레임워크를 사용하여 로컬 AWS 환경을 통해 채팅 토큰 생성을 관리하는 라이브 로컬 서버를 생성합니다.

이제 AWS 보안 인증 정보가 올바르게 설정되었을 것입니다. 단계별 지침은 [개발을 위한 AWS 자격 증명 및 리전 설정](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/setup-credentials.html)을 참조하세요.

`chatterbox`라는 새 디렉터리를 생성하고 그 안에서 또 다른 디렉토리 `auth-server`를 생성합니다.

서버 폴더는 다음과 같은 구조를 갖습니다.

```
- auth-server
  - src
    - main
      - kotlin
        - com
          - chatterbox
            - authserver
              - Application.kt
       - resources
         - application.conf
         - logback.xml
   - build.gradle.kts
```

*참고: 여기에서 코드를 참조된 파일에 직접 복사하거나 붙여넣을 수 있습니다.*

다음으로 인증 서버가 작동하는 데 필요한 모든 종속 항목과 플러그인을 추가합니다.

**Kotlin 스크립트:**

```
// ./auth-server/build.gradle.kts

plugins {
   application
   kotlin("jvm")
   kotlin("plugin.serialization").version("1.7.10")
}

application {
   mainClass.set("io.ktor.server.netty.EngineMain")
}

dependencies {
   implementation("software.amazon.awssdk:ivschat:2.18.1")
   implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.20")

   implementation("io.ktor:ktor-server-core:2.1.3")
   implementation("io.ktor:ktor-server-netty:2.1.3")
   implementation("io.ktor:ktor-server-content-negotiation:2.1.3")
   implementation("io.ktor:ktor-serialization-kotlinx-json:2.1.3")

   implementation("ch.qos.logback:logback-classic:1.4.4")
}
```

이제 인증 서버의 로깅 기능을 설정해야 합니다. (자세한 정보는 [로거 구성](https://ktor.io/docs/logging.html#configure-logger)을 참조하세요.)

**XML:**

```
// ./auth-server/src/main/resources/logback.xml

<configuration>
   <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
      <encoder>
         <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
      </encoder>
   </appender>
   <root level="trace">
      <appender-ref ref="STDOUT"/>
   </root>
   <logger name="org.eclipse.jetty" level="INFO"/>
   <logger name="io.netty" level="INFO"/>
</configuration>
```

[Ktor](https://ktor.io/docs/welcome.html) 서버에는 `resources` 디렉터리의 `application.*` 파일에서 자동으로 로드되는 구성 설정이 필요하므로 구성 설정도 추가합니다. (자세한 정보는 [파일으로 구성](https://ktor.io/docs/configurations.html#configuration-file)을 참조하세요.)

**HOCON**:

```
// ./auth-server/src/main/resources/application.conf

ktor {
   deployment {
      port = 3000
   }
   application {
      modules = [ com.chatterbox.authserver.ApplicationKt.main ]
   }
}
```

마지막으로 서버를 구현해 보겠습니다.

**Kotlin**:

```
// ./auth-server/src/main/kotlin/com/chatterbox/authserver/Application.kt

package com.chatterbox.authserver

import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import software.amazon.awssdk.services.ivschat.IvschatClient
import software.amazon.awssdk.services.ivschat.model.CreateChatTokenRequest

@Serializable
data class ChatTokenParams(var userId: String, var roomIdentifier: String)

@Serializable
data class ChatToken(
   val token: String,
   val sessionExpirationTime: String,
   val tokenExpirationTime: String,
)

fun Application.main() {
   install(ContentNegotiation) {
      json(Json)
   }

   routing {
      post("/create_chat_token") {
         val callParameters = call.receive<ChatTokenParams>()
         val request = CreateChatTokenRequest.builder().roomIdentifier(callParameters.roomIdentifier)
            .userId(callParameters.userId).build()
         val token = IvschatClient.create()
            .createChatToken(request)

         call.respond(
            ChatToken(
                token.token(),
                token.sessionExpirationTime().toString(),
                token.tokenExpirationTime().toString()
            )
         )
      }
   }
}
```

## Chatterbox 프로젝트 생성
<a name="chat-android-rooms-chatterbox"></a>

Android 프로젝트를 생성하려면 [Android 스튜디오](https://developer.android.com/studio)를 설치하고 엽니다.

공식 Android [프로젝트 생성 가이드](https://developer.android.com/studio/projects/create-project)에 나와 있는 단계를 따릅니다.
+ [프로젝트 유형 선택](https://developer.android.com/studio/projects/create-project)에서 Chatterbox 앱을 위한 **빈 활동** 프로젝트 템플릿을 선택합니다.
+ [프로젝트 구성](https://developer.android.com/studio/projects/create-project#configure)에서 다음 구성 필드 값을 선택합니다.
  + **이름**: My App
  + **패키지 이름**: com.chatterbox.myapp
  + **저장 위치**: 이전 단계에서 만든`chatterbox` 디렉터리를 지정합니다.
  + **언어**: Kotlin
  + **최소 API 레벨**: API 21: Android 5.0(Lollipop)

모든 구성 매개 변수를 올바르게 지정한 후 `chatterbox` 폴더 내의 파일 구조는 다음과 같아야 합니다.

```
- app
  - build.gradle
  ...
- gradle
- .gitignore
- build.gradle
- gradle.properties
- gradlew
- gradlew.bat
- local.properties
- settings.gradle
- auth-server
  - src
    - main
      - kotlin
        - com
          - chatterbox
            - authserver
              - Application.kt
       - resources
         - application.conf
         - logback.xml
   - build.gradle.kts
```

이제 작동하는 Android 프로젝트가 있으므로 `build.gradle` 종속 항목에 [com.amazonaws:ivs-chat-messaging](https://mvnrepository.com/artifact/com.amazonaws/ivs-chat-messaging)을 추가할 수 있습니다. ([Gradle](https://gradle.org/) 빌드 툴킷에 대한 자세한 정보는 [빌드 구성](https://developer.android.com/build)을 참조하세요.)

**참고:** 모든 코드 조각의 맨 위에는 프로젝트에서 변경해야 하는 파일의 경로가 있습니다. 경로는 프로젝트 루트의 상대 경로입니다.

아래 코드에서 `<version>`을 챗 Android SDK의 현재 버전 번호(예: 1.0.0)로 대체하세요.**

**Kotlin**:

```
// ./app/build.gradle

plugins {
// ...
}

android {
// ...
}

dependencies {
   implementation("com.amazonaws:ivs-chat-messaging:<version>")
// ...
}
```

새 종속 항목이 추가된 후 Android 스튜디오에서 **Gradle 파일과 프로젝트 동기화**를 실행하여 프로젝트를 새 종속 항목과 동기화합니다. (자세한 정보는 [빌드 종속 항목 추가](https://developer.android.com/build/dependencies)를 참조하세요.)

이전 섹션에서 생성한 인증 서버를 프로젝트 루트에서 편리하게 실행하기 위해 이 서버를 `settings.gradle`에 새 모듈로 포함시킵니다. (자세한 정보는 [Gradle을 사용하여 소프트웨어 구성 요소 구조화 및 빌드](https://docs.gradle.org/current/userguide/multi_project_builds.html)를 참조하세요.)

**Kotlin 스크립트:**

```
// ./settings.gradle

// ...

rootProject.name = "Chatterbox"
include ':app'
include ':auth-server'
```

이제부터 `auth-server`가 Android 프로젝트에 포함되므로 프로젝트 루트에서 다음 명령으로 인증 서버를 실행할 수 있습니다.

**쉘:**

```
./gradlew :auth-server:run
```

## 채팅룸에 연결 및 연결 업데이트 관찰
<a name="chat-android-rooms-connect-state"></a>

채팅룸 연결을 열기 위해 활동이 처음 생성될 때 실행되는 [onCreate() 활동 수명 주기 콜백](https://developer.android.com/guide/components/activities/activity-lifecycle)을 사용합니다. [ChatRoom 생성자](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html)를 사용하려면 룸 연결을 인스턴스화하기 위해 `region` 및 `tokenProvider`를 제공해야 합니다.

**참고:** 아래 조각의 `fetchChatToken` 함수는 [다음 섹션](#chat-android-rooms-token-provider)에서 구현됩니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp

// ...
import androidx.appcompat.app.AppCompatActivity
// ...

// AWS region of the room that was created in Getting Started with Amazon IVS Chat
const val REGION = "us-west-2"

class MainActivity : AppCompatActivity() {
    private var room: ChatRoom? = null
    // ...

   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)

      // Create room instance
      room = ChatRoom(REGION, ::fetchChatToken)
   }

// ...
}
```

채팅룸 연결의 변화를 표시하고 대응하는 것은 `chatterbox`와 같은 채팅 앱을 만드는 데 필수적인 부분입니다. 룸과 상호작용을 시작하기 전에 채팅룸 연결 상태 이벤트를 구독하여 업데이트를 받아야 합니다.

[ChatRoom](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html)은 수명 주기 이벤트를 발생시키기 위해 [ChatRoomListener 인터페이스](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/listener.html) 구현을 연결할 것으로 예상합니다. 현재 리스너 함수는 호출 시 확인 메시지만 로그합니다.

**Kotlin**:

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

// ...
package com.chatterbox.myapp
// ...
const val TAG = "IVSChat-App"

class MainActivity : AppCompatActivity() {
// ...

    private val roomListener = object : ChatRoomListener {
        override fun onConnecting(room: ChatRoom) {
            Log.d(TAG, "onConnecting")
        }

        override fun onConnected(room: ChatRoom) {
            Log.d(TAG, "onConnected")
        }

        override fun onDisconnected(room: ChatRoom, reason: DisconnectReason) {
            Log.d(TAG, "onDisconnected $reason")
        }

        override fun onMessageReceived(room: ChatRoom, message: ChatMessage) {
            Log.d(TAG, "onMessageReceived $message")
        }

        override fun onMessageDeleted(room: ChatRoom, event: DeleteMessageEvent) {
            Log.d(TAG, "onMessageDeleted $event")
        }

        override fun onEventReceived(room: ChatRoom, event: ChatEvent) {
            Log.d(TAG, "onEventReceived $event")
        }

        override fun onUserDisconnected(room: ChatRoom, event:    DisconnectUserEvent) {
            Log.d(TAG, "onUserDisconnected $event")
        }
    }
}
```

이제 `ChatRoomListener`를 구현했으므로 룸 인스턴스에 연결해 보겠습니다.

**Kotlin**:

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)

   // Create room instance
   room = ChatRoom(REGION, ::fetchChatToken).apply {
      listener = roomListener
   }
}

private val roomListener = object : ChatRoomListener {
// ...
}
```

이 다음으로 룸 연결 상태를 읽을 수 있는 기능을 제공해야 합니다. `MainActivity.kt` [속성](https://kotlinlang.org/docs/properties.html)에 이를 보관하고 룸의 기본 DISCONNECTED 상태로 초기화합니다([IVS 챗 Android SDK 참조](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/latest/)의 `ChatRoom state` 참조). 로컬 상태를 최신 상태로 유지하려면 state-updater 함수를 구현해야 합니다. 이 함수를 `updateConnectionState`라고 해 보겠습니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

enum class ConnectionState {
   CONNECTED,
   DISCONNECTED,
   LOADING
}

class MainActivity : AppCompatActivity() {
   private var connectionState = ConnectionState.DISCONNECTED
// ...

   private fun updateConnectionState(state: ConnectionState) {
      connectionState = state

      when (state) {
         ConnectionState.CONNECTED -> {
            Log.d(TAG, "room connected")
         }
         ConnectionState.DISCONNECTED -> {
            Log.d(TAG, "room disconnected")
         }
         ConnectionState.LOADING -> {
            Log.d(TAG, "room loading")
         }
      }
   }
}
```

다음으로 state-updater 함수를 [ChatRoom.Listener](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/listener.html) 속성과 통합합니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

class MainActivity : AppCompatActivity() {
// ...

   private val roomListener = object : ChatRoomListener {
      override fun onConnecting(room: ChatRoom) {
         Log.d(TAG, "onConnecting")
         runOnUiThread {
            updateConnectionState(ConnectionState.LOADING)
         }
      }

      override fun onConnected(room: ChatRoom) {
         Log.d(TAG, "onConnected")
         runOnUiThread {
            updateConnectionState(ConnectionState.CONNECTED)
         }
      }

      override fun onDisconnected(room: ChatRoom, reason: DisconnectReason) {
         Log.d(TAG, "[${Thread.currentThread().name}] onDisconnected")
         runOnUiThread {
            updateConnectionState(ConnectionState.DISCONNECTED)
         }
      }
   }
}
```

이제 [ChatRoom](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html) 상태 업데이트를 저장하고, 듣고, 반응할 수 있게 되었으므로 연결을 초기화할 차례입니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

enum class ConnectionState {
   CONNECTED,
   DISCONNECTED,
   LOADING
}

class MainActivity : AppCompatActivity() {
   private var connectionState = ConnectionState.DISCONNECTED
// ...

   private fun connect() {
      try {
         room?.connect()
      } catch (ex: Exception) {
         Log.e(TAG, "Error while calling connect()", ex)
      }
   }

   private val roomListener = object : ChatRoomListener {
      // ...
      override fun onConnecting(room: ChatRoom) {
         Log.d(TAG, "onConnecting")
         runOnUiThread {
            updateConnectionState(ConnectionState.LOADING)
         }
      }

      override fun onConnected(room: ChatRoom) {
         Log.d(TAG, "onConnected")
         runOnUiThread {
            updateConnectionState(ConnectionState.CONNECTED)
         }
      }
      // ...
   }
}
```

## 토큰 공급자 구축
<a name="chat-android-rooms-token-provider"></a>

이제 애플리케이션에서 채팅 토큰을 생성하고 관리하는 함수를 만들 차례입니다. 이 예에서는 [Android용 Retrofit HTTP 클라이언트](https://square.github.io/retrofit/)를 사용합니다.

네트워크 트래픽을 보내려면 먼저 Android용 네트워크 보안 구성을 설정해야 합니다. (자세한 정보는 [네트워크 보안 구성](https://developer.android.com/privacy-and-security/security-config)을 참조하세요.) [앱 매니페스트](https://developer.android.com/guide/topics/manifest/manifest-intro) 파일에 네트워크 권한을 추가하는 것부터 시작합니다. 새로운 네트워크 보안 구성을 가리키는 추가된 `user-permission` 태그와 `networkSecurityConfig` 속성에 유의하세요. 아래 코드에서 `<version>`을 챗 Android SDK의 현재 버전 번호(예: 1.0.0)로 대체하세요.**

**XML**:

```
// ./app/src/main/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.chatterbox.myapp">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:allowBackup="true"
        android:fullBackupContent="@xml/backup_rules"
        android:label="@string/app_name"
        android:networkSecurityConfig="@xml/network_security_config"
// ...

// ./app/build.gradle


dependencies {
   implementation("com.amazonaws:ivs-chat-messaging:<version>")
// ...

   implementation("com.squareup.retrofit2:retrofit:2.9.0")
}
```

`10.0.2.2` 및 `localhost` 도메인을 신뢰할 수 있는 것으로 선언하여 백엔드와 메시지 교환을 시작합니다.

**XML**:

```
// ./app/src/main/res/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">10.0.2.2</domain>
        <domain includeSubdomains="true">localhost</domain>
    </domain-config>
</network-security-config>
```

다음으로 HTTP 응답 구문 분석을 위한 [Gson 변환기 추가](https://github.com/square/retrofit/tree/trunk/retrofit-converters/gson)와 함께 새로운 종속 항목을 추가해야 합니다. 아래 코드에서 `<version>`을 챗 Android SDK의 현재 버전 번호(예: 1.0.0)로 대체하세요.**

**Kotlin 스크립트**:

```
// ./app/build.gradle

dependencies {
   implementation("com.amazonaws:ivs-chat-messaging:<version>")
// ...

   implementation("com.squareup.retrofit2:retrofit:2.9.0")
}
```

채팅 토큰을 검색하려면 `chatterbox` 앱에서 POST HTTP 요청을 해야 합니다. Retrofit이 구현할 수 있도록 요청을 인터페이스로 정의합니다. ([Retrofit 설명서](https://square.github.io/retrofit/)를 참조하세요. 또한 [CreateChatToken](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/API_CreateChatToken.html#API_CreateChatToken_RequestBody) 작업 사양도 숙지하세요.)

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/network/ApiService.kt

package com.chatterbox.myapp.network
// ...


import androidx.annotation.Keep
import com.amazonaws.ivs.chat.messaging.ChatToken
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST

data class CreateTokenParams(var userId: String, var roomIdentifier: String)

interface ApiService {
   @POST("create_chat_token")
   fun createChatToken(@Body params: CreateTokenParams): Call<ChatToken>
}
```

이제 네트워킹을 설정했으므로 채팅 토큰을 생성하고 관리하는 함수를 추가할 차례입니다. 프로젝트가 [생성](#chat-android-rooms-chatterbox)되었을 때 자동으로 생성된 `MainActivity.kt`에 함수를 추가합니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt


package com.chatterbox.myapp

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import com.amazonaws.ivs.chat.messaging.*
import com.chatterbox.myapp.network.CreateTokenParams
import com.chatterbox.myapp.network.RetrofitFactory
import retrofit2.Call
import java.io.IOException
import retrofit2.Callback
import retrofit2.Response

// custom tag for logging purposes
const val TAG = "IVSChat-App"

// any ID to be associated with auth token
const val USER_ID = "test user id"
// ID of the room the app wants to access. Must be an ARN. See Amazon Resource Names(ARNs)
const val ROOM_ID = "arn:aws:..."
// AWS region of the room that was created in Getting Started with Amazon IVS Chat
const val REGION = "us-west-2"

class MainActivity : AppCompatActivity() {
   private val service = RetrofitFactory.makeRetrofitService()
   private lateinit var userId: String

   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
   }

   private fun fetchChatToken(callback: ChatTokenCallback) {
      val params = CreateTokenParams(userId, ROOM_ID)
      service.createChatToken(params).enqueue(object : Callback<ChatToken> {
         override fun onResponse(call: Call<ChatToken>, response: Response<ChatToken>) {
            val token = response.body()
            if (token == null) {
               Log.e(TAG, "Received empty token response")
               callback.onFailure(IOException("Empty token response"))
               return
            }

            Log.d(TAG, "Received token response $token")
            callback.onSuccess(token)
         }

         override fun onFailure(call: Call<ChatToken>, throwable: Throwable) {
            Log.e(TAG, "Failed to fetch token", throwable)
            callback.onFailure(throwable)
         }
      })
   }
}
```

## 다음 단계
<a name="chat-android-rooms-next-steps"></a>

이제 채팅룸 연결을 설정했으므로 이 Android 자습서의 2부인 [메시지 및 이벤트](chat-sdk-android-tutorial-messages-events.md)로 이동하세요.

# IVS 챗 클라이언트 메시징 SDK: Android 자습서 2부: 메시지 및 이벤트
<a name="chat-sdk-android-tutorial-messages-events"></a>

이 자습서의 두 번째(마지막) 부분은 여러 섹션으로 나뉩니다.

1. [메시지 전송을 위한 UI 만들기](#chat-android-messages-events-create_ui)

   1. [UI 기본 레이아웃](#create-ui-main-layout)

   1. [텍스트를 일관되게 표시하기 위한 UI 추상화 텍스트 셀](#create-ui-text-cell)

   1. [UI 왼쪽 채팅 메시지](#create-ui-left-chat-message)

   1. [UI 오른쪽 채팅 메시지](#create-ui-right-chat-message)

   1. [UI 추가 색상 값](#create-ui-color-values)

1. [뷰 결합 적용](#chat-android-messages-events-view-binding)

1. [채팅 메시지 요청 관리](#chat-android-messages-events-requests)

1. [최종 단계](#chat-android-messages-events-final-steps)

전체 SDK 설명서를 보려면 우선 [Amazon IVS Chat Client Messaging SDK](chat-sdk.md)(Amazon IVS Chat 사용 설명서**에서 참조) 및 [Chat Client Messaging: SDK for Android Reference](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/latest/)(GitHub)를 참조하세요.

## 사전 조건
<a name="chat-android-messages-events-prerequisite"></a>

이 자습서의 1부인 [채팅룸](chat-sdk-android-tutorial-chat-rooms.md)을 완료해야 합니다.

## 메시지 전송을 위한 UI 만들기
<a name="chat-android-messages-events-create_ui"></a>

채팅룸 연결을 성공적으로 초기화했으므로 이제 첫 번째 메시지를 보낼 차례입니다. 이 기능에는 UI가 필요합니다. 다음을 추가합니다.
+ `connect/disconnect` 버튼
+ `send` 버튼으로 메시지 입력
+ 동적 메시지 목록. 이를 빌드하기 위해 Android Jetpack [RecyclerView](https://developer.android.com/develop/ui/views/layout/recyclerview)를 사용합니다.

### UI 기본 레이아웃
<a name="create-ui-main-layout"></a>

Android 개발자 문서에서 Android 젯팩 [레이아웃](https://developer.android.com/develop/ui/views/layout/declaring-layout)을 참조하세요.

**XML:**

```
// ./app/src/main/res/layout/activity_main.xml


<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                     xmlns:app="http://schemas.android.com/apk/res-auto"
                                                     xmlns:tools="http://schemas.android.com/tools"
                                                     android:layout_width="match_parent"
                                                     android:layout_height="match_parent">

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:app="http://schemas.android.com/apk/res-auto"
                  android:id="@+id/connect_view"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:gravity="center"
                  android:orientation="vertical">

        <androidx.cardview.widget.CardView
                android:id="@+id/connect_button"
                android:layout_width="match_parent"
                android:layout_height="48dp"
                android:layout_gravity=""
                android:layout_marginStart="16dp"
                android:layout_marginTop="4dp"
                android:layout_marginEnd="16dp"
                android:clickable="true"
                android:elevation="16dp"
                android:focusable="true"
                android:foreground="?android:attr/selectableItemBackground"
                app:cardBackgroundColor="@color/purple_500"
                app:cardCornerRadius="10dp">

            <TextView
                    android:id="@+id/connect_text"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentEnd="true"
                    android:layout_gravity="center"
                    android:layout_weight="1"
                    android:paddingHorizontal="12dp"
                    android:text="Connect"
                    android:textColor="@color/white"
                    android:textSize="16sp"/>

            <ProgressBar
                    android:id="@+id/activity_indicator"
                    android:layout_width="20dp"
                    android:layout_height="20dp"
                    android:layout_gravity="center"
                    android:layout_marginHorizontal="20dp"
                    android:indeterminateOnly="true"
                    android:indeterminateTint="@color/white"
                    android:indeterminateTintMode="src_atop"
                    android:keepScreenOn="true"
                    android:visibility="gone"/>
        </androidx.cardview.widget.CardView>

    </LinearLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/chat_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipToPadding="false"
            android:visibility="visible"
            tools:context=".MainActivity">

        <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                app:layout_constraintBottom_toTopOf="@+id/layout_message_input"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent">

            <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/recycler_view"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:clipToPadding="false"
                    android:paddingTop="70dp"
                    android:paddingBottom="20dp"/>
        </RelativeLayout>

        <RelativeLayout
                android:id="@+id/layout_message_input"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@android:color/white"
                android:clipToPadding="false"
                android:drawableTop="@android:color/black"
                android:elevation="18dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent">

            <EditText
                    android:id="@+id/message_edit_text"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginStart="16dp"
                    android:layout_toStartOf="@+id/send_button"
                    android:background="@android:color/transparent"
                    android:hint="Enter Message"
                    android:inputType="text"
                    android:maxLines="6"
                    tools:ignore="Autofill"/>

            <Button
                    android:id="@+id/send_button"
                    android:layout_width="84dp"
                    android:layout_height="48dp"
                    android:layout_alignParentEnd="true"
                    android:background="@color/black"
                    android:foreground="?android:attr/selectableItemBackground"
                    android:text="Send"
                    android:textColor="@color/white"
                    android:textSize="12dp"/>
        </RelativeLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>


</androidx.coordinatorlayout.widget.CoordinatorLayout>
```

### 텍스트를 일관되게 표시하기 위한 UI 추상화 텍스트 셀
<a name="create-ui-text-cell"></a>

**XML:**

```
// ./app/src/main/res/layout/common_cell.xml
   
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:id="@+id/layout_container"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:background="@color/light_gray"
              android:minWidth="100dp"
              android:orientation="vertical">

    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">

        <TextView
                android:id="@+id/card_message_me_text_view"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_marginBottom="8dp"
                android:maxWidth="260dp"
                android:paddingLeft="12dp"
                android:paddingTop="8dp"
                android:paddingRight="12dp"
                android:text="This is a Message"
                android:textColor="#ffffff"
                android:textSize="16sp"/>

        <TextView
                android:id="@+id/failed_mark"
                android:layout_width="40dp"
                android:layout_height="match_parent"
                android:paddingRight="5dp"
                android:src="@drawable/ic_launcher_background"
                android:text="!"
                android:textAlignment="viewEnd"
                android:textColor="@color/white"
                android:textSize="25dp"
                android:visibility="gone"/>
    </LinearLayout>

</LinearLayout>
```

### UI 왼쪽 채팅 메시지
<a name="create-ui-left-chat-message"></a>

**XML**:

```
// ./app/src/main/res/layout/card_view_left.xml
 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:layout_marginStart="8dp"
              android:layout_marginBottom="12dp"
              android:orientation="vertical">

    <TextView
            android:id="@+id/username_edit_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="UserName"/>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        <androidx.cardview.widget.CardView
                android:id="@+id/card_message_other"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="left"
                android:layout_marginBottom="4dp"
                android:foreground="?android:attr/selectableItemBackground"
                app:cardBackgroundColor="@color/light_gray_2"
                app:cardCornerRadius="10dp"
                app:cardElevation="0dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent">

            <include layout="@layout/common_cell"/>
        </androidx.cardview.widget.CardView>

        <TextView
                android:id="@+id/dateText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="4dp"
                android:layout_marginBottom="4dp"
                android:text="10:00"
                app:layout_constraintBottom_toBottomOf="@+id/card_message_other"
                app:layout_constraintLeft_toRightOf="@+id/card_message_other"/>
    </androidx.constraintlayout.widget.ConstraintLayout>


</LinearLayout>
```

### UI 오른쪽 채팅 메시지
<a name="create-ui-right-chat-message"></a>

**XML**:

```
// ./app/src/main/res/layout/card_view_right.xml
 
<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"                                                   xmlns:app="http://schemas.android.com/apk/res-auto"                                                   android:layout_width="match_parent"                                                   android:layout_height="wrap_content" 
android:layout_marginEnd="8dp">

    <androidx.cardview.widget.CardView
            android:id="@+id/card_message_me"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="right"
            android:layout_marginBottom="10dp"
            android:foreground="?android:attr/selectableItemBackground"
            app:cardBackgroundColor="@color/purple_500"
            app:cardCornerRadius="10dp"
            app:cardElevation="0dp"
            app:cardPreventCornerOverlap="false"
            app:cardUseCompatPadding="true"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent">

        <include layout="@layout/common_cell"/>

    </androidx.cardview.widget.CardView>

    <TextView
            android:id="@+id/dateText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginRight="12dp"
            android:layout_marginBottom="4dp"
            android:text="10:00"
            app:layout_constraintBottom_toBottomOf="@+id/card_message_me"
            app:layout_constraintRight_toLeftOf="@+id/card_message_me"/>

</androidx.constraintlayout.widget.ConstraintLayout>
```

### UI 추가 색상 값
<a name="create-ui-color-values"></a>

**XML:**

```
// ./app/src/main/res/values/colors.xml
 
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--    ...-->
    <color name="dark_gray">#4F4F4F</color>
    <color name="blue">#186ED3</color>
    <color name="dark_red">#b30000</color>
    <color name="light_gray">#B7B7B7</color>
    <color name="light_gray_2">#eef1f6</color>
</resources>
```

## 뷰 결합 적용
<a name="chat-android-messages-events-view-binding"></a>

Android [뷰 결합](https://developer.android.com/topic/libraries/view-binding) 기능을 활용하여 XML 레이아웃의 결합 클래스를 참조할 수 있습니다. 이 기능을 사용하려면 `./app/build.gradle`의 `viewBinding` 빌드 옵션을 `true`로 설정합니다.

**Kotlin 스크립트:**

```
 // ./app/build.gradle

android {
//    ...

    buildFeatures {
        viewBinding = true
    }
//    ...
}
```

이제 UI를 Kotlin 코드와 연결할 차례입니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt
package com.chatterbox.myapp
// ...
const val TAG = "Chatterbox-MyApp"


class MainActivity : AppCompatActivity() {
//    ...

    private fun sendMessage(request: SendMessageRequest) {
        try {
            room?.sendMessage(
                request,
                object : SendMessageCallback {
                    override fun onRejected(request: SendMessageRequest, error: ChatError) {
                        runOnUiThread {
                            entries.addFailedRequest(request)
                            scrollToBottom()
                            Log.e(TAG, "Message rejected: ${error.errorMessage}")
                        }
                    }
                }
            )

            entries.addPendingRequest(request)

            binding.messageEditText.text.clear()
            scrollToBottom()
        } catch (error: Exception) {
            Log.e(TAG, error.message ?: "Unknown error occurred")
        }
    }

    private fun scrollToBottom() {
        binding.recyclerView.smoothScrollToPosition(entries.size - 1)
    }

    private fun sendButtonClick(view: View) {
        val content = binding.messageEditText.text.toString()
        if (content.trim().isEmpty()) {
            return
        }

        val request = SendMessageRequest(content)
        sendMessage(request)
    }
}
```

또한 채팅 메시지 컨텍스트 메뉴를 사용하여 호출할 수 있는 메시지를 삭제하고 채팅에서 사용자의 연결을 끊는 메서드를 추가합니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
//    ...

class MainActivity : AppCompatActivity() {
//    ...

    private fun deleteMessage(request: DeleteMessageRequest) {
        room?.deleteMessage(
            request,
            object : DeleteMessageCallback {
                override fun onRejected(request: DeleteMessageRequest, error: ChatError) {
                    runOnUiThread {
                        Log.d(TAG, "Delete message rejected: ${error.errorMessage}")
                    }
                }
            }
        )
    }

    private fun disconnectUser(request: DisconnectUserRequest) {
        room?.disconnectUser(
            request,
            object : DisconnectUserCallback {
                override fun onRejected(request: DisconnectUserRequest, error: ChatError) {
                    runOnUiThread {
                        Log.d(TAG, "Disconnect user rejected: ${error.errorMessage}")
                    }
                }
            }
        )
    }
}
```

## 채팅 메시지 요청 관리
<a name="chat-android-messages-events-requests"></a>

가능한 모든 상태를 통해 채팅 메시지 요청을 관리할 수 있는 방법이 필요합니다.
+ 보류 중(Pending) - 메시지가 채팅룸에 전송되었지만 아직 확인 또는 거부되지 않았습니다.
+ 확인됨(Confirmed) - 우리를 포함한 모든 사용자에게 채팅방에 메시지를 보냈습니다.
+ 거부됨(Rejected) - 채팅룸에서 오류 객체가 포함된 메시지를 거부했습니다.

확인되지 않은 채팅 요청과 채팅 메시지는 [목록](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/mutable-list-of.html)에 보관됩니다. 이 목록에는 `ChatEntries.kt`라는 별도의 클래스가 필요합니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/ChatEntries.kt

package com.chatterbox.myapp

import com.amazonaws.ivs.chat.messaging.entities.ChatMessage
import com.amazonaws.ivs.chat.messaging.requests.SendMessageRequest

sealed class ChatEntry() {
    class Message(val message: ChatMessage) : ChatEntry()
    class PendingRequest(val request: SendMessageRequest) : ChatEntry()
    class FailedRequest(val request: SendMessageRequest) : ChatEntry()
}

class ChatEntries {
    /* This list is kept in sorted order. ChatMessages are sorted by date, while pending and failed requests are kept in their original insertion point. */
    val entries = mutableListOf<ChatEntry>()
    var adapter: ChatListAdapter? = null

    val size get() = entries.size

    /**
     * Insert pending request at the end.
     */
    fun addPendingRequest(request: SendMessageRequest) {
        val insertIndex = entries.size
        entries.add(insertIndex, ChatEntry.PendingRequest(request))
        adapter?.notifyItemInserted(insertIndex)
    }

    /**
     * Insert received message at proper place based on sendTime. This can cause removal of pending requests.
     */
    fun addReceivedMessage(message: ChatMessage) {
        /* Skip if we have already handled that message. */
        val existingIndex = entries.indexOfLast { it is ChatEntry.Message && it.message.id == message.id }
        if (existingIndex != -1) {
            return
        }

        val removeIndex = entries.indexOfLast {
            it is ChatEntry.PendingRequest && it.request.requestId == message.requestId
        }
        if (removeIndex != -1) {
            entries.removeAt(removeIndex)
        }

        val insertIndexRaw = entries.indexOfFirst { it is ChatEntry.Message && it.message.sendTime > message.sendTime }
        val insertIndex = if (insertIndexRaw == -1) entries.size else insertIndexRaw
        entries.add(insertIndex, ChatEntry.Message(message))

        if (removeIndex == -1) {
            adapter?.notifyItemInserted(insertIndex)
        } else if (removeIndex == insertIndex) {
            adapter?.notifyItemChanged(insertIndex)
        } else {
            adapter?.notifyItemRemoved(removeIndex)
            adapter?.notifyItemInserted(insertIndex)
        }
    }

    fun addFailedRequest(request: SendMessageRequest) {
        val removeIndex = entries.indexOfLast {
            it is ChatEntry.PendingRequest && it.request.requestId == request.requestId
        }
        if (removeIndex != -1) {
            entries.removeAt(removeIndex)
            entries.add(removeIndex, ChatEntry.FailedRequest(request))
            adapter?.notifyItemChanged(removeIndex)
        } else {
            val insertIndex = entries.size
            entries.add(insertIndex, ChatEntry.FailedRequest(request))
            adapter?.notifyItemInserted(insertIndex)
        }
    }

    fun removeMessage(messageId: String) {
        val removeIndex = entries.indexOfFirst { it is ChatEntry.Message && it.message.id == messageId }
        entries.removeAt(removeIndex)
        adapter?.notifyItemRemoved(removeIndex)
    }

    fun removeFailedRequest(requestId: String) {
        val removeIndex = entries.indexOfFirst { it is ChatEntry.FailedRequest && it.request.requestId == requestId }
        entries.removeAt(removeIndex)
        adapter?.notifyItemRemoved(removeIndex)
    }

    fun removeAll() {
        entries.clear()
    }
}
```

목록을 UI와 연결하기 위해 [어댑터](https://developer.android.com/reference/android/widget/Adapter)를 사용합니다. 자세한 정보는 [AdapterView를 사용하여 데이터에 결합](https://developer.android.com/develop/ui/views/layout/binding) 및 [생성된 결합 클래스](https://developer.android.com/topic/libraries/data-binding/generated-binding)를 참조하세요.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/ChatListAdapter.kt

package com.chatterbox.myapp

import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.recyclerview.widget.RecyclerView
import com.amazonaws.ivs.chat.messaging.requests.DisconnectUserRequest
import java.text.DateFormat


class ChatListAdapter(
    private val entries: ChatEntries,
    private val onDisconnectUser: (request: DisconnectUserRequest) -> Unit,
) :
    RecyclerView.Adapter<ChatListAdapter.ViewHolder>() {
    var context: Context? = null
    var userId: String? = null

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val container: LinearLayout = view.findViewById(R.id.layout_container)
        val textView: TextView = view.findViewById(R.id.card_message_me_text_view)
        val failedMark: TextView = view.findViewById(R.id.failed_mark)
        val userNameText: TextView? = view.findViewById(R.id.username_edit_text)
        val dateText: TextView? = view.findViewById(R.id.dateText)
    }

    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        if (viewType == 0) {
            val rightView = LayoutInflater.from(viewGroup.context).inflate(R.layout.card_view_right, viewGroup, false)
            return ViewHolder(rightView)
        }
        val leftView = LayoutInflater.from(viewGroup.context).inflate(R.layout.card_view_left, viewGroup, false)
        return ViewHolder(leftView)
    }

    override fun getItemViewType(position: Int): Int {
        // Int 0 indicates to my message while Int 1 to other message
        val chatMessage = entries.entries[position]
        return if (chatMessage is ChatEntry.Message && chatMessage.message.sender.userId != userId) 1 else 0
    }

    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
        return when (val entry = entries.entries[position]) {
            is ChatEntry.Message -> {
                viewHolder.textView.text = entry.message.content

                val bgColor = if (entry.message.sender.userId == userId) {
                    R.color.purple_500
                } else {
                    R.color.light_gray_2
                }
                viewHolder.container.setBackgroundColor(ContextCompat.getColor(context!!, bgColor))

                if (entry.message.sender.userId != userId) {
                    viewHolder.textView.setTextColor(Color.parseColor("#000000"))
                }

                viewHolder.failedMark.isGone = true

                viewHolder.itemView.setOnCreateContextMenuListener { menu, _, _ ->
                    menu.add("Kick out").setOnMenuItemClickListener {
                        val request = DisconnectUserRequest(entry.message.sender.userId, "Some reason")
                        onDisconnectUser(request)
                        true
                    }
                }

                viewHolder.userNameText?.text = entry.message.sender.userId
                viewHolder.dateText?.text =
                    DateFormat.getTimeInstance(DateFormat.SHORT).format(entry.message.sendTime)
            }

            is ChatEntry.PendingRequest -> {
                viewHolder.container.setBackgroundColor(ContextCompat.getColor(context!!, R.color.light_gray))
                viewHolder.textView.text = entry.request.content
                viewHolder.failedMark.isGone = true
                viewHolder.itemView.setOnCreateContextMenuListener(null)
                viewHolder.dateText?.text = "Sending"
            }

            is ChatEntry.FailedRequest -> {
                viewHolder.textView.text = entry.request.content
                viewHolder.container.setBackgroundColor(ContextCompat.getColor(context!!, R.color.dark_red))
                viewHolder.failedMark.isGone = false
                viewHolder.dateText?.text = "Failed"
            }
        }
    }

    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
        super.onAttachedToRecyclerView(recyclerView)
        context = recyclerView.context
    }

    override fun getItemCount() = entries.entries.size
}
```

## 최종 단계
<a name="chat-android-messages-events-final-steps"></a>

이제 `ChatEntries` 클래스를 `MainActivity`에 결합하여 새 어댑터를 연결할 차례입니다.

**Kotlin**:

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

import com.chatterbox.myapp.databinding.ActivityMainBinding
import com.chatterbox.myapp.ChatListAdapter
import com.chatterbox.myapp.ChatEntries

class MainActivity : AppCompatActivity() {
    // ...
    private var entries = ChatEntries()
    private lateinit var adapter: ChatListAdapter
    private lateinit var binding: ActivityMainBinding

    /* see https://developer.android.com/topic/libraries/data-binding/generated-binding#create */
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        /* Create room instance. */
        room = ChatRoom(REGION, ::fetchChatToken).apply {
            listener = roomListener
        }

        binding.sendButton.setOnClickListener(::sendButtonClick)
        binding.connectButton.setOnClickListener { connect() }

        setUpChatView()

        updateConnectionState(ConnectionState.DISCONNECTED)
    }

    private fun setUpChatView() {
        /* Setup Android Jetpack RecyclerView - see https://developer.android.com/develop/ui/views/layout/recyclerview.*/
        adapter = ChatListAdapter(entries, ::disconnectUser)
        entries.adapter = adapter

        val recyclerViewLayoutManager = LinearLayoutManager(this@MainActivity, LinearLayoutManager.VERTICAL, false)
        binding.recyclerView.layoutManager = recyclerViewLayoutManager
        binding.recyclerView.adapter = adapter

        binding.sendButton.setOnClickListener(::sendButtonClick)
        binding.messageEditText.setOnEditorActionListener { _, _, event ->
            val isEnterDown = (event.action == KeyEvent.ACTION_DOWN) && (event.keyCode == KeyEvent.KEYCODE_ENTER)
            if (!isEnterDown) {
                return@setOnEditorActionListener false
            }

            sendButtonClick(binding.sendButton)
            return@setOnEditorActionListener true
        }
    }
}
```

채팅 요청을 계속 추적하는 클래스(`ChatEntries`)가 이미 있으므로 `roomListener`에 `entries` 조작을 위한 코드를 구현할 준비가 되었습니다. 대응 중인 이벤트에 따라 `entries` 및 `connectionState`를 업데이트합니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

class MainActivity : AppCompatActivity() {
    //...
    

    private fun sendMessage(request: SendMessageRequest) {
    //...

    }

    private fun scrollToBottom() {
        binding.recyclerView.smoothScrollToPosition(entries.size - 1)
    }

    private val roomListener = object : ChatRoomListener {
        override fun onConnecting(room: ChatRoom) {
            Log.d(TAG, "[${Thread.currentThread().name}] onConnecting")
            runOnUiThread {
                updateConnectionState(ConnectionState.LOADING)
            }
        }

        override fun onConnected(room: ChatRoom) {
            Log.d(TAG, "[${Thread.currentThread().name}] onConnected")
            runOnUiThread {
                updateConnectionState(ConnectionState.CONNECTED)
            }
        }

        override fun onDisconnected(room: ChatRoom, reason: DisconnectReason) {
            Log.d(TAG, "[${Thread.currentThread().name}] onDisconnected")
            runOnUiThread {
                updateConnectionState(ConnectionState.DISCONNECTED)
                entries.removeAll()
            }
        }

        override fun onMessageReceived(room: ChatRoom, message: ChatMessage) {
            Log.d(TAG, "[${Thread.currentThread().name}] onMessageReceived $message")
            runOnUiThread {
                entries.addReceivedMessage(message)
                scrollToBottom()
            }
        }

        override fun onEventReceived(room: ChatRoom, event: ChatEvent) {
            Log.d(TAG, "[${Thread.currentThread().name}] onEventReceived $event")
        }

        override fun onMessageDeleted(room: ChatRoom, event: DeleteMessageEvent) {
            Log.d(TAG, "[${Thread.currentThread().name}] onMessageDeleted $event")
        }

        override fun onUserDisconnected(room: ChatRoom, event: DisconnectUserEvent) {
            Log.d(TAG, "[${Thread.currentThread().name}] onUserDisconnected $event")
        }
    }
}
```

이제 애플리케이션을 실행할 수 있을 것입니다\$1 ([앱 빌드 및 실행](https://developer.android.com/studio/run#basic-build-run)을 참조하세요.) 앱을 사용할 때는 반드시 백엔드 서버가 실행 중이어야 합니다. 터미널에서 `./gradlew :auth-server:run` 명령어를 프로젝트 루트에서 사용하거나 Android 스튜디오에서 `auth-server:run` Gradle 작업을 직접 실행하여 시작할 수 있습니다.

# IVS 챗 클라이언트 메시징 SDK: Kotlin 코루틴 자습서 1부: 채팅룸
<a name="chat-sdk-kotlin-tutorial-chat-rooms"></a>

본 문서는 두 파트로 구성된 자습서 중 첫 번째 파트에 해당하는 자습서입니다. [Kotlin](https://kotlinlang.org/) 프로그래밍 언어 및 [코루틴](https://kotlinlang.org/docs/coroutines-overview.html)을 사용하여 완전한 기능을 갖춘 Android 앱을 구축하여 Amazon IVS Chat 메시징 SDK로 작업하기 위한 필수 사항을 알아봅니다. 여기에서 지칭하는 앱은 *Chatterbox*라고 합니다.

모듈을 시작하기 전에 몇 분 정도 시간을 내어 사전 조건, 채팅 토큰의 주요 개념, 채팅룸 생성에 필요한 백엔드 서버를 숙지해 두세요.

이 자습서는 IVS 챗 메시징 SDK를 처음 사용하는 숙련된 Android 개발자를 위해 만들어졌습니다. Kotlin 프로그래밍 언어와 Android 플랫폼에서 UI를 만드는 데 익숙해야 합니다.

이 자습서의 첫 번째 부분은 여러 섹션으로 나뉩니다.

1. [로컬 인증/권한 부여 서버 설정](#chat-kotlin-rooms-auth-server)

1. [Chatterbox 프로젝트 생성](#chat-kotlin-rooms-chatterbox)

1. [채팅룸에 연결 및 연결 업데이트 관찰](#chat-kotlin-rooms-connect)

1. [토큰 공급자 구축](#chat-kotlin-rooms-token-provider)

1. [다음 단계](#chat-kotlin-rooms-next-steps)

전체 SDK 설명서를 보려면 우선 [Amazon IVS Chat Client Messaging SDK](chat-sdk.md)(Amazon IVS Chat 사용 설명서**에서 참조) 및 [Chat Client Messaging: SDK for Android Reference](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/latest/)(GitHub)를 참조하세요.

## 사전 조건
<a name="chat-kotlin-rooms-prerequisites"></a>
+ Kotlin과 Android 플랫폼에서 애플리케이션을 만드는 데 익숙해야 합니다. Android용 애플리케이션을 만드는 데 익숙하지 않은 경우 Android 개발자를 위한 [첫 앱 빌드](https://developer.android.com/codelabs/basic-android-kotlin-compose-first-app#0) 가이드에서 기본 사항을 배워 보세요.
+ [Amazon IVS Chat 시작하기](getting-started-chat.md)를 읽고 이해합니다.
+ 기존 IAM 정책에 정의된 `CreateChatToken` 및 `CreateRoom` 기능을 사용하여 AWS IAM 사용자를 생성합니다. ([Amazon IVS Chat 시작하기](getting-started-chat.md)를 참조하세요.)
+ 이 사용자의 비밀/액세스 키가 AWS 보안 인증 파일에 저장되어 있는지 확인합니다. 지침은 [AWS CLI 사용 설명서](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html)(특히 [구성 및 보안 인증 파일 설정](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html))를 참조합니다.
+ 채팅룸을 생성하고 ARN을 저장합니다. [Amazon IVS Chat 시작하기](getting-started-chat.md)을(를) 참조하세요. (ARN을 저장하지 않은 경우 나중에 콘솔이나 Chat API를 사용하여 조회할 수 있습니다.)

## 로컬 인증/권한 부여 서버 설정
<a name="chat-kotlin-rooms-auth-server"></a>

백엔드 서버는 채팅룸을 생성하고 IVS 챗 Android SDK가 채팅룸의 클라이언트를 인증하고 권한을 부여하는 데 필요한 채팅 토큰을 생성하는 일을 맡습니다.

Amazon IVS 채팅 시작하기에서 [채팅 토큰 생성](getting-started-chat-auth.md)을 참조하세요.** 플로우차트에서 볼 수 있듯이 서버 측 코드는 채팅 토큰 생성을 담당합니다 즉, 앱은 서버 측 애플리케이션에서 채팅 토큰을 요청하여 채팅 토큰을 생성하는 자체 수단을 제공해야 합니다.

저희는 [Ktor](https://ktor.io/) 프레임워크를 사용하여 로컬 AWS 환경을 통해 채팅 토큰 생성을 관리하는 라이브 로컬 서버를 생성합니다.

이제 AWS 보안 인증 정보가 올바르게 설정되었을 것입니다. 단계별 지침은 [Set up AWS temporary credentials and AWS Region for development](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/setup-credentials.html)를 참조하세요.

`chatterbox`라는 새 디렉터리를 생성하고 그 안에서 또 다른 디렉토리 `auth-server`를 생성합니다.**

서버 폴더는 다음과 같은 구조를 갖습니다.

```
- auth-server
  - src
    - main
      - kotlin
        - com
          - chatterbox
            - authserver
              - Application.kt
       - resources
         - application.conf
         - logback.xml
   - build.gradle.kts
```

*참고: 여기에서 코드를 참조된 파일에 직접 복사하거나 붙여넣을 수 있습니다.*

다음으로 인증 서버가 작동하는 데 필요한 모든 종속 항목과 플러그인을 추가합니다.

**Kotlin 스크립트:**

```
// ./auth-server/build.gradle.kts

plugins {
   application
   kotlin("jvm")
   kotlin("plugin.serialization").version("1.7.10")
}

application {   
   mainClass.set("io.ktor.server.netty.EngineMain")
}

dependencies {
   implementation("software.amazon.awssdk:ivschat:2.18.1")
   implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.20")

   implementation("io.ktor:ktor-server-core:2.1.3")
   implementation("io.ktor:ktor-server-netty:2.1.3")
   implementation("io.ktor:ktor-server-content-negotiation:2.1.3")
   implementation("io.ktor:ktor-serialization-kotlinx-json:2.1.3")

   implementation("ch.qos.logback:logback-classic:1.4.4")
}
```

이제 인증 서버의 로깅 기능을 설정해야 합니다. (자세한 정보는 [로거 구성](https://ktor.io/docs/logging.html#configure-logger)을 참조하세요.)

**XML:**

```
// ./auth-server/src/main/resources/logback.xml

<configuration>
   <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
      <encoder>
         <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
      </encoder>
   </appender>
   <root level="trace">
      <appender-ref ref="STDOUT"/>
   </root>
   <logger name="org.eclipse.jetty" level="INFO"/>
   <logger name="io.netty" level="INFO"/>
</configuration>
```

[Ktor](https://ktor.io/docs/welcome.html) 서버에는 `resources` 디렉터리의 `application.*` 파일에서 자동으로 로드되는 구성 설정이 필요하므로 구성 설정도 추가합니다. (자세한 정보는 [파일으로 구성](https://ktor.io/docs/configurations.html#configuration-file)을 참조하세요.)

**HOCON**:

```
// ./auth-server/src/main/resources/application.conf

ktor {
   deployment {
      port = 3000
   }
   application {
      modules = [ com.chatterbox.authserver.ApplicationKt.main ]
   }
}
```

마지막으로 서버를 구현해 보겠습니다.

**Kotlin:**

```
// ./auth-server/src/main/kotlin/com/chatterbox/authserver/Application.kt

package com.chatterbox.authserver

import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import software.amazon.awssdk.services.ivschat.IvschatClient
import software.amazon.awssdk.services.ivschat.model.CreateChatTokenRequest

@Serializable
data class ChatTokenParams(var userId: String, var roomIdentifier: String)

@Serializable
data class ChatToken(
   val token: String,
   val sessionExpirationTime: String,
   val tokenExpirationTime: String,
)

fun Application.main() {
   install(ContentNegotiation) {
      json(Json)
   }

   routing {
      post("/create_chat_token") {
         val callParameters = call.receive<ChatTokenParams>()
         val request = CreateChatTokenRequest.builder().roomIdentifier(callParameters.roomIdentifier)
            .userId(callParameters.userId).build()
         val token = IvschatClient.create()
            .createChatToken(request)

         call.respond(
            ChatToken(
                token.token(),
                token.sessionExpirationTime().toString(),
                token.tokenExpirationTime().toString()
            )
         )
      }
   }
}
```

## Chatterbox 프로젝트 생성
<a name="chat-kotlin-rooms-chatterbox"></a>

Android 프로젝트를 생성하려면 [Android 스튜디오](https://developer.android.com/studio)를 설치하고 엽니다.

공식 Android [프로젝트 생성 가이드](https://developer.android.com/studio/projects/create-project)에 나와 있는 단계를 따릅니다.
+ [프로젝트 선택](https://developer.android.com/studio/projects/create-project)에서 Chatterbox 앱을 위한 **빈 활동** 프로젝트 템플릿을 선택합니다.
+ [프로젝트 구성](https://developer.android.com/studio/projects/create-project#configure)에서 다음 구성 필드 값을 선택합니다.
  + **이름**: My App
  + **패키지 이름**: com.chatterbox.myapp
  + **저장 위치**: 이전 단계에서 만든`chatterbox` 디렉터리를 지정합니다.
  + **언어**: Kotlin
  + **최소 API 레벨**: API 21: Android 5.0(Lollipop)

모든 구성 매개 변수를 올바르게 지정한 후 `chatterbox` 폴더 내의 파일 구조는 다음과 같아야 합니다.

```
- app
  - build.gradle
  ...
- gradle
- .gitignore
- build.gradle
- gradle.properties
- gradlew
- gradlew.bat
- local.properties
- settings.gradle
- auth-server
  - src
    - main
      - kotlin
        - com
          - chatterbox
            - authserver
              - Application.kt
       - resources
         - application.conf
         - logback.xml
   - build.gradle.kts
```

이제 작동하는 안드로이드 프로젝트가 있으므로 `build.gradle` 종속 항목에 [com.amazonaws:ivs-chat-messaging](https://mvnrepository.com/artifact/com.amazonaws/ivs-chat-messaging) 및 [org.jetbrains.kotlinx:kotlinx-coroutines-core](https://github.com/Kotlin/kotlinx.coroutines)를 추가할 수 있습니다. ([Gradle](https://gradle.org/) 빌드 툴킷에 대한 자세한 정보는 [빌드 구성](https://developer.android.com/build)을 참조하세요.) 

**참고:** 모든 코드 조각의 맨 위에는 프로젝트에서 변경해야 하는 파일의 경로가 있습니다. 경로는 프로젝트 루트의 상대 경로입니다.

**Kotlin:**

```
// ./app/build.gradle

plugins {
// ...
}

android {
// ...
}

dependencies {
    implementation 'com.amazonaws:ivs-chat-messaging:1.1.0'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'

// ...
}
```

새 종속 항목이 추가된 후 Android 스튜디오에서 **Gradle 파일과 프로젝트 동기화**를 실행하여 프로젝트를 새 종속 항목과 동기화합니다. (자세한 정보는 [빌드 종속 항목 추가](https://developer.android.com/build/dependencies)를 참조하세요.)

이전 섹션에서 생성한 인증 서버를 프로젝트 루트에서 편리하게 실행하기 위해 이 서버를 `settings.gradle`에 새 모듈로 포함시킵니다. (자세한 정보는 [Gradle을 사용하여 소프트웨어 구성 요소 구조화 및 빌드](https://docs.gradle.org/current/userguide/multi_project_builds.html)를 참조하세요.)

**Kotlin 스크립트:**

```
// ./settings.gradle

// ...

rootProject.name = "My App"
include ':app'
include ':auth-server'
```

이제부터 `auth-server`가 Android 프로젝트에 포함되므로 프로젝트 루트에서 다음 명령으로 인증 서버를 실행할 수 있습니다.

**쉘:**

```
./gradlew :auth-server:run         
```

## 채팅룸에 연결 및 연결 업데이트 관찰
<a name="chat-kotlin-rooms-connect"></a>

채팅룸 연결을 열기 위해 활동이 처음 생성될 때 실행되는 [onCreate() 활동 수명 주기 콜백](https://developer.android.com/guide/components/activities/activity-lifecycle)을 사용합니다. [ChatRoom 생성자](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html)를 사용하려면 룸 연결을 인스턴스화하기 위해 `region` 및 `tokenProvider`를 제공해야 합니다.

**참고:** 아래 조각의 `fetchChatToken` 함수는 [다음 섹션](#chat-kotlin-rooms-token-provider)에서 구현됩니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

// AWS region of the room that was created in Getting Started with Amazon IVS Chat
const val REGION = "us-west-2"

class MainActivity : AppCompatActivity() {
    private var room: ChatRoom? = null
    // ...

   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)

      // Create room instance
      room = ChatRoom(REGION, ::fetchChatToken)
   }

// ...
}
```

채팅룸 연결의 변화를 표시하고 대응하는 것은 `chatterbox`와 같은 채팅 앱을 만드는 데 필수적인 부분입니다. 룸과 상호작용을 시작하기 전에 채팅룸 연결 상태 이벤트를 구독하여 업데이트를 받아야 합니다.

코루틴용 챗 SDK에서 [https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html)은 [Flow](https://kotlinlang.org/docs/flow.html)에서 룸 수명 주기 이벤트를 처리할 것으로 예상합니다. 현재 함수는 호출 시 확인 메시지만 로그합니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

const val TAG = "Chatterbox-MyApp"

class MainActivity : AppCompatActivity() {
// ...

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...

        // Create room instance
        room = ChatRoom(REGION, ::fetchChatToken).apply {
            lifecycleScope.launch {
                stateChanges().collect { state ->
                    Log.d(TAG, "state change to $state")
                }
            }

            lifecycleScope.launch {
                receivedMessages().collect { message ->
                    Log.d(TAG, "messageReceived $message")
                }
            }

            lifecycleScope.launch {
                receivedEvents().collect { event ->
                    Log.d(TAG, "eventReceived $event")
                }
            }

            lifecycleScope.launch {
                deletedMessages().collect { event ->
                    Log.d(TAG, "messageDeleted $event")
                }
            }

            lifecycleScope.launch {
                disconnectedUsers().collect { event ->
                    Log.d(TAG, "userDisconnected $event")
                }
            }
        }
    }
}
```

이 다음으로 룸 연결 상태를 읽을 수 있는 기능을 제공해야 합니다. `MainActivity.kt` [속성](https://kotlinlang.org/docs/properties.html)에 이를 보관하고 룸의 기본 DISCONNECTED 상태로 초기화합니다([IVS 챗 Android SDK 참조](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/latest/)의 `ChatRoom` `state` 참조). 로컬 상태를 최신 상태로 유지하려면 state-updater 함수를 구현해야 합니다. 이 함수를 `updateConnectionState`라고 해 보겠습니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

class MainActivity : AppCompatActivity() {
   private var connectionState = ChatRoom.State.DISCONNECTED

// ...

   private fun updateConnectionState(state: ChatRoom.State) {
      connectionState = state

     when (state) {
          ChatRoom.State.CONNECTED -> {
              Log.d(TAG, "room connected")
          }
          ChatRoom.State.DISCONNECTED -> {
              Log.d(TAG, "room disconnected")
          }
          ChatRoom.State.CONNECTING -> {
              Log.d(TAG, "room connecting")
          }
      }
}
```

다음으로 state-updater 함수를 [ChatRoom.Listener](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/listener.html) 속성과 통합합니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

class MainActivity : AppCompatActivity() {
// ...

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...

        // Create room instance
        room = ChatRoom(REGION, ::fetchChatToken).apply {
            lifecycleScope.launch {
                stateChanges().collect { state ->
                    Log.d(TAG, "state change to $state")
                    updateConnectionState(state)

                }
            }

      // ...

      }
   }
}
```

이제 [ChatRoom](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html) 상태 업데이트를 저장하고, 듣고, 반응할 수 있게 되었으므로 연결을 초기화할 차례입니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

class MainActivity : AppCompatActivity() {
// ...

   private fun connect() {
      try {
         room?.connect()
      } catch (ex: Exception) {
         Log.e(TAG, "Error while calling connect()", ex)
      }
   }

   // ...
}
```

## 토큰 공급자 구축
<a name="chat-kotlin-rooms-token-provider"></a>

이제 애플리케이션에서 채팅 토큰을 생성하고 관리하는 함수를 만들 차례입니다. 이 예에서는 [Android용 Retrofit HTTP 클라이언트](https://square.github.io/retrofit/)를 사용합니다.

네트워크 트래픽을 보내려면 먼저 Android용 네트워크 보안 구성을 설정해야 합니다. (자세한 정보는 [네트워크 보안 구성](https://developer.android.com/privacy-and-security/security-config)을 참조하세요.) [앱 매니페스트](https://developer.android.com/guide/topics/manifest/manifest-intro) 파일에 네트워크 권한을 추가하는 것부터 시작합니다. 새로운 네트워크 보안 구성을 가리키는 추가된 `user-permission` 태그와 `networkSecurityConfig` 속성에 유의하세요. 아래 코드에서 `<version>`을 챗 Android SDK의 현재 버전 번호(예: 1.1.0)로 대체하세요.****

**XML:**

```
// ./app/src/main/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.chatterbox.myapp">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:allowBackup="true"
        android:fullBackupContent="@xml/backup_rules"
        android:label="@string/app_name"
        android:networkSecurityConfig="@xml/network_security_config"
// ...

// ./app/build.gradle


dependencies {
   implementation("com.amazonaws:ivs-chat-messaging:<version>")
// ...

   implementation("com.squareup.retrofit2:retrofit:2.9.0")
   implementation("com.squareup.retrofit2:converter-gson:2.9.0")
}
```

로컬 IP 주소(예:`10.0.2.2` 및 `localhost` 도메인)를 신뢰할 수 있는 것으로 선언하여 백엔드와 메시지 교환을 시작합니다.

**XML:**

```
// ./app/src/main/res/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">10.0.2.2</domain>
        <domain includeSubdomains="true">localhost</domain>
    </domain-config>
</network-security-config>
```

다음으로 HTTP 응답 구문 분석을 위한 [Gson 변환기 추가](https://github.com/square/retrofit/tree/trunk/retrofit-converters/gson)와 함께 새로운 종속 항목을 추가해야 합니다. 아래 코드에서 `<version>`을 챗 Android SDK의 현재 버전 번호(예: 1.1.0)로 대체하세요.****

**Kotlin 스크립트:**

```
// ./app/build.gradle

dependencies {
   implementation("com.amazonaws:ivs-chat-messaging:<version>")
// ...

   implementation("com.squareup.retrofit2:retrofit:2.9.0")
   implementation("com.squareup.retrofit2:converter-gson:2.9.0")
}
```

채팅 토큰을 검색하려면 `chatterbox` 앱에서 POST HTTP 요청을 해야 합니다. Retrofit이 구현할 수 있도록 요청을 인터페이스로 정의합니다. ([Retrofit 설명서](https://square.github.io/retrofit/)를 참조하세요. 또한 [CreateChatToken](https://docs.aws.amazon.com/ivs/latest/ChatAPIReference/API_CreateChatToken.html#API_CreateChatToken_RequestBody) 작업 사양도 숙지하세요.)

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/network/ApiService.kt

package com.chatterbox.myapp.network

import com.amazonaws.ivs.chat.messaging.ChatToken
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST

data class CreateTokenParams(var userId: String, var roomIdentifier: String)

interface ApiService {
   @POST("create_chat_token")
   fun createChatToken(@Body params: CreateTokenParams): Call<ChatToken>
}


// ./app/src/main/java/com/chatterbox/myapp/network/RetrofitFactory.kt

package com.chatterbox.myapp.network

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitFactory {
   private const val BASE_URL = "http://10.0.2.2:3000"

   fun makeRetrofitService(): ApiService {
       return Retrofit.Builder()
           .baseUrl(BASE_URL)
           .addConverterFactory(GsonConverterFactory.create())
           .build().create(ApiService::class.java)
   }
}
```

이제 네트워킹을 설정했으므로 채팅 토큰을 생성하고 관리하는 함수를 추가할 차례입니다. 프로젝트가 [생성](#chat-kotlin-rooms-chatterbox)되었을 때 자동으로 생성된 `MainActivity.kt`에 함수를 추가합니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import com.amazonaws.ivs.chat.messaging.*
import com.amazonaws.ivs.chat.messaging.coroutines.*
import com.chatterbox.myapp.network.CreateTokenParams
import com.chatterbox.myapp.network.RetrofitFactory
import retrofit2.Call
import java.io.IOException
import retrofit2.Callback
import retrofit2.Response

// custom tag for logging purposes
const val TAG = "Chatterbox-MyApp"

// any ID to be associated with auth token
const val USER_ID = "test user id"
// ID of the room the app wants to access. Must be an ARN. See Amazon Resource Names(ARNs)
const val ROOM_ID = "arn:aws:..."
// AWS region of the room that was created in Getting Started with Amazon IVS Chat
const val REGION = "us-west-2"

class MainActivity : AppCompatActivity() {

   private val service = RetrofitFactory.makeRetrofitService()
   private var userId: String = USER_ID

// ...

   private fun fetchChatToken(callback: ChatTokenCallback) {
      val params = CreateTokenParams(userId, ROOM_ID)
      service.createChatToken(params).enqueue(object : Callback<ChatToken> {
         override fun onResponse(call: Call<ChatToken>, response: Response<ChatToken>) {
            val token = response.body()
            if (token == null) {
               Log.e(TAG, "Received empty token response")
               callback.onFailure(IOException("Empty token response"))
               return
            }

            Log.d(TAG, "Received token response $token")
            callback.onSuccess(token)
         }

         override fun onFailure(call: Call<ChatToken>, throwable: Throwable) {
            Log.e(TAG, "Failed to fetch token", throwable)
            callback.onFailure(throwable)
         }
      })
   }
}
```

## 다음 단계
<a name="chat-kotlin-rooms-next-steps"></a>

이제 채팅룸 연결을 설정했으므로 이 Kotlin 코루틴 자습서의 2부인 [메시지 및 이벤트](chat-sdk-kotlin-tutorial-messages-events.md)로 이동하세요.

# IVS 챗 클라이언트 메시징 SDK: Kotlin 코루틴 자습서 2부: 메시지 및 이벤트
<a name="chat-sdk-kotlin-tutorial-messages-events"></a>

이 자습서의 두 번째(마지막) 부분은 여러 섹션으로 나뉩니다.

1. [메시지 전송을 위한 UI 만들기](#chat-kotlin-messages-events-ui)

   1. [UI 기본 레이아웃](#chat-kotlin-messages-events-ui-main)

   1. [텍스트를 일관되게 표시하기 위한 UI 추상화 텍스트 셀](#chat-kotlin-messages-events-consistent-text)

   1. [UI 왼쪽 채팅 메시지](#chat-kotlin-messages-events-ui-left)

   1. [UI 오른쪽 메시지](#chat-kotlin-messages-events-ui-right)

   1. [UI 추가 색상 값](#chat-kotlin-messages-events-additional-color)

1. [뷰 결합 적용](#chat-kotlin-messages-events-apply-view-binding)

1. [채팅 메시지 요청 관리](#chat-kotlin-messages-events-chat-message)

1. [최종 단계](#chat-kotlin-messages-events-final-steps)

전체 SDK 설명서를 보려면 우선 [Amazon IVS Chat Client Messaging SDK](chat-sdk.md)(Amazon IVS Chat 사용 설명서**에서 참조) 및 [Chat Client Messaging: SDK for Android Reference](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/latest/)(GitHub)를 참조하세요.

## 사전 조건
<a name="chat-kotlin-messages-events-prerequisite"></a>

이 자습서의 1부인 [채팅룸](chat-sdk-kotlin-tutorial-chat-rooms.md)을 완료해야 합니다.

## 메시지 전송을 위한 UI 만들기
<a name="chat-kotlin-messages-events-ui"></a>

채팅룸 연결을 성공적으로 초기화했으므로 이제 첫 번째 메시지를 보낼 차례입니다. 이 기능에는 UI가 필요합니다. 다음을 추가합니다.
+ `connect`/`disconnect` 버튼
+ `send` 버튼으로 메시지 입력
+ 동적 메시지 목록. 이를 빌드하기 위해 Android Jetpack [RecyclerView](https://developer.android.com/develop/ui/views/layout/recyclerview)를 사용합니다.

### UI 기본 레이아웃
<a name="chat-kotlin-messages-events-ui-main"></a>

Android 개발자 문서에서 Android 젯팩 [레이아웃](https://developer.android.com/develop/ui/views/layout/declaring-layout)을 참조하세요.

**XML:**

```
// ./app/src/main/res/layout/activity_main.xml


<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                     xmlns:app="http://schemas.android.com/apk/res-auto"
                                                     xmlns:tools="http://schemas.android.com/tools"
                                                     android:layout_width="match_parent"
                                                     android:layout_height="match_parent">

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:app="http://schemas.android.com/apk/res-auto"
                  android:id="@+id/connect_view"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:gravity="center"
                  android:orientation="vertical">

        <androidx.cardview.widget.CardView
                android:id="@+id/connect_button"
                android:layout_width="match_parent"
                android:layout_height="48dp"
                android:layout_gravity=""
                android:layout_marginStart="16dp"
                android:layout_marginTop="4dp"
                android:layout_marginEnd="16dp"
                android:clickable="true"
                android:elevation="16dp"
                android:focusable="true"
                android:foreground="?android:attr/selectableItemBackground"
                app:cardBackgroundColor="@color/purple_500"
                app:cardCornerRadius="10dp">

            <TextView
                    android:id="@+id/connect_text"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentEnd="true"
                    android:layout_gravity="center"
                    android:layout_weight="1"
                    android:paddingHorizontal="12dp"
                    android:text="Connect"
                    android:textColor="@color/white"
                    android:textSize="16sp"/>

            <ProgressBar
                    android:id="@+id/activity_indicator"
                    android:layout_width="20dp"
                    android:layout_height="20dp"
                    android:layout_gravity="center"
                    android:layout_marginHorizontal="20dp"
                    android:indeterminateOnly="true"
                    android:indeterminateTint="@color/white"
                    android:indeterminateTintMode="src_atop"
                    android:keepScreenOn="true"
                    android:visibility="gone"/>
        </androidx.cardview.widget.CardView>

    </LinearLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/chat_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipToPadding="false"
            android:visibility="visible"
            tools:context=".MainActivity">

        <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                app:layout_constraintBottom_toTopOf="@+id/layout_message_input"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent">

            <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/recycler_view"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:clipToPadding="false"
                    android:paddingTop="70dp"
                    android:paddingBottom="20dp"/>
        </RelativeLayout>

        <RelativeLayout
                android:id="@+id/layout_message_input"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@android:color/white"
                android:clipToPadding="false"
                android:drawableTop="@android:color/black"
                android:elevation="18dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent">

            <EditText
                    android:id="@+id/message_edit_text"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginStart="16dp"
                    android:layout_toStartOf="@+id/send_button"
                    android:background="@android:color/transparent"
                    android:hint="Enter Message"
                    android:inputType="text"
                    android:maxLines="6"
                    tools:ignore="Autofill"/>

            <Button
                    android:id="@+id/send_button"
                    android:layout_width="84dp"
                    android:layout_height="48dp"
                    android:layout_alignParentEnd="true"
                    android:background="@color/black"
                    android:foreground="?android:attr/selectableItemBackground"
                    android:text="Send"
                    android:textColor="@color/white"
                    android:textSize="12dp"/>
        </RelativeLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>


</androidx.coordinatorlayout.widget.CoordinatorLayout>
```

### 텍스트를 일관되게 표시하기 위한 UI 추상화 텍스트 셀
<a name="chat-kotlin-messages-events-consistent-text"></a>

**XML:**

```
// ./app/src/main/res/layout/common_cell.xml
   
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:id="@+id/layout_container"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:background="@color/light_gray"
              android:minWidth="100dp"
              android:orientation="vertical">

    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">

        <TextView
                android:id="@+id/card_message_me_text_view"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_marginBottom="8dp"
                android:maxWidth="260dp"
                android:paddingLeft="12dp"
                android:paddingTop="8dp"
                android:paddingRight="12dp"
                android:text="This is a Message"
                android:textColor="#ffffff"
                android:textSize="16sp"/>

        <TextView
                android:id="@+id/failed_mark"
                android:layout_width="40dp"
                android:layout_height="match_parent"
                android:paddingRight="5dp"
                android:src="@drawable/ic_launcher_background"
                android:text="!"
                android:textAlignment="viewEnd"
                android:textColor="@color/white"
                android:textSize="25dp"
                android:visibility="gone"/>
    </LinearLayout>

</LinearLayout>
```

### UI 왼쪽 채팅 메시지
<a name="chat-kotlin-messages-events-ui-left"></a>

**XML:**

```
// ./app/src/main/res/layout/card_view_left.xml
 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:layout_marginStart="8dp"
              android:layout_marginBottom="12dp"
              android:orientation="vertical">

    <TextView
            android:id="@+id/username_edit_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="UserName"/>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        <androidx.cardview.widget.CardView
                android:id="@+id/card_message_other"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="left"
                android:layout_marginBottom="4dp"
                android:foreground="?android:attr/selectableItemBackground"
                app:cardBackgroundColor="@color/light_gray_2"
                app:cardCornerRadius="10dp"
                app:cardElevation="0dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent">

            <include layout="@layout/common_cell"/>
        </androidx.cardview.widget.CardView>

        <TextView
                android:id="@+id/dateText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="4dp"
                android:layout_marginBottom="4dp"
                android:text="10:00"
                app:layout_constraintBottom_toBottomOf="@+id/card_message_other"
                app:layout_constraintLeft_toRightOf="@+id/card_message_other"/>
    </androidx.constraintlayout.widget.ConstraintLayout>


</LinearLayout>
```

### UI 오른쪽 메시지
<a name="chat-kotlin-messages-events-ui-right"></a>

**XML:**

```
// ./app/src/main/res/layout/card_view_right.xml
 
<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"                                                   xmlns:app="http://schemas.android.com/apk/res-auto"                                                   android:layout_width="match_parent"                                                   android:layout_height="wrap_content" 
android:layout_marginEnd="8dp">

    <androidx.cardview.widget.CardView
            android:id="@+id/card_message_me"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="right"
            android:layout_marginBottom="10dp"
            android:foreground="?android:attr/selectableItemBackground"
            app:cardBackgroundColor="@color/purple_500"
            app:cardCornerRadius="10dp"
            app:cardElevation="0dp"
            app:cardPreventCornerOverlap="false"
            app:cardUseCompatPadding="true"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent">

        <include layout="@layout/common_cell"/>

    </androidx.cardview.widget.CardView>

    <TextView
            android:id="@+id/dateText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginRight="12dp"
            android:layout_marginBottom="4dp"
            android:text="10:00"
            app:layout_constraintBottom_toBottomOf="@+id/card_message_me"
            app:layout_constraintRight_toLeftOf="@+id/card_message_me"/>

</androidx.constraintlayout.widget.ConstraintLayout>
```

### UI 추가 색상 값
<a name="chat-kotlin-messages-events-additional-color"></a>

**XML:**

```
// ./app/src/main/res/values/colors.xml
 
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--    ...-->
    <color name="dark_gray">#4F4F4F</color>
    <color name="blue">#186ED3</color>
    <color name="dark_red">#b30000</color>
    <color name="light_gray">#B7B7B7</color>
    <color name="light_gray_2">#eef1f6</color>
</resources>
```

## 뷰 결합 적용
<a name="chat-kotlin-messages-events-apply-view-binding"></a>

Android [뷰 결합](https://developer.android.com/topic/libraries/view-binding) 기능을 활용하여 XML 레이아웃의 결합 클래스를 참조할 수 있습니다. 이 기능을 사용하려면 `./app/build.gradle`의 `viewBinding` 빌드 옵션을 `true`로 설정합니다.

**Kotlin 스크립트:**

```
 // ./app/build.gradle

android {
//    ...

    buildFeatures {
        viewBinding = true
    }
//    ...
}
```

이제 UI를 Kotlin 코드와 연결할 차례입니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

class MainActivity : AppCompatActivity() {
    // ...
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Create room instance
        room = ChatRoom(REGION, ::fetchChatToken).apply {
            // ...
        }

        binding.sendButton.setOnClickListener(::sendButtonClick)
        binding.connectButton.setOnClickListener {connect()}

        setUpChatView()

        updateConnectionState(ChatRoom.State.DISCONNECTED)
    }

    private fun sendMessage(request: SendMessageRequest) {
        lifecycleScope.launch {
           try {
               binding.messageEditText.text.clear()
               room?.awaitSendMessage(request)
           } catch (exception: ChatException) {
               Log.e(TAG, "Message rejected: ${exception.message}")
           } catch (exception: Exception) {
               Log.e(TAG, exception.message ?: "Unknown error occurred")
           }
        }
    }

    private fun sendButtonClick(view: View) {
        val content = binding.messageEditText.text.toString()
        if (content.trim().isEmpty()) {
            return
        }

        val request = SendMessageRequest(content)
        sendMessage(request)
    }
// ...

}
```

또한 채팅 메시지 컨텍스트 메뉴를 사용하여 호출할 수 있는 메시지를 삭제하고 채팅에서 사용자의 연결을 끊는 메서드를 추가합니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
//    ...

class MainActivity : AppCompatActivity() {
//    ...

    private fun deleteMessage(request: DeleteMessageRequest) {
        lifecycleScope.launch {
           try {
               room?.awaitDeleteMessage(request)
           } catch (exception: ChatException) {
               Log.e(TAG, "Delete message rejected: ${exception.message}")
           } catch (exception: Exception) {
               Log.e(TAG, exception.message ?: "Unknown error occurred")
           }
        }
    }

    private fun disconnectUser(request: DisconnectUserRequest) {
        lifecycleScope.launch {
           try {
               room?.awaitDisconnectUser(request)
           } catch (exception: ChatException) {
               Log.e(TAG, "Disconnect user rejected: ${exception.message}")
           } catch (exception: Exception) {
               Log.e(TAG, exception.message ?: "Unknown error occurred")
           }
        }
    }
}
```

## 채팅 메시지 요청 관리
<a name="chat-kotlin-messages-events-chat-message"></a>

가능한 모든 상태를 통해 채팅 메시지 요청을 관리할 수 있는 방법이 필요합니다.
+ 보류 중(Pending) - 메시지가 채팅룸에 전송되었지만 아직 확인 또는 거부되지 않았습니다.
+ 확인됨(Confirmed) - 우리를 포함한 모든 사용자에게 채팅방에 메시지를 보냈습니다.
+ 거부됨(Rejected) - 채팅룸에서 오류 객체가 포함된 메시지를 거부했습니다.

확인되지 않은 채팅 요청과 채팅 메시지는 [목록](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/mutable-list-of.html)에 보관됩니다. 이 목록에는 `ChatEntries.kt`라는 별도의 클래스가 필요합니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/ChatEntries.kt

package com.chatterbox.myapp

import com.amazonaws.ivs.chat.messaging.entities.ChatMessage
import com.amazonaws.ivs.chat.messaging.requests.SendMessageRequest

sealed class ChatEntry() {
    class Message(val message: ChatMessage) : ChatEntry()
    class PendingRequest(val request: SendMessageRequest) : ChatEntry()
    class FailedRequest(val request: SendMessageRequest) : ChatEntry()
}

class ChatEntries {
    /* This list is kept in sorted order. ChatMessages are sorted by date, while pending and failed requests are kept in their original insertion point. */
    val entries = mutableListOf<ChatEntry>()
    var adapter: ChatListAdapter? = null

    val size get() = entries.size

    /**
     * Insert pending request at the end.
     */
    fun addPendingRequest(request: SendMessageRequest) {
        val insertIndex = entries.size
        entries.add(insertIndex, ChatEntry.PendingRequest(request))
        adapter?.notifyItemInserted(insertIndex)
    }

    /**
     * Insert received message at proper place based on sendTime. This can cause removal of pending requests.
     */
    fun addReceivedMessage(message: ChatMessage) {
        /* Skip if we have already handled that message. */
        val existingIndex = entries.indexOfLast { it is ChatEntry.Message && it.message.id == message.id }
        if (existingIndex != -1) {
            return
        }

        val removeIndex = entries.indexOfLast {
            it is ChatEntry.PendingRequest && it.request.requestId == message.requestId
        }
        if (removeIndex != -1) {
            entries.removeAt(removeIndex)
        }

        val insertIndexRaw = entries.indexOfFirst { it is ChatEntry.Message && it.message.sendTime > message.sendTime }
        val insertIndex = if (insertIndexRaw == -1) entries.size else insertIndexRaw
        entries.add(insertIndex, ChatEntry.Message(message))

        if (removeIndex == -1) {
            adapter?.notifyItemInserted(insertIndex)
        } else if (removeIndex == insertIndex) {
            adapter?.notifyItemChanged(insertIndex)
        } else {
            adapter?.notifyItemRemoved(removeIndex)
            adapter?.notifyItemInserted(insertIndex)
        }
    }

    fun addFailedRequest(request: SendMessageRequest) {
        val removeIndex = entries.indexOfLast {
            it is ChatEntry.PendingRequest && it.request.requestId == request.requestId
        }
        if (removeIndex != -1) {
            entries.removeAt(removeIndex)
            entries.add(removeIndex, ChatEntry.FailedRequest(request))
            adapter?.notifyItemChanged(removeIndex)
        } else {
            val insertIndex = entries.size
            entries.add(insertIndex, ChatEntry.FailedRequest(request))
            adapter?.notifyItemInserted(insertIndex)
        }
    }

    fun removeMessage(messageId: String) {
        val removeIndex = entries.indexOfFirst { it is ChatEntry.Message && it.message.id == messageId }
        entries.removeAt(removeIndex)
        adapter?.notifyItemRemoved(removeIndex)
    }

    fun removeFailedRequest(requestId: String) {
        val removeIndex = entries.indexOfFirst { it is ChatEntry.FailedRequest && it.request.requestId == requestId }
        entries.removeAt(removeIndex)
        adapter?.notifyItemRemoved(removeIndex)
    }

    fun removeAll() {
        entries.clear()
    }
}
```

목록을 UI와 연결하기 위해 [어댑터](https://developer.android.com/reference/android/widget/Adapter)를 사용합니다. 자세한 정보는 [AdapterView를 사용하여 데이터에 결합](https://developer.android.com/develop/ui/views/layout/binding) 및 [생성된 결합 클래스](https://developer.android.com/topic/libraries/data-binding/generated-binding)를 참조하세요.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/ChatListAdapter.kt

package com.chatterbox.myapp

import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.recyclerview.widget.RecyclerView
import com.amazonaws.ivs.chat.messaging.requests.DisconnectUserRequest
import java.text.DateFormat


class ChatListAdapter(
    private val entries: ChatEntries,
    private val onDisconnectUser: (request: DisconnectUserRequest) -> Unit,
) :
    RecyclerView.Adapter<ChatListAdapter.ViewHolder>() {
    var context: Context? = null
    var userId: String? = null

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val container: LinearLayout = view.findViewById(R.id.layout_container)
        val textView: TextView = view.findViewById(R.id.card_message_me_text_view)
        val failedMark: TextView = view.findViewById(R.id.failed_mark)
        val userNameText: TextView? = view.findViewById(R.id.username_edit_text)
        val dateText: TextView? = view.findViewById(R.id.dateText)
    }

    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        if (viewType == 0) {
            val rightView = LayoutInflater.from(viewGroup.context).inflate(R.layout.card_view_right, viewGroup, false)
            return ViewHolder(rightView)
        }
        val leftView = LayoutInflater.from(viewGroup.context).inflate(R.layout.card_view_left, viewGroup, false)
        return ViewHolder(leftView)
    }

    override fun getItemViewType(position: Int): Int {
        // Int 0 indicates to my message while Int 1 to other message
        val chatMessage = entries.entries[position]
        return if (chatMessage is ChatEntry.Message && chatMessage.message.sender.userId != userId) 1 else 0
    }

    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
        return when (val entry = entries.entries[position]) {
            is ChatEntry.Message -> {
                viewHolder.textView.text = entry.message.content

                val bgColor = if (entry.message.sender.userId == userId) {
                    R.color.purple_500
                } else {
                    R.color.light_gray_2
                }
                viewHolder.container.setBackgroundColor(ContextCompat.getColor(context!!, bgColor))

                if (entry.message.sender.userId != userId) {
                    viewHolder.textView.setTextColor(Color.parseColor("#000000"))
                }

                viewHolder.failedMark.isGone = true

                viewHolder.itemView.setOnCreateContextMenuListener { menu, _, _ ->
                    menu.add("Kick out").setOnMenuItemClickListener {
                        val request = DisconnectUserRequest(entry.message.sender.userId, "Some reason")
                        onDisconnectUser(request)
                        true
                    }
                }

                viewHolder.userNameText?.text = entry.message.sender.userId
                viewHolder.dateText?.text =
                    DateFormat.getTimeInstance(DateFormat.SHORT).format(entry.message.sendTime)
            }

            is ChatEntry.PendingRequest -> {
                viewHolder.container.setBackgroundColor(ContextCompat.getColor(context!!, R.color.light_gray))
                viewHolder.textView.text = entry.request.content
                viewHolder.failedMark.isGone = true
                viewHolder.itemView.setOnCreateContextMenuListener(null)
                viewHolder.dateText?.text = "Sending"
            }

            is ChatEntry.FailedRequest -> {
                viewHolder.textView.text = entry.request.content
                viewHolder.container.setBackgroundColor(ContextCompat.getColor(context!!, R.color.dark_red))
                viewHolder.failedMark.isGone = false
                viewHolder.dateText?.text = "Failed"
            }
        }
    }

    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
        super.onAttachedToRecyclerView(recyclerView)
        context = recyclerView.context
    }

    override fun getItemCount() = entries.entries.size
}
```

## 최종 단계
<a name="chat-kotlin-messages-events-final-steps"></a>

이제 `ChatEntries` 클래스를 `MainActivity`에 결합하여 새 어댑터를 연결할 차례입니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

import com.chatterbox.myapp.databinding.ActivityMainBinding
import com.chatterbox.myapp.ChatListAdapter
import com.chatterbox.myapp.ChatEntries

class MainActivity : AppCompatActivity() {
    // ...
    private var entries = ChatEntries()
    private lateinit var adapter: ChatListAdapter

    // ...

    private fun setUpChatView() {
        adapter = ChatListAdapter(entries, ::disconnectUser)
        entries.adapter = adapter

        val recyclerViewLayoutManager = LinearLayoutManager(this@MainActivity, LinearLayoutManager.VERTICAL, false)
        binding.recyclerView.layoutManager = recyclerViewLayoutManager
        binding.recyclerView.adapter = adapter

        binding.sendButton.setOnClickListener(::sendButtonClick)
        binding.messageEditText.setOnEditorActionListener { _, _, event ->
            val isEnterDown = (event.action == KeyEvent.ACTION_DOWN) && (event.keyCode == KeyEvent.KEYCODE_ENTER)
            if (!isEnterDown) {
                return@setOnEditorActionListener false
            }

            sendButtonClick(binding.sendButton)
            return@setOnEditorActionListener true
        }
    }
}
```

채팅 요청을 계속 추적하는 클래스(`ChatEntries`)가 이미 있으므로 roomListener에 `entries` 조작을 위한 코드를 구현할 준비가 되었습니다. 대응 중인 이벤트에 따라 `entries` 및 `connectionState`를 업데이트합니다.

**Kotlin:**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

class MainActivity : AppCompatActivity() {
// ...


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Create room instance
        room = ChatRoom(REGION, ::fetchChatToken).apply {
            lifecycleScope.launch {
                stateChanges().collect { state ->
                    Log.d(TAG, "state change to $state")
                    updateConnectionState(state)
                    if (state == ChatRoom.State.DISCONNECTED) {
                       entries.removeAll()
                    }
                }
            }

            lifecycleScope.launch {
                receivedMessages().collect { message ->
                    Log.d(TAG, "messageReceived $message")
                    entries.addReceivedMessage(message)
                }
            }

            lifecycleScope.launch {
                receivedEvents().collect { event ->
                    Log.d(TAG, "eventReceived $event")
                }
            }

            lifecycleScope.launch {
                deletedMessages().collect { event ->
                    Log.d(TAG, "messageDeleted $event")
                    entries.removeMessage(event.messageId)
                }
            }

            lifecycleScope.launch {
                disconnectedUsers().collect { event ->
                    Log.d(TAG, "userDisconnected $event")
                }
            }
        }

        binding.sendButton.setOnClickListener(::sendButtonClick)
        binding.connectButton.setOnClickListener {connect()}

        setUpChatView()

        updateConnectionState(ChatRoom.State.DISCONNECTED)
    }

// ...

}
```

이제 애플리케이션을 실행할 수 있을 것입니다\$1 ([앱 빌드 및 실행](https://developer.android.com/studio/run#basic-build-run)을 참조하세요.) 앱을 사용할 때는 반드시 백엔드 서버가 실행 중이어야 합니다. 터미널에서 `./gradlew :auth-server:run` 명령어를 프로젝트 루트에서 사용하거나 Android 스튜디오에서 `auth-server:run` Gradle 작업을 직접 실행하여 시작할 수 있습니다.

# IVS Chat Client SDK: iOS 설명서
<a name="chat-sdk-ios"></a>

Amazon Interactive Video(IVS) Chat Client Messaging iOS SDK는 Apple의 [Swift 프로그래밍 언어](https://developer.apple.com/swift/)를 사용하는 플랫폼에 [IVS Chat Messaging API](https://docs.aws.amazon.com//ivs/latest/chatmsgapireference/welcome.html)를 통합할 수 있는 인터페이스를 제공합니다.

**IVS Chat Client Messaging iOS SDK의 최신 버전:** 1.0.1([릴리스 정보](https://docs.aws.amazon.com//ivs/latest/ChatUserGuide/release-notes.html#aug08-25))

**참조 문서 및 자습서:** Amazon IVS Chat Client Messaging iOS SDK에서 사용할 수 있는 가장 중요한 메서드에 대한 자세한 내용은 [https://aws.github.io/amazon-ivs-chat-messaging-sdk-ios/1.0.1/](https://aws.github.io/amazon-ivs-chat-messaging-sdk-ios/1.0.1/)의 참조 문서를 확인하세요. 이 리포지토리에는 다양한 기사와 자습서도 포함되어 있습니다.

**샘플 코드:** GitHub의 [https://github.com/aws-samples/amazon-ivs-chat-for-ios-demo](https://github.com/aws-samples/amazon-ivs-chat-for-ios-demo)에서 iOS 샘플 리포지토리를 참조하세요.

**플랫폼 요구 사항:** 개발 환경에는 iOS 13.0 이상이 필요합니다.

# IVS Chat Client Messaging iOS SDK 시작하기
<a name="chat-ios-getting-started"></a>

[Swift Package Manager](#chat-ios-install-sdk-swiftpm)를 사용하여 SDK를 통합하는 것을 권장합니다. 대안으로 [수동으로 프레임워크를 통합](#chat-ios-install-sdk-manual)할 수 있습니다.

SDK를 통합한 후 관련 Swift 파일 상단에 다음 코드를 추가하여 SDK를 가져올 수 있습니다.

```
import AmazonIVSChatMessaging
```

## Swift Package Manager
<a name="chat-ios-install-sdk-swiftpm"></a>

Swift Package Manager 프로젝트에서 `AmazonIVSChatMessaging` 라이브러리를 사용하려면 이를 패키지의 종속성과 관련 대상의 종속성에 추가합니다.

1. [https://ivschat.live-video.net/1.0.1/AmazonIVSChatMessaging.xcframework.zip](https://ivschat.live-video.net/1.0.1/AmazonIVSChatMessaging.xcframework.zip)에서 최신 `.xcframework`를 다운로드합니다.

1. 터미널에서 다음을 실행합니다.

   ```
   shasum -a 256 path/to/downloaded/AmazonIVSChatMessaging.xcframework.zip
   ```

1. 프로젝트의 `Package.swift` 파일에 표시된 것과 같이 이전 단계의 출력을 가져와서 `.binaryTarget`의 체크섬(checksum) 속성에 붙여넣습니다.

   ```
   let package = Package(
      // name, platforms, products, etc.
      dependencies: [
         // other dependencies
      ],
      targets: [
         .target(
            name: "<target-name>",
            dependencies: [
               // If you want to only bring in the SDK
               .binaryTarget(
                  name: "AmazonIVSChatMessaging",
                  url: "https://ivschat.live-video.net/1.0.1/AmazonIVSChatMessaging.xcframework.zip",
                  checksum: "<SHA-extracted-using-steps-detailed-above>"
               ),
               // your other dependencies
            ],
         ),
         // other targets
      ]
   )
   ```

## 수동 설치
<a name="chat-ios-install-sdk-manual"></a>

1. [https://ivschat.live-video.net/1.0.1/AmazonIVSChatMessaging.xcframework.zip](https://ivschat.live-video.net/1.0.1/AmazonIVSChatMessaging.xcframework.zip)에서 최신 버전을 다운로드합니다.

1. 아카이브 콘텐츠의 압축을 풉니다. `AmazonIVSChatMessaging.xcframework`에는 디바이스와 시뮬레이터 모두에 대한 SDK가 포함되어 있습니다.

1. 애플리케이션 대상에 대해 **General(일반)** 탭의 **프레임워크, 라이브러리 및 포함된 콘텐츠(Frameworks, Libraries, and Embedded Content)** 섹션으로 끌어 추출된 `AmazonIVSChatMessaging.xcframework`를 포함합니다.  
![\[애플리케이션 대상에 대한 일반(General) 탭의 프레임워크, 라이브러리 및 포함된 콘텐츠(Frameworks, Libraries, and Embedded Content) 섹션.\]](http://docs.aws.amazon.com/ko_kr/ivs/latest/ChatUserGuide/images/Chat_SDK_iOS_Manual_Installation.png)

# IVS Chat Client Messaging iOS SDK 사용
<a name="chat-ios-using-sdk"></a>

이 문서에서는 Amazon IVS Chat Client Messaging iOS SDK 사용과 관련된 단계를 안내합니다.

## 채팅 룸에 연결
<a name="chat-ios-connect-room"></a>

시작하기 전에 [Amazon IVS Chat 시작하기](getting-started-chat.md)의 내용을 숙지해야 합니다. 또한 [웹](https://github.com/aws-samples/amazon-ivs-chat-web-demo), [Android](https://github.com/aws-samples/amazon-ivs-chat-for-android-demo) 및 [iOS](https://github.com/aws-samples/amazon-ivs-chat-for-ios-demo)에 대한 예제 앱도 참조하세요.

채팅 룸에 연결하려면 앱에 백엔드에서 제공한 채팅 토큰을 검색할 수 있는 방법이 필요합니다. 애플리케이션은 아마도 백엔드에 대한 네트워크 요청을 사용하여 채팅 토큰을 검색할 것입니다.

가져온 채팅 토큰을 SDK와 통신하려면 SDK의 `ChatRoom` 모델에서 `async` 함수 또는 초기화 시점에서 제공된 `ChatTokenProvider` 프로토콜을 따르는 객체의 인스턴스를 제공해야 합니다. 이러한 메서드 중 하나에서 반환되는 값은 SDK의 `ChatToken` 모델의 인스턴스여야 합니다.

**참고:** 백엔드에서 검색된 데이터를 사용하여 `ChatToken` 모델의 인스턴스를 채웁니다. `ChatToken` 인스턴스의 초기화에 필요한 필드는 [CreateChatToken](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/API_CreateChatToken.html) 응답의 필드와 동일합니다. `ChatToken` 모델의 인스턴스 초기화에 대한 자세한 내용은 [ChatToken의 인스턴스 생성](#chat-ios-create-chattoken)을 참조하세요. *백엔드*에서 앱에 대한 `CreateChatToken` 응답의 데이터를 제공해야 합니다. 채팅 토큰을 생성하기 위해 백엔드와 통신하는 방법은 앱과 인프라에 따라 달라집니다.

`ChatToken`을 SDK로 제공하는 방법을 선택한 후, 백엔드에서 연결하려는 채팅 룸을 생성하는 데 사용한 토큰 공급자와 *AWS 리전*으로 `ChatRoom` 인스턴스를 초기화하고 `.connect()`를 호출합니다. `.connect()`는 비동기 함수를 반환합니다.

```
import AmazonIVSChatMessaging

let room = ChatRoom(
   awsRegion: <region-your-backend-created-the-chat-room-in>,
   tokenProvider: <your-chosen-token-provider-strategy>
)
try await room.connect()
```

### ChatTokenProvider 프로토콜 준수
<a name="chat-ios-chattokenprovider-protocol"></a>

`ChatRoom`에 대한 이니셜라이저의 `tokenProvider` 파라미터에 대해 `ChatTokenProvider`의 인스턴스를 제공할 수 있습니다. 다음은 `ChatTokenProvider`를 준수하는 객체의 예입니다.

```
import AmazonIVSChatMessaging

// This object should exist somewhere in your app
class ChatService: ChatTokenProvider {
   func getChatToken() async throws -> ChatToken {
      let request = YourApp.getTokenURLRequest
      let data = try await URLSession.shared.data(for: request).0
      ...
      return ChatToken(
         token: String(data: data, using: .utf8)!,
         tokenExpirationTime: ..., // this is optional
         sessionExpirationTime: ... // this is optional
      )
   }
}
```

그런 다음 이 준수 객체의 인스턴스를 가져와 `ChatRoom`의 이니셜라이저에 전달할 수 있습니다.

```
// This should be the same AWS Region that you used to create
// your Chat Room in the Control Plane
let awsRegion = "us-west-2"
let service = ChatService()
let room = ChatRoom(
   awsRegion: awsRegion,
   tokenProvider: service
)
try await room.connect()
```

### Swift에서 비동기 함수 제공
<a name="chat-ios-retrievechattoken-async-function"></a>

애플리케이션의 네트워크 요청을 관리하는 데 사용하는 관리자가 이미 있다고 가정해 봅시다. 값이 다음과 같을 것입니다.

```
import AmazonIVSChatMessaging

class EndpointManager {
   func getAccounts() async -> AppUser {...}
   func signIn(user: AppUser) async {...}
   ...
}
```

관리자에 다른 함수를 추가하여 백엔드에서 `ChatToken`을 검색할 수 있습니다.

```
import AmazonIVSChatMessaging

class EndpointManager {
   ...
   func retrieveChatToken() async -> ChatToken {...}
}
```

그런 다음 `ChatRoom`을 초기화할 때 Swift에서 해당 함수에 대한 참조를 사용합니다.

```
import AmazonIVSChatMessaging

let endpointManager: EndpointManager
let room = ChatRoom(
   awsRegion: endpointManager.awsRegion,
   tokenProvider: endpointManager.retrieveChatToken
)
try await room.connect()
```

## ChatToken의 인스턴스 생성
<a name="chat-ios-create-chattoken"></a>

SDK에 제공된 이니셜라이저를 사용하여 `ChatToken`의 인스턴스를 쉽게 만들 수 있습니다. `Token.swift`의 설명서에서 `ChatToken`의 속성에 대해 자세히 알아보세요.

```
import AmazonIVSChatMessaging

let chatToken = ChatToken(
   token: <token-string-retrieved-from-your-backend>,
   tokenExpirationTime: nil, // this is optional
   sessionExpirationTime: nil // this is optional
)
```

### Decodable 사용
<a name="chat-ios-create-chattoken-decodable"></a>

IVS Chat API와 인터페이스하는 동안 백엔드에서 프런트엔드 애플리케이션으로 [CreateChatToken](https://docs.aws.amazon.com/ivs/latest/ChatAPIReference/API_CreateChatToken.html) 응답을 전달하기로 결정한 경우 `ChatToken`의 Swift `Decodable` 프로토콜 준수를 활용할 수 있습니다. 하지만 한 가지 문제가 있습니다.

`CreateChatToken` 응답 페이로드는 [ISO 8601 인터넷 타임스탬프 표준](https://en.wikipedia.org/wiki/ISO_8601)을 사용하여 형식이 지정된 날짜 문자열을 사용합니다. 일반적으로 Swift에서 `JSONDecoder.DateDecodingStrategy.iso8601`을 `JSONDecoder`의 `.dateDecodingStrategy`속성에 대한 값으로 [제공합니다](https://www.hackingwithswift.com/example-code/language/how-to-use-iso-8601-dates-with-jsondecoder-and-codable). 하지만 `CreateChatToken`은 문자열에 고정밀 분수 초를 사용하며, 이는 `JSONDecoder.DateDecodingStrategy.iso8601`에서 지원되지 않습니다.

편의를 위해 SDK는 `JSONDecoder.DateDecodingStrategy`에 대한 공개 확장 프로그램에 `ChatToken`의 인스턴스를 디코딩할 때 성공적으로 `JSONDecoder`를 사용할 수 있도록 하는 추가 `.preciseISO8601` 전략을 제공합니다.

```
import AmazonIVSChatMessaging

// The CreateChatToken data forwarded by your backend
let responseData: Data

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .preciseISO8601
let token = try decoder.decode(ChatToken.self, from: responseData)
```

## 채팅 룸 연결 해제
<a name="chat-ios-disconnect-room"></a>

성공적으로 연결한 `ChatRoom` 인스턴스에서 수동으로 연결을 해제하려면 `room.disconnect()`를 호출합니다. 기본적으로 채팅 룸은 할당이 취소되면 이 함수를 자동으로 호출합니다.

```
import AmazonIVSChatMessaging

let room = ChatRoom(...)
try await room.connect()

// Disconnect
room.disconnect()
```

## 채팅 메시지/이벤트 받기
<a name="chat-ios-receive-message"></a>

채팅 룸에서 메시지를 보내고 받으려면 `ChatRoom`의 인스턴스를 성공적으로 초기화하고 `room.connect()`를 호출한 후 `ChatRoomDelegate` 프로토콜을 준수하는 객체를 제공해야 합니다. `UIViewController` 사용의 일반적인 예제:

```
import AmazonIVSChatMessaging
import Foundation
import UIKit

class ViewController: UIViewController {
   let room: ChatRoom = ChatRoom(
      awsRegion: "us-west-2",
      tokenProvider: EndpointManager.shared
   )

   override func viewDidLoad() {
      super.viewDidLoad()
      Task { try await setUpChatRoom() }
   }

   private func setUpChatRoom() async throws {
      // Set the delegate to start getting notifications for room events
      room.delegate = self
      try await room.connect()
   }
}

extension ViewController: ChatRoomDelegate {
   func room(_ room: ChatRoom, didReceive message: ChatMessage) { ... }
   func room(_ room: ChatRoom, didReceive event: ChatEvent) { ... }
   func room(_ room: ChatRoom, didDelete message: DeletedMessageEvent) { ... }
}
```

## 연결 변경 시 알림 받기
<a name="chat-ios-room-connection-state"></a>

예상대로 완전히 연결되기 전까지는 메시지를 보내는 등의 작업을 수행할 수 없습니다. SDK의 아키텍처는 비동기 API를 통해 백그라운드 스레드에서 ChatRoom 연결을 권장하려 합니다. 메시지 전송 버튼 등을 비활성화하는 기능을 UI에 빌드하려는 경우 SDK는 채팅 룸의 연결 상태가 변경될 때 알림을 받는 두 가지 방법으로 `Combine` 또는 `ChatRoomDelegate` 사용을 제공합니다. 아래에서 설명합니다.

**중요:** 네트워크 연결 끊어짐 등으로 인해 채팅 룸의 연결 상태도 변경될 수 있습니다. 앱을 빌드할 때 이 점을 고려하세요.

### Combine 사용
<a name="room-connection-state-combine"></a>

`ChatRoom`의 모든 인스턴스에는 `state` 속성의 형식으로 자체 `Combine` 게시자가 제공됩니다.

```
import AmazonIVSChatMessaging
import Combine

var cancellables: Set<AnyCancellable> = []

let room = ChatRoom(...)
room.state.sink { state in
   switch state {
   case .connecting:
      let image = UIImage(named: "antenna.radiowaves.left.and.right")
      sendMessageButton.setImage(image, for: .normal)
      sendMessageButton.isEnabled = false
   case .connected:
      let image = UIImage(named: "paperplane.fill")
      sendMessageButton.setImage(image, for: .normal)
      sendMessageButton.isEnabled = true
   case .disconnected:
      let image = UIImage(named: "antenna.radiowaves.left.and.right.slash")
      sendMessageButton.setImage(image, for: .normal)
      sendMessageButton.isEnabled = false
   }
}.assign(to: &cancellables)

// Connect to `ChatRoom` on a background thread
Task(priority: .background) {
   try await room.connect()
}
```

### ChatRoomDelegate 사용
<a name="room-connection-state-chatroomdelegate"></a>

또는 `ChatRoomDelegate`를 준수하는 객체 내에서 `roomDidConnect(_:)`, `roomIsConnecting(_:)` 및 `roomDidDisconnect(_:)` 함수를 사용합니다. 다음은 `UIViewController` 사용의 예입니다.

```
import AmazonIVSChatMessaging
import Foundation
import UIKit

class ViewController: UIViewController {
   let room: ChatRoom = ChatRoom(
      awsRegion: "us-west-2",
      tokenProvider: EndpointManager.shared
   )

   override func viewDidLoad() {
      super.viewDidLoad()
      Task { try await setUpChatRoom() }
   }

   private func setUpChatRoom() async throws {
      // Set the delegate to start getting notifications for room events
      room.delegate = self
      try await room.connect()
   }
}

extension ViewController: ChatRoomDelegate {
   func roomDidConnect(_ room: ChatRoom) {
      print("room is connected!")
   }
   func roomIsConnecting(_ room: ChatRoom) {
      print("room is currently connecting or fetching a token")
   }
   func roomDidDisconnect(_ room: ChatRoom) {
      print("room disconnected!")
   }
}
```

## 채팅 룸에서 작업 수행
<a name="chat-ios-room-actions"></a>

채팅 룸에서 수행할 수 있는 작업(예: 메시지 전송, 메시지 삭제, 사용자 연결 해제)이 가능한지 여부는 사용자마다 다릅니다. 이러한 작업 중 하나를 수행하려면 연결된 `ChatRoom`에서 `perform(request:)`를 호출하고, SDK에서 제공된 `ChatRequest` 객체 중 하나의 인스턴스를 전달합니다. 지원되는 요청은 `Request.swift`입니다.

채팅 룸의 일부 작업을 수행하려면 백엔드 애플리케이션이 `CreateChatToken`을 호출할 때 연결된 사용자에게 특정 기능을 부여해야 합니다. 설계상 SDK는 연결된 사용자의 기능을 식별할 수 없습니다. 따라서 `ChatRoom`의 연결된 인스턴스에서 중재자 작업을 수행해 볼 수 있습니다. 컨트롤 플레인 API는 최종적으로 해당 작업의 성공 여부를 결정합니다.

`room.perform(request:)`을 거치는 모든 작업은 룸에서 수신한 모델과 요청 객체 모두의 `requestId`와 일치하는 모델의 인스턴스(유형은 요청 객체 자체와 연결됨)를 수신할 때까지 대기합니다. 요청에 문제가 있는 경우 `ChatRoom`은 항상 `ChatError` 형식으로 오류를 반환합니다. `ChatError`의 정의는 `Error.swift`입니다.

### 메시지 전송
<a name="room-action-send-message"></a>

채팅 메시지를 보내려면 `SendMessageRequest`의 인스턴스를 사용합니다.

```
import AmazonIVSChatMessaging

let room = ChatRoom(...)
try await room.connect()
try await room.perform(
   request: SendMessageRequest(
      content: "Release the Kraken!"
   )
)
```

위에서 언급한 바와 같이 `room.perform(request:)`은 `ChatRoom`에서 `ChatMessage`를 수신하면 반환됩니다. 요청에 문제가 있는 경우(예: 룸의 메시지 문자 제한 초과) `ChatError`의 인스턴스가 대신 반환됩니다. 이후 유용한 정보를 UI에 표시할 수 있습니다.

```
import AmazonIVSChatMessaging

do {
   let message = try await room.perform(
      request: SendMessageRequest(
         content: "Release the Kraken!"
      )
   )
   print(message.id)
} catch let error as ChatError {
   switch error.errorCode {
   case .invalidParameter:
      print("Exceeded the character limit!")
   case .tooManyRequests:
      print("Exceeded message request limit!")
   default:
      break
   }

   print(error.errorMessage)
}
```

### 메시지에 메타데이터 추가
<a name="room-action-message-metadata"></a>

[메시지를 보낼](#room-action-send-message) 때 관련 메타데이터를 추가할 수 있습니다. `SendMessageRequest`에는 `attributes` 속성이 있고, 이 속성을 사용하여 요청을 초기화할 수 있습니다. 첨부한 데이터는 다른 사람들이 채팅 룸에서 해당 메시지를 받을 때 메시지에 첨부됩니다.

다음은 보내는 메시지에 이모트 데이터를 첨부하는 예시입니다.

```
import AmazonIVSChatMessaging

let room = ChatRoom(...)
try await room.connect()
try await room.perform(
   request: SendMessageRequest(
      content: "Release the Kraken!",
      attributes: [
         "messageReplyId" : "<other-message-id>",
         "attached-emotes" : "krakenCry,krakenPoggers,krakenCheer"
      ]
   )
)
```

`SendMessageRequest`에서 `attributes`를 사용하는 것이 채팅 제품에서 복잡한 기능을 빌드하는 데 매우 유용할 수 있습니다. 예를 들어 `SendMessageRequest`의 `[String : String]` 속성 사전을 사용하여 스레딩 기능을 빌드할 수 있습니다\$1

`attributes` 페이로드는 매우 유연하고 강력합니다. 이를 사용하여 다른 방법으로는 할 수 없는 메시지 관련 정보를 도출하세요. 예를 들어 메시지 문자열을 구문 분석하여 이모트 등의 정보를 가져오는 것보다 속성을 사용하는 것이 훨씬 쉽습니다.

### 메시지 삭제
<a name="room-action-delete-message"></a>

채팅 메시지를 삭제하는 것은 메시지를 보내는 것과 같습니다. `ChatRoom`에서 `room.perform(request:)` 함수를 호출하고 `DeleteMessageRequest`의 인스턴스를 생성하면 됩니다.

받은 챗 메시지의 이전 인스턴스에 쉽게 액세스하려면 `DeleteMessageRequest`의 이니셜라이저로 `message.id`의 값을 전달합니다.

필요한 경우 UI에 표시할 수 있도록 이유 문자열을 `DeleteMessageRequest`에 제공합니다.

```
import AmazonIVSChatMessaging

let room = ChatRoom(...)
try await room.connect()
try await room.perform(
   request: DeleteMessageRequest(
      id: "<other-message-id-to-delete>",
      reason: "Abusive chat is not allowed!"
   )
)
```

이는 중재자 작업이며, 사용자는 실제로 다른 사용자의 메시지를 삭제할 수 있는 권한이 없을 수 있습니다. 사용자가 적절한 기능 없이 메시지를 삭제하려고 할 때 Swift의 반환 가능한 함수 메커니즘을 사용하여 UI에 오류 메시지를 표시할 수 있습니다.

백엔드에서 사용자에 대해 `CreateChatToken`을 호출할 때 `"DELETE_MESSAGE"`를 `capabilities` 필드를 전달하여 연결된 채팅 사용자에 대해 해당 기능을 활성화합니다.

다음은 적절한 권한 없이 메시지를 삭제하려고 할 때 발생하는 기능 오류의 예입니다.

```
import AmazonIVSChatMessaging

do {
   // `deleteEvent` is the same type as the object that gets sent to
   // `ChatRoomDelegate`'s `room(_:didDeleteMessage:)` function
   let deleteEvent = try await room.perform(
      request: DeleteMessageRequest(
         id: "<other-message-id-to-delete>",
         reason: "Abusive chat is not allowed!"
      )
   )
   dataSource.messages[deleteEvent.messageID] = nil
   tableView.reloadData()
} catch let error as ChatError {
   switch error.errorCode {
   case .forbidden:
      print("You cannot delete another user's messages. You need to be a mod to do that!")
   default:
      break
   }

   print(error.errorMessage)
}
```

### 다른 사용자 연결 해제
<a name="room-action-disconnect-user"></a>

`room.perform(request:)`을 사용하여 채팅 룸에서 다른 사용자의 연결을 해제합니다. 구체적으로 `DisconnectUserRequest`의 인스턴스를 사용합니다. `ChatRoom`에서 수신하는 모든 `ChatMessage`에는 `sender` 속성이 있으며, `DisconnectUserRequest`의 인스턴스로 적절히 초기화해야 하는 사용자 ID가 포함되어 있습니다. 연결 해제 요청에 대한 이유 문자열을 제공할 수도 있습니다.

```
import AmazonIVSChatMessaging

let room = ChatRoom(...)
try await room.connect()

let message: ChatMessage = dataSource.messages["<message-id>"]
let sender: ChatUser = message.sender
let userID: String = sender.userId
let reason: String = "You've been disconnected due to abusive behavior"

try await room.perform(
   request: DisconnectUserRequest(
      id: userID,
      reason: reason
   )
)
```

다음은 중재자 작업의 또 다른 예이며, 다른 사용자의 연결을 해제하려고 할 수 있지만 `DISCONNECT_USER` 기능이 없는 한 연결을 해제할 수 없습니다. 기능은 백엔드 애플리케이션이 `CreateChatToken`을 호출하고 `"DISCONNECT_USER"` 문자열을 `capabilities` 필드에 주입할 때 설정됩니다.

사용자에게 다른 사용자의 연결을 해제하는 기능이 없는 경우 `room.perform(request:)`은 다른 요청과 마찬가지로 `ChatError`의 인스턴스를 반환합니다. 중재자 권한 부족으로 인해 요청이 실패하는지 확인하기 위해 오류의 `errorCode` 속성을 검사할 수 있습니다.

```
import AmazonIVSChatMessaging

do {
   let message: ChatMessage = dataSource.messages["<message-id>"]
   let sender: ChatUser = message.sender
   let userID: String = sender.userId
   let reason: String = "You've been disconnected due to abusive behavior"

   try await room.perform(
      request: DisconnectUserRequest(
         id: userID,
         reason: reason
      )
   )
} catch let error as ChatError {
   switch error.errorCode {
   case .forbidden:
      print("You cannot disconnect another user. You need to be a mod to do that!")
   default:
      break
   }

   print(error.errorMessage)
}
```

# IVS Chat Client Messaging SDK: iOS 자습서
<a name="chat-sdk-ios-tutorial"></a>

 Amazon Interactive Video(IVS) Chat Client Messaging iOS SDK는 Apple의 [Swift 프로그래밍 언어](https://developer.apple.com/swift/)를 사용하는 플랫폼에 [IVS Chat Messaging API](https://docs.aws.amazon.com//ivs/latest/chatmsgapireference/welcome.html)를 통합할 수 있는 인터페이스를 제공합니다.

 Chat iOS SDK 자습서는 [https://aws.github.io/amazon-ivs-chat-messaging-sdk-ios/latest/tutorials/table-of-contents/](https://aws.github.io/amazon-ivs-chat-messaging-sdk-ios/latest/tutorials/table-of-contents/)를 참조하세요.

# IVS Chat Client Messaging SDK: JavaScipt 설명서
<a name="chat-sdk-js"></a>



Amazon Interactive Video(IVS) Chat Client Messaging JavaScript SDK를 사용하면 웹 브라우저를 사용하는 플랫폼에서 [https://docs.aws.amazon.com/ivs/latest/chatmsgapireference/welcome.html](https://docs.aws.amazon.com/ivs/latest/chatmsgapireference/welcome.html)를 통합할 수 있습니다.

**IVS Chat Client Messaging JavaScript SDK의 최신 버전:** 1.0.2([릴리스 정보](https://docs.aws.amazon.com//ivs/latest/ChatUserGuide/release-notes.html#nov09-22))

**참조 문서:** Amazon IVS Chat Client Messaging JavaScrip SDK에서 사용할 수 있는 가장 중요한 메서드에 대한 자세한 내용은 [https://aws.github.io/amazon-ivs-chat-messaging-sdk-js/1.0.2/](https://aws.github.io/amazon-ivs-chat-messaging-sdk-js/1.0.2/)의 참조 문서를 확인하세요.

**샘플 코드:** JavaScript SDK를 사용하는 웹 전용 데모는 GitHub의 샘플 리포지토리를 참조하십시오. [https://github.com/aws-samples/amazon-ivs-chat-web-demo](https://github.com/aws-samples/amazon-ivs-chat-web-demo)

# IVS Chat Client Messaging JavaScript SDK 시작하기
<a name="chat-js-getting-started"></a>

시작하기 전에 [Amazon IVS Chat 시작하기](getting-started-chat.md)의 내용을 숙지해야 합니다.

## 패키지 추가
<a name="chat-js-add-package"></a>

다음을 사용하십시오.

```
$ npm install --save amazon-ivs-chat-messaging
```

또는 다음을 사용하십시오.

```
$ yarn add amazon-ivs-chat-messaging
```

## React Native Support
<a name="chat-js-react-native-support"></a>

IVS 채팅 클라이언트 메시징 JavaScript SDK에는 `crypto.getRandomValues` 메서드를 사용하는 `uuid` 종속성이 있습니다. 이 메서드는 React Native에서 지원되지 않으므로 추가 폴리필을 설치하고 `react-native-get-random-value` `index.js` 파일 상단에서 가져와야 합니다.

```
import 'react-native-get-random-values';
import {AppRegistry} from 'react-native';
import App from './src/App';
import {name as appName} from './app.json';

AppRegistry.registerComponent(appName, () => App);
```

## 백엔드 설정
<a name="chat-js-setup-backend"></a>

이 통합에는 [Amazon IVS Chat API](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/Welcome.html)와 통신하는 서버의 엔드포인트가 필요합니다. [공식 AWS 라이브러리](https://aws.amazon.com/developer/tools/)를 사용하여 서버에서 Amazon IVS API에 액세스합니다. 공개 패키지(예: [node.js](https://www.npmjs.com/package/aws-sdk), [java](https://github.com/aws/aws-sdk-java), [go](https://github.com/aws/aws-sdk-go))의 여러 언어로 액세스할 수 있습니다.

Amazon IVS Chat API [CreateChatToken](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/API_CreateChatToken.html) 엔드포인트와 통신하는 서버 작업을 생성하여 채팅 사용자를 위한 채팅 토큰을 생성합니다.

# IVS Chat Client Messaging JavaScript SDK 사용
<a name="chat-js-using-sdk"></a>

이 문서에서는 Amazon IVS Chat Client Messaging JavaScript SDK 사용과 관련된 단계를 안내합니다.

## 채팅 룸 인스턴스 초기화
<a name="chat-js-initialize-room"></a>

`ChatRoom` 클래스의 인스턴스를 만듭니다. 인스턴스 생성을 위해`regionOrUrl`(채팅룸이 호스팅되는 AWS 리전) 및`tokenProvider`(토큰 가져오기 메서드는 다음 단계에서 생성)를 통과해야 합니다.

```
const room = new ChatRoom({
  regionOrUrl: 'us-west-2',
  tokenProvider: tokenProvider,
});
```

## 토큰 공급자 함수
<a name="chat-js-token-provider-function"></a>

백엔드에서 채팅 토큰을 가져오는 비동기(asynchronous) 토큰 공급자 함수를 생성합니다.

```
type ChatTokenProvider = () => Promise<ChatToken>;
```

함수는 매개 변수를 받지 않아야 하며 채팅 토큰 객체가 포함된 [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) 반환해야 합니다.

```
type ChatToken = {
  token: string;
  sessionExpirationTime?: Date;
  tokenExpirationTime?: Date;
}
```

이 함수는 [ChatRoom 객체를 초기화하는](#chat-js-initialize-room) 데 필요하므로 아래의 내용 중`<token>` 및`<date-time>` 필드에 백엔드에서 받은 값을 입력하세요.

```
// You will need to fetch a fresh token each time this method is called by
// the IVS Chat Messaging SDK, since each token is only accepted once.
function tokenProvider(): Promise<ChatToken> {
  // Call your backend to fetch chat token from IVS Chat endpoint: 
  // e.g. const token = await appBackend.getChatToken()
  return {
    token: "<token>",
    sessionExpirationTime: new Date("<date-time>"),
    tokenExpirationTime: new Date("<date-time>")
  }
}
```

 반드시 ChatRoom`tokenProvider` 생성자에 전달하세요. ChatRoom은 연결이 중단되거나 세션이 만료되면 토큰을 새로 고침 합니다. ChatRoom에서 자동으로 토큰을 처리하므로 토큰을 다른 곳에 저장하는 데 `tokenProvider`를 사용하지 마세요.

## 이벤트 수신
<a name="chat-js-receive-event"></a>

다음으로 채팅룸 이벤트를 구독하여 대화방에서 전달되는 메시지와 이벤트뿐만 아니라 라이프사이클 이벤트를 수신하세요.

```
/**
* Called when room is establishing the initial connection or reestablishing
* connection after socket failure/token expiration/etc
*/
const unsubscribeOnConnecting = room.addListener('connecting', () => { });

/** Called when connection has been established. */
const unsubscribeOnConnected = room.addListener('connect', () => { });

/** Called when a room has been disconnected. */
const unsubscribeOnDisconnected = room.addListener('disconnect', () => { });

/** Called when a chat message has been received. */
const unsubscribeOnMessageReceived = room.addListener('message', (message) => {
 /* Example message:
  * {
  *   id: "5OPsDdX18qcJ",
  *   sender: { userId: "user1" },
  *   content: "hello world",
  *   sendTime: new Date("2022-10-11T12:46:41.723Z"),
  *   requestId: "d1b511d8-d5ed-4346-b43f-49197c6e61de"
  * }
  */
});

/** Called when a chat event has been received. */
const unsubscribeOnEventReceived = room.addListener('event', (event) => {
 /* Example event:
  * {
  *   id: "5OPsDdX18qcJ",
  *   eventName: "customEvent,
  *   sendTime: new Date("2022-10-11T12:46:41.723Z"),
  *   requestId: "d1b511d8-d5ed-4346-b43f-49197c6e61de",
  *   attributes: { "Custom Attribute": "Custom Attribute Value" }
  * }
  */
});

/** Called when `aws:DELETE_MESSAGE` system event has been received. */
const unsubscribeOnMessageDelete = room.addListener('messageDelete', (deleteMessageEvent) => {
 /* Example delete message event:
  * {
  *   id: "AYk6xKitV4On",
  *   messageId: "R1BLTDN84zEO",
  *   reason: "Spam",
  *   sendTime: new Date("2022-10-11T12:56:41.113Z"),
  *   requestId: "b379050a-2324-497b-9604-575cb5a9c5cd",
  *   attributes: { MessageID: "R1BLTDN84zEO", Reason: "Spam" }
  * }
  */
});

/** Called when `aws:DISCONNECT_USER` system event has been received. */
const unsubscribeOnUserDisconnect = room.addListener('userDisconnect', (disconnectUserEvent) => {
 /* Example event payload:
  * {
  *   id: "AYk6xKitV4On",
  *   userId": "R1BLTDN84zEO",
  *   reason": "Spam",
  *   sendTime": new Date("2022-10-11T12:56:41.113Z"),
  *   requestId": "b379050a-2324-497b-9604-575cb5a9c5cd",
  *   attributes": { UserId: "R1BLTDN84zEO", Reason: "Spam" }
  * }
  */
});
```

## 채팅룸에 연결
<a name="chat-js-connect-to-chat-room"></a>

기본 초기화의 마지막 단계는 WebSocket 연결을 설정하여 특정 룸에 연결하는 것입니다. 이를 위해 룸 인스턴스 내에서 `connect()` 메서드를 호출합니다.

```
room.connect();
```

 SDK가 서버에서 받은 채팅 토큰으로 인코딩된 채팅룸과의 연결을 설정하려고 시도합니다.

 `connect()`을 호출하면 룸이`connecting` 상태로 전환되어`connecting` 이벤트가 발생합니다. 룸이 성공적으로 연결되면`connected` 상태로 전환되고`connect` 이벤트가 발생합니다.

 토큰을 가져오거나 WebSocket에 연결할 때 문제가 발생하여 연결 실패가 발생할 수 있습니다. 이 경우, 룸은`maxReconnectAttempts` 생성자 매개변수에 표시된 횟수까지 자동으로 다시 연결을 시도합니다. 재연결을 시도하는 동안에는 룸이`connecting` 상태이며 추가 이벤트가 발생하지 않습니다. 재연결 시도 횟수를 모두 소진하면 해당 룸이`disconnected` 상태로 전환되고 관련 연결 해제 이유가 포함된`disconnect` 이벤트가 발생합니다. `disconnected` 상태에서는 룸이 더 이상 연결을 시도하지 않습니다. 연결 프로세스를 트리거하려면 `connect()`를 다시 호출해야 합니다.

## 채팅룸에서 작업 수행
<a name="chat-js-room-actions"></a>

Amazon IVS Chat Messaging SDK는 메시지 전송, 메시지 삭제 및 다른 사용자 연결 해제를 위한 사용자 작업을 제공합니다. 이들 기능은 `ChatRoom` 인스턴스에서 사용할 수 있습니다. 요청 확인 또는 거부 수신을 허용하는 `Promise` 객체를 반환합니다.

### 메시지 전송
<a name="chat-js-room-actions-send-message"></a>

이 요청의 경우 채팅 토큰에 `SEND_MESSAGE` 기능이 인코딩되어 있어야 합니다.

메시지 전송 요청 트리거:

```
const request = new SendMessageRequest('Test Echo');
room.sendMessage(request);
```

요청에 대한 확인 또는 거부를 얻으려면 `await` 반환된 약속(promise)을 받거나 다음 `then()` 메서드를 사용하세요.

```
try {
  const message = await room.sendMessage(request);
  // Message was successfully sent to chat room
} catch (error) {
  // Message request was rejected. Inspect the `error` parameter for details.
}
```

### 메시지 삭제
<a name="chat-js-room-actions-delete-message"></a>

이 요청의 경우 채팅 토큰에 `DELETE_MESSAGE` 기능이 인코딩되어 있어야 합니다.

조절 목적으로 메시지를 삭제하려면 다음`deleteMessage()` 메서드를 호출하십시오.

```
const request = new DeleteMessageRequest(messageId, 'Reason for deletion');
room.deleteMessage(request);
```

요청에 대한 확인 또는 거부를 얻으려면 `await` 반환된 promise를 받거나 다음 `then()` 메서드를 사용하세요.

```
try {
  const deleteMessageEvent = await room.deleteMessage(request);
  // Message was successfully deleted from chat room
} catch (error) {
  // Delete message request was rejected. Inspect the `error` parameter for details.
}
```

### 다른 사용자 연결 해제
<a name="chat-js-room-actions-disconnect-user"></a>

이 요청의 경우 채팅 토큰에 `DISCONNECT_USER` 기능이 인코딩되어 있어야 합니다.

조절 목적으로 다른 사용자의 연결 해제`disconnectUser()`: 

```
const request = new DisconnectUserRequest(userId, 'Reason for disconnecting user');
room.disconnectUser(request);
```

요청에 대한 확인 또는 거부를 얻으려면 `await` 반환된 promise를 받거나 다음 `then()` 메서드를 사용하세요.

```
try {
  const disconnectUserEvent = await room.disconnectUser(request);
  // User was successfully disconnected from the chat room
} catch (error) {
  // Disconnect user request was rejected. Inspect the `error` parameter for details.
}
```

## 채팅룸 연결 해제
<a name="chat-js-disconnect-room"></a>

채팅룸과의 연결을 해제하려면 `room` 인스턴스에서 `disconnect()` 메서드를 호출하세요.

```
room.disconnect();
```

이 메서드를 호출하면 룸이 기본 WebSocket을 순서대로 닫습니다. 룸 인스턴스가`disconnected` 상태로 전환되고 `disconnect` 이유가 `"clientDisconnect"`로 설정된 연결 해제 이벤트가 발생합니다.

# IVS Chat Client Messaging SDK: JavaScript 자습서 1부: 채팅룸
<a name="chat-sdk-js-tutorial-chat-rooms"></a>

본 문서는 두 파트로 구성된 자습서 중 첫 번째 파트에 해당하는 자습서입니다. 이 자습서에서는 JavaScript/TypeScript를 통해 완전한 기능을 갖춘 앱을 구축하여 Amazon IVS Chat Client Messaging JavaScript SDK로 작업하기 위한 필수 사항을 설명하고 있습니다. 여기에서 지칭하는 앱은 *Chatterbox*라고 합니다.

이 자습서는 숙련된 개발자이나, Amazon IVS Chat Messaging SDK를 처음 접하는 사용자를 위해 작성되었습니다. 사용자는 이미 JavaScript/TypeScript 프로그래밍 언어와 React 라이브러리에 대한 친숙도가 있어야 합니다.

간략하게 나타내자면 Amazon IVS Chat Client Messaging JavaScript SDK를 Chat JS SDK라고 합니다.

**참고**: 경우에 따라 JavaScript와 TypeScript의 코드 예제가 동일하므로 서로 합쳐져 있습니다.

이 자습서의 첫 번째 부분은 여러 섹션으로 나뉩니다.

1. [로컬 인증/권한 부여 서버 설정](#chat-js-rooms-auth-server)

1. [Chatterbox 프로젝트 생성](#chat-js-rooms-chatterbox)

1. [채팅 룸에 연결](#chat-js-rooms-connect)

1. [토큰 공급자 구축](#chat-js-rooms-token-provider)

1. [연결 업데이트 관찰](#chat-js-rooms-connection-state)

1. [전송 버튼 구성 요소 생성](#chat-js-rooms-send-button)

1. [메시지 입력 생성](#chat-js-rooms-message-input)

1. [다음 단계](#chat-js-rooms-next-steps)

전체 SDK 설명서를 보려면 우선 [Amazon IVS Chat Client Messaging SDK](chat-sdk.md)(Amazon IVS Chat 사용 설명서**에서 참조) 및 [Chat Client Messaging: SDK for JavaScript Reference](https://aws.github.io/amazon-ivs-chat-messaging-sdk-js/latest/)(GitHub)를 참조하세요.

## 사전 조건
<a name="chat-js-rooms-prerequisites"></a>
+ 사용자는 JavaScript/TypeScript 및 React 라이브러리에 익숙하여야 합니다. React 사용이 처음이라면 [React 튜토리얼](https://react.dev/learn/tutorial-tic-tac-toe)에서 기본 사항을 검토해 보시기 바랍니다.
+ [Amazon IVS Chat 시작하기](getting-started-chat.md)를 읽고 이해합니다.
+ 기존 IAM 정책에 정의된 CreateChatToken 및 CreateRoom 기능을 사용하여 AWS IAM 사용자를 생성합니다. ([Amazon IVS Chat 시작하기](getting-started-chat.md)를 참조하세요.)
+ 이 사용자의 비밀/액세스 키가 AWS 보안 인증 파일에 저장되어 있는지 확인합니다. 지침은 [AWS CLI 사용 설명서](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html)(특히 [구성 및 보안 인증 파일 설정](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html))를 참조합니다.
+ 채팅룸을 생성하고 ARN을 저장합니다. [Amazon IVS Chat 시작하기](getting-started-chat.md)을(를) 참조하세요. (ARN을 저장하지 않은 경우 나중에 콘솔이나 Chat API를 사용하여 조회할 수 있습니다.)
+ NPM 또는 Yarn 패키지 관리자를 사용하여 Node.js 14\$1 환경을 설치합니다.

## 로컬 인증/권한 부여 서버 설정
<a name="chat-js-rooms-auth-server"></a>

백엔드 애플리케이션은 채팅룸을 생성하고 Chat JS SDK가 채팅룸의 클라이언트를 인증하고 권한을 부여하는 데 필요한 채팅 토큰을 생성하는 일을 맡습니다. AWS 키는 모바일 앱에 안전하게 저장할 수 없으므로 자체 백엔드를 사용해야 합니다. 실력 좋은 공격자는 이러한 키를 추출하여 AWS 계정에 액세스할 수 있습니다.

Amazon IVS 채팅 시작하기에서 [채팅 토큰 생성](getting-started-chat-auth.md)을 참조하세요.** 순서도에서 볼 수 있듯이 서버 측 애플리케이션은 채팅 토큰 생성을 담당합니다. 즉, 앱은 서버 측 애플리케이션에서 채팅 토큰을 요청하여 채팅 토큰을 생성하는 자체 수단을 제공해야 합니다.

이 섹션에서는 백엔드에서 토큰 공급자를 생성하는 기본 사항에 대해 알아봅니다. AWS 환경을 사용하여 채팅 토큰 생성을 관리하는 로컬 서버를 만들기 위해 Express 프레임워크를 사용합니다.

NPM을 사용하여 빈 `npm` 프로젝트를 생성합니다. 애플리케이션을 보관할 디렉터리를 생성하고 이를 작업 디렉터리로 만듭니다.

```
$ mkdir backend & cd backend
```

`npm init`을 사용하여 애플리케이션의 `package.json` 파일을 생성합니다.

```
$ npm init
```

이 명령은 애플리케이션의 이름 및 버전을 비롯한 여러 항목을 입력하라는 메시지를 표시합니다. 지금은 **RETURN** 키를 눌러 대부분의 기본값을 그대로 사용할 수 있습니다. 단, 다음은 예외입니다.

```
entry point: (index.js)
```

**RETURN** 키를 눌러 제안된 기본 파일 이름 `index.js`를 그대로 사용하거나 주 파일의 이름을 원하는 대로 입력합니다.

이제 필요한 종속 항목을 설치합니다.

```
$ npm install express aws-sdk cors dotenv
```

`aws-sdk`에는 루트 디렉터리에 있는 `.env`라는 파일에서 자동으로 로드되는 구성 환경 변수가 필요합니다. 이를 구성하려면 `.env`라는 새 파일을 생성하고 누락된 구성 정보를 입력합니다.

```
# .env

# The region to send service requests to.
AWS_REGION=us-west-2

# Access keys use an access key ID and secret access key
# that you use to sign programmatic requests to AWS.

# AWS access key ID.
AWS_ACCESS_KEY_ID=...

# AWS secret access key.
AWS_SECRET_ACCESS_KEY=...
```

이제 `npm init` 명령에서 위에 입력한 이름으로 루트 디렉터리에 진입점(entry-point) 파일을 생성합니다. 이 경우 `index.js`를 사용하고 필요한 모든 패키지를 가져옵니다.

```
// index.js
import express from 'express';
import AWS from 'aws-sdk';
import 'dotenv/config';
import cors from 'cors';
```

이제 `express`의 새 인스턴스를 생성합니다.

```
const app = express();
const port = 3000;

app.use(express.json());
app.use(cors({ origin: ['http://127.0.0.1:5173'] }));
```

그런 다음 토큰 공급자를 위한 첫 번째 엔드포인트 POST 메서드를 생성할 수 있습니다. 요청 본문에서 필수 파라미터(`roomId`, `userId`, `capabilities` 및 `sessionDurationInMinutes`)를 가져옵니다.

```
app.post('/create_chat_token', (req, res) => {
  const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {};
});
```

필수 필드의 유효성 검사를 추가합니다.

```
app.post('/create_chat_token', (req, res) => {
  const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {};

  if (!roomIdentifier || !userId) {
    res.status(400).json({ error: 'Missing parameters: `roomIdentifier`, `userId`' });
    return;
  }
});
```

POST 메서드를 준비한 후 인증/권한 부여의 핵심 기능을 위해 `aws-sdk`와 `createChatToken`을 통합합니다.

```
app.post('/create_chat_token', (req, res) => {
  const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {};

  if (!roomIdentifier || !userId || !capabilities) {
    res.status(400).json({ error: 'Missing parameters: `roomIdentifier`, `userId`, `capabilities`' });
    return;
  }

  ivsChat.createChatToken({ roomIdentifier, userId, capabilities, sessionDurationInMinutes }, (error, data) => {
    if (error) {
      console.log(error);
      res.status(500).send(error.code);
    } else if (data.token) {
      const { token, sessionExpirationTime, tokenExpirationTime } = data;
      console.log(`Retrieved Chat Token: ${JSON.stringify(data, null, 2)}`);

      res.json({ token, sessionExpirationTime, tokenExpirationTime });
    }
  });
});
```

파일 끝에 `express` 앱의 포트 리스너를 추가합니다.

```
app.listen(port, () => {
  console.log(`Backend listening on port ${port}`);
});
```

이제 프로젝트의 루트에서 다음 명령으로 서버를 실행할 수 있습니다.

```
$ node index.js
```

**팁**: 이 서버는 https://localhost:3000에서 URL 요청을 수락합니다.

## Chatterbox 프로젝트 생성
<a name="chat-js-rooms-chatterbox"></a>

먼저 `chatterbox`라는 React 프로젝트를 생성합니다. 다음 명령을 실행합니다.

```
npx create-react-app chatterbox
```

[Node 패키지 관리자](https://www.npmjs.com/) 또는 [Yarn 패키지 관리자](https://yarnpkg.com/)를 통해 Chat Client Messaging JS SDK를 통합할 수 있습니다.
+ Npm: `npm install amazon-ivs-chat-messaging`
+ Yarn: `yarn add amazon-ivs-chat-messaging`

## 채팅 룸에 연결
<a name="chat-js-rooms-connect"></a>

여기서는 `ChatRoom`을 생성하고 비동기 메서드를 사용하여 연결합니다. `ChatRoom` 클래스는 Chat JS SDK에 대한 사용자 연결을 관리합니다. 채팅룸에 성공적으로 연결하려면 React 애플리케이션 내에서 `ChatToken`의 인스턴스를 제공해야 합니다.

기본 `chatterbox` 프로젝트에서 생성한 `App` 파일로 이동하여 두 `<div>` 태그 사이의 모든 내용을 삭제합니다. 미리 입력된 코드는 필요하지 않습니다. 이 시점에서 `App`은 거의 비어 있습니다.

```
// App.jsx / App.tsx

import * as React from 'react';

export default function App() {
  return <div>Hello!</div>;
}
```

새 `ChatRoom` 인스턴스를 생성하고 `useState` 후크를 사용하여 상태로 전달합니다. 이를 위해서는 `regionOrUrl`(채팅룸이 호스팅되는 AWS 리전) 및`tokenProvider`(이후 단계에서 생성되는 백엔드 인증/권한 부여 흐름에 사용됨)를 전달해야 합니다.

**중요**: [Amazon IVS Chat 시작하기](getting-started-chat-create-room.md)에서 방을 생성한 리전과 동일한 AWS 리전을 사용해야 합니다. API는 AWS 리전 서비스입니다. 지원되는 리전 및 Amazon IVS Chat HTTPS 서비스 엔드포인트 목록은 [Amazon IVS Chat 리전](https://docs.aws.amazon.com/general/latest/gr/ivs.html#ivs_region) 페이지를 참조하세요.

```
// App.jsx / App.tsx

import React, { useState } from 'react';
import { ChatRoom } from 'amazon-ivs-chat-messaging';

export default function App() {
  const [room] = useState(() =>
    new ChatRoom({
      regionOrUrl: process.env.REGION as string,
      tokenProvider: () => {},
    }),
  );

  return <div>Hello!</div>;
}
```

## 토큰 공급자 구축
<a name="chat-js-rooms-token-provider"></a>

다음 단계로 `ChatRoom` 생성자에 필요한 파라미터 없는`tokenProvider` 함수를 구축해야 합니다. 먼저, [로컬 인증/권한 부여 서버 설정](#chat-js-rooms-auth-server)에서 설정한 백엔드 애플리케이션에 POST 요청을 하는 `fetchChatToken` 함수를 생성하겠습니다. 채팅 토큰은 SDK가 성공적으로 채팅룸 연결을 설정하는 데 필요한 정보를 포함합니다. Chat API는 사용자의 ID, 채팅룸 내 기능 및 세션 기간을 검증하는 안전한 방법으로 이러한 토큰을 사용합니다.

프로젝트 탐색기에서 `fetchChatToken`이라는 새 TypeScript/JavaScript 파일을 생성합니다. `backend` 애플리케이션에 가져오기 요청을 구축하고 응답에서 `ChatToken` 객체를 반환합니다. 채팅 토큰을 생성하는 데 필요한 요청 본문 속성을 추가합니다. [Amazon 리소스 이름(ARN)](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html)에 정의된 규칙을 사용합니다. 이러한 속성은 [CreateChatToken 작업](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/API_CreateChatToken.html#API_CreateChatToken_RequestBody)에 문서화되어 있습니다.

**참고**: 여기서 사용하는 URL은 백엔드 애플리케이션을 실행했을 때 로컬 서버에서 생성한 URL과 동일한 URL입니다.

------
#### [ TypeScript ]

```
// fetchChatToken.ts

import { ChatToken } from 'amazon-ivs-chat-messaging';

type UserCapability = 'DELETE_MESSAGE' | 'DISCONNECT_USER' | 'SEND_MESSAGE';

export async function fetchChatToken(
  userId: string,
  capabilities: UserCapability[] = [],
  attributes?: Record<string, string>,
  sessionDurationInMinutes?: number,
): Promise<ChatToken> {
  const response = await fetch(`${process.env.BACKEND_BASE_URL}/create_chat_token`, {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      userId,
      roomIdentifier: process.env.ROOM_ID,
      capabilities,
      sessionDurationInMinutes,
      attributes
    }),
  });

  const token = await response.json();

  return {
    ...token,
    sessionExpirationTime: new Date(token.sessionExpirationTime),
    tokenExpirationTime: new Date(token.tokenExpirationTime),
  };
}
```

------
#### [ JavaScript ]

```
// fetchChatToken.js

export async function fetchChatToken(
  userId,
  capabilities = [],
  attributes,
  sessionDurationInMinutes) {
  const response = await fetch(`${process.env.BACKEND_BASE_URL}/create_chat_token`, {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      userId,
      roomIdentifier: process.env.ROOM_ID,
      capabilities,
      sessionDurationInMinutes,
      attributes
    }),
  });

  const token = await response.json();

  return {
    ...token,
    sessionExpirationTime: new Date(token.sessionExpirationTime),
    tokenExpirationTime: new Date(token.tokenExpirationTime),
  };
}
```

------

## 연결 업데이트 관찰
<a name="chat-js-rooms-connection-state"></a>

채팅 앱을 생성하기 위해서는 채팅룸 연결 상태 변화에 대해 필수적으로 대응해야 합니다. 관련 이벤트 구독부터 시작해 보겠습니다.

```
// App.jsx / App.tsx

import React, { useState, useEffect } from 'react';
import { ChatRoom } from 'amazon-ivs-chat-messaging';
import { fetchChatToken } from './fetchChatToken';

export default function App() {
  const [room] = useState(
    () =>
      new ChatRoom({
        regionOrUrl: process.env.REGION as string,
        tokenProvider: () => fetchChatToken('Mike', ['SEND_MESSAGE']),
      }),
  );

  useEffect(() => {
    const unsubscribeOnConnecting = room.addListener('connecting', () => {});
    const unsubscribeOnConnected = room.addListener('connect', () => {});
    const unsubscribeOnDisconnected = room.addListener('disconnect', () => {});

    return () => {
      // Clean up subscriptions.
      unsubscribeOnConnecting();
      unsubscribeOnConnected();
      unsubscribeOnDisconnected();
    };
  }, [room]);

  return <div>Hello!</div>;
}
```

다음으로 연결 상태를 읽을 수 있는 기능을 제공해야 합니다. `useState` 후크를 사용하여 `App`에서 로컬 상태를 생성하고 각 리스너 내에서 연결 상태를 설정합니다.

------
#### [ TypeScript ]

```
// App.tsx

import React, { useState, useEffect } from 'react';
import { ChatRoom, ConnectionState } from 'amazon-ivs-chat-messaging';
import { fetchChatToken } from './fetchChatToken';

export default function App() {  
  const [room] = useState(
    () =>
      new ChatRoom({
        regionOrUrl: process.env.REGION as string,
        tokenProvider: () => fetchChatToken('Mike', ['SEND_MESSAGE']),
      }),
  );
  const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected');

  useEffect(() => {
    const unsubscribeOnConnecting = room.addListener('connecting', () => {
      setConnectionState('connecting');
    });

    const unsubscribeOnConnected = room.addListener('connect', () => {
      setConnectionState('connected');
    });

    const unsubscribeOnDisconnected = room.addListener('disconnect', () => {
      setConnectionState('disconnected');
    });

    return () => {
      unsubscribeOnConnecting();
      unsubscribeOnConnected();
      unsubscribeOnDisconnected();
    };
  }, [room]);

  return <div>Hello!</div>;
}
```

------
#### [ JavaScript ]

```
// App.jsx

import React, { useState, useEffect } from 'react';
import { ChatRoom } from 'amazon-ivs-chat-messaging';
import { fetchChatToken } from './fetchChatToken';

export default function App() {
  const [room] = useState(
    () =>
      new ChatRoom({
        regionOrUrl: process.env.REGION,
        tokenProvider: () => fetchChatToken('Mike', ['SEND_MESSAGE']),
      }),
  );
  const [connectionState, setConnectionState] = useState('disconnected');

  useEffect(() => {
    const unsubscribeOnConnecting = room.addListener('connecting', () => {
      setConnectionState('connecting');
    });

    const unsubscribeOnConnected = room.addListener('connect', () => {
      setConnectionState('connected');
    });

    const unsubscribeOnDisconnected = room.addListener('disconnect', () => {
      setConnectionState('disconnected');
    });

    return () => {
      unsubscribeOnConnecting();
      unsubscribeOnConnected();
      unsubscribeOnDisconnected();
    };
  }, [room]);

  return <div>Hello!</div>;
}
```

------

연결 상태를 구독한 후 연결 상태를 표시하고 `useEffect` 후크 내의 `room.connect` 메서드를 사용하여 채팅룸에 연결합니다.

```
// App.jsx / App.tsx

// ...

useEffect(() => {
  const unsubscribeOnConnecting = room.addListener('connecting', () => {
    setConnectionState('connecting');
  });

  const unsubscribeOnConnected = room.addListener('connect', () => {
    setConnectionState('connected');
  });

  const unsubscribeOnDisconnected = room.addListener('disconnect', () => {
    setConnectionState('disconnected');
  });

  room.connect();

  return () => {
    unsubscribeOnConnecting();
    unsubscribeOnConnected();
    unsubscribeOnDisconnected();
  };
}, [room]);

// ...

return (
  <div>
    <h4>Connection State: {connectionState}</h4>
  </div>
);

// ...
```

채팅룸 연결을 성공적으로 구현했습니다.

## 전송 버튼 구성 요소 생성
<a name="chat-js-rooms-send-button"></a>

이 섹션에서는 연결 상태 별로 각기 다른 디자인의 전송(send) 버튼을 생성합니다. 전송 버튼을 사용하면 채팅룸에서 메시지를 쉽게 보낼 수 있습니다. 또한 연결이 끊겼거나 채팅 세션이 만료 등과 같이 메시지를 전송 가능 여부 및 시기를 시각적으로 표시하는 역할을 합니다.

먼저, Chatterbox 프로젝트의 `src` 디렉터리에 새 파일을 생성하고 이름을 `SendButton`로 지정합니다. 그런 다음 채팅 애플리케이션용 버튼을 표시할 구성 요소를 생성합니다. `SendButton`을 내보내고 `App`으로 가져옵니다. 비어 있는 `<div></div>` 사이에 `<SendButton />`을 추가합니다.

------
#### [ TypeScript ]

```
// SendButton.tsx

import React from 'react';

interface Props {
  onPress?: () => void;
  disabled?: boolean;
}

export const SendButton = ({ onPress, disabled }: Props) => {
  return (
    <button disabled={disabled} onClick={onPress}>
      Send
    </button>
  );
};

// App.tsx

import { SendButton } from './SendButton';

// ...

return (
  <div>
    <div>Connection State: {connectionState}</div>
    <SendButton />
  </div>
);
```

------
#### [ JavaScript ]

```
// SendButton.jsx

import React from 'react';

export const SendButton = ({ onPress, disabled }) => {
  return (
    <button disabled={disabled} onClick={onPress}>
      Send
    </button>
  );
};

// App.jsx

import { SendButton } from './SendButton';

// ...

return (
  <div>
    <div>Connection State: {connectionState}</div>
    <SendButton />
  </div>
);
```

------

다음으로 `App`에서 `onMessageSend`라는 함수를 정의하고 이를 `SendButton onPress` 속성에 전달합니다. `isSendDisabled`라는 다른 변수를 정의하여(방이 연결되어 있지 않을 때 메시지를 보내지 못하도록 함) `SendButton disabled` 속성에 전달합니다.

```
// App.jsx / App.tsx

// ...

const onMessageSend = () => {};

const isSendDisabled = connectionState !== 'connected';

return (
  <div>
    <div>Connection State: {connectionState}</div>
    <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
  </div>
);

// ...
```

## 메시지 입력 생성
<a name="chat-js-rooms-message-input"></a>

Chatterbox 메시지 표시줄은 채팅룸에 메시지를 보내기 위해 상호 작용하는 구성 요소입니다. 일반적으로 메시지 작성을 위한 텍스트 입력과 메시지를 보내기 위한 버튼을 포함합니다.

`MessageInput` 구성 요소를 생성하려면 먼저 `src` 디렉터리에 새 파일을 생성하고 이름을 `MessageInput`로 지정합니다. 그런 다음 채팅 애플리케이션용 입력을 표시할 통제된 입력 구성 요소를 생성합니다. `MessageInput`을 내보내고 `App`로 가져옵니다(`<SendButton />` 위로).

기본값으로 빈 문자열이 있는 `useState` 후크를 사용하여 `messageToSend`라는 새 상태를 생성합니다. 앱 본문에서 `messageToSend`를 `MessageInput`의 `value`로 전달하고 `onMessageChange` 속성에 `setMessageToSend`를 전달합니다.

------
#### [ TypeScript ]

```
// MessageInput.tsx

import * as React from 'react';

interface Props {
  value?: string;
  onValueChange?: (value: string) => void;
}

export const MessageInput = ({ value, onValueChange }: Props) => {
  return (
    <input type="text" value={value} onChange={(e) => onValueChange?.(e.target.value)} placeholder="Send a message" />
  );
};


// App.tsx

// ...  

import { MessageInput } from './MessageInput';

// ...

export default function App() {
  const [messageToSend, setMessageToSend] = useState('');

// ...

return (
  <div>
    <h4>Connection State: {connectionState}</h4>
    <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
    <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
  </div>
);
```

------
#### [ JavaScript ]

```
// MessageInput.jsx

import * as React from 'react';

export const MessageInput = ({ value, onValueChange }) => {
  return (
    <input type="text" value={value} onChange={(e) => onValueChange?.(e.target.value)} placeholder="Send a message" />
  );
};

// App.jsx

// ...  

import { MessageInput } from './MessageInput';

// ...

export default function App() {
  const [messageToSend, setMessageToSend] = useState('');

// ...


return (
  <div>
    <h4>Connection State: {connectionState}</h4>
    <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
    <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
  </div>
);
```

------

## 다음 단계
<a name="chat-js-rooms-next-steps"></a>

이제 Chatterbox용 메시지 표시줄 구축을 완료했으므로 이 JavaScript 자습서의 2부인 [메시지 및 이벤트](chat-sdk-js-tutorial-messages-events.md)로 이동하세요.

# IVS Chat Client Messaging SDK: JavaScript 자습서 2부: 메시지 및 이벤트
<a name="chat-sdk-js-tutorial-messages-events"></a>

이 자습서의 두 번째(마지막) 부분은 여러 섹션으로 나뉩니다.

1. [채팅 메시지 이벤트 구독](#chat-js-messages-events-subscribe)

1. [받은 메시지 보기](#chat-js-messages-events-show)

   1.  [메시지 구성 요소 생성](#chat-js-messages-create-component)

   1. [현재 사용자가 전송한 메시지 인식](#chat-js-messages-recognize)

   1. [메시지 목록 구성 요소 생성](#chat-js-messages-create-list-component)

   1. [채팅 메시지 목록 렌더링](#chat-js-messages-render-list)

1. [채팅룸에서 작업 수행](#chat-js-messages-events-room-actions)

   1. [메시지 전송](#chat-js-room-actions-sending-message)

   1. [메시지 삭제](#chat-js-room-actions-deleting-message)

1. [다음 단계](#chat-js-messages-events-next-steps)

**참고**: 경우에 따라 JavaScript와 TypeScript의 코드 예제가 동일하므로 서로 합쳐져 있습니다.

전체 SDK 설명서를 보려면 우선 [Amazon IVS Chat Client Messaging SDK](chat-sdk.md)(Amazon IVS Chat 사용 설명서**에서 참조) 및 [Chat Client Messaging: SDK for JavaScript Reference](https://aws.github.io/amazon-ivs-chat-messaging-sdk-js/latest/)(GitHub)를 참조하세요.

## 사전 조건
<a name="chat-js-messages-events-prerequisite"></a>

이 자습서의 1부인 [채팅룸](chat-sdk-js-tutorial-chat-rooms.md)을 완료해야 합니다.

## 채팅 메시지 이벤트 구독
<a name="chat-js-messages-events-subscribe"></a>

`ChatRoom` 인스턴스는 채팅룸에서 이벤트가 발생할 때 이벤트를 사용하여 통신합니다. 채팅 환경을 구현하기 위해서는 우선 연결된 방에서 다른 사람이 메시지를 보내고 있을 때 이를 사용자에게 보여줄 수 있어야 합니다.

여기서 채팅 메시지 이벤트를 구독합니다. 다음 단계에서는 생성한 메시지 목록을 업데이트하는 방법에 대해 살펴보겠습니다. 해당 목록은 각 메시지/이벤트마다 업데이트됩니다.

`App`의 `useEffect` 후크 내에서 모든 메시지 이벤트를 구독합니다.

```
// App.tsx / App.jsx

useEffect(() => {
  // ...
  const unsubscribeOnMessageReceived = room.addListener('message', (message) => {});

  return () => {
    // ...
    unsubscribeOnMessageReceived();
  };
}, []);
```

## 받은 메시지 보기
<a name="chat-js-messages-events-show"></a>

메시지를 받는 것은 채팅 환경의 핵심 부분입니다. Chat JS SDK를 사용하여 채팅룸에 연결된 다른 사용자로부터 이벤트를 쉽게 수신하도록 코드를 설정할 수 있습니다.

나중에 여기서 생성한 구성 요소를 활용하여 채팅룸에서 작업을 수행하는 방법을 보여 드리겠습니다.

`App`에서 `messages`라는 `ChatMessage` 배열 형식으로 `messages`라는 상태를 정의합니다.

------
#### [ TypeScript ]

```
// App.tsx

// ...

import { ChatRoom, ChatMessage, ConnectionState } from 'amazon-ivs-chat-messaging';

export default function App() {
  const [messages, setMessages] = useState<ChatMessage[]>([]);

  //...
}
```

------
#### [ JavaScript ]

```
// App.jsx

// ...

export default function App() {
  const [messages, setMessages] = useState([]);

  //...
}
```

------

다음으로 `message` 리스너 함수에서 `messages` 배열에 `message`를 추가합니다.

```
// App.jsx / App.tsx

// ...

const unsubscribeOnMessageReceived = room.addListener('message', (message) => {
  setMessages((msgs) => [...msgs, message]);
});

// ...
```

아래에서는 받은 메시지를 표시하는 작업을 단계별로 설명합니다.

1.  [메시지 구성 요소 생성](#chat-js-messages-create-component)

1. [현재 사용자가 전송한 메시지 인식](#chat-js-messages-recognize)

1. [메시지 목록 구성 요소 생성](#chat-js-messages-create-list-component)

1. [채팅 메시지 목록 렌더링](#chat-js-messages-render-list)

### 메시지 구성 요소 생성
<a name="chat-js-messages-create-component"></a>

`Message` 구성 요소는 채팅룸에서 받은 메시지의 내용의 렌더링(rendering)을 담당합니다. 이 섹션에서는 `App`에서 개별 채팅 메시지를 렌더링하기 위한 메시지 구성 요소를 생성합니다.

`src` 디렉터리에 새 파일을 생성하고 이름을 `Message`로 지정합니다. 이 구성 요소의 `ChatMessage` 형식을 전달하고 `ChatMessage` 속성에서 `content` 문자열을 전달하여 채팅룸 메시지 리스너에서 받은 메시지 텍스트를 표시합니다. 프로젝트 탐색기에서 `Message`로 이동합니다.

------
#### [ TypeScript ]

```
// Message.tsx

import * as React from 'react';
import { ChatMessage } from 'amazon-ivs-chat-messaging';

type Props = {
  message: ChatMessage;
}

export const Message = ({ message }: Props) => {
  return (
    <div style={{ backgroundColor: 'silver', padding: 6, borderRadius: 10, margin: 10 }}>
      <p>{message.content}</p>
    </div>
  );
};
```

------
#### [ JavaScript ]

```
// Message.jsx

import * as React from 'react';

export const Message = ({ message }) => {
  return (
    <div style={{ backgroundColor: 'silver', padding: 6, borderRadius: 10, margin: 10 }}>
      <p>{message.content}</p>
    </div>
  );
};
```

------

팁: 이 구성 요소를 사용하여 메시지 행에 렌더링하려는 다양한 속성(예: 아바타 URL, 사용자 이름, 메시지가 전송된 시각의 타임스탬프)을 저장할 수 있습니다.

### 현재 사용자가 전송한 메시지 인식
<a name="chat-js-messages-recognize"></a>

현재 사용자가 전송한 메시지를 인식하려면 코드를 수정하고 현재 사용자의 `userId`를 저장하기 위한 React 컨텍스트를 만듭니다.

`src` 디렉터리에 새 파일을 생성하고 이름을 `UserContext`로 지정합니다.

------
#### [ TypeScript ]

```
// UserContext.tsx

import React, { ReactNode, useState, useContext, createContext } from 'react';

type UserContextType = {
  userId: string;
  setUserId: (userId: string) => void;
};

const UserContext = createContext<UserContextType | undefined>(undefined);

export const useUserContext = () => {
  const context = useContext(UserContext);

  if (context === undefined) {
    throw new Error('useUserContext must be within UserProvider');
  }

  return context;
};

type UserProviderType = {
  children: ReactNode;
}

export const UserProvider = ({ children }: UserProviderType) => {
  const [userId, setUserId] = useState('Mike');

  return <UserContext.Provider value={{ userId, setUserId }}>{children}</UserContext.Provider>;
};
```

------
#### [ JavaScript ]

```
// UserContext.jsx

import React, { useState, useContext, createContext } from 'react';

const UserContext = createContext(undefined);

export const useUserContext = () => {
  const context = useContext(UserContext);

  if (context === undefined) {
    throw new Error('useUserContext must be within UserProvider');
  }

  return context;
};

export const UserProvider = ({ children }) => {
  const [userId, setUserId] = useState('Mike');

  return <UserContext.Provider value={{ userId, setUserId }}>{children}</UserContext.Provider>;
};
```

------

참고: 여기서는 `useState` 후크를 사용하여 `userId` 값을 저장했습니다. 향후에는 사용자 컨텍스트를 변경하거나 로그인 용도로 `setUserId`를 사용할 수 있습니다.

다음으로 이전에 생성한 컨텍스트를 사용하여 `userId`를 `tokenProvider`에 전달된 첫 번째 파라미터로 교체합니다.

```
// App.jsx / App.tsx

// ...

import { useUserContext } from './UserContext';

// ...


export default function App() {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const { userId } = useUserContext();
  const [room] = useState(
    () =>
      new ChatRoom({
        regionOrUrl: process.env.REGION,
        tokenProvider: () => tokenProvider(userId, ['SEND_MESSAGE']),
      }),
  );

  // ...
}
```

`Message` 구성 요소에서 이전에 생성한 `UserContext`를 사용하고, `isMine` 변수를 선언하고, 발신자의 `userId`와 컨텍스트의 `userId`를 일치시키고, 현재 사용자에게 다양한 스타일의 메시지를 적용하세요.

------
#### [ TypeScript ]

```
// Message.tsx

import * as React from 'react';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { useUserContext } from './UserContext';

type Props = {
  message: ChatMessage;
}

export const Message = ({ message }: Props) => {
  const { userId } = useUserContext();

  const isMine = message.sender.userId === userId;

  return (
    <div style={{ backgroundColor: isMine ? 'lightblue' : 'silver', padding: 6, borderRadius: 10, margin: 10 }}>
      <p>{message.content}</p>
    </div>
  );
};
```

------
#### [ JavaScript ]

```
// Message.jsx

import * as React from 'react';
import { useUserContext } from './UserContext';

export const Message = ({ message }) => {
  const { userId } = useUserContext();

  const isMine = message.sender.userId === userId;

  return (
    <div style={{ backgroundColor: isMine ? 'lightblue' : 'silver', padding: 6, borderRadius: 10, margin: 10 }}>
      <p>{message.content}</p>
    </div>
  );
};
```

------

### 메시지 목록 구성 요소 생성
<a name="chat-js-messages-create-list-component"></a>

`MessageList` 구성 요소는 시간이 지남에 따라 채팅룸의 대화를 표시하는 것을 담당합니다. `MessageList` 파일은 모든 메시지를 담는 컨테이너입니다. `Message`는 `MessageList`에 있는 한 줄입니다.

`src` 디렉터리에 새 파일을 생성하고 이름을 `MessageList`로 지정합니다. `ChatMessage` 배열 형식의 `messages`로 `Props`을 정의합니다. 본문 내에서 `messages` 속성을 매핑하고 `Message` 구성 요소에 `Props`를 전달합니다.

------
#### [ TypeScript ]

```
// MessageList.tsx

import React from 'react';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { Message } from './Message';

interface Props {
  messages: ChatMessage[];
}

export const MessageList = ({ messages }: Props) => {
  return (
    <div>
      {messages.map((message) => (
        <Message key={message.id} message={message}/>
      ))}
    </div>
  );
};
```

------
#### [ JavaScript ]

```
// MessageList.jsx

import React from 'react';
import { Message } from './Message';

export const MessageList = ({ messages }) => {
  return (
    <div>
      {messages.map((message) => (
        <Message key={message.id} message={message} />
      ))}
    </div>
  );
};
```

------

### 채팅 메시지 목록 렌더링
<a name="chat-js-messages-render-list"></a>

이제 새 `MessageList`를 기본 `App` 구성 요소로 가져옵니다.

```
// App.jsx / App.tsx

import { MessageList } from './MessageList';
// ...

return (
  <div style={{ display: 'flex', flexDirection: 'column', padding: 10 }}>
    <h4>Connection State: {connectionState}</h4>
    <MessageList messages={messages} />
    <div style={{ flexDirection: 'row', display: 'flex', width: '100%', backgroundColor: 'red' }}>
      <MessageInput value={messageToSend} onValueChange={setMessageToSend} />
      <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
    </div>
  </div>
);

// ...
```

이제 `App`이 채팅룸에서 받은 메시지를 렌더링하기 위한 퍼즐 조각이 모두 다 준비되었습니다. 생성한 구성 요소를 활용하여 채팅룸에서 작업을 수행하는 방법을 보려면 다음을 계속합니다.

## 채팅룸에서 작업 수행
<a name="chat-js-messages-events-room-actions"></a>

채팅룸 내에서 메시지를 보내고 중재자 작업을 수행하는 것은 채팅룸과 상호 작용하는 주요 방법 중 일부입니다. 여기서는 다양한 `ChatRequest` 객체를 사용하여 Chatterbox에서 메시지 전송, 메시지 삭제, 다른 사용자 연결 끊기와 같은 일반적인 작업을 수행하는 방법을 알아봅니다.

채팅룸의 모든 작업은 공통적인 패턴을 따릅니다. 채팅룸에서 수행하는 모든 작업에는 해당하는 요청 객체가 있습니다. 각 요청에는 요청 확인 시 받는 해당 응답 객체가 있습니다.

채팅 토큰을 생성할 때 사용자에게 올바른 권한이 부여되면 사용자는 요청 객체를 통해 이에 해당하는 작업을 성공적으로 수행하여 채팅룸에서 어떤 요청을 수행할 수 있는지 확인할 수 있습니다.

아래에서는 [메시지를 전송](#chat-js-room-actions-sending-message)하고 [메시지를 삭제](#chat-js-room-actions-deleting-message)하는 방법을 설명합니다.

### 메시지 전송
<a name="chat-js-room-actions-sending-message"></a>

`SendMessageRequest` 클래스를 사용하여 채팅룸에서 메시지를 보낼 수 있습니다 이 단계에서는 [메시지 입력 생성](chat-sdk-js-tutorial-chat-rooms.md#chat-js-rooms-message-input)(이 자습서의 1부)에서 생성한 구성 요소를 통해 메시지 요청을 보내도록 `App`을 수정합니다.

우선, `useState` 후크를 사용하여 `isSending`라는 새 boolean 속성을 정의합니다. 이속성을 통해 `isSendDisabled` 상수를 사용하여 `button` HTML 요소의 비활성화 상태를 전환할 수 있습니다. `SendButton`의 이벤트 핸들러(event handler)에서 `messageToSend`의 값을 지우고 `isSending`을 true로 설정합니다.

이 버튼으로 API를 호출하므로 `isSending` boolean을 추가하면 요청이 완료될 때까지 `SendButton`에서 사용자 상호 작용을 비활성화함으로써 여러 API 호출이 동시에 발생하는 것을 방지할 수 있습니다.**

```
// App.jsx / App.tsx

// ...

const [isSending, setIsSending] = useState(false);

// ...

const onMessageSend = () => {
  setIsSending(true);
  setMessageToSend('');
};

// ...

const isSendDisabled = connectionState !== 'connected' || isSending;

// ...
```

새 `SendMessageRequest` 인스턴스를 생성하고 생성자에 메시지 콘텐츠를 전달하여 요청을 준비합니다. `isSending`과 `messageToSend` 상태를 설정한 후 `sendMessage` 메서드를 호출하여 채팅룸으로 요청을 보냅니다. 마지막으로 요청 확인 또는 거절 수신 시`isSending` 플래그를 지웁니다.

------
#### [ TypeScript ]

```
// App.tsx

// ...
import { ChatMessage, ChatRoom, ConnectionState, SendMessageRequest } from 'amazon-ivs-chat-messaging'
// ...

const onMessageSend = async () => {
  const request = new SendMessageRequest(messageToSend);
  setIsSending(true);
  setMessageToSend('');

  try {
    const response = await room.sendMessage(request);
  } catch (e) {
    console.log(e);
    // handle the chat error here...
  } finally {
    setIsSending(false);
  }
};

// ...
```

------
#### [ JavaScript ]

```
// App.jsx

// ...
import { ChatRoom, SendMessageRequest } from 'amazon-ivs-chat-messaging'
// ...

const onMessageSend = async () => {
  const request = new SendMessageRequest(messageToSend);
  setIsSending(true);
  setMessageToSend('');

  try {
    const response = await room.sendMessage(request);
  } catch (e) {
    console.log(e);
    // handle the chat error here...
  } finally {
    setIsSending(false);
  }
};

// ...
```

------

Chatterbox를 실행해 보세요. `MessageInput`으로 초안을 작성하고 `SendButton`을 탭하여 메시지를 보내 보세요. 이전에 생성한 `MessageList` 내에 전송한 메시지가 렌더링된 것을 볼 수 있을 것입니다.

### 메시지 삭제
<a name="chat-js-room-actions-deleting-message"></a>

채팅룸에서 메시지를 삭제하려면 적절한 기능이 있어야 합니다. 기능은 채팅룸에 인증할 때 사용하는 채팅 토큰을 초기화하는 동안에 부여됩니다. 이 섹션의 목적에 따라 [로컬 인증/권한 부여 서버 설정](chat-sdk-js-tutorial-chat-rooms.md#chat-js-rooms-auth-server)(이 자습서의 1부)의 `ServerApp`에서 중재자 기능을 지정할 수 있습니다. 이 작업은 [토큰 공급자 구축](chat-sdk-js-tutorial-chat-rooms.md#chat-js-rooms-token-provider)(1부)에서 생성한 `tokenProvider` 객체를 사용하여 앱에서 수행됩니다.

여기서는 메시지를 삭제하는 함수를 추가하여 `Message`를 수정합니다.

먼저 `App.tsx`를 열고 `DELETE_MESSAGE` 기능을 추가합니다.(`capabilities`는 `tokenProvider` 함수의 두 번째 파라미터입니다.)

참고: 이는 `ServerApp`이 IVS Chat API에 결과 채팅 토큰과 연결된 사용자는 채팅룸에서 메시지를 삭제할 수 있음을 알리는 방법입니다. 실제 상황에서는 서버 앱 인프라의 사용자 기능을 관리하기 위한 백엔드 로직이 더 복잡할 수 있습니다.

------
#### [ TypeScript ]

```
// App.tsx

// ...

const [room] = useState( () =>
    new ChatRoom({
      regionOrUrl: process.env.REGION as string,
      tokenProvider: () => tokenProvider(userId, ['SEND_MESSAGE', 'DELETE_MESSAGE']),
    }),
);

// ...
```

------
#### [ JavaScript ]

```
// App.jsx

// ...

const [room] = useState( () =>
  new ChatRoom({
    regionOrUrl: process.env.REGION,
    tokenProvider: () => tokenProvider(userId, ['SEND_MESSAGE', 'DELETE_MESSAGE']),
  }),
);

// ...
```

------

다음 단계에서는 삭제 버튼을 표시하도록 `Message`를 업데이트합니다.

`Message`를 열고 초기 값이 `false`인 `useState` 후크를 사용하여 `isDeleting`라는 새 boolean 상태를 정의합니다. 이 상태를 사용하여 `isDeleting`의 현재 상태에 따라 `Button`의 내용이 달라지도록 업데이트합니다. `isDeleting`이 true일 때 버튼을 비활성화하면 동시에 메시지 삭제 요청을 두 번 시도할 수 없습니다.

------
#### [ TypeScript ]

```
// Message.tsx

import React, { useState } from 'react';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { useUserContext } from './UserContext';

type Props = {
  message: ChatMessage;
}

export const Message = ({ message }: Props) => {
  const { userId } = useUserContext();
  const [isDeleting, setIsDeleting] = useState(false);

  const isMine = message.sender.userId === userId;

  return (
    <div style={{ backgroundColor: isMine ? 'lightblue' : 'silver', padding: 6, borderRadius: 10, margin: 10 }}>
      <p>{message.content}</p>
      <button disabled={isDeleting}>Delete</button>
    </div>
  );
};
```

------
#### [ JavaScript ]

```
// Message.jsx

import React from 'react';
import { useUserContext } from './UserContext';

export const Message = ({ message }) => {
  const { userId } = useUserContext();
  const [isDeleting, setIsDeleting] = useState(false);

  return (
    <div style={{ backgroundColor: isMine ? 'lightblue' : 'silver', padding: 6, borderRadius: 10, margin: 10 }}>
      <p>{message.content}</p>
      <button disabled={isDeleting}>Delete</button>
    </div>
  );
};
```

------

문자열을 파라미터 중 하나로 받아 `Promise`를 반환하는 `onDelete`라는 새 함수를 정의합니다. `Button` 작업 종료의 본문에서 `setIsDeleting`를 사용하여 `onDelete`를 호출하기 전후에 `isDeleting` boolean을 전환합니다. 문자열 파라미터의 경우 구성 요소 메시지 ID를 전달합니다.

------
#### [ TypeScript ]

```
// Message.tsx

import React, { useState } from 'react';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { useUserContext } from './UserContext';

export type Props = {
  message: ChatMessage;
  onDelete(id: string): Promise<void>;
};

export const Message = ({ message onDelete }: Props) => {
  const { userId } = useUserContext();
  const [isDeleting, setIsDeleting] = useState(false);
  const isMine = message.sender.userId === userId;
  const handleDelete = async () => {
    setIsDeleting(true);
    try {
      await onDelete(message.id);
    } catch (e) {
      console.log(e);
      // handle chat error here...
    } finally {
      setIsDeleting(false);
    }
  };

  return (
    <div style={{ backgroundColor: isMine ? 'lightblue' : 'silver', padding: 6, borderRadius: 10, margin: 10 }}>
      <p>{content}</p>
      <button onClick={handleDelete} disabled={isDeleting}>
        Delete
      </button>
    </div>
  );
};
```

------
#### [ JavaScript ]

```
// Message.jsx

import React, { useState } from 'react';
import { useUserContext } from './UserContext';

export const Message = ({ message, onDelete }) => {
  const { userId } = useUserContext();
  const [isDeleting, setIsDeleting] = useState(false);
  const isMine = message.sender.userId === userId;
  const handleDelete = async () => {
    setIsDeleting(true);
    try {
      await onDelete(message.id);
    } catch (e) {
      console.log(e);
      // handle the exceptions here...
    } finally {
      setIsDeleting(false);
    }
  };

  return (
    <div style={{ backgroundColor: 'silver', padding: 6, borderRadius: 10, margin: 10 }}>
      <p>{message.content}</p>
      <button onClick={handleDelete} disabled={isDeleting}>
        Delete
      </button>
    </div>
  );
};
```

------

다음으로 `Message` 구성 요소의 최신 변경 사항을 반영하도록 `MessageList`를 업데이트합니다.

`MessageList`를 열고 문자열을 파라미터 중 하나로 받아 `Promise`를 반환하는 `onDelete`라는 새 함수를 정의합니다. `Message`를 업데이트하고 `Message`의 속성을 통해 전달합니다. 새로운 종료의 문자열 파라미터는 삭제하려는 메시지의 ID이며, 이 ID는 `Message`으로부터 전달됩니다.

------
#### [ TypeScript ]

```
// MessageList.tsx

import * as React from 'react';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { Message } from './Message';

interface Props {
  messages: ChatMessage[];
  onDelete(id: string): Promise<void>;
}

export const MessageList = ({ messages, onDelete }: Props) => {
  return (
    <>
      {messages.map((message) => (
        <Message key={message.id} onDelete={onDelete} content={message.content} id={message.id} />
      ))}
    </>
  );
};
```

------
#### [ JavaScript ]

```
// MessageList.jsx

import * as React from 'react';
import { Message } from './Message';

export const MessageList = ({ messages, onDelete }) => {
  return (
    <>
      {messages.map((message) => (
        <Message key={message.id} onDelete={onDelete} content={message.content} id={message.id} />
      ))}
    </>
  );
};
```

------

다음으로 `MessageList`의 최신 변경 사항을 반영하도록 `App`을 업데이트합니다.

`App`에서 `onDeleteMessage`라는 함수를 정의하고 이를 `MessageList onDelete` 속성에 전달합니다.

------
#### [ TypeScript ]

```
// App.tsx

// ...

const onDeleteMessage = async (id: string) => {};

return (
  <div style={{ display: 'flex', flexDirection: 'column', padding: 10 }}>
    <h4>Connection State: {connectionState}</h4>
    <MessageList onDelete={onDeleteMessage} messages={messages} />
    <div style={{ flexDirection: 'row', display: 'flex', width: '100%' }}>
      <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
      <SendButton disabled={isSendDisabled} onSendPress={onMessageSend} />
    </div>
  </div>
);

// ...
```

------
#### [ JavaScript ]

```
// App.jsx

// ...

const onDeleteMessage = async (id) => {};

return (
  <div style={{ display: 'flex', flexDirection: 'column', padding: 10 }}>
    <h4>Connection State: {connectionState}</h4>
    <MessageList onDelete={onDeleteMessage} messages={messages} />
    <div style={{ flexDirection: 'row', display: 'flex', width: '100%' }}>
      <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
      <SendButton disabled={isSendDisabled} onSendPress={onMessageSend} />
    </div>
  </div>
);

// ...
```

------

관련 메시지 ID를 생성자 파라미터에 전달하는 `DeleteMessageRequest`의 새 인스턴스를 생성하여 요청을 준비하고 위에서 준비한 요청을 수락하는 `deleteMessage`를 호출합니다.

------
#### [ TypeScript ]

```
// App.tsx

// ...

const onDeleteMessage = async (id: string) => {
  const request = new DeleteMessageRequest(id);
  await room.deleteMessage(request);
};

// ...
```

------
#### [ JavaScript ]

```
// App.jsx

// ...

const onDeleteMessage = async (id) => {
  const request = new DeleteMessageRequest(id);
  await room.deleteMessage(request);
};

// ...
```

------

다음으로 방금 삭제한 메시지가 생략된 새 메시지 목록을 반영하도록 `messages` 상태를 업데이트합니다.

`useEffect` 후크에서`messageDelete` 이벤트를 수신하고 `message` 파라미터와 일치하는 ID가 있는 메시지를 삭제하여 `messages` 상태 배열을 업데이트합니다.

참고: 현재 사용자나 방에 있는 다른 사용자가 메시지를 삭제하는 경우 `messageDelete` 이벤트가 발생할 수 있습니다. 이벤트 핸들러에서(`deleteMessage` 요청 옆 대신) 이벤트를 처리하면 메시지 삭제 처리를 통합할 수 있습니다.

```
// App.jsx / App.tsx

// ...

const unsubscribeOnMessageDeleted = room.addListener('messageDelete', (deleteMessageEvent) => {
  setMessages((prev) => prev.filter((message) => message.id !== deleteMessageEvent.id));
});

return () => {
  // ...

  unsubscribeOnMessageDeleted();
};

// ...
```

이제 채팅 앱의 채팅룸에서 사용자를 삭제할 수 있습니다.

## 다음 단계
<a name="chat-js-messages-events-next-steps"></a>

테스트의 일환으로 방에서 다른 사용자의 연결을 끊는 등 다양한 작업을 시도해보세요.

# IVS 챗 클라이언트 메시징 SDK: React Native 자습서 1부: 채팅룸
<a name="chat-sdk-react-tutorial-chat-rooms"></a>

본 문서는 두 파트로 구성된 자습서 중 첫 번째 파트에 해당하는 자습서입니다. 이 자습서에서는 React Native를 통해 완전한 기능을 갖춘 앱을 구축하여 Amazon IVS Chat Client Messaging JavaScript SDK로 작업하기 위한 필수 사항을 설명하고 있습니다. 여기에서 지칭하는 앱은 *Chatterbox*라고 합니다.

이 자습서는 숙련된 개발자이지만 Amazon IVS Chat Messaging SDK를 처음 접하는 사용자를 위해 작성되었습니다. 사용자는 TypeScript 또는 JavaScript 프로그래밍 언어, React Native 라이브러리에 대한 친숙도가 있어야 합니다.

간략하게 나타내자면 Amazon IVS Chat Client Messaging JavaScript SDK를 Chat JS SDK라고 합니다.

**참고**: 경우에 따라 JavaScript와 TypeScript의 코드 예제가 동일하므로 서로 합쳐져 있습니다.

이 자습서의 첫 번째 부분은 여러 섹션으로 나뉩니다.

1. [로컬 인증/권한 부여 서버 설정](#chat-react-rooms-auth-server)

1. [Chatterbox 프로젝트 생성](#chat-react-rooms-chatterbox)

1. [채팅 룸에 연결](#chat-react-rooms-connect)

1. [토큰 공급자 구축](#chat-react-rooms-token-provider)

1. [연결 업데이트 관찰](#chat-react-rooms-connection-state)

1. [전송 버튼 구성 요소 생성](#chat-react-rooms-send-button)

1. [메시지 입력 생성](#chat-react-rooms-message-input)

1. [다음 단계](#chat-react-rooms-next-steps)

## 사전 조건
<a name="chat-react-rooms-prerequisites"></a>
+ 사용자는 TypeScript 또는 JavaScript 및 React Native 라이브러리에 익숙하여야 합니다. React Native에 익숙하지 않다면 [React Native 소개](https://reactnative.dev/docs/tutorial)에서 기본 사항을 검토해 보시기 바랍니다.
+ [Amazon IVS Chat 시작하기](getting-started-chat.md)를 읽고 이해합니다.
+ 기존 IAM 정책에 정의된 CreateChatToken 및 CreateRoom 기능을 사용하여 AWS IAM 사용자를 생성합니다. ([Amazon IVS Chat 시작하기](getting-started-chat.md)를 참조하세요.)
+ 이 사용자의 비밀/액세스 키가 AWS 보안 인증 파일에 저장되어 있는지 확인합니다. 지침은 [AWS CLI 사용 설명서](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html)(특히 [구성 및 보안 인증 파일 설정](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html))를 참조합니다.
+ 채팅룸을 생성하고 ARN을 저장합니다. [Amazon IVS Chat 시작하기](getting-started-chat.md)을(를) 참조하세요. (ARN을 저장하지 않은 경우 나중에 콘솔이나 Chat API를 사용하여 조회할 수 있습니다.)
+ NPM 또는 Yarn 패키지 관리자를 사용하여 Node.js 14\$1 환경을 설치합니다.

## 로컬 인증/권한 부여 서버 설정
<a name="chat-react-rooms-auth-server"></a>

백엔드 애플리케이션은 채팅룸을 생성하고 Chat JS SDK가 채팅룸의 클라이언트를 인증하고 권한을 부여하는 데 필요한 채팅 토큰을 생성하는 일을 맡습니다. AWS 키는 모바일 앱에 안전하게 저장할 수 없으므로 자체 백엔드를 사용해야 합니다. 실력 좋은 공격자는 이러한 키를 추출하여 AWS 계정에 액세스할 수 있습니다.

Amazon IVS 채팅 시작하기에서 [채팅 토큰 생성](getting-started-chat-auth.md)을 참조하세요.** 순서도에서 볼 수 있듯이 서버 측 애플리케이션은 채팅 토큰 생성을 담당합니다. 즉, 앱은 서버 측 애플리케이션에서 채팅 토큰을 요청하여 채팅 토큰을 생성하는 자체 수단을 제공해야 합니다.

이 섹션에서는 백엔드에서 토큰 공급자를 생성하는 기본 사항에 대해 알아봅니다. AWS 환경을 사용하여 채팅 토큰 생성을 관리하는 로컬 서버를 만들기 위해 Express 프레임워크를 사용합니다.

NPM을 사용하여 빈 `npm` 프로젝트를 생성합니다. 애플리케이션을 보관할 디렉터리를 생성하고 이를 작업 디렉터리로 만듭니다.

```
$ mkdir backend & cd backend
```

`npm init`을 사용하여 애플리케이션의 `package.json` 파일을 생성합니다.

```
$ npm init
```

이 명령은 애플리케이션의 이름 및 버전을 비롯한 여러 항목을 입력하라는 메시지를 표시합니다. 지금은 **RETURN** 키를 눌러 대부분의 기본값을 그대로 사용할 수 있습니다. 단, 다음은 예외입니다.

```
entry point: (index.js)
```

**RETURN** 키를 눌러 제안된 기본 파일 이름 `index.js`를 그대로 사용하거나 주 파일의 이름을 원하는 대로 입력합니다.

이제 필요한 종속 항목을 설치합니다.

```
$ npm install express aws-sdk cors dotenv
```

`aws-sdk`에는 루트 디렉터리에 있는 `.env`라는 파일에서 자동으로 로드되는 구성 환경 변수가 필요합니다. 이를 구성하려면 `.env`라는 새 파일을 생성하고 누락된 구성 정보를 입력합니다.

```
# .env

# The region to send service requests to.
AWS_REGION=us-west-2

# Access keys use an access key ID and secret access key
# that you use to sign programmatic requests to AWS.

# AWS access key ID.
AWS_ACCESS_KEY_ID=...

# AWS secret access key.
AWS_SECRET_ACCESS_KEY=...
```

이제 `npm init` 명령에서 위에 입력한 이름으로 루트 디렉터리에 진입점(entry-point) 파일을 생성합니다. 이 경우 `index.js`를 사용하고 필요한 모든 패키지를 가져옵니다.

```
// index.js
import express from 'express';
import AWS from 'aws-sdk';
import 'dotenv/config';
import cors from 'cors';
```

이제 `express`의 새 인스턴스를 생성합니다.

```
const app = express();
const port = 3000;

app.use(express.json());
app.use(cors({ origin: ['http://127.0.0.1:5173'] }));
```

그런 다음 토큰 공급자를 위한 첫 번째 엔드포인트 POST 메서드를 생성할 수 있습니다. 요청 본문에서 필수 파라미터(`roomId`, `userId`, `capabilities` 및 `sessionDurationInMinutes`)를 가져옵니다.

```
app.post('/create_chat_token', (req, res) => {
  const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {};
});
```

필수 필드의 유효성 검사를 추가합니다.

```
app.post('/create_chat_token', (req, res) => {
  const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {};

  if (!roomIdentifier || !userId) {
    res.status(400).json({ error: 'Missing parameters: `roomIdentifier`, `userId`' });
    return;
  }
});
```

POST 메서드를 준비한 후 인증/권한 부여의 핵심 기능을 위해 `aws-sdk`와 `createChatToken`을 통합합니다.

```
app.post('/create_chat_token', (req, res) => {
  const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {};

  if (!roomIdentifier || !userId || !capabilities) {
    res.status(400).json({ error: 'Missing parameters: `roomIdentifier`, `userId`, `capabilities`' });
    return;
  }

  ivsChat.createChatToken({ roomIdentifier, userId, capabilities, sessionDurationInMinutes }, (error, data) => {
    if (error) {
      console.log(error);
      res.status(500).send(error.code);
    } else if (data.token) {
      const { token, sessionExpirationTime, tokenExpirationTime } = data;
      console.log(`Retrieved Chat Token: ${JSON.stringify(data, null, 2)}`);

      res.json({ token, sessionExpirationTime, tokenExpirationTime });
    }
  });
});
```

파일 끝에 `express` 앱의 포트 리스너를 추가합니다.

```
app.listen(port, () => {
  console.log(`Backend listening on port ${port}`);
});
```

이제 프로젝트의 루트에서 다음 명령으로 서버를 실행할 수 있습니다.

```
$ node index.js
```

**팁**: 이 서버는 https://localhost:3000에서 URL 요청을 수락합니다.

## Chatterbox 프로젝트 생성
<a name="chat-react-rooms-chatterbox"></a>

먼저 `chatterbox`라는 React Native 프로젝트를 생성합니다. 다음 명령을 실행합니다.

```
npx create-expo-app
```

또는 TypeScript 템플릿을 사용하여 expo 프로젝트를 생성합니다.

```
npx create-expo-app -t expo-template-blank-typescript
```

[Node 패키지 관리자](https://www.npmjs.com/) 또는 [Yarn 패키지 관리자](https://yarnpkg.com/)를 통해 Chat Client Messaging JS SDK를 통합할 수 있습니다.
+ Npm: `npm install amazon-ivs-chat-messaging`
+ Yarn: `yarn add amazon-ivs-chat-messaging`

## 채팅 룸에 연결
<a name="chat-react-rooms-connect"></a>

여기서는 `ChatRoom`을 생성하고 비동기 메서드를 사용하여 연결합니다. `ChatRoom` 클래스는 Chat JS SDK에 대한 사용자 연결을 관리합니다. 채팅룸에 성공적으로 연결하려면 React 애플리케이션 내에서 `ChatToken`의 인스턴스를 제공해야 합니다.

기본 `chatterbox` 프로젝트에서 생성한 `App` 파일로 이동하여 함수 구성 요소가 반환하는 모든 내용을 삭제합니다. 미리 입력된 코드는 필요하지 않습니다. 이 시점에서 `App`은 거의 비어 있습니다.

**TypeScript/JavaScript**:

```
// App.tsx / App.jsx

import * as React from 'react';
import { Text } from 'react-native';

export default function App() {
  return <Text>Hello!</Text>;
}
```

새 `ChatRoom` 인스턴스를 생성하고 `useState` 후크를 사용하여 상태로 전달합니다. 이를 위해서는 `regionOrUrl`(채팅룸이 호스팅되는 AWS 리전) 및`tokenProvider`(이후 단계에서 생성되는 백엔드 인증/권한 부여 흐름에 사용됨)를 전달해야 합니다.

**중요**: [Amazon IVS Chat 시작하기](getting-started-chat-create-room.md)에서 방을 생성한 리전과 동일한 AWS 리전을 사용해야 합니다. API는 AWS 리전 서비스입니다. 지원되는 리전 및 Amazon IVS Chat HTTPS 서비스 엔드포인트 목록은 [Amazon IVS Chat 리전](https://docs.aws.amazon.com/general/latest/gr/ivs.html#ivs_region) 페이지를 참조하세요.

**TypeScript/JavaScript**:

```
// App.jsx / App.tsx

import React, { useState } from 'react';
import { Text } from 'react-native';
import { ChatRoom } from 'amazon-ivs-chat-messaging';

export default function App() {
  const [room] = useState(() =>
    new ChatRoom({
      regionOrUrl: process.env.REGION,
      tokenProvider: () => {},
    }),
  );

  return <Text>Hello!</Text>;
}
```

## 토큰 공급자 구축
<a name="chat-react-rooms-token-provider"></a>

다음 단계로 `ChatRoom` 생성자에 필요한 파라미터 없는`tokenProvider` 함수를 구축해야 합니다. 먼저, [로컬 인증/권한 부여 서버 설정](#chat-react-rooms-auth-server)에서 설정한 백엔드 애플리케이션에 POST 요청을 하는 `fetchChatToken` 함수를 생성하겠습니다. 채팅 토큰은 SDK가 성공적으로 채팅룸 연결을 설정하는 데 필요한 정보를 포함합니다. Chat API는 사용자의 ID, 채팅룸 내 기능 및 세션 기간을 검증하는 안전한 방법으로 이러한 토큰을 사용합니다.

프로젝트 탐색기에서 `fetchChatToken`이라는 새 TypeScript/JavaScript 파일을 생성합니다. `backend` 애플리케이션에 가져오기 요청을 구축하고 응답에서 `ChatToken` 객체를 반환합니다. 채팅 토큰을 생성하는 데 필요한 요청 본문 속성을 추가합니다. [Amazon 리소스 이름(ARN)](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html)에 정의된 규칙을 사용합니다. 이러한 속성은 [CreateChatToken 작업](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/API_CreateChatToken.html#API_CreateChatToken_RequestBody)에 문서화되어 있습니다.

**참고**: 여기서 사용하는 URL은 백엔드 애플리케이션을 실행했을 때 로컬 서버에서 생성한 URL과 동일한 URL입니다.

------
#### [ TypeScript ]

```
// fetchChatToken.ts

import { ChatToken } from 'amazon-ivs-chat-messaging';

type UserCapability = 'DELETE_MESSAGE' | 'DISCONNECT_USER' | 'SEND_MESSAGE';

export async function fetchChatToken(
  userId: string,
  capabilities: UserCapability[] = [],
  attributes?: Record<string, string>,
  sessionDurationInMinutes?: number,
): Promise<ChatToken> {
  const response = await fetch(`${process.env.BACKEND_BASE_URL}/create_chat_token`, {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      userId,
      roomIdentifier: process.env.ROOM_ID,
      capabilities,
      sessionDurationInMinutes,
      attributes
    }),
  });

  const token = await response.json();

  return {
    ...token,
    sessionExpirationTime: new Date(token.sessionExpirationTime),
    tokenExpirationTime: new Date(token.tokenExpirationTime),
  };
}
```

------
#### [ JavaScript ]

```
// fetchChatToken.js

export async function fetchChatToken(
  userId,
  capabilities = [],
  attributes,
  sessionDurationInMinutes) {
  const response = await fetch(`${process.env.BACKEND_BASE_URL}/create_chat_token`, {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      userId,
      roomIdentifier: process.env.ROOM_ID,
      capabilities,
      sessionDurationInMinutes,
      attributes
    }),
  });

  const token = await response.json();

  return {
    ...token,
    sessionExpirationTime: new Date(token.sessionExpirationTime),
    tokenExpirationTime: new Date(token.tokenExpirationTime),
  };
}
```

------

## 연결 업데이트 관찰
<a name="chat-react-rooms-connection-state"></a>

채팅 앱을 생성하기 위해서는 채팅룸 연결 상태 변화에 대해 필수적으로 대응해야 합니다. 관련 이벤트 구독부터 시작해 보겠습니다.

**TypeScript/JavaScript**:

```
// App.tsx / App.jsx

import React, { useState, useEffect } from 'react';
import { Text } from 'react-native';
import { ChatRoom } from 'amazon-ivs-chat-messaging';
import { fetchChatToken } from './fetchChatToken';

export default function App() {
  const [room] = useState(
    () =>
      new ChatRoom({
        regionOrUrl: process.env.REGION,
        tokenProvider: () => fetchChatToken('Mike', ['SEND_MESSAGE']),
      }),
  );

  useEffect(() => {
    const unsubscribeOnConnecting = room.addListener('connecting', () => {});
    const unsubscribeOnConnected = room.addListener('connect', () => {});
    const unsubscribeOnDisconnected = room.addListener('disconnect', () => {});

    return () => {
      // Clean up subscriptions.
      unsubscribeOnConnecting();
      unsubscribeOnConnected();
      unsubscribeOnDisconnected();
    };
  }, [room]);

  return <Text>Hello!</Text>;
}
```

다음으로 연결 상태를 읽을 수 있는 기능을 제공해야 합니다. `useState` 후크를 사용하여 `App`에서 로컬 상태를 생성하고 각 리스너 내에서 연결 상태를 설정합니다.

**TypeScript/JavaScript**:

```
// App.tsx / App.jsx

import React, { useState, useEffect } from 'react';
import { Text } from 'react-native';
import { ChatRoom, ConnectionState } from 'amazon-ivs-chat-messaging';
import { fetchChatToken } from './fetchChatToken';

export default function App() {  
  const [room] = useState(
    () =>
      new ChatRoom({
        regionOrUrl: process.env.REGION,
        tokenProvider: () => fetchChatToken('Mike', ['SEND_MESSAGE']),
      }),
  );
  const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected');

  useEffect(() => {
    const unsubscribeOnConnecting = room.addListener('connecting', () => {
      setConnectionState('connecting');
    });

    const unsubscribeOnConnected = room.addListener('connect', () => {
      setConnectionState('connected');
    });

    const unsubscribeOnDisconnected = room.addListener('disconnect', () => {
      setConnectionState('disconnected');
    });

    return () => {
      unsubscribeOnConnecting();
      unsubscribeOnConnected();
      unsubscribeOnDisconnected();
    };
  }, [room]);

  return <Text>Hello!</Text>;
}
```

연결 상태를 구독한 후 연결 상태를 표시하고 `useEffect` 후크 내의 `room.connect` 메서드를 사용하여 채팅룸에 연결합니다.

**TypeScript/JavaScript**:

```
// App.tsx / App.jsx

// ...

useEffect(() => {
  const unsubscribeOnConnecting = room.addListener('connecting', () => {
    setConnectionState('connecting');
  });

  const unsubscribeOnConnected = room.addListener('connect', () => {
    setConnectionState('connected');
  });

  const unsubscribeOnDisconnected = room.addListener('disconnect', () => {
    setConnectionState('disconnected');
  });

  room.connect();

  return () => {
    unsubscribeOnConnecting();
    unsubscribeOnConnected();
    unsubscribeOnDisconnected();
  };
}, [room]);

// ...

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
  </SafeAreaView>
);

const styles = StyleSheet.create({
  root: {
    flex: 1,
  }
});

// ...
```

채팅룸 연결을 성공적으로 구현했습니다.

## 전송 버튼 구성 요소 생성
<a name="chat-react-rooms-send-button"></a>

이 섹션에서는 연결 상태 별로 각기 다른 디자인의 전송(send) 버튼을 생성합니다. 전송 버튼을 사용하면 채팅룸에서 메시지를 쉽게 보낼 수 있습니다. 또한 연결이 끊겼거나 채팅 세션이 만료 등과 같이 메시지를 전송 가능 여부 및 시기를 시각적으로 표시하는 역할을 합니다.

먼저, Chatterbox 프로젝트의 `src` 디렉터리에 새 파일을 생성하고 이름을 `SendButton`로 지정합니다. 그런 다음 채팅 애플리케이션용 버튼을 표시할 구성 요소를 생성합니다. `SendButton`을 내보내고 `App`으로 가져옵니다. 비어 있는 `<View></View>` 사이에 `<SendButton />`을 추가합니다.

------
#### [ TypeScript ]

```
// SendButton.tsx

import React from 'react';
import { TouchableOpacity, Text, ActivityIndicator, StyleSheet } from 'react-native';

interface Props {
  onPress?: () => void;
  disabled: boolean;
  loading: boolean;
}

export const SendButton = ({ onPress, disabled, loading }: Props) => {
  return (
    <TouchableOpacity style={styles.root} disabled={disabled} onPress={onPress}>
      {loading ? <Text>Send</Text> : <ActivityIndicator />}
    </TouchableOpacity>
  );
};

const styles = StyleSheet.create({
  root: {
    width: 50,
    height: 50,
    borderRadius: 30,
    marginLeft: 10,
    justifyContent: 'center',
    alignContent: 'center',
  }
});

// App.tsx

import { SendButton } from './SendButton';

// ...

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <SendButton />
  </SafeAreaView>
);
```

------
#### [ JavaScript ]

```
// SendButton.jsx

import React from 'react';
import { TouchableOpacity, Text, ActivityIndicator, StyleSheet } from 'react-native';

export const SendButton = ({ onPress, disabled, loading }) => {
  return (
    <TouchableOpacity style={styles.root} disabled={disabled} onPress={onPress}>
      {loading ? <Text>Send</Text> : <ActivityIndicator />}
    </TouchableOpacity>
  );
};

const styles = StyleSheet.create({
  root: {
    width: 50,
    height: 50,
    borderRadius: 30,
    marginLeft: 10,
    justifyContent: 'center',
    alignContent: 'center',
  }
});

// App.jsx

import { SendButton } from './SendButton';

// ...

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <SendButton />
  </SafeAreaView>
);
```

------

다음으로 `App`에서 `onMessageSend`라는 함수를 정의하고 이를 `SendButton onPress` 속성에 전달합니다. `isSendDisabled`라는 다른 변수를 정의하여(방이 연결되어 있지 않을 때 메시지를 보내지 못하도록 함) `SendButton disabled` 속성에 전달합니다.

**TypeScript/JavaScript**:

```
// App.jsx / App.tsx

// ...

const onMessageSend = () => {};

const isSendDisabled = connectionState !== 'connected';

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
  </SafeAreaView>
);

// ...
```

## 메시지 입력 생성
<a name="chat-react-rooms-message-input"></a>

Chatterbox 메시지 표시줄은 채팅룸에 메시지를 보내기 위해 상호 작용하는 구성 요소입니다. 일반적으로 메시지 작성을 위한 텍스트 입력과 메시지를 보내기 위한 버튼을 포함합니다.

`MessageInput` 구성 요소를 생성하려면 먼저 `src` 디렉터리에 새 파일을 생성하고 이름을 `MessageInput`로 지정합니다. 그런 다음 채팅 애플리케이션용 입력을 표시할 입력 구성 요소를 생성합니다. `MessageInput`을 내보내고 `App`로 가져옵니다(`<SendButton />` 위로).

기본값으로 빈 문자열이 있는 `useState` 후크를 사용하여 `messageToSend`라는 새 상태를 생성합니다. 앱 본문에서 `messageToSend`를 `MessageInput`의 `value`로 전달하고 `onMessageChange` 속성에 `setMessageToSend`를 전달합니다.

------
#### [ TypeScript ]

```
// MessageInput.tsx

import * as React from 'react';

interface Props {
  value?: string;
  onValueChange?: (value: string) => void;
}

export const MessageInput = ({ value, onValueChange }: Props) => {
  return (
    <TextInput style={styles.input} value={value} onChangeText={onValueChange} placeholder="Send a message" />
  );
};

const styles = StyleSheet.create({
  input: {
    fontSize: 20,
    backgroundColor: 'rgb(239,239,240)',
    paddingHorizontal: 18,
    paddingVertical: 15,
    borderRadius: 50,
    flex: 1,
  }
})

// App.tsx

// ...

import { MessageInput } from './MessageInput';

// ...

export default function App() {
  const [messageToSend, setMessageToSend] = useState('');

// ...

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <View style={styles.messageBar}>
      <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
      <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
    </View>
  </SafeAreaView>
);

const styles = StyleSheet.create({
  root: {
    flex: 1,
  },
  messageBar: {
    borderTopWidth: StyleSheet.hairlineWidth,
    borderTopColor: 'rgb(160,160,160)',
    flexDirection: 'row',
    padding: 16,
    alignItems: 'center',
    backgroundColor: 'white',
  }
});
```

------
#### [ JavaScript ]

```
// MessageInput.jsx

import * as React from 'react';

export const MessageInput = ({ value, onValueChange }) => {
  return (
    <TextInput style={styles.input} value={value} onChangeText={onValueChange} placeholder="Send a message" />
  );
};

const styles = StyleSheet.create({
  input: {
    fontSize: 20,
    backgroundColor: 'rgb(239,239,240)',
    paddingHorizontal: 18,
    paddingVertical: 15,
    borderRadius: 50,
    flex: 1,
  }
})

// App.jsx

// ...

import { MessageInput } from './MessageInput';

// ...

export default function App() {
  const [messageToSend, setMessageToSend] = useState('');

// ...

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <View style={styles.messageBar}>
      <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
      <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
    </View>
  </SafeAreaView>
);

const styles = StyleSheet.create({
  root: {
    flex: 1,
  },
  messageBar: {
    borderTopWidth: StyleSheet.hairlineWidth,
    borderTopColor: 'rgb(160,160,160)',
    flexDirection: 'row',
    padding: 16,
    alignItems: 'center',
    backgroundColor: 'white',
  }
});
```

------

## 다음 단계
<a name="chat-react-rooms-next-steps"></a>

이제 Chatterbox용 메시지 표시줄 구축을 완료했으므로 이 React Native 자습서의 2부인 [메시지 및 이벤트](chat-sdk-react-tutorial-messages-events.md)로 이동하세요.

# IVS 챗 클라이언트 메시징 SDK: React Native 자습서 2부: 메시지 및 이벤트
<a name="chat-sdk-react-tutorial-messages-events"></a>

이 자습서의 두 번째(마지막) 부분은 여러 섹션으로 나뉩니다.

1. [채팅 메시지 이벤트 구독](#chat-react-messages-events-subscribe)

1. [받은 메시지 보기](#chat-react-messages-events-show)

   1.  [메시지 구성 요소 생성](#chat-react-messages-create-component)

   1. [현재 사용자가 전송한 메시지 인식](#chat-react-messages-recognize)

   1. [채팅 메시지 목록 렌더링](#chat-react-messages-render-list)

1. [채팅룸에서 작업 수행](#chat-react-messages-events-room-actions)

   1. [메시지 전송](#chat-react-room-actions-sending-message)

   1. [메시지 삭제](#chat-react-room-actions-deleting-message)

1. [다음 단계](#chat-react-messages-events-next-steps)

**참고**: 경우에 따라 JavaScript와 TypeScript의 코드 예제가 동일하므로 서로 합쳐져 있습니다.

## 사전 조건
<a name="chat-react-messages-events-prerequisite"></a>

이 자습서의 1부인 [채팅룸](chat-sdk-react-tutorial-chat-rooms.md)을 완료해야 합니다.

## 채팅 메시지 이벤트 구독
<a name="chat-react-messages-events-subscribe"></a>

`ChatRoom` 인스턴스는 채팅룸에서 이벤트가 발생할 때 이벤트를 사용하여 통신합니다. 채팅 환경을 구현하기 위해서는 우선 연결된 방에서 다른 사람이 메시지를 보내고 있을 때 이를 사용자에게 보여줄 수 있어야 합니다.

여기서 채팅 메시지 이벤트를 구독합니다. 다음 단계에서는 생성한 메시지 목록을 업데이트하는 방법에 대해 살펴보겠습니다. 해당 목록은 각 메시지/이벤트마다 업데이트됩니다.

`App`의 `useEffect` 후크 내에서 모든 메시지 이벤트를 구독합니다.

**TypeScript/JavaScript**:

```
// App.tsx / App.jsx

useEffect(() => {
  // ...
  const unsubscribeOnMessageReceived = room.addListener('message', (message) => {});

  return () => {
    // ...
    unsubscribeOnMessageReceived();
  };
}, []);
```

## 받은 메시지 보기
<a name="chat-react-messages-events-show"></a>

메시지를 받는 것은 채팅 환경의 핵심 부분입니다. Chat JS SDK를 사용하여 채팅룸에 연결된 다른 사용자로부터 이벤트를 쉽게 수신하도록 코드를 설정할 수 있습니다.

나중에 여기서 생성한 구성 요소를 활용하여 채팅룸에서 작업을 수행하는 방법을 보여 드리겠습니다.

`App`에서 `messages`라는 `ChatMessage` 배열 형식으로 `messages`라는 상태를 정의합니다.

------
#### [ TypeScript ]

```
// App.tsx

// ...

import { ChatRoom, ChatMessage, ConnectionState } from 'amazon-ivs-chat-messaging';

export default function App() {
  const [messages, setMessages] = useState<ChatMessage[]>([]);

  //...
}
```

------
#### [ JavaScript ]

```
// App.jsx

// ...

import { ChatRoom, ConnectionState } from 'amazon-ivs-chat-messaging';

export default function App() {
  const [messages, setMessages] = useState([]);

  //...
}
```

------

다음으로 `message` 리스너 함수에서 `messages` 배열에 `message`를 추가합니다.

**TypeScript/JavaScript**:

```
// App.tsx / App.jsx

// ...

const unsubscribeOnMessageReceived = room.addListener('message', (message) => {
  setMessages((msgs) => [...msgs, message]);
});

// ...
```

아래에서는 받은 메시지를 표시하는 작업을 단계별로 설명합니다.

1.  [메시지 구성 요소 생성](#chat-react-messages-create-component)

1. [현재 사용자가 전송한 메시지 인식](#chat-react-messages-recognize)

1. [채팅 메시지 목록 렌더링](#chat-react-messages-render-list)

### 메시지 구성 요소 생성
<a name="chat-react-messages-create-component"></a>

`Message` 구성 요소는 채팅룸에서 받은 메시지의 내용의 렌더링(rendering)을 담당합니다. 이 섹션에서는 `App`에서 개별 채팅 메시지를 렌더링하기 위한 메시지 구성 요소를 생성합니다.

`src` 디렉터리에 새 파일을 생성하고 이름을 `Message`로 지정합니다. 이 구성 요소의 `ChatMessage` 형식을 전달하고 `ChatMessage` 속성에서 `content` 문자열을 전달하여 채팅룸 메시지 리스너에서 받은 메시지 텍스트를 표시합니다. 프로젝트 탐색기에서 `Message`로 이동합니다.

------
#### [ TypeScript ]

```
// Message.tsx

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { ChatMessage } from 'amazon-ivs-chat-messaging';

type Props = {
  message: ChatMessage;
}

export const Message = ({ message }: Props) => {
  return (
    <View style={styles.root}>
      <Text>{message.sender.userId}</Text>
      <Text style={styles.textContent}>{message.content}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  root: {
    backgroundColor: 'silver',
    padding: 6,
    borderRadius: 10,
    marginHorizontal: 12,
    marginVertical: 5,
    marginRight: 50,
  },
  textContent: {
    fontSize: 17,
    fontWeight: '500',
    flexShrink: 1,
  },
});
```

------
#### [ JavaScript ]

```
// Message.jsx

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';

export const Message = ({ message }) => {
  return (
    <View style={styles.root}>
      <Text>{message.sender.userId}</Text>
      <Text style={styles.textContent}>{message.content}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  root: {
    backgroundColor: 'silver',
    padding: 6,
    borderRadius: 10,
    marginHorizontal: 12,
    marginVertical: 5,
    marginRight: 50,
  },
  textContent: {
    fontSize: 17,
    fontWeight: '500',
    flexShrink: 1,
  },
});
```

------

**팁**: 이 구성 요소를 사용하여 메시지 행에 렌더링하려는 다양한 속성(예: 아바타 URL, 사용자 이름, 메시지가 전송된 시각의 타임스탬프)을 저장할 수 있습니다.

### 현재 사용자가 전송한 메시지 인식
<a name="chat-react-messages-recognize"></a>

현재 사용자가 전송한 메시지를 인식하려면 코드를 수정하고 현재 사용자의 `userId`를 저장하기 위한 React 컨텍스트를 만듭니다.

`src` 디렉터리에 새 파일을 생성하고 이름을 `UserContext`로 지정합니다.

------
#### [ TypeScript ]

```
// UserContext.tsx

import React from 'react';

const UserContext = React.createContext<string | undefined>(undefined);

export const useUserContext = () => {
  const context = React.useContext(UserContext);

  if (context === undefined) {
    throw new Error('useUserContext must be within UserProvider');
  }

  return context;
};

export const UserProvider = UserContext.Provider;
```

------
#### [ JavaScript ]

```
// UserContext.jsx

import React from 'react';

const UserContext = React.createContext(undefined);

export const useUserContext = () => {
  const context = React.useContext(UserContext);

  if (context === undefined) {
    throw new Error('useUserContext must be within UserProvider');
  }

  return context;
};

export const UserProvider = UserContext.Provider;
```

------

참고: 여기서는 `useState` 후크를 사용하여 `userId` 값을 저장했습니다. 향후에는 사용자 컨텍스트를 변경하거나 로그인 용도로 `setUserId`를 사용할 수 있습니다.

다음으로 이전에 생성한 컨텍스트를 사용하여 `userId`를 `tokenProvider`에 전달된 첫 번째 파라미터로 교체합니다. 아래에 지정된 대로 토큰 공급자에 `SEND_MESSAGE` 기능을 추가해야 합니다. 메시지를 보내는 데 필요합니다.

------
#### [ TypeScript ]

```
// App.tsx

// ...

import { useUserContext } from './UserContext';

// ...


export default function App() {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const userId = useUserContext();
  const [room] = useState(
    () =>
      new ChatRoom({
        regionOrUrl: process.env.REGION,
        tokenProvider: () => tokenProvider(userId, ['SEND_MESSAGE']),
      }),
  );

  // ...
}
```

------
#### [ JavaScript ]

```
// App.jsx

// ...

import { useUserContext } from './UserContext';

// ...


export default function App() {
  const [messages, setMessages] = useState([]);
  const userId = useUserContext();
  const [room] = useState(
    () =>
      new ChatRoom({
        regionOrUrl: process.env.REGION,
        tokenProvider: () => tokenProvider(userId, ['SEND_MESSAGE']),
      }),
  );

  // ...
}
```

------

`Message` 구성 요소에서 이전에 생성한 `UserContext`를 사용하고, `isMine` 변수를 선언(declare)하고, 발신자의 `userId`와 컨텍스트의 `userId`를 일치시킨 후 현재 사용자에게 다양한 스타일의 메시지를 적용하세요.

------
#### [ TypeScript ]

```
// Message.tsx

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { useUserContext } from './UserContext';

type Props = {
  message: ChatMessage;
}

export const Message = ({ message }: Props) => {
  const userId = useUserContext();

  const isMine = message.sender.userId === userId;

  return (
    <View style={[styles.root, isMine && styles.mine]}>
      {!isMine && <Text>{message.sender.userId}</Text>}
      <Text style={styles.textContent}>{message.content}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  root: {
    backgroundColor: 'silver',
    padding: 6,
    borderRadius: 10,
    marginHorizontal: 12,
    marginVertical: 5,
    marginRight: 50,
  },
  textContent: {
    fontSize: 17,
    fontWeight: '500',
    flexShrink: 1,
  },
  mine: {
    flexDirection: 'row-reverse',
    backgroundColor: 'lightblue',
  },
});
```

------
#### [ JavaScript ]

```
// Message.jsx

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { useUserContext } from './UserContext';

export const Message = ({ message }) => {
  const userId = useUserContext();

  const isMine = message.sender.userId === userId;

  return (
    <View style={[styles.root, isMine && styles.mine]}>
      {!isMine && <Text>{message.sender.userId}</Text>}
      <Text style={styles.textContent}>{message.content}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  root: {
    backgroundColor: 'silver',
    padding: 6,
    borderRadius: 10,
    marginHorizontal: 12,
    marginVertical: 5,
    marginRight: 50,
  },
  textContent: {
    fontSize: 17,
    fontWeight: '500',
    flexShrink: 1,
  },
  mine: {
    flexDirection: 'row-reverse',
    backgroundColor: 'lightblue',
  },
});
```

------

### 채팅 메시지 목록 렌더링
<a name="chat-react-messages-render-list"></a>

이제 `FlatList` 및 `Message` 구성 요소를 사용하여 메시지를 나열합니다.

------
#### [ TypeScript ]

```
// App.tsx

// ...

const renderItem = useCallback<ListRenderItem<ChatMessage>>(({ item }) => {
  return (
    <Message key={item.id} message={item} />
  );
}, []);

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <FlatList inverted data={messages} renderItem={renderItem} />
    <View style={styles.messageBar}>
      <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
      <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
    </View>
  </SafeAreaView>
);

// ...
```

------
#### [ JavaScript ]

```
// App.jsx

// ...

const renderItem = useCallback(({ item }) => {
  return (
    <Message key={item.id} message={item} />
  );
}, []);

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <FlatList inverted data={messages} renderItem={renderItem} />
    <View style={styles.messageBar}>
      <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
      <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
    </View>
  </SafeAreaView>
);

// ...
```

------

이제 `App`이 채팅룸에서 받은 메시지를 렌더링하기 위한 퍼즐 조각이 모두 다 준비되었습니다. 생성한 구성 요소를 활용하여 채팅룸에서 작업을 수행하는 방법을 보려면 다음을 계속합니다.

## 채팅룸에서 작업 수행
<a name="chat-react-messages-events-room-actions"></a>

메시지를 보내고 중재자 작업을 수행하는 것은 채팅룸과 상호 작용하는 주요 방법 중 일부입니다. 여기서는 다양한 채팅 요청 객체를 사용하여 Chatterbox에서 메시지 전송, 메시지 삭제, 다른 사용자 연결 끊기와 같은 일반적인 작업을 수행하는 방법을 알아봅니다.

채팅룸의 모든 작업은 공통적인 패턴을 따릅니다. 채팅룸에서 수행하는 모든 작업에는 해당하는 요청 객체가 있습니다. 각 요청에는 요청 확인 시 받는 해당 응답 객체가 있습니다.

채팅 토큰을 생성할 때 사용자에게 올바른 기능이 부여되면 해당 사용자는 요청 객체를 사용하여 해당 작업을 성공적으로 수행하여 채팅룸에서 어떤 요청을 수행할 수 있는지 확인할 수 있습니다.

아래에서는 [메시지를 전송](#chat-react-room-actions-sending-message)하고 [메시지를 삭제](#chat-react-room-actions-deleting-message)하는 방법을 설명합니다.

### 메시지 전송
<a name="chat-react-room-actions-sending-message"></a>

`SendMessageRequest` 클래스를 사용하여 채팅룸에서 메시지를 보낼 수 있습니다 이 단계에서는 [메시지 입력 생성](chat-sdk-react-tutorial-chat-rooms.md#chat-react-rooms-message-input)(이 자습서의 1부)에서 생성한 구성 요소를 통해 메시지 요청을 보내도록 `App`을 수정합니다.

우선, `useState` 후크를 사용하여 `isSending`라는 새 boolean 속성을 정의합니다. 이 새 속성을 사용하면 `isSendDisabled` 상수를 사용하여 `button` 요소의 비활성화 상태를 전환할 수 있습니다. `SendButton`의 이벤트 핸들러(event handler)에서 `messageToSend`의 값을 지우고 `isSending`을 true로 설정합니다.

이 버튼으로 API를 호출하므로 `isSending` boolean을 추가하면 요청이 완료될 때까지 `SendButton`에서 사용자 상호 작용을 비활성화함으로써 여러 API 호출이 동시에 발생하는 것을 방지할 수 있습니다.**

참고: 메시지 전송은 위의 [현재 사용자가 전송한 메시지 인식](#chat-react-messages-recognize)에서 설명한 것처럼 토큰 공급자에 `SEND_MESSAGE` 기능을 추가한 경우에만 작동합니다.

**TypeScript/JavaScript**:

```
// App.tsx / App.jsx

// ...

const [isSending, setIsSending] = useState(false);

// ...

const onMessageSend = () => {
  setIsSending(true);
  setMessageToSend('');
};

// ...

const isSendDisabled = connectionState !== 'connected' || isSending;

// ...
```

새 `SendMessageRequest` 인스턴스를 생성하고 생성자에 메시지 콘텐츠를 전달하여 요청을 준비합니다. `isSending`과 `messageToSend` 상태를 설정한 후 `sendMessage` 메서드를 호출하여 채팅룸으로 요청을 보냅니다. 마지막으로 요청 확인 또는 거절 수신 시`isSending` 플래그를 지웁니다.

**TypeScript/JavaScript**:

```
// App.tsx / App.jsx

// ...
import { ChatRoom, ConnectionState, SendMessageRequest } from 'amazon-ivs-chat-messaging'
// ...

const onMessageSend = async () => {
  const request = new SendMessageRequest(messageToSend);
  setIsSending(true);
  setMessageToSend('');

  try {
    const response = await room.sendMessage(request);
  } catch (e) {
    console.log(e);
    // handle the chat error here...
  } finally {
    setIsSending(false);
  }
};

// ...
```

Chatterbox를 실행해 보세요. `MessageBar`으로 초안을 작성하고 `SendButton`을 탭하여 메시지를 보내 보세요. 이전에 생성한 `MessageList` 내에 전송한 메시지가 렌더링된 것을 볼 수 있을 것입니다.

### 메시지 삭제
<a name="chat-react-room-actions-deleting-message"></a>

채팅룸에서 메시지를 삭제하려면 적절한 기능이 있어야 합니다. 기능은 채팅룸에 인증할 때 사용하는 채팅 토큰을 초기화하는 동안에 부여됩니다. 이 섹션의 목적에 따라 [로컬 인증/권한 부여 서버 설정](chat-sdk-react-tutorial-chat-rooms.md#chat-react-rooms-auth-server)(이 자습서의 1부)의 `ServerApp`에서 중재자 기능을 지정할 수 있습니다. 이 작업은 [토큰 공급자 구축](chat-sdk-react-tutorial-chat-rooms.md#chat-react-rooms-token-provider)(1부)에서 생성한 `tokenProvider` 객체를 사용하여 앱에서 수행됩니다.

여기서는 메시지를 삭제하는 함수를 추가하여 `Message`를 수정합니다.

먼저 `App.tsx`를 열고 `DELETE_MESSAGE` 기능을 추가합니다.(`capabilities`는 `tokenProvider` 함수의 두 번째 파라미터입니다.)

참고: 이는 `ServerApp`이 IVS Chat API에 결과 채팅 토큰과 연결된 사용자는 채팅룸에서 메시지를 삭제할 수 있음을 알리는 방법입니다. 실제 상황에서는 서버 앱 인프라의 사용자 기능을 관리하기 위한 백엔드 로직이 더 복잡할 수 있습니다.

**TypeScript/JavaScript**:

```
// App.tsx / App.jsx

// ...

const [room] = useState(() =>
    new ChatRoom({
      regionOrUrl: process.env.REGION,
      tokenProvider: () => tokenProvider(userId, ['SEND_MESSAGE', 'DELETE_MESSAGE']),
    }),
);

// ...
```

다음 단계에서는 삭제 버튼을 표시하도록 `Message`를 업데이트합니다.

문자열을 파라미터 중 하나로 받아 `Promise`를 반환하는 `onDelete`라는 새 함수를 정의합니다. 문자열 파라미터의 경우 구성 요소 메시지 ID를 전달합니다.

------
#### [ TypeScript ]

```
// Message.tsx

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { useUserContext } from './UserContext';

export type Props = {
  message: ChatMessage;
  onDelete(id: string): Promise<void>;
};

export const Message = ({ message, onDelete }: Props) => {
  const userId = useUserContext();

  const isMine = message.sender.userId === userId;
  const handleDelete = () => onDelete(message.id);

  return (
    <View style={[styles.root, isMine && styles.mine]}>
      {!isMine && <Text>{message.sender.userId}</Text>}
      <View style={styles.content}>
        <Text style={styles.textContent}>{message.content}</Text>
        <TouchableOpacity onPress={handleDelete}>
          <Text>Delete<Text/>
        </TouchableOpacity>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  root: {
    backgroundColor: 'silver',
    padding: 6,
    borderRadius: 10,
    marginHorizontal: 12,
    marginVertical: 5,
    marginRight: 50,
  },
  content: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
  },
  textContent: {
    fontSize: 17,
    fontWeight: '500',
    flexShrink: 1,
  },
  mine: {
    flexDirection: 'row-reverse',
    backgroundColor: 'lightblue',
  },
});
```

------
#### [ JavaScript ]

```
// Message.jsx

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { useUserContext } from './UserContext';

export const Message = ({ message, onDelete }) => {
  const userId = useUserContext();

  const isMine = message.sender.userId === userId;
  const handleDelete = () => onDelete(message.id);

  return (
    <View style={[styles.root, isMine && styles.mine]}>
      {!isMine && <Text>{message.sender.userId}</Text>}
      <View style={styles.content}>
        <Text style={styles.textContent}>{message.content}</Text>
        <TouchableOpacity onPress={handleDelete}>
          <Text>Delete<Text/>
        </TouchableOpacity>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  root: {
    backgroundColor: 'silver',
    padding: 6,
    borderRadius: 10,
    marginHorizontal: 12,
    marginVertical: 5,
    marginRight: 50,
  },
  content: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
  },
  textContent: {
    fontSize: 17,
    fontWeight: '500',
    flexShrink: 1,
  },
  mine: {
    flexDirection: 'row-reverse',
    backgroundColor: 'lightblue',
  },
});
```

------

다음으로 `FlatList` 구성 요소의 최신 변경 사항을 반영하도록 `renderItem`를 업데이트합니다.

`App`에서 `handleDeleteMessage`라는 함수를 정의하고 이를 `MessageList onDelete` 속성에 전달합니다.

------
#### [ TypeScript ]

```
// App.tsx

// ...

const handleDeleteMessage = async (id: string) => {};

const renderItem = useCallback<ListRenderItem<ChatMessage>>(({ item }) => {
  return (
    <Message key={item.id} message={item} onDelete={handleDeleteMessage} />
  );
}, [handleDeleteMessage]);

// ...
```

------
#### [ JavaScript ]

```
// App.jsx

// ...

const handleDeleteMessage = async (id) => {};

const renderItem = useCallback(({ item }) => {
  return (
    <Message key={item.id} message={item} onDelete={handleDeleteMessage} />
  );
}, [handleDeleteMessage]);

// ...
```

------

관련 메시지 ID를 생성자 파라미터에 전달하는 `DeleteMessageRequest`의 새 인스턴스를 생성하여 요청을 준비하고 위에서 준비한 요청을 수락하는 `deleteMessage`를 호출합니다.

------
#### [ TypeScript ]

```
// App.tsx

// ...

const handleDeleteMessage = async (id: string) => {
  const request = new DeleteMessageRequest(id);
  await room.deleteMessage(request);
};

// ...
```

------
#### [ JavaScript ]

```
// App.jsx

// ...

const handleDeleteMessage = async (id) => {
  const request = new DeleteMessageRequest(id);
  await room.deleteMessage(request);
};

// ...
```

------

다음으로 방금 삭제한 메시지가 생략된 새 메시지 목록을 반영하도록 `messages` 상태를 업데이트합니다.

`useEffect` 후크에서`messageDelete` 이벤트를 수신하고 `message` 파라미터와 일치하는 ID가 있는 메시지를 삭제하여 `messages` 상태 배열을 업데이트합니다.

참고: 현재 사용자나 방에 있는 다른 사용자가 메시지를 삭제하는 경우 `messageDelete` 이벤트가 발생할 수 있습니다. 이벤트 핸들러에서(`deleteMessage` 요청 옆 대신) 이벤트를 처리하면 메시지 삭제 처리를 통합할 수 있습니다.

**TypeScript/JavaScript**:

```
// App.tsx / App.jsx

// ...

const unsubscribeOnMessageDeleted = room.addListener('messageDelete', (deleteMessageEvent) => {
  setMessages((prev) => prev.filter((message) => message.id !== deleteMessageEvent.id));
});

return () => {
  // ...

  unsubscribeOnMessageDeleted();
};

// ...
```

이제 채팅 앱의 채팅룸에서 사용자를 삭제할 수 있습니다.

## 다음 단계
<a name="chat-react-messages-events-next-steps"></a>

테스트의 일환으로 방에서 다른 사용자의 연결을 끊는 등 다양한 작업을 시도해보세요.

# IVS 챗 클라이언트 메시징 SDK: React 및 React Native 모범 사례
<a name="chat-sdk-react-best-practices"></a>

이 문서에서는 React 및 React Native용 Amazon IVS Chat Messaging SDK를 사용하는 가장 중요한 방법을 설명합니다. 이 정보를 활용하여 React 앱 내에서 일반적인 채팅 기능을 빌드하고 IVS 챗 메시징 SDK의 고급 부분을 더 심층적으로 분석하는 데 필요한 배경지식을 얻을 수 있습니다.

## 채팅룸 이니셜라이저 후크 생성
<a name="chatroom-initializer-hook"></a>

`ChatRoom` 클래스에는 연결 상태를 관리하고 수신된 메시지 및 삭제된 메시지와 같은 이벤트를 수신하기 위한 코어 채팅 메서드와 리스너가 포함되어 있습니다. 여기서는 채팅 인스턴스를 후크에 올바르게 저장하는 방법을 보여줍니다.

### 구현
<a name="chatroom-initializer-hook-implementation"></a>

------
#### [ TypeScript ]

```
// useChatRoom.ts

import React from 'react';
import { ChatRoom, ChatRoomConfig } from 'amazon-ivs-chat-messaging';

export const useChatRoom = (config: ChatRoomConfig) => {
  const [room] = React.useState(() => new ChatRoom(config));

  return { room };
};
```

------
#### [ JavaScript ]

```
import React from 'react';
import { ChatRoom } from 'amazon-ivs-chat-messaging';

export const useChatRoom = (config) => {
  const [room] = React.useState(() => new ChatRoom(config));

  return { room };
};
```

------

참고: 구성 파라미터를 즉시 업데이트할 수 없으므로 `setState` 후크의 `dispatch` 메서드를 사용하지 않습니다. SDK는 인스턴스를 한 번 생성하며, 토큰 공급자를 업데이트할 수 없습니다.

**중요**: `ChatRoom` 이니셜라이저 후크를 한 번 사용하여 새 채팅룸 인스턴스를 초기화하세요.

### 예제
<a name="chatroom-initializer-hook-example"></a>

**TypeScript/JavaScript**:

```
// ...

const MyChatScreen = () => {
  const userId = 'Mike';
  const { room } = useChatRoom({
    regionOrUrl: SOCKET_URL,
    tokenProvider: () => tokenProvider(ROOM_ID, ['SEND_MESSAGE']),
  });

  const handleConnect = () => {
    room.connect();
  };

  // ...
};

// ...
```

### 연결 상태 수신 대기
<a name="chatroom-initializer-hook-connection-state"></a>

원하는 경우 채팅룸 후크에서 연결 상태 업데이트를 구독할 수 있습니다.

#### 구현
<a name="connection-state-implementation"></a>

------
#### [ TypeScript ]

```
// useChatRoom.ts

import React from 'react';
import { ChatRoom, ChatRoomConfig, ConnectionState } from 'amazon-ivs-chat-messaging';

export const useChatRoom = (config: ChatRoomConfig) => {
  const [room] = useState(() => new ChatRoom(config));

  const [state, setState] = React.useState<ConnectionState>('disconnected');

  React.useEffect(() => {
    const unsubscribeOnConnecting = room.addListener('connecting', () => {
      setState('connecting');
    });

    const unsubscribeOnConnected = room.addListener('connect', () => {
      setState('connected');
    });

    const unsubscribeOnDisconnected = room.addListener('disconnect', () => {
      setState('disconnected');
    });

    return () => {
      unsubscribeOnConnecting();
      unsubscribeOnConnected();
      unsubscribeOnDisconnected();
    };
  }, []);

  return { room, state };
};
```

------
#### [ JavaScript ]

```
// useChatRoom.js

import React from 'react';
import { ChatRoom } from 'amazon-ivs-chat-messaging';

export const useChatRoom = (config) => {
  const [room] = useState(() => new ChatRoom(config));

  const [state, setState] = React.useState('disconnected');

  React.useEffect(() => {
    const unsubscribeOnConnecting = room.addListener('connecting', () => {
      setState('connecting');
    });

    const unsubscribeOnConnected = room.addListener('connect', () => {
      setState('connected');
    });

    const unsubscribeOnDisconnected = room.addListener('disconnect', () => {
      setState('disconnected');
    });

    return () => {
      unsubscribeOnConnecting();
      unsubscribeOnConnected();
      unsubscribeOnDisconnected();
    };
  }, []);

  return { room, state };
};
```

------

## 채팅룸 인스턴스 공급자
<a name="chatroom-instance-provider"></a>

prop 드릴링을 방지하기 위해 다른 구성 요소에서 후크를 사용하려면 React `context`를 사용하여 채팅룸 공급자를 생성할 수 있습니다.

### 구현
<a name="chatroom-instance-provider-implementation"></a>

------
#### [ TypeScript ]

```
// ChatRoomContext.tsx

import React from 'react';
import { ChatRoom } from 'amazon-ivs-chat-messaging';

const ChatRoomContext = React.createContext<ChatRoom | undefined>(undefined);

export const useChatRoomContext = () => {
  const context = React.useContext(ChatRoomContext);

  if (context === undefined) {
    throw new Error('useChatRoomContext must be within ChatRoomProvider');
  }

  return context;
};

export const ChatRoomProvider = ChatRoomContext.Provider;
```

------
#### [ JavaScript ]

```
// ChatRoomContext.jsx

import React from 'react';
import { ChatRoom } from 'amazon-ivs-chat-messaging';

const ChatRoomContext = React.createContext(undefined);

export const useChatRoomContext = () => {
  const context = React.useContext(ChatRoomContext);

  if (context === undefined) {
    throw new Error('useChatRoomContext must be within ChatRoomProvider');
  }

  return context;
};

export const ChatRoomProvider = ChatRoomContext.Provider;
```

------

### 예제
<a name="chatroom-instance-provider-example"></a>

`ChatRoomProvider`를 생성한 이후에 `useChatRoomContext`에서 인스턴스를 사용할 수 있습니다.

**중요**: 채팅 화면과 중간에 있는 다른 구성 요소 사이에 `context`에 액세스해야 하는 경우에만 공급자를 루트 수준으로 설정하여 연결을 수신하는 동안 불필요한 재렌더링이 발생하지 않도록 하세요. 아니면 공급자를 채팅 화면에 최대한 가깝게 배치하세요.

**TypeScript/JavaScript**:

```
// AppContainer

const AppContainer = () => {
  const { room } = useChatRoom({
    regionOrUrl: SOCKET_URL,
    tokenProvider: () => tokenProvider(ROOM_ID, ['SEND_MESSAGE']),
  });

  return (
    <ChatRoomProvider value={room}>
      <MyChatScreen />
    </ChatRoomProvider>
  );
};

// MyChatScreen

const MyChatScreen = () => {
  const room = useChatRoomContext();

  const handleConnect = () => {
    room.connect();
  };
  // ...
};

// ...
```

## 메시지 리스너 생성
<a name="message-listener"></a>

수신되는 모든 메시지를 최신 상태로 유지하려면 `message` 및 `deleteMessage` 이벤트를 구독해야 합니다. 다음은 구성 요소에 채팅 메시지를 제공하는 몇 가지 코드입니다.

**중요**: 채팅 메시지 리스너가 메시지 상태를 업데이트할 때 여러 번 다시 렌더링될 수 있으므로 성능을 위해 `ChatMessageContext`를 `ChatRoomProvider`와 구분합니다. `ChatMessageProvider`를 사용할 구성 요소에 `ChatMessageContext`를 적용해야 합니다.

### 구현
<a name="message-listener-implementation"></a>

------
#### [ TypeScript ]

```
// ChatMessagesContext.tsx

import React from 'react';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { useChatRoomContext } from './ChatRoomContext';

const ChatMessagesContext = React.createContext<ChatMessage[] | undefined>(undefined);

export const useChatMessagesContext = () => {
  const context = React.useContext(ChatMessagesContext);

  if (context === undefined) {
    throw new Error('useChatMessagesContext must be within ChatMessagesProvider);
  }

  return context;
};

export const ChatMessagesProvider = ({ children }: { children: React.ReactNode }) => {
  const room = useChatRoomContext();

  const [messages, setMessages] = React.useState<ChatMessage[]>([]);

  React.useEffect(() => {
    const unsubscribeOnMessageReceived = room.addListener('message', (message) => {
      setMessages((msgs) => [message, ...msgs]);
    });

    const unsubscribeOnMessageDeleted = room.addListener('messageDelete', (deleteEvent) => {
      setMessages((prev) => prev.filter((message) => message.id !== deleteEvent.messageId));
    });

    return () => {
      unsubscribeOnMessageDeleted();
      unsubscribeOnMessageReceived();
    };
  }, [room]);

  return <ChatMessagesContext.Provider value={messages}>{children}</ChatMessagesContext.Provider>;
};
```

------
#### [ JavaScript ]

```
// ChatMessagesContext.jsx

import React from 'react';
import { useChatRoomContext } from './ChatRoomContext';

const ChatMessagesContext = React.createContext(undefined);

export const useChatMessagesContext = () => {
  const context = React.useContext(ChatMessagesContext);

  if (context === undefined) {
    throw new Error('useChatMessagesContext must be within ChatMessagesProvider);
  }

  return context;
};

export const ChatMessagesProvider = ({ children }) => {
  const room = useChatRoomContext();

  const [messages, setMessages] = React.useState([]);

  React.useEffect(() => {
    const unsubscribeOnMessageReceived = room.addListener('message', (message) => {
      setMessages((msgs) => [message, ...msgs]);
    });

    const unsubscribeOnMessageDeleted = room.addListener('messageDelete', (deleteEvent) => {
      setMessages((prev) => prev.filter((message) => message.id !== deleteEvent.messageId));
    });

    return () => {
      unsubscribeOnMessageDeleted();
      unsubscribeOnMessageReceived();
    };
  }, [room]);

  return <ChatMessagesContext.Provider value={messages}>{children}</ChatMessagesContext.Provider>;
};
```

------

### React의 예
<a name="message-listener-example-react"></a>

**중요**: 메시지 컨테이너를 `ChatMessagesProvider`로 래핑해야 합니다. `Message` 행은 메시지 내용을 표시하는 예제 구성 요소입니다.

**TypeScript/JavaScript**:

```
// your message list component...

import React from 'react';
import { useChatMessagesContext } from './ChatMessagesContext';

const MessageListContainer = () => {
  const messages = useChatMessagesContext();

  return (
    <React.Fragment>
      {messages.map((message) => (
        <MessageRow message={message} />
      ))}
    </React.Fragment>
  );
};
```

### React Native의 예
<a name="message-listener-example-react-native"></a>

기본적으로 `ChatMessage`에는 `FlatList`에서 각 행에 대한 React 키로 자동으로 사용되는 `id`가 포함되어 있으므로, `keyExtractor`를 전달할 필요가 없습니다.

------
#### [ TypeScript ]

```
// MessageListContainer.tsx

import React from 'react';
import { ListRenderItemInfo, FlatList } from 'react-native';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { useChatMessagesContext } from './ChatMessagesContext';

const MessageListContainer = () => {
  const messages = useChatMessagesContext();

  const renderItem = useCallback(({ item }: ListRenderItemInfo<ChatMessage>) => <MessageRow />, []);

  return <FlatList data={messages} renderItem={renderItem} />;
};
```

------
#### [ JavaScript ]

```
// MessageListContainer.jsx

import React from 'react';
import { FlatList } from 'react-native';
import { useChatMessagesContext } from './ChatMessagesContext';

const MessageListContainer = () => {
  const messages = useChatMessagesContext();

  const renderItem = useCallback(({ item }) => <MessageRow />, []);

  return <FlatList data={messages} renderItem={renderItem} />;
};
```

------

## 앱 내 여러 채팅룸 인스턴스
<a name="multiple-chatroom-instances"></a>

앱에서 여러 개의 동시 채팅룸을 사용하는 경우 채팅마다 공급자를 생성하여 해당 채팅 공급자에서 사용하는 것이 좋습니다. 이 예에서는 도움말 봇 및 고객 도움말 채팅을 생성합니다. 둘 모두를 위한 공급자를 생성합니다.

------
#### [ TypeScript ]

```
// SupportChatProvider.tsx

import React from 'react';
import { SUPPORT_ROOM_ID, SOCKET_URL } from '../../config';
import { tokenProvider } from '../tokenProvider';
import { ChatRoomProvider } from './ChatRoomContext';
import { useChatRoom } from './useChatRoom';

export const SupportChatProvider = ({ children }: { children: React.ReactNode }) => {
  const { room } = useChatRoom({
    regionOrUrl: SOCKET_URL,
    tokenProvider: () => tokenProvider(SUPPORT_ROOM_ID, ['SEND_MESSAGE']),
  });

  return <ChatRoomProvider value={room}>{children}</ChatRoomProvider>;
};

// SalesChatProvider.tsx

import React from 'react';
import { SALES_ROOM_ID, SOCKET_URL } from '../../config';
import { tokenProvider } from '../tokenProvider';
import { ChatRoomProvider } from './ChatRoomContext';
import { useChatRoom } from './useChatRoom';

export const SalesChatProvider = ({ children }: { children: React.ReactNode }) => {
  const { room } = useChatRoom({
    regionOrUrl: SOCKET_URL,
    tokenProvider: () => tokenProvider(SALES_ROOM_ID, ['SEND_MESSAGE']),
  });

  return <ChatRoomProvider value={room}>{children}</ChatRoomProvider>;
};
```

------
#### [ JavaScript ]

```
// SupportChatProvider.jsx

import React from 'react';
import { SUPPORT_ROOM_ID, SOCKET_URL } from '../../config';
import { tokenProvider } from '../tokenProvider';
import { ChatRoomProvider } from './ChatRoomContext';
import { useChatRoom } from './useChatRoom';

export const SupportChatProvider = ({ children }) => {
  const { room } = useChatRoom({
    regionOrUrl: SOCKET_URL,
    tokenProvider: () => tokenProvider(SUPPORT_ROOM_ID, ['SEND_MESSAGE']),
  });

  return <ChatRoomProvider value={room}>{children}</ChatRoomProvider>;
};

// SalesChatProvider.jsx

import React from 'react';
import { SALES_ROOM_ID, SOCKET_URL } from '../../config';
import { tokenProvider } from '../tokenProvider';
import { ChatRoomProvider } from './ChatRoomContext';
import { useChatRoom } from './useChatRoom';

export const SalesChatProvider = ({ children }) => {
  const { room } = useChatRoom({
    regionOrUrl: SOCKET_URL,
    tokenProvider: () => tokenProvider(SALES_ROOM_ID, ['SEND_MESSAGE']),
  });

  return <ChatRoomProvider value={room}>{children}</ChatRoomProvider>;
};
```

------

### React의 예
<a name="multiple-chatroom-instances-example-react"></a>

이제 동일한 `ChatRoomProvider`를 사용하는 다른 채팅 공급자를 사용할 수 있습니다. 나중에 각 화면/보기에서 동일한 `useChatRoomContext`를 다시 사용할 수 있습니다.

**TypeScript/JavaScript**:

```
// App.tsx / App.jsx

const App = () => {
  return (
    <Routes>
      <Route
        element={
          <SupportChatProvider>
            <SupportChatScreen />
          </SupportChatProvider>
        }
      />
      <Route
        element={
          <SalesChatProvider>
            <SalesChatScreen />
          </SalesChatProvider>
        }
      />
    </Routes>
  );
};
```

### React Native의 예
<a name="multiple-chatroom-instances-example-react-native"></a>

**TypeScript/JavaScript**:

```
// App.tsx / App.jsx

const App = () => {
  return (
    <Stack.Navigator>
      <Stack.Screen name="SupportChat">
        <SupportChatProvider>
          <SupportChatScreen />
        </SupportChatProvider>
      </Stack.Screen>
      <Stack.Screen name="SalesChat">
        <SalesChatProvider>
          <SalesChatScreen />
        </SalesChatProvider>
      </Stack.Screen>
    </Stack.Navigator>
  );
};
```

**TypeScript/JavaScript**:

```
// SupportChatScreen.tsx / SupportChatScreen.jsx

// ...

const SupportChatScreen = () => {
  const room = useChatRoomContext();

  const handleConnect = () => {
    room.connect();
  };

  return (
    <>
      <Button title="Connect" onPress={handleConnect} />
      <MessageListContainer />
    </>
  );
};

// SalesChatScreen.tsx / SalesChatScreen.jsx

// ...

const SalesChatScreen = () => {
  const room = useChatRoomContext();

  const handleConnect = () => {
    room.connect();
  };

  return (
    <>
      <Button title="Connect" onPress={handleConnect} />
      <MessageListContainer />
    </>
  );
};
```