본문으로 건너뛰기

최소 컨트랙트 작성하기

이 레슨에서는 이전 레슨에서 배운 개념(Package, Module, Object)을 사용하여 실제로 동작하는 컨트랙트를 작성합니다. 어렵지 않습니다. 카운터용 struct 하나와 entry fun 두 개만 작성하면 됩니다.


사전 조건


컨트랙트 작성하기

카운터 컨트랙트의 전체 코드입니다. 먼저 읽어보고, 다음 섹션에서 시도할 방법을 선택하세요.

module my_first_package::counter {

/// 카운터 오브젝트
public struct Counter has key {
id: UID,
value: u64,
}

/// 카운터를 생성하여 트랜잭션의 발신자에게 전송합니다
entry fun create(ctx: &mut TxContext) {
let counter = Counter {
id: object::new(ctx),
value: 0,
};
transfer::transfer(counter, ctx.sender());
}

/// 카운터 값을 1 증가시킵니다
entry fun increment(counter: &mut Counter) {
counter.value = counter.value + 1;
}
}

직접 해보기

두 가지 방법으로 시도할 수 있습니다. 편한 방법을 선택하세요.

방법 A: Move Playground

로컬 환경 설정이 필요 없습니다. 브라우저에서 바로 편집·컴파일·배포까지 할 수 있습니다.

my_first_package
Move.toml
sources
my_first_package.move
Move.toml
README.md
Console
Build ready.

방법 B: VSCode + CLI

VSCode에서 my_first_package를 열고, sources/ 폴더 안의 .move 파일을 엽니다.

sui move new my_first_package 명령으로 프로젝트를 생성하면 sources/my_first_package.move 파일이 자동으로 생성되어 있습니다. 이 파일의 내용을 위의 코드로 전부 교체하세요.

VSCode에서 파일을 연 상태

코드 입력 후의 VSCode 화면 (에러 없음)

코드를 작성했다면, 다음 절차로 빌드 에러가 없는지 확인합니다.

1. devnet의 체인 ID 확인

sui client chain-identifier

2. Move.toml 끝에 환경 설정 추가

표시된 ID를 사용하여 추가합니다(ID는 예시입니다. 실제로 표시된 값을 사용하세요):

[environments]
devnet = "a63d14dc"

devnet은 리셋될 때마다 체인 ID가 변경됩니다. 에러가 다시 발생하면 같은 절차로 ID를 업데이트하세요.

testnet·mainnet에서는 불필요

testnetmainnet은 Sui의 표준 환경으로 처음부터 인식되므로 이 설정은 필요하지 않습니다. [environments] 섹션 추가가 필요한 것은 devnet뿐입니다.

3. 빌드 실행

cd ~/sui-projects/my_first_package
sui move build

성공하면 다음과 같이 표시됩니다:

INCLUDING DEPENDENCY MoveStdlib
INCLUDING DEPENDENCY Sui
BUILDING my_first_package
에러가 발생하면

Sui Extension이 설치되어 있다면 VSCode에서 실시간으로 에러가 빨간 밑줄로 표시됩니다. 에러가 있는 줄에 커서를 올리면 설명이 표시됩니다.

자주 발생하는 원인:

  • 세미콜론(;) 빠뜨림
  • 괄호 닫기 누락

코드 핵심 포인트 확인

L13에서 배운 개념과 연결하며 확인해 보겠습니다.

entry 키워드

entry fun create(ctx: &mut TxContext) {

entry를 붙이면 이 함수는 지갑이나 CLI(sui client call)에서 트랜잭션으로 직접 호출할 수 있게 됩니다.

Move의 함수 가시성(visibility) 정리

fun 앞에 붙이는 키워드에 따라 "어디서 호출할 수 있는지"가 달라집니다.

작성법지갑·CLI에서다른 컨트랙트에서사용 상황
fun같은 모듈 내에서만 사용하는 내부 헬퍼
entry fun지갑·CLI에서 직접 호출하는 트랜잭션 진입점
public(package) fun⚠️ 같은 패키지만패키지 내 여러 모듈에서 공유하는 함수(자주 사용)
public fun누구나 실행 가능 — 보안을 충분히 고려한 후 꼭 필요한 경우에만

참고: public fun은 기술적으로 지갑에서도 호출할 수 있지만, 주된 목적은 다른 컨트랙트나 모듈에서 호출하는 함수를 공개하는 것입니다. 트랜잭션으로 직접 호출하려면 entry fun을 사용하는 것이 올바른 선택입니다.

이번 createincrement는 "지갑에서 호출만 하고, 다른 컨트랙트에서는 사용하지 않는" 용도이므로 entry fun이 적합합니다.

entry fun에는 다음과 같은 제약이 있습니다:

  • 반환 값이 있는 경우, 해당 타입은 drop ability를 가져야 합니다
  • 같은 트랜잭션 내에서 entry fun에 전달한 오브젝트를 다른 함수에 재사용할 수 없습니다
  • 참조(&T / &mut T)를 반환하는 함수는 지갑에서 호출할 수 없습니다

has key에 대하여

public struct Counter has key {

L13에서는 has key, store 예시를 보았습니다. 여기서는 key만 사용합니다.

  • key → Sui 오브젝트로 존재할 수 있음(필수)
  • store → 다른 오브젝트의 필드에 넣을 수 있고, public_transfer로 전송할 수 있음

이 카운터는 다른 오브젝트에 저장할 예정이 없으므로 key만으로 충분합니다. 이후 레슨에서 둘의 차이가 필요해질 때 다시 확인합니다.

transfer::transfer에 대하여

transfer::transfer(counter, ctx.sender());

생성한 오브젝트를 "누군가에게 전달"하는 작업입니다. 전달하지 않고 함수를 종료하려 하면 컴파일 에러가 발생합니다. 이는 Move의 Linear Types 시스템으로, 오브젝트의 행방이 항상 추적됩니다.

ctx.sender()는 트랜잭션을 전송한 주소를 반환합니다. 자기 자신에게 전송함으로써 해당 오브젝트를 자신이 소유한 상태가 됩니다.


완료 체크리스트

  • struct Counterhas key로 구현했다
  • createincremententry fun으로 구현했다
  • 에러 없이 컴파일이 성공했다 (Playground 또는 sui move build)

이번 레슨에서 한 것

  • entry fun을 사용하여 지갑·CLI에서 호출할 수 있는 함수를 구현했다
  • has key로 Sui 오브젝트로 동작하는 struct를 정의했다
  • transfer::transfer로 오브젝트를 자기 자신에게 전송하는 것을 확인했다
  • 컨트랙트가 에러 없이 컴파일되는 것을 확인했다