신규: Helius가 Light Protocol을 인수했습니다
Solana Web3.js 2.0 SDK로 개발을 시작하는 방법
블로그/개발

Solana Web3.js 2.0 SDK로 개발을 시작하는 방법

개발자 경험 엔지니어X의 Anam AnsariLinkedIn의 Anam Ansari
개발자 교육X의 Mike MacCanaLinkedIn의 Mike MacCana
읽는 데 10분

시작하기에 앞서 이 글을 검토해 주신 Evan과 Nick에게 감사드립니다. 두 분의 소중한 피드백과 통찰이 큰 도움이 되었습니다.

소개

Solana Web3.js SDK는 Node.js, 웹, React Native 플랫폼에서 Solana 애플리케이션을 개발할 수 있는 강력한 TypeScript 및 JavaScript 라이브러리입니다. 2024년 11월 7일, Anza는 많은 기대를 모은 2.0 SDK 업데이트를 공개했습니다. 다양한 최신 JavaScript 기능과 개선 사항이 추가되었습니다. 주요 변경 사항으로는 bigint와 암호화를 위한 표준 JS 타입, 더 작은 번들 크기 등이 있으며, 개발자에게 중요한 업그레이드입니다. 

@solana/web3.js을 사용해 왔다면 소프트웨어를 새로운 v2.0 패키지로 포팅하거나, 버전을 명시해 v1.x로 고정해야 합니다. 

이 글에서는 Web3.js 2.0 SDK의 최신 업데이트를 살펴보고, 마이그레이션 과정을 안내하며, 시작에 도움이 되는 예제를 제공합니다. 

이 글은 트랜잭션 전송, Solana 계정 모델, 블록해시, 우선순위 수수료 등 Solana의 기본 개념을 충분히 이해하고 있으며 TypeScript 또는 JavaScript 사용 경험이 있다고 가정합니다. 이전 버전의 Web3.js SDK 사용 경험을 권장하지만 필수는 아닙니다. 시작해 보겠습니다!

Web3.js 2.0의 새로운 기능은 무엇인가요?

새로운 Web3.js 2.0 SDK가 제공하는 기능을 빠르게 살펴보겠습니다. 

1. 성능 개선

더 빠른 암호화 연산: Node.js와 최신 브라우저 같은 현대적인 JavaScript 환경의 네이티브 암호화 API를 활용해 키페어 생성, 트랜잭션 서명, 메시지 검증 속도가 최대 10배 빨라졌습니다.

2. 더 작고 효율적인 애플리케이션

Web3.js 2.0은 완전한 트리 셰이킹을 지원합니다. 라이브러리에서 실제로 사용하는 부분만 포함해 번들 크기를 최소화할 수 있습니다. 또한 새로운 SDK는 외부 종속성이 전혀 없어 가볍고 안전하게 빌드할 수 있습니다.

3. 향상된 유연성

이제 개발자는 다음과 같은 방식으로 맞춤형 솔루션을 만들 수 있습니다.

  • 커스텀 메서드로 RPC 인스턴스 정의  
  • 특화된 네트워크 전송 방식 또는 트랜잭션 서명자 사용  
  • 네트워킹, 트랜잭션 확인, 코덱을 위한 커스텀 프리미티브 구성  

‍온체인 프로그램을 위한 새로운 TypeScript 클라이언트는 이제 @solana-program GitHub 조직에서 호스팅됩니다. 이 클라이언트는 Codama를 사용해 자동 생성되므로 개발자가 커스텀 프로그램용 클라이언트를 빠르게 생성할 수 있습니다. 

지금 Web3.js v2로 전환해야 할까요?

2025년 2월 기준:

  • JS/TS로 새로운 Solana 앱을 만들면서 시스템 프로그램, 토큰 프로그램, 연결 토큰 프로그램 및 기타 일반적인 기존 프로그램을 사용한다면 지금 web3.js v2를 사용할 수 있습니다.
  • Anchor를 사용해 커스텀 온체인 앱을 만든다면 기다리는 편이 좋을 수 있습니다. 아직 Anchor는 web3.js v2를 기본 지원하지 않습니다. 향후 Anchor 업데이트를 기다리는 것도 좋습니다. 또는 Codama를 사용해 온체인 앱용 TypeScript 클라이언트를 만들 수 있지만 작업이 조금 더 필요합니다.

web3.js 버전 1에서 마이그레이션하기

‍web3.js v1을 사용해 본 적이 있다면 다음과 같은 주요 차이점을 확인하세요.

키페어

기존에 **Keypair**을 사용하던 모든 곳에서 이제 **KeyPairSigner**을 사용합니다. **Keypair.generate()**은 이제 **generateKeyPairSigner()**입니다. 또한 일반적인 JS/TS camelCase 표기법에 따라 이제 모든 곳에서 keypair를 keyPair로 표기합니다.

이제 비밀 키는 **privateKey**라고 하며 **keyPairSigner.privateKey**에서 접근할 수 있습니다. 일반적으로 web3.js v1에서 secretKey를 사용하던 모든 곳에 web3.js v2의 **KeyPairSigner**을 사용합니다.

주소 / 공개 키

web3.js v1에서 PublicKey를 사용하던 곳은 web3.js v2에서 단순히 address를 사용합니다. 예를 들어 **KeyPairSigner**에는 공개 키에 해당하는 keypairSigner.address 속성이 있습니다. address 함수를 사용하면 문자열 공개 키를 주소로 만들 수 있습니다.

SOL 및 토큰 수량

수량에는 네이티브 JS BigInt 타입을 사용합니다. 따라서 숫자 끝에 **n**을 추가해 1 대신 **1n**으로 표시합니다. 

팩토리

많은 기능을 설정할 수 있으므로 사전 정의된 구현(예: doThing()) 대신 팩토리(doThingFactory())를 사용해 자체 doThing() 함수를 만들 수 있습니다. 예를 들면 다음과 같습니다.

  • 트랜잭션을 전송하고 확인하려면 원하는 옵션으로 **sendAndConfirmTransactionFactory()**을 한 번 실행해 커스텀 sendAndConfirmTransaction() 함수를 반환받습니다. 이후 트랜잭션을 전송하고 확인해야 할 때마다 **sendAndConfirmTransaction()**을 사용할 수 있습니다. 
  • devnet 또는 localnet에서 에어드롭을 받으려면 **airdropFactory()**을 한 번 실행해 원하는 때마다 에어드롭에 사용할 수 있는 커스텀 airdrop() 함수를 반환받습니다.

Web3.js 2.0으로 트랜잭션을 전송하는 방법

Web3.js 2.0을 사용해 다른 지갑으로 lamport를 전송하는 클라이언트 측 프로그램을 만들어 보겠습니다. 이 프로그램에서는 트랜잭션 성공률을 높이고 확인 시간을 단축하는 기법을 소개합니다. 

다음 트랜잭션 전송 모범 사례를 따르겠습니다. 

  1. confirmed 커밋먼트 수준으로 최신 블록해시 가져오기  
  2. Helius Priority Fee API의 권장 사항에 따라 우선순위 수수료 설정
  3. 컴퓨트 유닛 최적화  
  4. maxRetries를 0으로, skipPreflight를 true로 설정해 트랜잭션 전송  

이 접근 방식은 네트워크 혼잡 상황에서도 최적의 성능과 안정성을 보장합니다. 

사전 요구 사항

  • Node.js 설치  
  • 호환되는 IDE(예: VS Code 또는 Cursor)  

설치

먼저 애플리케이션 구조를 잡기 위한 기본 Node.js 프로젝트를 만듭니다.

다음 명령어를 실행해 종속성과 프로젝트 메타데이터를 관리하는 package.json 파일을 만듭니다.

Shell commands
npm init -y

src 디렉터리를 만들고, 그 안에 메인 코드를 작성할 index.ts 파일을 추가합니다.

Shell commands
mkdir src  
touch src/index.ts

다음으로 npm을 사용해 Solana Web3.js 2.0 SDK 작업에 필요한 종속성을 설치합니다.

Shell commands
npm install @solana/web3.js@2 @solana-program/system @solana-program/compute-budget esrun

각 패키지에 대한 설명은 다음과 같습니다.

  • @solana/web3.js: Solana 트랜잭션을 생성하고 관리하는 데 필요한 Solana Web3.js 2.0 SDK입니다
  • @solana-program/system: Solana 시스템 프로그램에 대한 액세스를 제공해 lamport 전송과 같은 작업을 지원합니다
  • @solana-program/compute-budget: 트랜잭션의 우선순위 수수료를 설정하고 컴퓨트 유닛을 최적화하는 데 사용합니다
  • esrun은 설정이나 래퍼 함수 없이 명령줄에서 TypeScript 앱을 실행하는 간단한 방법입니다.

전송 주소 정의

index.ts에서 lamport를 전송할 출발지와 목적지 주소를 정의하겠습니다. 제공된 문자열에서 목적지 공개 키를 생성하기 위해 address() 함수를 사용합니다.

출발지는 **secretKey**을 사용해 **KeyPair**을 파생합니다.

send-transaction.ts
import { address, createKeyPairSignerFromBytes, getBase58Encoder } from "@solana/web3.js";

const destinationAddress = address("public-key-to-send-lamports-to");
const secretKey = "add-your-private-key";
const sourceKeypair = await createKeyPairSignerFromBytes(getBase58Encoder().encode(secretKey));

RPC 연결 설정

이제 필요한 RPC 연결을 설정할 수 있습니다. createSolanaRpc 함수는 기본 HTTP 전송 방식을 사용해 RPC 서버와 통신합니다. 대부분의 사용 사례에 충분합니다.

마찬가지로 **createSolanaRpcSubscriptions**을 사용해 WebSocket 연결을 설정합니다. **rpc_url**과 **wss_url**은 Helius 대시보드에서 확인할 수 있습니다. 가입하거나 로그인한 뒤 “엔드포인트” 섹션으로 이동하면 됩니다.

sendAndConfirmTransactionFactory 함수는 재사용 가능한 트랜잭션 전송기를 생성합니다. 이 전송기는 트랜잭션 전송을 위한 RPC 연결과 트랜잭션 상태 모니터링을 위한 RPC 구독이 필요합니다.

send-transaction.ts
import {
  // ...
  createSolanaRpcSubscriptions,
  createSolanaRpc,
  sendAndConfirmTransactionFactory,
} from "@solana/web3.js";

const rpc_url = "https://mainnet.helius-rpc.com/?api-key=<your-key>";
const wss_url = "wss://mainnet.helius-rpc.com/?api-key=<your-key>";

const rpc = createSolanaRpc(rpc_url);
const rpcSubscriptions = createSolanaRpcSubscriptions(wss_url);

const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
  rpc,
  rpcSubscriptions,
});

전송 명령 생성

최근 블록해시를 포함하면 중복을 방지하고 트랜잭션에 유효 기간을 부여할 수 있습니다. 모든 트랜잭션이 실행 대상으로 승인되려면 유효한 블록해시를 포함해야 합니다. 이 트랜잭션에서는 confirmed 커밋먼트 수준으로 최신 블록해시를 가져옵니다.

다음으로 **getTransferSolInstruction()**을 사용해 시스템 프로그램이 제공하는 사전 정의된 전송 명령을 생성합니다. 여기에는 수량과 출발지

, 목적지를 지정해야 합니다. 출발지는 항상 Signer여야 하며 목적지는 공개 주소여야 합니다.

send-transaction.ts
import {
  // ...
  lamports,
} from "@solana/web3.js";
import { getTransferSolInstruction } from "@solana-program/system";

/**
 * STEP 1: CREATE THE TRANSFER TRANSACTION
 */
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();

const instruction = getTransferSolInstruction({
  amount: lamports(1n),
  destination: destinationAddress,
  source: sourceKeypair,
});

트랜잭션 메시지 생성

이제 트랜잭션 메시지를 생성합니다. 모든 트랜잭션 메시지가 버전을 인식하므로 더 이상 서로 다른 타입(예: Transaction 및 VersionedTransaction)을 처리할 필요가 없습니다. 

출발지를 수수료 지불자로 설정하고, 블록해시를 포함하며, lamport 전송 명령을 추가합니다.

send-transaction.ts
import {
  // ...
  pipe,
  createTransactionMessage,
  setTransactionMessageFeePayer,
  setTransactionMessageLifetimeUsingBlockhash,
  appendTransactionMessageInstruction,
} from "@solana/web3.js";

// ...

const transactionMessage = pipe(
  createTransactionMessage({ version: 0 }),
  (message) => setTransactionMessageFeePayer(sourceKeypair.address, message),
  (message) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, message),
  (message) => appendTransactionMessageInstruction(instruction, message),
);

console.log("Transaction message created");

함수형 프로그래밍에서 흔히 사용하는 pipe 함수는 한 함수의 출력이 다음 함수의 입력이 되는 함수 시퀀스를 만듭니다. 여기서는 수수료 지불자와 유효 기간을 설정하고 명령을 추가하는 등의 변환을 적용해 트랜잭션 메시지를 단계별로 생성합니다.

트랜잭션 메시지 초기화:

**createTransactionMessage({ version: 0 })**은 기본 트랜잭션 메시지로 시작합니다.

수수료 지불자 설정:

**message => setTransactionMessageFeePayer(fromKeypair.address, message)**은 수수료 지불자의 주소를 추가합니다.

블록해시를 사용해 유효 기간 설정

**message => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, message)**은 최신 블록해시를 사용해 일정 기간 트랜잭션이 유효하도록 합니다.

전송 명령 추가

**message => appendTransactionMessageInstruction(instruction, message)**은 메시지에 작업(예: lamport 전송)을 추가합니다.

각 화살표 함수 **message => (...)**는 메시지를 수정하고 업데이트된 메시지를 다음 단계로 전달해 완전히 새로 구성된 트랜잭션 메시지를 생성합니다.

트랜잭션 서명

지정된 서명자인 출발지 **Keypair**을 사용해 트랜잭션에 서명합니다.

send-transaction.ts
import {
  // ...
  signTransactionMessageWithSigners,
} from "@solana/web3.js";
// ...

/**
 * STEP 2: SIGN THE TRANSACTION
 */
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
console.log("Transaction signed");

우선순위 수수료 추정

이 단계에서 트랜잭션 전송 및 확인을 진행할 수 있습니다. 하지만 우선순위 수수료를 설정하고 컴퓨트 유닛을 조정해 트랜잭션을 최적화해야 합니다. 이러한 최적화는 특히 네트워크가 혼잡할 때 트랜잭션 성공률을 높이고 확인 시간을 줄이는 데 도움이 됩니다.

우선순위 수수료를 설정하기 위해 Helius Priority Fee API를 사용합니다. 여기에는 Base64 형식으로 직렬화된 트랜잭션이 필요합니다. API는 Base58 인코딩도 지원하지만 현재 SDK가 트랜잭션을 Base64 형식으로 직접 제공하므로 과정이 더 간단합니다.

send-transaction.ts
import {
  // ...
  getBase64EncodedWireTransaction,
} from "@solana/web3.js";

/**
 * STEP 3: GET PRIORITY FEE FROM SIGNED TRANSACTION
 */

const base64EncodedWireTransaction = getBase64EncodedWireTransaction(signedTransaction);

const response = await fetch(rpc_url, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: "helius-example",
    method: "getPriorityFeeEstimate",
    params: [
      {
        transaction: base64EncodedWireTransaction,
        options: {
          transactionEncoding: "base64",
          priorityLevel: "High",
        },
      },
    ],
  }),
});
const { result } = await response.json();
const priorityFee = result.priorityFeeEstimate;
console.log("Setting priority fee to ", priorityFee);

일반적으로 priorityLevel을 High으로 설정하면 충분합니다. 하지만 직렬화된 트랜잭션과 계정 키를 사용하는 고급 우선순위 수수료 전략을 구현하면 네트워크 혼잡 시 트랜잭션 성공률을 크게 높일 수 있습니다.

컴퓨트 유닛 최적화

다음으로 트랜잭션 메시지가 실제로 소비하는 컴퓨트 유닛을 추정합니다.

그런 다음 이 값에 1.1을 곱해 10%의 버퍼를 추가합니다. 이 버퍼는 우선순위 수수료와 이후 추가할 컴퓨트 유닛 명령이 사용하는 컴퓨트 유닛을 고려합니다. 

lamport 전송과 같은 일부 명령은 컴퓨트 유닛 추정치가 더 낮을 수 있습니다. 충분한 리소스를 확보하기 위해 추정치가 임계값보다 낮으면 컴퓨트 유닛을 최소 1000으로 설정하는 안전장치를 추가했습니다.

send-transaction.ts
import {
  // ...
  getComputeUnitEstimateForTransactionMessageFactory,
} from "@solana/web3.js";

/**
 * STEP 4: OPTIMIZE COMPUTE UNITS
 */
const getComputeUnitEstimateForTransactionMessage = getComputeUnitEstimateForTransactionMessageFactory({
  rpc,
});
// Request an estimate of the actual compute units this message will consume.
let computeUnitsEstimate = await getComputeUnitEstimateForTransactionMessage(transactionMessage);
computeUnitsEstimate = computeUnitsEstimate < 1000 ? 1000 : Math.ceil(computeUnitsEstimate * 1.1);
console.log("Setting compute units to ", computeUnitsEstimate);

트랜잭션 재구성 및 서명

이제 이 트랜잭션에 필요한 우선순위 수수료와 컴퓨트 유닛을 확보했습니다. 트랜잭션에 이미 서명했으므로 새 명령을 직접 추가할 수 없습니다. 대신 새 블록해시로 전체 트랜잭션 메시지를 다시 구성합니다. 

블록해시는 약 1~2분 동안만 유효하며 우선순위 수수료와 컴퓨트 유닛을 가져오는 데 시간이 걸립니다. 트랜잭션 전송 중 블록해시가 만료되는 위험을 피하려면 트랜잭션을 재구성할 때 새 블록해시를 가져오는 편이 안전합니다.

재구성한 트랜잭션에는 다음 두 가지 명령을 추가합니다. 

  1. 우선순위 수수료를 설정하는 명령 하나
  2. 컴퓨트 유닛을 설정하는 명령 하나 

마지막으로 업데이트된 트랜잭션에 서명해 제출할 준비를 합니다.

send-transaction.ts
import {
  // ...
  appendTransactionMessageInstructions,
} from "@solana/web3.js";
import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction } from "@solana-program/compute-budget";

/**
 * STEP 5: REBUILD AND SIGN FINAL TRANSACTION
 */
const { value: finalLatestBlockhash } = await rpc.getLatestBlockhash().send();

const finalTransactionMessage = appendTransactionMessageInstructions(
  [
    getSetComputeUnitPriceInstruction({ microLamports: priorityFee }),
    getSetComputeUnitLimitInstruction({ units: computeUnitsEstimate }),
  ],
  transactionMessage,
);

setTransactionMessageLifetimeUsingBlockhash(finalLatestBlockhash, finalTransactionMessage);

const finalSignedTransaction = await signTransactionMessageWithSigners(finalTransactionMessage);
console.log("Rebuilt the transaction and signed it");

트랜잭션 전송 및 확인

다음으로 sendAndConfirmTransaction 함수를 사용해 서명된 트랜잭션을 전송하고 확인합니다.

커밋먼트 수준은 앞서 가져온 블록해시와 일치하도록 confirmed로 설정하고, **maxRetries**는 **0**으로 설정합니다. skipPreflight 옵션은 true로 설정해 더 빠른 실행을 위해 사전 검사를 건너뜁니다. 다만 트랜잭션 서명이 검증되었고 다른 오류가 없다고 확신할 때만 사용해야 합니다.

**sendAndConfirmTransaction**은 앞서 RPC 및 RPC 구독 URL을 모두 제공해 생성했습니다. RPC 구독 URL로 트랜잭션 상태를 확인하므로 수동 폴링이 필요 없습니다.

오류 처리 섹션의 코드는 사전 검사 중 발생한 오류를 확인합니다. **skipPreflight**을 **true**로 설정했으므로 이 검사는 불필요합니다. 하지만 true로 설정하지 않는 경우에는 유용합니다.

send-transaction.ts
import {
  getSignatureFromTransaction,
  isSolanaError,
  SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
} from "@solana/web3.js";
import { getSystemErrorMessage, isSystemError } from "@solana-program/system";

/**
 * STEP 6: SEND AND CONFIRM THE FINAL TRANSACTION
 */

console.log("Sending and confirming transaction");
await sendAndConfirmTransaction(finalSignedTransaction, {
  commitment: "confirmed",
  maxRetries: 0n,
  skipPreflight: true,
});
console.log("Transfer confirmed: ", getSignatureFromTransaction(finalSignedTransaction));

코드 실행

마지막으로 코드를 실행합니다. npx esrun send-transaction.ts

결론

Solana Web3.js 2.0 SDK 출시는 개발자가 Solana에서 더 빠르고 효율적이며 확장 가능한 애플리케이션을 만들 수 있도록 지원하는 혁신적인 업데이트입니다. 최신 JavaScript 표준을 적용하고 네이티브 암호화 API, 트리 셰이킹, 자동 생성 TypeScript 클라이언트 같은 기능을 도입해 개발자 경험과 애플리케이션 성능을 크게 향상했습니다.

프로그래밍 예제의 전체 코드는 GitHub에서 확인할 수 있습니다.

여기까지 읽어주셔서 감사합니다, anon! 아래에 이메일 주소를 입력하면 Solana의 새로운 소식을 빠짐없이 받아볼 수 있습니다. 더 자세히 알아볼 준비가 되셨나요? Helius 블로그의 최신 글을 살펴보고 오늘도 Solana 여정을 이어가세요.

리소스

Helius 구독하기

최신 Solana 개발 소식을 확인하고 새 게시물 알림을 받아보세요

확대 이미지