# 저장소

**URL:** https://heroiclabs.com/docs/kr/nakama/tutorials/unity/pirate-panic/storage/
**Summary:** Pirate Panic 튜토리얼 게임에서 Nakama 저장소 엔진을 사용하는 방법을 알아보세요.

---


# 저장소

대부분의 게임의 경우, 중요한 플레이어 데이터(예: 인벤토리 또는 게임 통계)를 계속 추적해야 합니다. 싱글플레이어 게임의 경우, 클라이언트에 로컬로 정보를 저장하면 되지만, 멀티플레이어 게임에서는 데이터를 동시에 여러 개의 클라이언트로 전송하거나 변경되지 않도록 보호해야 합니다.

Nakama [저장소 엔진](../../../../concepts/storage/collections/)을 통해 서버에서 사용자 정보를 안전하게 저장하고 액세스할 수 있습니다.

이 섹션에서는 아래 항목에 대해서 살펴봅니다:

* 서버 데이터베이스에서 읽기 및 쓰기
* 가상 화폐를 보유하기 위한 지갑 생성
* 지갑 장부를 통해 트랜잭션 관리
* 지갑 내에 사용자 지정 메타 데이터 포함

## 모음

Nakama에서 사용자 지정 데이터는 특정 사용자에 대한 데이터를 보유하는 `objects`의 그룹을 포함하는 `collections`(으)로 구성됩니다. 이 구조를 통해 유사한 정보를 저장하고 쉽게 액세스할 수 있습니다.

Pirate Panic의 경우, 플레이어는 대결에서 사용할 카드 덱을 들고 있습니다:

![덱 탐색기]({{< fingerprint_image "/images/pages/nakama/tutorials/unity/pirate-panic/storage-deck.png" >}})

덱의 상태를 서버에 저장하여 사용자가 게임을 종료했다가 다시 참여해도 덱의 정보가 남아있도록 합니다.

### 서버에서 읽기 및 쓰기

먼저, 각 플레이어가 시작할 카드를 설정합니다:

**deck.ts**
```typescript
let deck: any = {};

const DefaultDeckCards = [
  {
    type: 1,
    level: 1,
  },
  ... // more cards here
]

DefaultDeckCards.forEach(c => {
  deck[nk.uuidv4()] = c;
});
```

여기서, 기존의 덱 데이터는 개체 문자 배열로 표시됩니다. 각각의 개체에는 카드 유형과 카드가 업그레이드된 수준을 표시하는 두 개의 숫자 속성이 포함됩니다.

카드의 유형과 수준은 Pirate Panic에만 표시되기 때문에 게임에서는 자체적으로 사용자 지정 정보가 포함될 확률이 높습니다. 예를 들어, 각각의 개체에는 취득한 날짜, 특별한 변이, 플레이어가 알아야 할 특이사항 등이 포함될 수 있습니다.

그런 다음, `uuidv4()` 함수를 사용하여 고유한 ID를 부여하고, 데이터를 저장소 개체로 변환해야 합니다. 이 작업은 동일한 카드의 상태를 추적할 때 필요합니다.

이 덱이 있는 경우, `storageWrite` 함수를 사용하여 저장할 수 있습니다:

**deck.ts**
```typescript
nk.storageWrite([
  {
    key: "card_collection",
    collection: "user_cards",
    userId: userId,
    value: deck,
    permissionRead: 2,
    permissionWrite: 0,
  }
]);
```

다음 구조를 사용하는 집합을 생성합니다:

```typescript
card_collection
 user_cards: {
    userId: some-user-id-3fb1d6, // some arbitrary ID
    permissionRead: 2,
    permissionWrite: 2,
    ... // some other properties
    value: {
      some-random-id: { // another arbitrary ID
        type: 1,
        level: 1,
      }
      ... // more cards
    }
 }
```

나중에 이 카드에 액세스하려면 요청 개체를 사용하여 `storageRead`(을)를 호출할 수 있습니다:

**deck.ts**
```typescript
function loadUserCards(nk: nkruntime.Nakama, logger: nkruntime.Logger, userId: string): CardCollection {
  let storageReadReq: nkruntime.StorageReadRequest = {
    key: DeckCollectionKey,
    collection: DeckCollectionName,
    userId: userId,
  }

  let objects: nkruntime.StorageObject[] = nk.storageRead([storageReadReq]);

  // Get the original collection for processing
  let storedCardCollection = objects[0].value;
  ...
}
```

단일 `storageRead` 호출을 사용하여 여러 `StorageReadRequest`을(를) `storageRead`의 배열에 추가하고, 여러 요청 사항을 동시에 생성할 수 있습니다.

### 클라이언트에서 읽기 및 쓰기

Unity 클라이언트에서 집합과 인터페이스를 적용하기 위해서 [친구 찾기](../friends/#finding-friends)에서와 동일하게 RPC를 사용합니다. 이렇게 하면 사용자의 요청 사항 유형과 모든 민감한 작업이 서버에서만 진행되도록 제어할 수 있습니다.

로컬 플레이어가 함수 `ReadStorageObjectsAsync`을(를) 직접 사용하여 데이터를 읽을 수 있습니다:

**ProfilePanel.cs**
```csharp
StorageObjectId personalStorageId = new StorageObjectId();
personalStorageId.Collection = "personal";
personalStorageId.UserId = _connection.Session.UserId;
personalStorageId.Key = "player_data";

IApiStorageObjects personalStorageObjects = await _connection.Client.ReadStorageObjectsAsync(_connection.Session, personalStorageId);
```

여기서는, 찾고자 하는 개체를 식별할 때 사용하는 `StorageObjectId` `struct`(으)로 `Collection`, `UserId`, `Key` 속성을 전달합니다.

### 권한 추가

Nakama는 민감한 데이터에 승인되지 않은 액세스를 방지하기 위해서 데이터베이스에서 읽기 및 쓰기 권한 처리를 지원합니다.

세 가지 `permissionRead` 수준을 통해 개체 읽기를 보호할 수 있습니다:

* `0`: 아무도 개체를 읽을 수 없습니다(서버 제외).
* `1`: 해당 개체(`userId` 대결)를 보유한 사용자만 읽을 수 있습니다.
* `2`: 아무나 이 개체를 읽을 수 있습니다.

쓰기에는 두 가지 수준이 있습니다. `0`은(는) 쓰기가 허용되지 않으며, `1`은(는) 소유자에 대해서만 쓰기만 허용됩니다. 쓰기의 경우, 서버는 권한을 건너뛰고 모든 개체를 작성할 수 있습니다. 사용자는 수준 `0`을(를) 사용하여 데이터를 직접 편집하지 않고 서버에서 쓰기 요청 사항을 확인합니다.

예를 들어, 사용자 통계를 모든 사람이 볼 수 있도록 하려고 하지만, 서버에서만 변경할 수 있습니다:

```typescript
const writeStats: nkruntime.StorageWriteRequest = {
  collection: "stats",
  key: "public",
  permissionRead: 2,
  permissionWrite: 0,
  value: initialState,
  userId: ctx.userId,
}
```

### 조건부 읽기 및 쓰기

여러 사용자가 동일한 개체에 액세스(쓰기 포함)가 필요할 수 있습니다.

수정 사항이 충돌하지 않도록 `version` 속성을 모든 쓰기 요청으로 전달할 수 있습니다. 서버에서 `writeStorageObjects`을(를) 사용하거나 클라이언트에서 `WriteStorageObjectsAsync`을(를) 사용하면, 입력된 버전이 데이터베이스에 저장된 버전과 일치할 때에만 쓰기 작업을 할 수 있습니다.

이 기능은 Pirate Panic에 표시되지 않지만, [조건부 저장](../../../../concepts/storage/collections/#conditional-writes)에 대해서 배우고 게임 내에서 실행할 수 있습니다.

## 지갑

일반적인 집합 외에 Nakama에는 지갑 기능이 포함되어 게임 내 화폐로 트랜잭션을 하거나 저장할 수 있도록 지원합니다.

Pirate Panic의 경우, 보석을 화폐로 사용하기 때문에 새로운 카드를 구입할 때 보석을 사용할 수 있습니다.

![보석!]({{< fingerprint_image "/images/pages/nakama/tutorials/unity/pirate-panic/storage-gems.png" >}})

### 지갑 수량 업데이트

집합과 달리, 지갑은 서버에서 직접 업데이트하여 사용자가 원하지 않거나 착취하는 형태의 트랜잭션이 발생하지 않도록 합니다.

네 개의 매개 변수를 사용하는 `walletUpdate`을(를) 통해 서버 측에서 지갑의 값을 변경할 수 있습니다.

* 변경할 지갑의 사용자 ID
* 업데이트할 수량과 모든 화폐 이름이 일치하는 키 값 쌍을 포함하는 개체
* 메타데이터 개체
* 장부 업데이트가 필요한 경우 서버에 알림을 제공하는 부울

Pirate Panic에는 한 개의 화폐만 있기 때문에 보석이 추가될 때마다 `changeset`을(를) 조정하지 않고 도움말 함수를 작성하여 숫자를 전달하면 됩니다:

**economy.ts**
```typescript
const currencyKeyName = "gems" // Can be changed to whatever you want!
...
function updateWallet(nk: nkruntime.Nakama, userId: string, amount: number, metadata: {[key: string]: any}): nkruntime.WalletUpdateResult {
  const changeset = {
    [currencyKeyName]: amount,
  }
  let result = nk.walletUpdate(userId, changeset, metadata, true);

  return result;
}
```

`amount`은(는) 현재 지갑에서 잔고를 변경할 양이고, 현재 잔고를 완전히 대체하지 않는다는 것을 유의합니다. 예를 들어, 지갑에 보석이 900개 있고 `updateWallet`에 100개가 호출된 경우, 새로운 잔고는 1,000(100이 아님)이 됩니다.

이 항목을 RPC에 적용하여 특정한 상황에서 Unity 클라이언트가 보석을 추가할 수 있도록 합니다. 예를 들어, 무료로 보석을 제공하는 버튼을 만들려고 합니다:

![]({{< fingerprint_image "/images/pages/nakama/tutorials/unity/pirate-panic/storage-freegems.png" >}})

RPC는 다음과 같이 표시됩니다:

```typescript
const rpcAddUserGems: nkruntime.RpcFunction = function(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama): string {
  let walletUpdateResult = updateWallet(nk, ctx.userId, 100, {});
  let updateString = JSON.stringify(walletUpdateResult);

  logger.debug("Added 100 gems to user %s wallet: %s", ctx.userId, updateString);

  return updateString;
}
```

Unity에서 호출하려면 다음과 같이 합니다:

```csharp
private void Awake() {
    ...
    _getFreeGemsButton.onClick.AddListener(HandleAddFreeGems); // Bind function to button
}
...
private async void HandleAddFreeGems() {
    // Call the RPC we just made!
    IApiRpc newGems = await _connection.Client.RpcAsync(_connection.Session, "add_user_gems");
    // IMPORTANT: Update the account instance to get the latest results!
    _connection.Account = await _connection.Client.GetAccountAsync(_connection.Session);
    ...
}
```

음의 수량을 전달하여 플레이어의 지갑에서 보석을 차감할 수도 있습니다. 예를 들어, 100개의 보석을 사용하여 카드를 구매하려면 `updateWallet(nk, ctx.userId, -100, {});`을(를) 호출합니다.

### 사용자 지정 메타데이터

메타데이터 객체를 사용하면 모든 사용자 지정 정보를 지갑 트랜잭션에 입력할 수 있습니다. `walletLedgerList`에 있는 모든 항목으로 정보가 전달되어 나중에 참조할 수 있습니다.

예를 들어, 대결 ID에 보상을 연결하여 같은 장소에서 보상이 제공되고 플레이어에 대한 전체 보상을 볼 수 있도록 하려면 다음과 같이 합니다:

```typescript
let metadata = {
  source: "match_reward",
  match_id: request.matchId,
};

updateWallet(nk, ctx.userId, score, metadata);
```

### 지갑 수량 가져오기

클라이언트 측의 지갑 정보는 인증 설정 시 사용한 연결 정보를 보유하는 `GameConnection` 클래스에서 `connection.Account`이(가) `IApiAccount` 인스턴스인 `connection.Account.Wallet`에 저장됩니다.

문자열로 전환된 JSON 개체 형식으로 제공되는 `Wallet` 변수를 얻으면, 다음 코드를 사용하여 지갑에서 많은 보석을 추출할 수 있습니다:

```csharp
private int GetGems(string wallet)
{
    Dictionary<string, int> currency = wallet.FromJson<Dictionary<string, int>>();

    if (currency.ContainsKey("gems"))
    {
        return currency["gems"];
    }

    return 0;
}
```

Pirate Panic의 경우, `GameConnection`을(를) 생성했기 때문에 다음을 사용하여 `GetGems`을(를) 호출할 수 있습니다:

```csharp
GetGems(_connection.Account.Wallet);
```

### 트랜잭션 장부

위에서 true로 설정한 `updateLedger`에 한 개의 최종 매개 변수가 있습니다.

`true`(으)로 설정된 `updateLedger`와(과) 함께 `walletUpdate`이(가) 호출되면 새로운 장부 항목이 생성됩니다. 이 작업을 통해 이전 트랜잭션과 세부내역을 살펴볼 수 있습니다.

과거부터 가장 최근까지 정렬된 모든 트랜잭션의 목록을 보려면 `walletLedgerList`을(를) 호출합니다.

그런 다음, `cursor`을(를) 사용하여 원하는 항목으로 이동할 수 있습니다. 예를 들어, 가장 최근의 커서 업데이트를 보려면 다음과 같이 합니다:

**match.ts**
```typescript
let items = nk.walletLedgerList(ctx.userId, 100);
while (items.cursor) {
  items = nk.walletLedgerList(ctx.userId, 100, items.cursor);
}
```

이 작업은 현재 대결의 보상으로 이동할 때 유용합니다. 최초 보상에 커서 업데이트가 되도록 설정하고 커서 업데이트를 사용하지 않는 상태에서 증분 업데이트(예: 타워 캡처)를 지속하면 이 전략을 사용하여 가장 최신의 대결에서 모든 트랜잭션의 목록을 가져올 수 있습니다.

## 다음 주제

[매치메이킹](../matchmaking/)
