최소 컨트랙트 작성하기
이 레슨에서는 이전 레슨에서 배운 개념(Package, Module, Object)을 사용하여 실제로 동작하는 컨트랙트를 작성합니다. 어렵지 않습니다. 카운터용 struct 하나와 entry fun 두 개만 작성하면 됩니다.
사전 조건
- Move 프로젝트 생성하기 완료
- Move의 동작 원리 알아보기 읽기 완료
- VSCode를 사용하는 경우: Sui Extension 설치 완료
컨트랙트 작성하기
카운터 컨트랙트의 전체 코드입니다. 먼저 읽어보고, 다음 섹션에서 시도할 방법을 선택하세요.
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
로컬 환경 설정이 필요 없습니다. 브라우저에서 바로 편집·컴파일·배포까지 할 수 있습니다.
Build ready.
방법 B: VSCode + CLI
VSCode에서 my_first_package를 열고, sources/ 폴더 안의 .move 파일을 엽니다.
sui move new my_first_package 명령으로 프로젝트를 생성하면 sources/my_first_package.move 파일이 자동으로 생성되어 있습니다. 이 파일의 내용을 위의 코드로 전부 교체하세요.


코드를 작성했다면, 다음 절차로 빌드 에러가 없는지 확인합니다.
1. devnet의 체인 ID 확인
sui client chain-identifier
2. Move.toml 끝에 환경 설정 추가
표시된 ID를 사용하여 추가합니다(ID는 예시입니다. 실제로 표시된 값을 사용하세요):
[environments]
devnet = "a63d14dc"
devnet은 리셋될 때마다 체인 ID가 변경됩니다. 에러가 다시 발생하면 같은 절차로 ID를 업데이트하세요.
testnet과 mainnet은 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)에서 트랜잭션으로 직접 호출할 수 있게 됩니다.
fun 앞에 붙이는 키워드에 따라 "어디서 호출할 수 있는지"가 달라집니다.
| 작성법 | 지갑·CLI에서 | 다른 컨트랙트에서 | 사용 상황 |
|---|---|---|---|
fun | ❌ | ❌ | 같은 모듈 내에서만 사용하는 내부 헬퍼 |
entry fun | ✅ | ❌ | 지갑·CLI에서 직접 호출하는 트랜잭션 진입점 |
public(package) fun | ❌ | ⚠️ 같은 패키지만 | 패키지 내 여러 모듈에서 공유하는 함수(자주 사용) |
public fun | ✅ | ✅ | 누구나 실행 가능 — 보안을 충분히 고려한 후 꼭 필요한 경우에만 |
참고:
public fun은 기술적으로 지갑에서도 호출할 수 있지만, 주된 목적은 다른 컨트랙트나 모듈에서 호출하는 함수를 공개하는 것입니다. 트랜잭션으로 직접 호출하려면entry fun을 사용하는 것이 올바른 선택입니다.
이번 create와 increment는 "지갑에서 호출만 하고, 다른 컨트랙트에서는 사용하지 않는" 용도이므로 entry fun이 적합합니다.
entry fun에는 다음과 같은 제약이 있습니다:
- 반환 값이 있는 경우, 해당 타입은
dropability를 가져야 합니다 - 같은 트랜잭션 내에서
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 Counter를has key로 구현했다 -
create와increment를entry fun으로 구현했다 - 에러 없이 컴파일이 성공했다 (Playground 또는
sui move build)
이번 레슨에서 한 것
-
entry fun을 사용하여 지갑·CLI에서 호출할 수 있는 함수를 구현했다 -
has key로 Sui 오브젝트로 동작하는 struct를 정의했다 -
transfer::transfer로 오브젝트를 자기 자신에게 전송하는 것을 확인했다 - 컨트랙트가 에러 없이 컴파일되는 것을 확인했다