
TON 프로젝트 개발 튜토리얼 (1): 소스 코드 관점에서 TON 체인에 NFT를 생성하는 방법
작자: @Web3Mario
요약: 이전 TON 기술 소개 글에 이어, 최근 TON 공식 개발 문서를 깊이 있게 연구해 보았습니다. 학습 과정에서 다소 높은 진입 장벽을 느꼈으며, 현재의 문서는 외부 개발자보다 내부 개발자를 위한 것처럼 보여 초보 개발자에게 친숙하지 않습니다. 따라서 저의 학습 경험을 바탕으로 TON Chain 프로젝트 개발 시리즈를 정리하여, 많은 분들이 TON DApp 개발에 빠르게 입문할 수 있도록 도움을 드리고자 합니다. 오류가 있을 경우 지적해 주시면 함께 배워 나가겠습니다.
EVM에서 NFT 개발과 TON 체인에서 NFT 개발의 차이점
DApp 개발자에게 FT 또는 NFT 발행은 일반적으로 가장 기본적인 요구사항입니다. 저는 이를 학습의 시작점으로 삼았습니다. 우선 EVM 기술 스택에서 NFT를 개발하는 것과 TON 체인에서 NFT를 개발하는 것 사이의 차이점을 살펴보겠습니다. EVM 기반의 NFT는 일반적으로 ERC-721 표준을 상속받아 사용합니다.所谓 NFT란 불가분한 암호화 자산 유형이며, 각 자산은 고유성을 가지며 특정 고유 속성을 가집니다. ERC-721은 이러한 자산 유형에 대한 일반적인 개발 패러다임입니다. 일반적인 ERC721 컨트랙트가 구현해야 하는 함수들과 저장해야 할 정보들을 살펴봅시다. 아래 이미지는 ERC721 인터페이스입니다. FT와 달리 전송 인터페이스에서는 수량 대신 전송할 tokenId를 입력해야 함을 알 수 있습니다. 이 tokenId는 NFT 자산의 고유성의 가장 기본적인 표현이며, 더 많은 속성을 담기 위해 각 tokenId마다 metadata를 기록하는 것이 일반적입니다. 이 metadata는 외부 링크이며, 해당 NFT의 확장 가능한 데이터를 저장하는데, 예를 들어 PFP 이미지 링크나 특정 속성 이름 등을 포함합니다.

Solidity에 익숙하거나 객체 지향 프로그래밍에 익숙한 개발자라면, 필요한 데이터 유형(예: 중요한 매핑 관계)을 정의하고, 원하는 기능에 따라 해당 데이터를 수정하는 로직을 개발함으로써 NFT 컨트랙트를 쉽게 구현할 수 있습니다.
그러나 TON 체인에서는 이런 방식이 달라집니다. 그 이유는 두 가지 핵심 요소 때문입니다:
l TON에서 데이터 저장은 Cell 기반으로 이루어지며, 동일 계정의 Cell들은 방향성 없는 비순환 그래프(DAG)로 연결됩니다. 이로 인해 영구 저장이 필요한 데이터는 무제한으로 증가할 수 없으며, DAG의 깊이가 쿼리 비용을 결정하기 때문입니다. 깊이가 무한히 늘어날 경우 쿼리 비용이 과도하게 증가하여 컨트랙트가 교착 상태에 빠질 수 있습니다.
l 고 동시성 성능을 추구하기 위해 TON은 직렬 실행 아키텍처를 포기하고, 병렬 처리에 최적화된 액터 모델(Actor model)을 도입하여 실행 환경을 재설계했습니다. 이로 인해 스마트 컨트랙트 간 호출은 '내부 메시지'를 보내는 비동기 방식만 허용되며, 상태 변경 여부와 관계없이 모든 호출이 이 규칙을 따릅니다. 또한 비동기 호출 실패 시 데이터 롤백을 어떻게 처리할지도 신중히 고려해야 합니다.
기술적인 다른 차이점들에 대해서는 이전 글에서 자세히 설명했으므로, 본 글에서는 스마트 컨트랙트 개발에 집중하겠습니다. 위 두 가지 설계 원칙으로 인해 TON의 스마트 컨트랙트 개발은 EVM과 큰 차이를 보입니다. 앞서 언급했듯, NFT 컨트랙트는 owners와 같은 매핑(mapping)을 정의하여 NFT 관련 데이터를 저장해야 하며, 특히 owners 매핑은 특정 tokenId에 대한 소유자 주소를 저장하여 NFT의 소유권을 결정하며, 전송은 이 소유권의 변경을 의미합니다. 이론적으로 무제한으로 확장 가능한 데이터 구조이므로 피하는 것이 좋습니다. 따라서 공식적으로 무제한 데이터 구조 존재 여부를 샤딩 기준으로 삼을 것을 권장합니다. 즉, 그러한 데이터 저장이 필요할 경우 주-종 컨트랙트 패턴(master-slave contract pattern)을 사용하여 각 키(key)에 해당하는 데이터를 서브 컨트랙트(sub-contract)로 관리하도록 하고, 마스터 컨트랙트는 전역 파라미터를 관리하거나 종 컨트랙트 간 내부 메시지 교환을 지원합니다.
즉, TON의 NFT도 유사한 아키텍처로 설계되어야 하며, 각 NFT는 독립된 종 컨트랙트로서 소유자 주소, metadata 등 고유 데이터를 저장하고, 마스터 컨트랙트는 NFT 이름(name), 심볼(symbol), 총 발행량(total supply) 등의 전역 데이터를 관리합니다.
아키텍처를 명확히 한 후에는 핵심 기능을 구현해야 합니다. 주-종 컨트랙트 방식을 채택했기 때문에 어떤 기능을 주 컨트랙트가 담당하고 어떤 기능을 종 컨트랙트가 담당할지 결정해야 하며, 양자가 어떤 내부 메시지를 통해 소통할지 명확히 해야 합니다. 또한 실행 오류 발생 시 이전 데이터를 어떻게 롤백할지도 고려해야 합니다. 일반적으로 복잡하고 대규모인 프로젝트를 개발하기 전에는 클래스 다이어그램을 작성하여 상호 간 정보 흐름을 명확히 하고, 내부 호출 실패 후의 롤백 로직을 철저히 검토하는 것이 중요합니다. 물론 이번 NFT 개발은 간단하지만, 유사한 검증을 수행할 수 있습니다.

소스 코드를 통해 TON 스마트 컨트랙트 개발 학습하기
TON은 C 언어와 유사한 정적 타입 언어인 Func를 스마트 컨트랙트 개발 언어로 선택했습니다. 이제 소스 코드를 통해 TON 스마트 컨트랙트를 어떻게 개발하는지 살펴보겠습니다. 본 설명은 TON 공식 문서의 NFT 예제를 기반으로 하며, 관심 있는 분들은 직접 참고하시기 바랍니다. 이 사례에서는 간단한 TON NFT 예제를 구현하였습니다. 먼저 컨트랙트 구조를 보면, 두 개의 주요 기능 컨트랙트와 세 개의 필수 라이브러리로 구성됩니다.

이 두 가지 주요 기능 컨트랙트는 위에서 설명한 원칙에 따라 설계되었습니다. 먼저 nft-collection 마스터 컨트랙트의 코드를 살펴보겠습니다:

여기서 첫 번째 핵심 개념을 소개합니다: TON 스마트 컨트랙트에서 데이터를 영구 저장하는 방법입니다. Solidity에서는 변수 유형에 따라 EVM이 자동으로 영구 저장을 처리하므로, 상태 변수는 실행 후 최신 값으로 자동 저장되며 개발자는 이 과정을 고려할 필요가 없습니다. 그러나 Func에서는 개발자가 직접 이 로직을 구현해야 합니다. 이는 C/C++에서 GC를 수동으로 관리하는 것과 유사하지만, 최근 언어들은 대부분 이를 자동화합니다. 코드를 살펴보면, 먼저 필요한 라이브러리를 가져온 후, load_data 함수는 영구 저장된 데이터를 읽는 역할을 합니다. get_data 함수를 통해 영구 저장된 컨트랙트 cell을 반환합니다. 이는 표준 라이브러리 stdlib.fc에서 제공하며, 일부 함수는 시스템 함수처럼 사용할 수 있습니다.
이 함수의 반환값은 TVM의 cell 타입입니다. 이전 설명에서 TON 블록체인의 모든 영구 데이터는 cell 트리에 저장됨을 알 수 있었습니다. 각 cell은 최대 1023비트의 임의 데이터와 최대 4개의 다른 cell 참조를 가질 수 있으며, stack 기반 TVM에서 메모리로 사용됩니다. cell은 tight encoding된 데이터를 저장하므로, 원시 데이터를 얻기 위해서는 cell을 slice 타입으로 변환해야 합니다. begin_parse 함수를 통해 cell을 slice로 변환한 후, slice에서 데이터 비트와 다른 cell 참조를 로드하여 내용을 확인할 수 있습니다. 주의할 점은 15행 코드에서 첫 번째 함수의 반환값에 바로 두 번째 함수를 호출하는 것은 Func의 문법 설탕(syntactic sugar)이라는 것입니다. 마지막으로 데이터 영구 저장 순서에 맞춰 순차적으로 값을 로드합니다. 이 과정은 Solidity와 달리 해시맵 기반이 아니므로 순서가 중요합니다.
save_data 함수는 반대 방향의 로직을 수행합니다. 여기서 새로운 타입인 builder가 등장합니다. 이는 cell 생성기를 의미합니다. 데이터 비트와 다른 cell 참조를 builder에 저장한 후, end_cell을 통해 최종적으로 새 cell을 생성합니다. 먼저 begin_cell 함수로 builder를 생성하고, store 관련 함수를 통해 데이터를 순차적으로 저장합니다. 이때 저장 순서는 앞서 설명한 로드 순서와 일치해야 합니다. 마지막으로 end_cell을 통해 새 cell을 생성하면 메모리에 관리되며, set_data를 통해 이 cell을 영구 저장합니다.

다음으로 비즈니스 관련 함수를 살펴보겠습니다. 먼저 컨트랙트를 통해 새 컨트랙트를 생성하는 방법을 알아야 합니다. 이는 앞서 설명한 주-종 아키텍처에서 자주 사용됩니다. TON에서는 스마트 컨트랙트 간 호출이 내부 메시지를 보내는 방식으로 이루어진다는 것을 알고 있습니다. 이는 send_raw_message 함수를 통해 구현되며, 첫 번째 인수는 인코딩된 메시지 cell이고, 두 번째 인수는 거래 실행 방식을 나타내는 플래그입니다. TON은 다양한 내부 메시지 전송 방식을 설정하였으며, 현재 3가지 Modes와 3가지 Flags가 있습니다. 하나의 Mode와 여러(또는 없을 수도 있는) Flags를 조합하여 원하는 mode를 만들 수 있으며, 조합은 단순히 값들의 합을 넣으면 됩니다.아래는 Modes와 Flags의 설명 표입니다:

첫 번째 주요 함수인 deploy_nft_item을 살펴보겠습니다. 이름에서 알 수 있듯이, 이 함수는 새로운 NFT 인스턴스를 생성(또는 민팅)하는 기능을 합니다. 일련의 작업을 통해 메시지를 인코딩한 후 send_raw_message를 통해 내부 메시지를 전송하며, 플래그 1을 선택하여 인코딩에서 지정한 fee만을 이번 실행의 gas fee로 사용합니다. 앞선 설명을 통해 이 인코딩 규칙이 새로운 스마트 컨트랙트 생성 방식에 해당한다는 것을 쉽게 알 수 있습니다.
그렇다면 구체적으로 어떻게 구현되는지 살펴보겠습니다.
바로 51행부터 보겠습니다. 위의 두 함수는 메시지 생성에 필요한 정보를 만드는 보조 함수이므로 나중에 다루겠습니다. 이것은 새로운 스마트 컨트랙트를 생성하는 내부 메시지의 인코딩 과정이며, 중간의 숫자들은 특정 기능을 갖는 내부 메시지를 구현하기 위한 플래그입니다. 다음 개념을 소개합니다: TON은 TL-B라는 이진 언어를 사용하여 메시지 실행 방식을 설명하며, 서로 다른 플래그를 설정하여 특정 기능을 가진 내부 메시지를 구현합니다. 가장 쉬운 예는 새 컨트랙트 생성과 기존 컨트랙트 함수 호출입니다. 51행은 전자의 경우, 즉 새로운 nft item 컨트랙트 생성에 해당하며, 주로 55~57행에서 지정됩니다. 먼저 55행의 긴 숫자열은 일련의 플래그이며, store_uint의 첫 번째 인수는 값, 두 번째 인수는 비트 길이입니다. 이 중 마지막 세 비트가 새 컨트랙트 생성 여부를 결정하며, 이진 값은 111(십진수로 4+2+1)입니다. 앞의 두 비트는 메시지가 StateInit 데이터를 포함함을 의미하며, 이 데이터는 새 컨트랙트의 소스코드와 초기화에 필요한 데이터입니다. 마지막 비트는 내부 메시지 페이로드를 포함함을 의미하며, 이는 관련 로직 실행 및 필요한 파라미터를 포함합니다. 따라서 66행 코드에서 이 세 비트를 설정하지 않은 것은 기존에 배포된 컨트랙트의 함수 호출임을 나타냅니다.자세한 인코딩 규칙은 여기서 확인 가능.
StateInit의 인코딩 규칙은 49행 코드에 해당하며, calculate_nft_item_state_init을 통해 계산됩니다. stateinit 데이터 인코딩도 정해진 TL-B 인코딩 규칙을 따르며, 몇 가지 플래그 외에 주로 두 부분, 즉 새 컨트랙트의 code와 초기화 data로 구성됩니다. data의 인코딩 순서는 새 컨트랙트에서 지정한 영구 cell 저장 순서와 일치해야 합니다. 36행에서 초기화 데이터는 item_index(ERC721의 tokenId와 유사)와 표준 함수 my_address로 반환되는 현재 컨트랙트 주소(collection_address)로 구성되며, 이 순서는 nft-item에서 선언된 순서와 일치합니다.
다음 개념은 TON에서 아직 생성되지 않은 스마트 컨트랙트의 주소를 미리 계산할 수 있다는 점입니다. 이는 Solidity의 create2 함수와 유사합니다. TON에서 새 주소는 workchain 식별자와 stateinit의 해시값을 결합하여 생성되며, 이 중 workchain은 TON의 무한 샤딩 아키텍처에 대응하기 위해 지정되어야 하며 현재는 고정값입니다. 표준 함수 workchain으로 얻을 수 있습니다. 후자는 표준 함수 cell_hash로 얻습니다. 따라서 이 예제에서 calculate_nft_item_address는 새 컨트랙트 주소를 미리 계산하는 함수이며, 그 값을 53행에서 메시지에 인코딩하여 내부 메시지의 수신 주소로 사용합니다. nft_content는 생성될 컨트랙트의 초기화 호출에 해당하며, 구체적인 구현은 다음 글에서 다룹니다.
send_royalty_params는 특정 읽기 전용 요청에 대한 내부 메시지 응답입니다. 앞서 강조했듯이 TON의 내부 메시지는 데이터 수정 작업뿐만 아니라 읽기 전용 작업도 이 방식으로 이루어져야 하므로, 이 컨트랙트는 해당 유형의 작업을 처리합니다. 먼저 67행은 요청에 대한 응답 후 요청자 콜백 함수를 표시하는 마커이며, 이후에는 요청된 item index와 해당 royalty 데이터가 반환됩니다.
다음 개념은 TON의 스마트 컨트랙트가 recv_internal과 recv_external이라는 두 개의 통일된 진입점만 가진다는 점입니다. 전자는 모든 내부 메시지의 통일된 호출 진입점이며, 후자는 외부 메시지의 진입점입니다. 개발자는 함수 내에서 message의 다른 플래그에 따라 switch 방식으로 다른 요청을 처리해야 하며, 이 플래그는 위의 67행 콜백 함수 마커입니다. 이 예제에서는 먼저 메시지의 빈 공간을 확인한 후, 메시지 정보를 각각 파싱합니다. 먼저 83행에서 sender_address를 파싱하여 후속 권한 검사를 위해 사용합니다. 여기서 ~ 연산자는 또 다른 문법 설탕입니다. 이후 op 연산 플래그를 파싱하고, 플래그에 따라 해당 요청을 처리합니다. 이 과정에서 앞서 설명한 함수들을 논리에 따라 호출합니다. 예를 들어 royalty 파라미터 요청에 응답하거나, 새로운 NFT를 민팅하고 전역 index를 자동 증가시킵니다.
다음 개념은 108행에 해당합니다. 이름만으로도 함수의 처리 로직을 짐작할 수 있는데, Solidity의 require 함수와 유사하게 Func에서는 표준 함수 throw_unless를 통해 예외를 발생시킵니다. 첫 번째 인수는 오류 코드, 두 번째 인수는 부울 값이며, false일 경우 해당 오류 코드와 함께 예외를 발생시킵니다. 이 행에서는 equal_slices를 사용하여 앞서 파싱한 sender_address가 이 컨트랙트의 영구 저장 owner_address와 같은지 판단하여 권한을 확인합니다.

마지막으로 코드 구조를 더욱 명확하게 하기 위해 영구 정보를 가져오는 보조 함수들을 정리하였으며, 이에 대해서는 생략하겠습니다. 개발자 여러분은 이러한 구조를 참고하여 자신의 스마트 컨트랙트를 개발할 수 있습니다.

TON 생태계의 DApp 개발은 매우 흥미롭습니다. EVM과는 큰 차이가 있으므로, 저는 일련의 글을 통해 TON 체인에서 DApp을 개발하는 방법을 소개할 예정입니다. 함께 배우며 이 기회를 잡아봅시다. 또한 Twitter에서 저와 소통해 주세요. 새로운 흥미로운 DApp 아이디어를 나누며 함께 개발해 봅시다.
TechFlow 공식 커뮤니티에 오신 것을 환영합니다
Telegram 구독 그룹:https://t.me/TechFlowDaily
트위터 공식 계정:https://x.com/TechFlowPost
트위터 영어 계정:https://x.com/BlockFlow_News














