> For the complete documentation index, see [llms.txt](/llms.txt)

# GS2-Chat

텍스트 채팅 기능




GS2-Chat 을 이용하면 게임 내에 텍스트 채팅을 손쉽게 구현할 수 있습니다.
텍스트 채팅은 새로운 메시지를 감지하기 위해 상시 접속 세션이 필요하며, 서버에 요구되는 요건도 복잡해집니다.

Game Server Services 를 이용하면 그러한 번거로운 상시 접속 세션 관리로부터 해방될 수 있습니다.

## 룸

GS2-Chat 을 이용하려면 먼저 룸을 생성해야 합니다.
룸은 플레이어가 자유롭게 생성할 수 있도록 할 수도 있고, 개발 측에서 준비한 룸을 플레이어가 사용하게 할 수도 있습니다.
또한 게시할 수 있는 사용자ID의 화이트리스트를 설정하여 특정 멤버 전용 룸을 구성하는 것도 가능합니다.

한 가지 주의해야 할 점은, GS2-Chat 은 룸당 1초에 3건까지만 메시지 게시를 보장한다는 것입니다.
이를 초과하는 빈도로 메시지를 게시하면 오류가 발생할 수 있습니다.
따라서 소셜 네트워크의 타임라인처럼 대량의 플레이어를 하나의 룸에 담는 용도로는 적합하지 않습니다.

### 비밀번호

룸에는 비밀번호를 설정할 수 있습니다.
비밀번호가 설정된 룸의 메시지를 가져오거나 메시지를 게시할 때는 비밀번호 지정이 필요합니다.

### 룸 설정 갱신

룸을 생성한 후에도 메타데이터, 비밀번호, 화이트리스트(게시 가능한 사용자ID 목록)를 갱신할 수 있습니다. 이를 통해 룸의 접근 권한을 동적으로 변경하거나 룸의 속성 정보를 변경할 수 있습니다.

## 구독

플레이어는 룸을 구독함으로써 룸에 게시되는 새로운 메시지에 대한 알림을 받을 수 있게 됩니다.
메시지에는 카테고리를 설정할 수 있으며, 룸 내 카테고리별로 구독이 가능합니다.

길드 채팅을 구현할 때 길드 마스터의 메시지에만 특별한 카테고리를 설정하여, 길드 멤버가 길드 마스터의 메시지만은 반드시 구독하도록 만드는 것도 가능합니다.

구독 설정은 이후에도 갱신할 수 있습니다. 수신할 카테고리의 추가·삭제나 오프라인 시 모바일 푸시 알림 전송 설정을 동적으로 변경할 수 있습니다.

구독에 따른 알림 처리는 GS2-Gateway 의 알림 기능이 사용되는데, GS2-Gateway 에는 알림 대상 플레이어가 오프라인인 경우
모바일 푸시 알림 등 게임 외부의 방법으로 알림을 보내는 기능이 마련되어 있습니다.

하나의 룸을 구독할 수 있는 인원수에 명확한 제한은 없지만, 알림을 전송하기 위해 GS2-Gateway 의 API 요청이 발생하므로 GS2 API 이용 요금이 알림 대상 1건마다 발생합니다.
또한 알림 대상이 많아지면 알림이 도착하기까지 걸리는 시간이 길어집니다.

룸 이름을 지정하여 해당 룸을 현재 누가 구독하고 있는지 목록을 가져올 수도 있습니다.

```plantuml
actor Player1
actor Player2
participant "GS2-Chat#Room"
participant "GS2-Gateway#Namespace"
participant "Firebase Cloud Messaging"
Player1 -> "GS2-Chat#Room" : Subscribe
Player2 -> "GS2-Chat#Room" : Post Message
"GS2-Chat#Room" -> "GS2-Gateway#Namespace" : Send Notification to Player1
"GS2-Gateway#Namespace" -> "Firebase Cloud Messaging" : Notification(if player is offline)
"GS2-Gateway#Namespace" -> Player1 : Notification
Player1 -> "GS2-Chat#Room" : Load New Message
```

## 메시지

룸에 메시지를 전송할 수 있습니다.
메시지 보관 기간은 네임스페이스 설정 `messageLifeTimeDays` 로 1~30일 범위에서 지정할 수 있으며, 보관 기간을 초과한 메시지는 삭제됩니다.

또한 최신 메시지를 일정 건수만큼 가져오는 기능도 마련되어 있습니다. 이를 통해 룸에 입장한 직후의 과거 로그 표시 등을 효율적으로 처리할 수 있습니다.

## 금칙어

메시지 페이로드에 부적절한 단어가 포함되어 있을 수 있습니다.
GS2-Script 와 연계하면 부적절한 단어가 포함된 메시지의 게시를 거부하거나, 메시지 페이로드를 재작성할 수 있습니다.

GS2-Script 는 HTTP 통신을 발행할 수 있으므로, 자체적으로 보유한 금칙어 체크 서버로 포워딩하여 처리하는 것도 가능합니다.

```plantuml
actor Player
participant "GS2-Chat#Room"
participant "GS2-Script#Script"
Player -> "GS2-Chat#Room" : Post Message
"GS2-Chat#Room" -> "GS2-Script#Script" : Trigger post message
"GS2-Script#Script" -> "GS2-Chat#Room" : Continue? / Replaced Message
```

## 카테고리 모델과 알림 설정

메시지 카테고리는 마스터 데이터로 정의할 수 있으며, 카테고리별로 액세스 토큰을 사용한 게시를 거부할지 여부나 오프라인 시 모바일 푸시 알림으로 전송할지 여부를 설정할 수 있습니다.

## 트랜잭션 액션

GS2-Chat 에서는 트랜잭션 액션을 제공하지 않습니다.

메시지 게시의 스크립트 트리거에서 다른 마이크로서비스의 API를 호출함으로써, 특정 메시지 게시에 따라 사용자 데이터를 재작성하는 등의 연계는 가능합니다.

## 스크립트 트리거

룸 생성·삭제, 메시지 게시, 구독 및 구독 해제 각 처리의 전후에 GS2-Script 를 호출할 수 있습니다. 동기 실행으로 검증이나 변조를 수행하거나, 비동기 실행으로 Amazon EventBridge 를 경유한 외부 연계를 구성할 수도 있습니다.

설정할 수 있는 주요 이벤트 트리거와 스크립트 설정 이름은 다음과 같습니다.

- `createRoomScript`(완료 알림: `createRoomDone`): 룸 생성의 전후
- `deleteRoomScript`(완료 알림: `deleteRoomDone`): 룸 삭제의 전후
- `postMessageScript`(완료 알림: `postMessageDone`): 메시지 게시의 전후
- `subscribeRoomScript`(완료 알림: `subscribeRoomDone`): 룸 구독의 전후
- `unsubscribeRoomScript`(완료 알림: `unsubscribeRoomDone`): 구독 해제의 전후

## 푸시 알림

설정할 수 있는 주요 푸시 알림과 설정 이름은 다음과 같습니다.

- `postNotification`: 구독 중인 룸에 새로운 게시물이 있을 때 알림

알림 카테고리는 `notificationTypes` 로 제어할 수 있으며, 오프라인 단말에 대한 모바일 푸시 전송 설정도 가능합니다.

## 구현 예제

### 룸 생성



**Unity**
```csharp

    var result = await gs2.Chat.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).CreateRoomAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Chat->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    );
    const auto Future = Domain->CreateRoom(
        "room-0001", // name
        nullptr, // metadata
        nullptr, // password
        nullptr // whiteListUserIds
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

    // obtain changed values / result values
    const auto Future2 = Future->GetTask().Result()->Model();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError()) return false;
    const auto Result = Future2->GetTask().Result();
```
**Godot**
```gdscript

var domain = ez.chat.namespace_(
        "namespace-0001"
    ).me(game_session)

var async_result = await domain.create_room(
    "room-0001", # name
    null, # metadata
    null, # password
    null # white_list_user_ids
)
if async_result.error != null:
    if async_result.error.type == "NoAccessPrivilegesException":
        # 룸에 설정된 화이트리스트에 로그인 중인 사용자가 포함되어 있지 않습니다
        pass
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 룸 구독



**Unity**
```csharp

    var result = await gs2.Chat.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Subscribe(
        roomName: "room-0001"
    ).SubscribeAsync(
        notificationTypes: new [] {
            new Gs2.Unity.Gs2Chat.Model.EzNotificationType {
                category = 0,
                enableTransferMobilePushNotification = false,
            },
        }
    );
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Chat->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Subscribe(
        "room-0001" // roomName
    );
    const auto Future = Domain->Subscribe(
        []
        {
            const auto v = MakeShared<TArray<TSharedPtr<Gs2::Chat::Model::FNotificationType>>>();
            v->Add(MakeShared<Gs2::Chat::Model::FNotificationType>());
            return v;
        }() // notificationTypes
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

    // obtain changed values / result values
    const auto Future2 = Future->GetTask().Result()->Model();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError()) return false;
    const auto Result = Future2->GetTask().Result();
```
**Godot**
```gdscript

var domain = ez.chat.namespace_(
        "namespace-0001"
    ).me(game_session).subscribe(
        "room-0001"
    )

var async_result = await domain.subscribe(
    [
        (Gs2ChatEzNotificationType.new()),
    ] # notification_types
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 룸 구독 해제



**Unity**
```csharp

    var result = await gs2.Chat.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Subscribe(
        roomName: "room-0001"
    ).UnsubscribeAsync(
    );
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Chat->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Subscribe(
        "room-0001" // roomName
    );
    const auto Future = Domain->Unsubscribe(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
```
**Godot**
```gdscript

var domain = ez.chat.namespace_(
        "namespace-0001"
    ).me(game_session).subscribe(
        "room-0001"
    )

var async_result = await domain.unsubscribe(
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 메시지 게시



**Unity**
```csharp

    var result = await gs2.Chat.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Room(
        roomName: "room-0001",
        password: null
    ).PostAsync(
        metadata: "MESSAGE_0001",
        category: 0
    );
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Chat->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Room(
        "room-0001", // roomName
        nullptr // password
    );
    const auto Future = Domain->Post(
        "MESSAGE_0001", // metadata
        0 // category
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

    // obtain changed values / result values
    const auto Future2 = Future->GetTask().Result()->Model();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError()) return false;
    const auto Result = Future2->GetTask().Result();
```
**Godot**
```gdscript

var domain = ez.chat.namespace_(
        "namespace-0001"
    ).me(game_session).room(
        "room-0001",
        null
    )

var async_result = await domain.post(
    "MESSAGE_0001", # metadata
    null # category
)
if async_result.error != null:
    if async_result.error.type == "NoAccessPrivilegesException":
        # 룸에 설정된 화이트리스트에 로그인 중인 사용자가 포함되어 있지 않습니다
        pass
    if async_result.error.type == "PasswordRequiredException":
        # 룸에 접근하려면 비밀번호 지정이 필요합니다
        pass
    if async_result.error.type == "PasswordIncorrectException":
        # 룸에 설정된 비밀번호와 지정한 비밀번호가 일치하지 않습니다
        pass
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 메시지 목록 가져오기



**Unity**
```csharp

    var items = await gs2.Chat.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Room(
        roomName: "room-0001",
        password: null
    ).MessagesAsync(
    ).ToListAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Chat->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Room(
        "room-0001", // roomName
        nullptr // password
    );
    const auto It = Domain->Messages(
    );
    TArray<Gs2::UE5::Chat::Model::FEzMessagePtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }
```
**Godot**
```gdscript

var iterator = ez.chat.namespace_(
        "namespace-0001"
    ).me(
        game_session
    ).room(
        "room-0001",
        null
    ).messages(
    )

var async_result = await iterator.load()
if async_result.error != null:
    # 에러를 처리
    push_error(str(async_result.error))
    return

var items = async_result.result

```


### 메시지 수신 알림 핸들링

MessagesAsync 는 캐시가 있는 경우 캐시를 응답하며, 메시지 목록에서 가져올 수 있는 데이터의 캐시는 메시지 수신 알림을 받았을 때 SDK 에 의해 자동으로 갱신됩니다.
따라서 일반적으로는 이 이벤트를 핸들링할 필요가 없으며, 필요한 시점에 MessagesAsync 를 호출하면 보통 캐시를 이용하여 최신 메시지 목록에 접근할 수 있습니다.



**Unity**
```csharp

    gs2.Chat.OnPostNotification += PostNotification =>
    {
        var namespaceName = PostNotification.NamespaceName;
        var roomName = PostNotification.RoomName;
    };
```
**Unreal Engine**
```cpp

    Gs2->Chat->OnPostNotification().AddLambda([](const auto Notification)
    {
        const auto NamespaceName = Notification->NamespaceNameValue;
        const auto RoomName = Notification->RoomNameValue;
    });
```
**Godot**
```gdscript

ez.gs2.notification_received.connect(func(message):
    if message.subject != "Gs2Chat:PostNotification":
        return
    var notification = message.parse()
    if notification == null:
        return
    var namespace_name = notification.namespace_name
    var room_name = notification.room_name
    var user_id = notification.user_id
    var category = notification.category
    var created_at = notification.created_at
)

```


## 기타 기능

### 클라이언트에서 게시할 수 없는 카테고리

마스터 데이터의 CategoryModel 에서 rejectAccessTokenPost 를 true 로 설정하면 해당 카테고리에는 액세스 토큰을 사용한 게시가 불가능해집니다.
이 기능을 활용하면 스크립트 등을 경유해서만 게시할 수 있는 카테고리를 준비하여, 시스템 메시지를 저장하는 용도로 사용할 수 있습니다.

### 플레이어에 의한 룸 생성 가능 여부

네임스페이스 설정의 allowCreateRoom 을 이용하면 플레이어가 자유롭게 룸을 생성할 수 있는지 여부를 관리 측에서 제어할 수 있습니다.

allowCreateRoom=true : 플레이어가 직접 룸을 생성 가능
allowCreateRoom=false : 운영 측이 준비한 룸만 이용 가능

대규모 룸 난립을 방지하고 싶거나 특정 룸만 허용하고 싶은 경우, 이 설정을 적절히 조정하시기 바랍니다.

## 메시지 카테고리 활용 예

메시지에는 카테고리를 설정할 수 있습니다. 카테고리를 사용하면 클라이언트 측에서 페이로드 파싱 방법을 전환할 수 있습니다.

예를 들어 다음과 같은 사용 방법이 가능합니다.

- `카테고리 0`: 텍스트가 페이로드에 그대로 포함되는 일반 텍스트 메시지
- `카테고리 1`: 페이로드에 스탬프 ID가 포함되는 스탬프 메시지

이처럼 카테고리를 메시지의 종류나 중요도별로 구분함으로써 게임 플레이어가 받는 알림의 종류를 스스로 설정할 수 있게 되어 사용자 경험을 향상시킬 수 있습니다.

### 마스터 데이터 관리
마스터 데이터를 등록함으로써 마이크로서비스에서 이용 가능한 데이터와 동작을 설정할 수 있습니다.

마스터 데이터의 종류에는 다음이 있습니다.

- `CategoryModel`: 카테고리 번호별 게시 가능 여부(`rejectAccessTokenPost`) 및 오프라인 시 모바일 푸시 알림 전송 설정

마스터 데이터 등록은 관리 콘솔에서 등록하는 방법 외에도, GitHub 에서 데이터를 반영하거나 GS2-Deploy 를 사용하여 CI 에서 등록하는 워크플로우를 구성하는 것도 가능합니다.

## 상세 레퍼런스

[GS2-Chat API 레퍼런스](../../api_reference/chat)



