TON 프로젝트 개발 튜토리얼 (1): 소스 코드 관점에서 TON 체인에서 NFT를 생성하는 방법

마리오가 Web3를 보다
2024-06-24 13:49:44
수집
현재 TON 공식 문서는 학습하기에 다소 어려움이 있어, 자신의 학습 경로를 바탕으로 TON Chain 프로젝트 개발에 관한 일련의 글을 정리해 보았습니다. 여러분이 TON DApp 개발에 빠르게 입문하는 데 도움이 되기를 바랍니다.

저자 :@Web3Mario(https://x.com/web3_mario)

요약 :이전의 TON 기술 소개 글을 이어서, 이 기간 동안 TON 공식 개발 문서를 깊이 연구해 보았습니다. 학습하기에는 여전히 약간의 장벽이 있는 것 같고, 현재의 문서 내용은 내부 개발 문서에 더 가까운 것 같습니다. 새로운 개발자에게는 그리 친숙하지 않기 때문에, 자신의 학습 경로를 바탕으로 TON Chain 프로젝트 개발에 관한 일련의 글을 정리해 보려고 합니다. 이를 통해 여러분이 TON DApp 개발에 빠르게 입문하는 데 도움이 되기를 바랍니다. 글에 오류가 있다면 지적해 주시면 감사하겠습니다. 함께 배우도록 합시다.

EVM에서 NFT 개발과 TON Chain에서 NFT 개발의 차이점

FT 또는 NFT를 발행하는 것은 DApp 개발자에게 일반적으로 가장 기본적인 요구 사항입니다. 그래서 저도 이를 학습의 출발점으로 삼았습니다. 먼저 EVM 기술 스택에서 NFT를 개발하는 것과 TON Chain에서의 차이점을 알아보겠습니다. EVM 기반의 NFT는 일반적으로 ERC-721 표준을 상속받는 것을 선택합니다. NFT란 분할할 수 없는 암호화 자산 유형을 의미하며, 각 자산은 고유성을 가지고 있습니다. 즉, 특정한 독점적인 특성이 존재합니다. ERC-721은 이러한 유형의 자산에 대한 일반적인 개발 패러다임입니다. 일반적인 ERC721 계약이 어떤 함수들을 구현해야 하고 어떤 정보를 기록해야 하는지 살펴보겠습니다. 아래 그림은 ERC721 인터페이스입니다. FT와는 달리, 전송 인터페이스에서 입력해야 하는 것은 전송할 tokenId이며, 수량이 아닙니다. 이 tokenId는 NFT 자산의 고유성을 가장 기본적으로 나타내며, 물론 더 많은 속성을 담기 위해 일반적으로 각 tokenId에 대한 메타데이터를 기록합니다. 이 메타데이터는 외부 링크로, 해당 NFT의 다른 확장 가능한 데이터(예: PFP 이미지 링크, 특정 속성 이름 등)를 저장합니다.

Solidity에 익숙하거나 객체 지향 개발에 익숙한 개발자에게는 이러한 스마트 계약을 구현하는 것이 쉬운 일입니다. 필요한 데이터 유형을 정의하고, 예를 들어 몇 가지 중요한 매핑 관계(mapping)를 설정한 후, 필요한 기능에 따라 이러한 데이터 수정 논리를 개발하면 NFT를 구현할 수 있습니다.

그러나 TON Chain에서는 모든 것이 다소 다르게 변합니다. 이러한 차이의 핵심 원인은 두 가지입니다:

  • TON에서는 데이터 저장이 Cell 기반으로 구현되며, 동일한 계정의 Cell은 방향성 비순환 그래프를 통해 구현됩니다. 이로 인해 지속적으로 저장해야 하는 데이터는 무한히 증가할 수 없게 됩니다. 방향성 비순환 그래프에서는 데이터 깊이가 쿼리 비용을 결정하기 때문에, 깊이가 무한히 늘어나면 쿼리 비용이 과도하게 높아져 계약이 교착 상태에 빠질 수 있습니다.
  • 높은 동시성 성능을 추구하기 위해, TON은 직렬 실행 아키텍처를 포기하고 Actor 모델이라는 병렬 전용 개발 패러다임을 채택하여 실행 환경을 재구성했습니다. 이로 인해 스마트 계약 간에는 소위 내부 메시지를 전송하여 비동기적으로 호출할 수밖에 없게 되며, 상태 수정 유형이든 읽기 전용 유형이든 이 원칙을 따라야 합니다. 또한 비동기 호출이 실패할 경우 데이터 롤백 문제를 어떻게 처리할지도 신중하게 고려해야 합니다.

물론 기술적인 다른 차이점에 대해서는 이전 글에서 자세히 논의한 바 있습니다. 본 글에서는 스마트 계약 개발에 집중하고자 하므로 논의하지 않겠습니다. 위의 두 가지 설계 원칙은 TON의 스마트 계약 개발을 EVM과 크게 다르게 만들었습니다. 시작 논의에서 우리는 NFT 계약에서 몇 가지 매핑 관계, 즉 mapping을 정의해야 한다는 것을 알았습니다. 이 중 가장 중요한 것은 owners로, 이 매핑은 특정 tokenID에 해당하는 NFT의 소유자 주소의 매핑 관계를 저장하여 NFT의 소유권을 결정합니다. 전송은 이 소유권의 수정을 의미합니다. 이론적으로 이는 무한한 데이터 구조가 될 수 있으므로 최대한 피해야 합니다. 따라서 공식적으로는 무한 데이터 구조의 존재 여부를 샤딩의 기준으로 삼는 것을 권장합니다. 즉, 유사한 데이터 저장 요구가 있을 때 주-종 계약 패러다임을 통해 대체하고, 각 키에 해당하는 데이터를 관리하기 위해 자식 계약을 생성하는 방식입니다. 그리고 주 계약을 통해 전역 매개변수를 관리하거나 자식 계약 간의 내부 정보 교환을 처리하도록 합니다.

이것은 TON의 NFT도 유사한 구조로 설계해야 함을 의미합니다. 각 NFT는 독립적인 자식 계약으로, 소유자 주소, 메타데이터 등과 같은 전용 데이터를 저장하며, 주 계약을 통해 NFT 이름, 심볼, 총 공급량 등의 전역 데이터를 관리합니다.

구조를 명확히 한 후, 다음으로는 핵심 기능의 요구 사항을 해결해야 합니다. 주-종 계약 방식을 채택했기 때문에, 어떤 기능이 주 계약에 의해 수행되고 어떤 기능이 자식 계약에 의해 수행되는지를 명확히 해야 하며, 두 계약 간에 어떤 내부 정보를 통해 소통할지도 정해야 합니다. 또한 실행 오류가 발생할 경우 이전 데이터를 어떻게 롤백할지도 고려해야 합니다. 일반적으로 복잡한 대형 프로젝트를 개발하기 전에 클래스 다이어그램을 통해 서로 간의 정보 흐름을 명확히 하고, 내부 호출 실패 후의 롤백 논리를 신중하게 생각하는 것이 필요합니다. 물론 위의 NFT 개발은 간단하지만 유사한 검증을 할 수 있습니다.

소스 코드에서 TON 스마트 계약 개발 배우기

TON은 스마트 계약 개발 언어로 C 언어와 유사한 정적 타입 언어인 Func를 설계했습니다. 이제 소스 코드를 통해 TON 스마트 계약을 개발하는 방법을 배워보겠습니다. TON 공식 문서의 NFT 예제를 선택하여 소개하겠습니다. 관심 있는 분들은 직접 찾아보실 수 있습니다. 이 사례에서는 간단한 TON NFT 예제를 구현했습니다. 계약 구조를 살펴보면, 두 개의 기능 계약과 세 개의 필수 라이브러리로 나뉩니다.

이 두 개의 주요 기능 계약은 위에서 언급한 원칙에 따라 설계되었습니다. 먼저 주 계약 nft-collection의 코드를 살펴보겠습니다:

여기서 첫 번째 지식 포인트는 TON 스마트 계약에서 데이터를 지속적으로 저장하는 방법입니다. Solidity에서는 데이터의 지속적인 저장이 EVM에 의해 매개변수 유형에 따라 자동으로 처리됩니다. 일반적으로 스마트 계약의 상태 변수는 실행이 끝난 후 최신 값에 따라 자동으로 지속적으로 저장됩니다. 개발자는 이 과정을 고려할 필요가 없습니다. 그러나 Func에서는 상황이 다릅니다. 개발자는 해당 처리 논리를 직접 구현해야 하며, 이 상황은 C와 C++에서 GC 과정을 고려해야 하는 것과 유사합니다. 그러나 다른 새로운 개발 언어는 일반적으로 이 부분의 논리를 자동으로 처리합니다. 코드를 살펴보면, 먼저 필요한 라이브러리를 가져오고, 첫 번째 함수 loaddata는 지속적으로 저장된 데이터를 읽는 데 사용됩니다. 그 논리는 먼저 getdata를 통해 지속적으로 저장된 계약의 cell을 반환하는 것입니다. 이는 표준 라이브러리인 stdlib.fc에 의해 구현됩니다. 일반적으로 이 중 일부 함수는 시스템 함수로 간주할 수 있습니다.

이 함수의 반환 값 유형은 cell이며, 이는 TVM의 cell 유형입니다. 이전 소개에서 우리는 TON 블록체인에서 모든 지속 데이터가 cell 트리에 저장된다는 것을 이미 알고 있습니다. 각 cell은 최대 1023 비트의 임의 데이터와 최대 네 개의 다른 cell에 대한 참조를 가질 수 있습니다. cell은 스택 기반 TVM에서 메모리로 사용됩니다. cell에는 압축된 데이터가 저장되며, 구체적인 평문 데이터를 얻으려면 cell을 slice라는 유형으로 변환해야 합니다. cell은 begin_parse 함수를 통해 slice 유형으로 변환할 수 있으며, slice에서 데이터 비트와 다른 cell에 대한 참조를 로드하여 cell의 데이터를 얻을 수 있습니다. 15행 코드에서 이러한 호출 방법은 func의 문법 설탕으로, 첫 번째 함수의 반환 값의 두 번째 함수를 직접 호출할 수 있습니다. 마지막으로 데이터 지속성 순서에 따라 해당 데이터를 순차적으로 로드합니다. 이 과정은 Solidity와 다르며, 해시맵 호출에 따라 이루어지지 않으므로 호출 순서를 어지럽힐 수 없습니다.

savedata 함수에서는 논리가 유사하지만, 이는 반대의 과정입니다. 여기서 새로운 유형 builder가 도입됩니다. 이는 cell 빌더의 유형입니다. 데이터 비트와 다른 cell에 대한 참조는 빌더에 저장될 수 있으며, 빌더는 최종적으로 새로운 cell로 완성될 수 있습니다. 먼저 표준 함수 begincell을 통해 빌더를 생성하고, 관련 함수들을 통해 순차적으로 저장합니다. 위에서 언급한 호출 순서와 이곳의 저장 순서는 일치해야 합니다. 마지막으로 endcell을 통해 새로운 cell 구축을 완료하며, 이때 해당 cell은 메모리에서 관리됩니다. 마지막으로 가장 바깥쪽의 setdata를 통해 해당 cell의 지속적인 저장을 완료할 수 있습니다.

이제 비즈니스 관련 함수들을 살펴보겠습니다. 먼저 계약을 통해 새로운 계약을 생성하는 방법에 대한 지식 포인트를 소개해야 합니다. 이는 방금 소개한 주-종 아키텍처에서 자주 사용됩니다. 우리는 TON에서 스마트 계약 간의 호출이 내부 메시지를 전송하는 방식으로 이루어진다는 것을 알고 있습니다. 이는 sendrawmessage라는 이름의 함수를 통해 구현됩니다. 첫 번째 매개변수는 메시지 인코딩된 cell이며, 두 번째 매개변수는 해당 거래의 실행 방식을 구분하는 식별자입니다. TON에서는 서로 다른 내부 메시지 전송 실행 방식을 설정할 수 있으며, 현재 3가지 메시지 모드와 3가지 메시지 플래그가 있습니다. 단일 모드를 여러 개(아마도 없을 수도 있는) 플래그와 조합하여 원하는 모드를 얻을 수 있습니다. 아래는모드와 플래그의 설명 표입니다.:

이제 첫 번째 주요 함수인 deploynftitem을 살펴보겠습니다. 이름에서 알 수 있듯이, 이는 새로운 NFT 인스턴스를 생성하거나 발행하는 데 사용되는 함수입니다. 여러 작업을 거쳐 msg를 인코딩한 후, sendrawmessage를 통해 해당 내부 계약을 전송하며, flag 1의 전송 식별자를 선택하여 인코딩된 수수료만을 이번 실행의 가스 수수료로 사용합니다. 위의 설명을 통해 우리는 이 인코딩 규칙이 새로운 스마트 계약을 생성하는 방식에 해당한다는 것을 쉽게 인식할 수 있습니다. 그럼 구체적으로 어떻게 구현되는지 살펴보겠습니다.

51행을 직접 살펴보면, 위의 두 함수는 메시지를 생성하는 데 필요한 정보를 제공하는 보조 함수입니다. 따라서 나중에 다시 살펴보겠습니다. 이는 스마트 계약을 생성하는 내부 메시지의 인코딩 과정입니다. 중간의 숫자들은 사실상 몇 가지 식별자로, 해당 내부 메시지의 요구 사항을 설명하는 데 사용됩니다. 여기서 다음 지식 포인트를 도입해야 합니다. TON은 메시지의 실행 방식을 설명하기 위해 TL-B라는 이진 언어를 선택했으며, 설정된 다양한 플래그를 통해 특정 기능의 내부 메시지를 구현합니다. 가장 쉽게 떠오르는 두 가지 사용 사례는 새로운 계약 생성과 이미 배포된 계약 함수 호출입니다. 51행의 이러한 방식은 전자를 나타내며, 새로운 nft item 계약을 생성하는 것입니다. 이는 주로 55, 56, 57행에서 지정됩니다. 먼저 55행의 긴 숫자는 일련의 식별자이며, store_uint의 첫 번째 매개변수는 값이고, 두 번째는 비트 길이입니다. 여기서 해당 내부 메시지가 계약 생성임을 나타내는 마지막 세 개의 플래그와 해당 이진 값이 111(십진수로 4+2+1)입니다. 여기서 앞의 두 개는 해당 메시지가 StateInit 데이터를 포함한다는 것을 나타내며, 이 데이터는 새로운 계약의 소스 코드와 초기화에 필요한 데이터입니다. 마지막 플래그는 내부 메시지가 부착되어 있음을 나타내며, 관련 논리를 실행하고 필요한 매개변수를 요구합니다. 따라서 66행 코드에서는 이 세 개의 데이터가 설정되지 않았음을 알 수 있으며, 이는 이미 배포된 계약의 함수 호출을 나타냅니다. 구체적인 인코딩 규칙은 여기에서 확인할 수 있습니다.

StateInit의 인코딩 규칙은 49행 코드에 해당하며, calculatenftitemstateinit을 통해 계산됩니다. 주의할 점은 stateinit 데이터의 인코딩도 정해진 TL-B 인코딩 규칙을 따르며, 일부 플래그 외에도 주로 두 부분인 새로운 계약 코드와 초기화 데이터가 포함됩니다. 데이터의 인코딩 순서는 새로운 계약이 지정한 지속적인 cell의 저장 순서와 일치해야 합니다. 36행에서 초기화 데이터에는 itemindex가 있으며, 이는 ERC721의 tokenId와 유사하며, 표준 함수 myaddress가 반환하는 현재 계약 주소인 collection_address가 포함됩니다. 이 데이터의 순서는 nft-item의 선언과 일치합니다.

다음 지식 포인트는 TON에서 모든 생성되지 않은 스마트 계약의 주소를 미리 계산할 수 있다는 점입니다. 이는 Solidity의 create2 함수와 유사합니다. TON에서 새로운 주소의 생성은 두 부분으로 구성되며, workchain 식별자와 stateinit의 해시 값을 결합하여 생성됩니다. 앞서 설명한 바와 같이 이는 TON의 무한 샤딩 아키텍처에 따라 지정되어야 하며, 현재는 통일된 값입니다. 표준 함수 workchain을 통해 얻을 수 있습니다. 후자는 표준 함수 cellhash를 통해 얻을 수 있습니다. 따라서 이 예제로 돌아가서, calculatenftitemaddress는 새로운 계약 주소를 미리 계산하는 함수입니다. 그리고 생성된 값은 53행에서 메시지에 인코딩되어 해당 내부 메시지의 수신 주소로 사용됩니다. nft_content는 생성된 계약의 초기화 호출에 해당하며, 구체적인 구현은 다음 글에서 소개하겠습니다.

sendroyaltyparams는 특정 읽기 요청에 대한 내부 메시지의 응답이 필요합니다. 이전 소개에서 우리는 TON에서 내부 메시지가 데이터 수정 작업뿐만 아니라 읽기 작업도 이러한 방식으로 구현해야 한다고 강조했습니다. 따라서 이 계약은 이러한 작업을 위한 것입니다. 먼저 67행은 요청에 대한 응답 후 요청자에게 콜백 함수의 식별자를 나타냅니다. 이는 반환될 데이터로, 요청된 item index와 해당 로열티 데이터를 포함합니다.

다음으로 TON의 스마트 계약에는 두 개의 통일된 진입점이 있으며, 각각 recvinternal과 recvexternal입니다. 전자는 모든 내부 메시지의 통일된 호출 진입점이며, 후자는 모든 외부 메시지의 통일된 호출 진입점입니다. 개발자는 함수 내부에서 필요에 따라 switch와 유사한 방식으로 메시지에 지정된 다양한 플래그에 따라 서로 다른 요청에 응답해야 합니다. 여기서의 플래그는 위의 67행의 콜백 함수 식별자입니다. 이 예제로 돌아가서, 먼저 메시지의 공백 검사를 수행하고, 이후 메시지의 정보를 순차적으로 분석합니다. 먼저 83행에서 sender_address를 분석하여 얻습니다. 이 매개변수는 이후 권한 검사를 위해 사용됩니다. 여기서의 ~ 연산자는 또 다른 문법 설탕입니다. 여기서는 더 이상 설명하지 않겠습니다. 이후 op 작업 플래그를 분석하고, 다양한 플래그에 따라 각각의 요청을 처리합니다. 여기서 특정 논리에 따라 위에서 언급한 함수들을 호출했습니다. 예를 들어 로열티 매개변수 요청에 응답하거나 새로운 nft를 발행하고 전역 인덱스를 증가시키는 등의 작업입니다.

다음 지식 포인트는 108행에 해당하며, 이름을 통해 해당 함수의 처리 논리를 알 수 있습니다. 이는 Solidity의 require 함수와 유사합니다. Func에서는 표준 함수 throwunless를 통해 예외를 발생시킵니다. 첫 번째 매개변수는 오류 코드이며, 두 번째는 검사 비트 불리언 값입니다. 만약 false일 경우 예외가 발생하며, 해당 오류 코드가 첨부됩니다. 이 행에서는 equalslices를 통해 위에서 분석한 senderaddress가 계약의 지속적인 저장소인 owneraddress와 같은지를 판단하여 권한을 확인합니다.

마지막으로 코드 구조를 더 명확하게 하기 위해, 지속적인 정보를 얻기 위한 일련의 보조 함수를 작성했습니다. 여기서는 더 이상 설명하지 않겠습니다. 개발자는 이러한 구조를 참고하여 자신의 스마트 계약을 개발할 수 있습니다.

TON 생태계의 DApp 개발은 정말 흥미로운 일입니다. EVM의 개발 패러다임과는 큰 차이가 있으므로, 저는 TON Chain에서 DApp을 개발하는 방법을 소개하는 일련의 글을 통해 여러분과 함께 배우고 이 기회를 잡고자 합니다. 또한 Twitter에서 저와 소통하며 새로운 흥미로운 DApp 아이디어를 교류하고 함께 개발하기를 환영합니다.

관련 태그
체인캐처(ChainCatcher)는 독자들에게 블록체인을 이성적으로 바라보고, 리스크 인식을 실제로 향상시키며, 다양한 가상 토큰 발행 및 조작에 경계해야 함을 상기시킵니다. 사이트 내 모든 콘텐츠는 시장 정보나 관련 당사자의 의견일 뿐이며 어떠한 형태의 투자 조언도 제공하지 않습니다. 만약 사이트 내에서 민감한 정보를 발견하면 “신고하기”를 클릭하여 신속하게 처리할 것입니다.
체인캐처 혁신가들과 함께하는 Web3 세상 구축