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

# 계정 구독 및 업데이트

> Laserstream을 사용하여 계정 업데이트를 구독하고 온체인 상태 변경을 효율적으로 추적하는 방법을 알아보세요.

온체인 변경에 대응해야 하는 애플리케이션을 구축할 때 계정 업데이트를 위해 RPC 엔드포인트를 폴링하는 것은 비효율적이고 느립니다. 계정 구독은 계정 상태 변경에 대한 실시간 업데이트를 애플리케이션에 직접 제공하여 이를 해결합니다.

이 가이드에서는 계정 구독에 대해 알아야 할 모든 것을 다룹니다: 그것이 무엇인지, 어떻게 작동하는지, 특정 사용 사례에 최적화하는 방법입니다.

***

## 계정 모델 컨텍스트

<Info>
  Solana 계정과 그 구조에 익숙한 경우 이 섹션을 생략하세요.
</Info>

Solana는 모든 데이터가 계정에 존재하는 계정 기반 모델을 사용합니다. 계정은 데이터와 메타데이터를 저장하는 컨테이너입니다. 각 계정에는 다음이 포함됩니다:

* **데이터**: 프로그램 상태, 토큰 잔액 또는 기타 정보가 저장된 실제 바이트
* **소유자**: 이 계정을 제어하고 데이터를 수정할 수 있는 프로그램
* **Lamports**: 렌트 면제를 위한 계정의 SOL 잔액
* **실행 가능 여부**: 이 계정이 프로그램 코드를 포함하는지 여부

프로그램은 상태를 내부에 저장하지 않는 무상태입니다. 대신, 별도의 계정을 생성하고 관리하여 상태를 저장합니다. 프로그램과 상호 작용할 때 읽거나 쓸 계정을 전달합니다.

이 설계는 계정 구독을 강력하게 만듭니다: 특정 계정, 프로그램이 소유한 모든 계정 또는 특정 조건에 맞는 계정의 변경을 감시할 수 있습니다.

***

## 기본 계정 구독

토큰 계정의 변경을 구독하는 간단한 예제로 시작해 보겠습니다. 이 스크립트는 토큰 잔액이 변경될 때마다 알림을 제공합니다:

```ts [expandable] theme={"system"}
import { subscribe, CommitmentLevel, SubscribeUpdate, LaserstreamConfig } from 'helius-laserstream';
import bs58 from 'bs58';

// Utility function to recursively convert Buffer objects to base58 strings
function convertBuffersToBase58(obj: any): any {
  if (obj === null || obj === undefined) {
    return obj;
  }
  
  if (Buffer.isBuffer(obj)) {
    return bs58.encode(obj);
  }
  
  if (Array.isArray(obj)) {
    return obj.map(convertBuffersToBase58);
  }
  
  if (typeof obj === 'object') {
    const result: any = {};
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        result[key] = convertBuffersToBase58(obj[key]);
      }
    }
    return result;
  }
  
  return obj;
}

async function main() {
  console.log('🏦 Basic Account Subscription Example');

  const config: LaserstreamConfig = {
    apiKey: 'YOUR_API_KEY', // from https://dashboard.helius.dev/
    endpoint: 'https://laserstream-mainnet-ewr.helius-rpc.com', // pick the closest region
  };

  const request = {
    accounts: {
      "token-accounts": {
        account: [], // Specific account pubkeys (empty = all)
        owner: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"], // Token program
        filters: [
          {
            // Only token accounts (165 bytes)
            datasize: 165
          }
        ]
      }
    },
    commitment: CommitmentLevel.CONFIRMED,
    transactions: {}, slots: {}, transactionsStatus: {}, blocks: {}, blocksMeta: {}, entry: {}, accountsDataSlice: []
  };

  const stream = await subscribe(
    config,
    request,
    async (update: SubscribeUpdate) => {
      const readableUpdate = convertBuffersToBase58(update);
      console.log('🏦 Account Update:', JSON.stringify(readableUpdate, null, 2));
    },
    async (err) => console.error('❌ Stream error:', err)
  );

  console.log(`✅ Account subscription started (id: ${stream.id})`);

  process.on('SIGINT', () => {
    console.log('\n🛑 Cancelling stream...');
    stream.cancel();
    process.exit(0);
  });
}

main().catch(console.error);
```

이 기본 구독을 실행하면 콘솔에 실시간 계정 업데이트가 스트리밍되는 것을 볼 수 있습니다:

```
🏦 Basic Account Subscription Example
✅ Account subscription started (id: xyz789)

🏦 Account Update: {
  "filters": ["token-accounts"],
  "account": {
    "account": {
      "pubkey": "BKMHWYLAX4un3HUbR7a3u9jPmzCiLNa4mSj1RiX11eWF",
      "lamports": "2039280",
      "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      "rentEpoch": "18446744073709551615",
      "data": "2NUx6Xw9QkmgJCyYUP3d8TPsjJhUpSM7hcy9Fi1juGc6g9DrpPFyGyvBZzu9qiAjFtyEDbNiLHYFJsq1dD5Wxr4LPcF9Dqs4AJa15L1N92pfinnoKVfCsVCcybhV1iwkCCTMeMyxTRA4tqJm6MrLwgKG3HmmwVdhsEuXjSsGJFXGzgfgPHucVzBEgAqcpH9JPpoaQyis2MFwRJLjenxzkE8xJzWHv1Zk2T",
      "writeVersion": "2697618495",
      "txnSignature": "5C9Hr5nG2j8eQz6inxPmfyjbYdmXddzUDyR1iQgEnjYQ3RNvuP4Zzc8t1enLNy7Rk8KNCtQPEQztENYWxkt9GaVD"
    },
    "slot": "352366983"
  },
  "createdAt": "2025-07-10T11:56:22.027Z"
}
```

**무슨 일이 일어났나요?** 우리의 구독이 완벽하게 작동했습니다! 우리는 Laserstream에게 토큰 계정 변경에 대해 알리라고 요청했고, 계정 `BKMHWYLAX4un3HUbR7a3u9jPmzCiLNa4mSj1RiX11eWF`에 대한 업데이트를 받았습니다.

이 계정에는 다음이 있습니다:

* **2,039,280 lamports** (\~0.002 SOL 잔액 - 이 토큰 계정의 렌트 면제 금액입니다)
* **소유자 프로그램** `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA` (이는 SPL 토큰 프로그램입니다)
* **거래 서명** `5C9Hr5nG2j8eQz6inxPmfyjbYdmXddzUDyR1iQgEnjYQ3RNvuP4Zzc8t1enLNy7Rk8KNCtQPEQztENYWxkt9GaVD` 이 계정 변경을 야기한 특정 거래를 보여줍니다
* **슬롯 352366983** 이 업데이트가 블록체인에서 발생한 시간을 나타냅니다
* **데이터 필드**는 base58로 인코딩된 165바이트의 계정 데이터를 포함합니다

### 데이터 크기를 사용한 계정 필터링 이해하기

데이터 필드는 실제 토큰 계정 구조를 포함하는 데 중요합니다. 이 이해를 사용하여 **스마트 계정 필터링**을 수행합시다.

#### 데이터 크기 필터링을 사용하는 이유는 무엇입니까?

필터링이 필요한 이유를 이해하려면 먼저 토큰 계정의 정의를 이해해야 합니다. **지갑이 보유한 각 토큰마다 온체인에 별도의 계정이 존재합니다.** 지갑이 3개의 다른 토큰(USDC, BONK, SOL)을 보유하고 있다면 실제로 1개의 지갑 계정(주요 SOL 계정)과 3개의 토큰 계정(각 토큰 유형마다 하나씩)이 있습니다. 각 토큰 계정은 정확히 165바이트이며, 보유하는 토큰(발행 주소), 소유자(지갑 주소), 토큰 양(금액)을 저장합니다.

토큰 프로그램은 Solana에서 **수백만 개의 계정**을 소유하고 있지만 사용자 잔액을 보유하는 "토큰 계정"이라고 생각되는 것들은 모두 아닙니다. 필터링 없이와 필터링과 함께 발생하는 상황은 다음과 같습니다:

**필터링 없이 - 범람:**

```ts theme={"system"}
accounts: {
  "all-token-program-accounts": {
    owner: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"] // ❌ Overwhelming!
  }
}
```

이것은 토큰 프로그램에 의해 소유된 모든 계정을 구독합니다. 이는 다음을 포함합니다:

* **토큰 계정** (165바이트) - 사용자 잔액: 수백만 개의 계정
* **발행 계정** (82바이트) - 토큰 정의: 수십만 개의 계정
* **멀티시그 계정** (355바이트) - 공유 지갑 제어: 수만 개의 계정
* **관련 토큰 프로그램 계정** (다양한 크기) - 수백만 개의 계정

<Warning>
  **결과:** 애플리케이션은 관심 없는 수백만 개의 계정 업데이트를 지속적으로 받습니다.
</Warning>

**스마트 필터링과 함께 - 정밀한 조작:**

```ts theme={"system"}
accounts: {
  "token-accounts-only": {
    owner: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
    filters: [{ datasize: 165 }] // ✅ Only standard token accounts
  }
}
```

이는 165바이트 계정만으로 축소되어, 사용자의 토큰 잔액 계정만을 추적하는 데 필요한 정확한 것입니다.

**차이점:**

* **필터링 없이:** 수백만 개의 계정 업데이트 (발행 생성, 멀티시그 변경 등)
* **데이터 크기 필터링과 함께:** 오직 토큰 잔액 변경만

이는 정확한 사용자 토큰 보유를 나타내는 계정에만 집중하여 노이즈를 크게 줄입니다.

#### 165바이트는 어디에서 왔나요?

이것은 마법이 아닙니다 - [SPL 토큰 프로그램의 계정 구조](https://github.com/solana-program/token/blob/d05d10807fe8cf157f6e1f024c708274c30c953a/program/src/state.rs#L87)에서 비롯됩니다. 소스 코드를 확인하면 `Account` 구조체가 정확히 165바이트로 정의되어 있는 것을 볼 수 있습니다:

```rust theme={"system"}
pub struct Account {
    pub mint: Pubkey,                    // 32 bytes
    pub owner: Pubkey,                   // 32 bytes  
    pub amount: u64,                     // 8 bytes
    pub delegate: COption<Pubkey>,       // 4 + 32 bytes
    pub state: AccountState,             // 1 byte
    pub is_native: COption<u64>,         // 4 + 8 bytes
    pub delegated_amount: u64,           // 8 bytes
    pub close_authority: COption<Pubkey> // 4 + 32 bytes
}
// Total: 32+32+8+36+1+12+8+36 = 165 bytes
```

이 고정된 크기를 통해 표준 토큰 계정을 정확하게 필터링하고 다음을 제외할 수 있습니다:

* 발행 계정 (82바이트)
* 멀티시그 계정 (355바이트)
* 관련 토큰 계정 프로그램 계정
* 다양한 크기의 기타 토큰 관련 계정

다른 프로그램에서 계정 크기를 계산하려면 [Anchor Space Reference](https://www.anchor-lang.com/docs/references/space)를 참조하세요 - Pubkey = 32바이트, u64 = 8바이트 등 서로 다른 데이터 유형이 차지하는 공간을 보여줍니다.

#### 계정 구조 디코딩하기

왜 165바이트를 필터링했는지 이해했으니, 이제 예제 계정 안에 무엇이 있는지 디코딩해 봅시다:

```
Base58 data: 2NUx6Xw9QkmgJCyYUP3d8TPsjJhUpSM7hcy9Fi1juGc6g9...
```

165바이트는 다음과 같이 구성됩니다:

* **바이트 0-31:** 발행 주소 (이 계정이 보유한 토큰)
* **바이트 32-63:** 소유자 주소 (이 토큰 계정의 소유자)
* **바이트 64-71:** 토큰 양 (계정에 있는 토큰 수)
* **바이트 72-164:** 추가 메타데이터 (대리인, 상태, 닫기 권한 등)

이 구조화된 접근 방식은 정밀한 제어를 제공합니다: 표준 토큰 계정에 대한 업데이트만 수신하고 다른 계정 유형의 잡음은 받지 않습니다.

### 필터 결합하기: 데이터 크기 + memcmp로 레이저 정밀도

발행 주소가 바이트 0-31에 있다는 것을 알았으니 더 구체적으로 접근할 수 있습니다. 예를 들어 USDC 토큰 계정만 모니터링하고 싶다고 가정해 봅시다. 우리는 우리의 `datasize` 필터를 `memcmp` 필터와 결합하여 정확한 발행 주소를 타겟팅할 수 있습니다:

```ts theme={"system"}
const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";

const request = {
  accounts: {
    "usdc-only": {
      owner: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
      filters: [
        { datasize: 165 },                    // Standard token accounts only
        { 
          memcmp: {
            offset: 0,                         // Mint address starts at byte 0
            base58: USDC_MINT                  // Match this specific mint
          }
        }
      ]
    }
  },
  // ... other config
};
```

**점진적 필터링 전략:**

1. **소유자 필터:** "토큰 프로그램이 소유한 계정을 주세요" (수백만 개의 계정)
2. **데이터 크기 필터:** "하지만 165바이트 표준 토큰 계정만" (수십만)
3. **Memcmp 필터:** "그리고 USDC를 보유하고 있는 계정만" (수천)

이러한 넓은 범위에서 구체적으로 나아가는 것은 효율적인 계정 모니터링의 핵심입니다. 각 필터는 결과 세트를 좁히므로, 관심 있는 정확한 업데이트만 수신합니다.

**중요:** 모든 필터는 AND 로직을 사용합니다 - 계정 업데이트가 트리거되려면 모든 조건이 만족되어야 합니다.

### USDC 계정 업데이트 읽기: 누가, 얼마나, 어디서?

이제 필터링된 업데이트가 실제로 무엇을 포함하는지 살펴봅시다. 토큰 계정이 변경될 때 주요 질문에 답하는 USDC 전용 모니터를 만들어 봅시다:

* **누가** 이 토큰 계정을 소유하고 있나요?
* **얼마나 많은** USDC가 이제 들어있나요?
* **어디에서** (어떤 특정 계정) 변경되었나요?
* **언제** 이 변경이 발생했나요?
* **어떤 거래**가 이 변경을 발생시켰나요?

원시 계정 업데이트는 디코딩해야 하는 이진 데이터를 포함합니다. Solana는 주소와 서명을 위해 base58 인코딩을 사용하므로, `bs58.encode()` 함수를 사용하여 이진 버퍼 객체를 읽을 수 있는 문자열로 변환합니다.

```ts [expandable] theme={"system"}
import { subscribe, CommitmentLevel, SubscribeUpdate, LaserstreamConfig } from 'helius-laserstream';
import bs58 from 'bs58';

async function main() {
  console.log('USDC Account Monitor');

  const config: LaserstreamConfig = {
    apiKey: 'YOUR_API_KEY', // from https://dashboard.helius.dev/
    endpoint: 'https://laserstream-mainnet-ewr.helius-rpc.com', // pick the closest region
  };

  const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";

  const request = {
    accounts: {
      "usdc-accounts": {
        account: [],
        owner: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
        filters: [
          { datasize: 165 },                           // Standard token accounts
          { memcmp: { offset: 0, base58: USDC_MINT } } // Only USDC
        ]
      }
    },
    commitment: CommitmentLevel.CONFIRMED,
    transactions: {}, slots: {}, transactionsStatus: {}, blocks: {}, blocksMeta: {}, entry: {}, accountsDataSlice: []
  };

  const stream = await subscribe(
    config,
    request,
    async (update: SubscribeUpdate) => {
      explainAccountUpdate(update);
    },
    async (err) => console.error('Stream error:', err)
  );

  console.log(`Account monitor started (id: ${stream.id})`);

  process.on('SIGINT', () => {
    console.log('\nCancelling stream...');
    stream.cancel();
    process.exit(0);
  });
}

function explainAccountUpdate(update: SubscribeUpdate) {
  if (!update.account) return;
  
  const account = update.account.account;
  
  // Decode the key addresses
  const tokenAccountAddress = bs58.encode(account.pubkey);
  const transactionSignature = account.txnSignature ? bs58.encode(account.txnSignature) : 'Unknown';
  
  // Extract and decode the token account data (165 bytes)
  const walletOwner = bs58.encode(account.data.slice(32, 64));       // Bytes 32-63: Owner
  const tokenAmount = account.data.readBigUInt64LE(64);              // Bytes 64-71: Amount
  const usdcAmount = Number(tokenAmount) / 1_000_000;                // Convert to USDC (6 decimals)
  
  console.log(`Account: ${tokenAccountAddress}`);
  console.log(`Owner: ${walletOwner}`);
  console.log(`Balance: ${usdcAmount.toLocaleString()} USDC`);
  console.log(`Slot: ${update.account.slot}`);
  console.log(`Transaction: ${transactionSignature.slice(0, 8)}...`);
  console.log('---');
}

main().catch(console.error);
```

이 USDC 모니터를 실행하면 다음과 같이 깔끔하고 구조화된 출력이 표시됩니다:

```
USDC Account Monitor
Account monitor started (id: abc123)

Account: 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU
Owner: 9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM
Balance: 1,500 USDC
Slot: 352154103
Transaction: 5v8fy0eJ...
---
Account: BQy5rNRxLfcaK6554PMzsg4VJsFXzwGnAnayb8TZKgZX
Owner: HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH
Balance: 0 USDC
Slot: 352154103
Transaction: 5v8fy0eJ...
---
```

각 블록은 상태가 변경된 USDC 계정을 나타냅니다. 첫 번째 계정은 이제 1,500 USDC를 보유하고 있으며, 두 번째 계정은 0 USDC로 비워졌습니다. 각 거래 후 현재 잔액을 즉시 얻을 수 있으며, 어떤 특정 계정이 변경되었고 언제 발생했는지도 볼 수 있습니다.

계정 구독은 각 계정에서 발생한 최종 결과를 보여주며, 거래 세부 정보는 아닙니다. 전체 거래 맥락 (누가 누구에게 보냈는지, 수수료 등)을 이해하려면 표시된 서명을 사용하여 전체 거래를 가져와야 합니다.

## 완전한 필터링 참조

우리가 사용한 기본 `owner`, `datasize`, `memcmp` 필터 외에도 계정 구독은 결과를 더욱 좁힐 수 있는 추가 필터링 옵션을 지원합니다:

### 특정 계정 필터링

공개 키로 정확한 계정을 모니터링하세요:

```ts theme={"system"}
accounts: {
  "specific-accounts": {
    account: [
      "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
      "BQy5rNRxLfcaK6554PMzsg4VJsFXzwGnAnayb8TZKgZX"
    ]
  }
}
```

이 접근 방식은 애플리케이션의 재무 계정 모니터링이나 특정 사용자 계정 모니터링처럼 정확히 어떤 계정이 중요한지 알고 있을 때 잘 작동합니다.

매우 큰 계정 세트의 경우, 명시적 공개 키 목록은 비용이 많이 듭니다 - 구독 요청에서 계정당 32바이트. \~10,000개 이상의 계정에서는 압축된 [쿠쿠 필터](/docs/ko/laserstream/cuckoo-filters) (\~계정당 3–4바이트)를 사용하여 단일 스트림에서 수십만의 계정을 추적하세요. Rust 및 JavaScript SDKs에서 사용 가능합니다.

### 결합된 필터링 전략

여러 필터 유형을 결합하는 데에서 강력함이 나옵니다. 정신적 모델은 다음과 같습니다:

1. `owner`로 **넓은 네트**를 던집니다 - "이 프로그램에서 관리하는 모든 계정을 주세요"
2. `datasize`으로 **구조로 필터링합니다** - "하지만 이 특정 유형의 계정만"
3. `memcmp`으로 **특정 데이터 타겟팅** - "그리고 이 특정 정보를 포함하는 것만"
4. `account`으로 **알려진 계정 모니터링** - "또는 내가 관심 있는 정확한 계정만 시청하세요"

예를 들어, 고가치 USDC 계정을 모니터링할 때:

```ts theme={"system"}
accounts: {
  "high-value-usdc": {
    owner: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
    filters: [
      { datasize: 165 },                           // Token accounts only
      { memcmp: { offset: 0, base58: USDC_MINT } } // USDC only
      // Note: You'd implement balance filtering in your callback logic
    ]
  }
}
```

핵심 통찰력은 각 필터가 수신하는 업데이트의 양을 줄인다는 것입니다. 필터링 없이 대부분의 계정 업데이트의 압도적인 양을 수신할 수 있습니다. 스마트 필터링으로, 경우에 맞는 업데이트만 받습니다.

### 큰 그림 이해하기

계정 구독을 데이터베이스 변경의 라이브 피드를 보는 것으로 생각하세요. Solana의 상태는 기본적으로 각 계정이 항목인 대규모 키-값 저장소입니다. 프로그램이 실행되면 이러한 계정을 수정합니다. 구독은 특정 항목이 실시간으로 변경되는 것을 감시할 수 있습니다.

필터링 시스템은 데이터베이스 인덱스와 같이 작동합니다 - "모든 변경"을 보는 것이 아니라 "이러한 기준을 충족하는 계정의 변경"을 보는 것입니다. 이는 관련 없는 데이터로 시스템을 혼란시키지 않고, 관련 온체인 이벤트에 즉시 반응하는 응답형 애플리케이션을 구축할 수 있게 합니다.

## 다른 프로그램에 이 패턴 적용하기

우리가 배운 접근 방식은 모든 Solana 프로그램에 적용할 수 있습니다. 일반적인 패턴은 다음과 같습니다:

1. **계정 구조 조사** - 프로그램의 소스 코드나 문서를 확인하세요
2. **소유자 필터링으로 시작** - 계정을 관리하는 프로그램을 타겟팅하세요
3. **구조 필터를 적용** - 계정 크기, 데이터 패턴, 기타 특성을 사용해 특정 계정 유형으로 좁히세요
4. **타겟 필터 추가** - 애플리케이션에 중요한 특정 계정, 상태 또는 데이터 값에 집중하세요.
