기존의 컨트랙트는 콘솔만 출력해주는 기능만 수행했었다. 이제 다른 사람이 메세지도 같이 입력하고 상호작용할 수 있도록 컨트랙트를 수정해보자.
이해가 가지 않았던 부분이 있어서 검색을 좀 해 봤다. 사진 출처는 빛이 나는 솔플님 블로그다.
이더리움에는 4가지 저장공간이 존재한다.
storage : 블록체인 상에 영구적으로 저장되는 공간이다. 함수 밖 변수, 함수 자체, struct와 배열 내부의 변수가 해당되며 gas fee가 비싸다.
memory : 임시적으로 저장되는 공간이다.(함수 외부 호출이 일어날 때마다 초기화 된다.) 함수 내부 변수, 파라미터, 리턴값이 해당되며 gas fee가 비교적 저렴하다. 단순히 구조체의 값을 복사하거나 임시 변수로 활용하고 싶다면 memory를 활용하면 된다.
stack : memory의 변수들이 stack에서 실행된다. 1024개의 slot이 있으며 공간이 한정적이다.
calldata : 함수 호출시 인자로 포함된 데이터(external function의 파라미터)들이 위치하는 공간이다. memory는 수정이 가능하지만 calldata는 수정 불가능. memory는 calldata로 부터 데이터를 가져와서 복사해서 사용한다. 함수 파라미터로 calldata를 사용하면 gas fee를 아낄 수 있다.
또한 storage와 memory를 구조체와 배열을 선언할 때 명시적으로 선언해줘야 한다. 안 그럼 솔리디티에서 경고를 보낸다.
indexed
는 event 내에서 사용 가능한 표현이다. 특정 event 내의 값을 가져올 때 사용하는데, event로 특정 값을 블록 내부에 기록하면 indexed
로 기록된 값은 나중에 필터링해서 가져올 수 있다.
생성자 즉, constructor
는 변수의 값을 초기화할 때 주로 사용한다. 컨트랙트를 인스턴스화 할 때 초기값을 넣어 줄 수 있다. 예를 들어 A라는 컨트랙트에서 constructor를 선언하고, B라는 컨트랙트에서 인스턴스화 해서 사용할 수 있다. 여기서 발생할 수 있는 문제점은 B 컨트랙트를 디플로이하는 상황에서 A 컨트랙트가 무거울 때 gas fee가 많이 나올 수 있다는 점이다.
인스턴스화 한다는 것 자체가 A 컨트랙트 자체를 가져오는 것이기 때문에 비용이 많이 든다. 한 블록에서 가스를 사용할 수 있는 양이 제한되어있기 때문에 제한 범위를 넘어서면 디플로이 자체를 할 수 없는 경우도 있다. 따라서 A 컨트랙트가 가벼울 때 인스턴스화 하는 것이 좋다.
컨트랙트를 수정했으니 새로운 함수를 테스트 하기 위해 run.js
파일도 수정해준다.
const main = async () => {
const [owner, randomPerson] = await hre.ethers.getSigners();
const waveContractFactory = await hre.ethers.getContractFactory("WavePortal");
const waveContract = await waveContractFactory.deploy();
await waveContract.deployed();
console.log("Contract deployed to:", waveContract.address);
console.log("Contract deployed by:", owner.address);
let waveCount;
waveCount = await waveContract.getTotalWaves();
let waveTxn = await waveContract.wave();
await waveTxn.wait();
waveCount = await waveContract.getTotalWaves();
waveTxn = await waveContract.connect(randomPerson).wave();
await waveTxn.wait();
waveCount = await waveContract.getTotalWaves();
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
const main = async () => {
//삭제됨 const [owner, randomPerson] = await hre.ethers.getSigners();
const waveContractFactory = await hre.ethers.getContractFactory("WavePortal");
const waveContract = await waveContractFactory.deploy();
await waveContract.deployed();
console.log("Contract deployed to:", waveContract.address);
//삭제됨 console.log("Contract deployed by:", owner.address);
let waveCount;
waveCount = await waveContract.getTotalWaves();
console.log(waveCount.toNumber());
//메세지 추가
let waveTxn = await waveContract.wave("A message!");
await waveTxn.wait(); //트랜젝션이 블록에 들어갈 때까지 기다린다.
//삭제됨 waveCount = await waveContract.getTotalWaves();
//기존 owner를 빼고 randomPerson만 생성해서 새로운 메세지 추가
const [_, randomPerson] = await hre.ethers.getSigners();
waveTxn = await waveContract.connect(randomPerson).wave("Another message!");
await waveTxn.wait();
//getAllWaves로 함수 변경
let allWaves = await waveContract.getAllWaves();
console.log(allWaves);
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
이제 아래 명령어를 실행해보면
npx hardhat run scripts/run.js
컨트랙트도 수정하고 테스트도 성공했으니 남은 일은 다시 디플로이 하는 것이다. 컨트랙트를 수정할 때마다 아래 절차를 잊지 않도록 하자.
npx hardhat run scripts/deploy.js --network rinkeby
위 명령어로 rinkeby 네트워크에 디플로이 하고, App.js
파일의 contractAddress
수정한다. 그리고 마지막으로 wavePortal
파일도 다시 복붙해주면 끝.
이제 컨트랙트를 활용할 수 있도록 프론트엔드를 수정해줘야 한다. 그래야 사용자들이 메세지도 남기고 새로운 컨트랙트와 상호작용 할 수 있을 것이다. App.jsx
에서 수정된 부분만 간단히 살펴보면 다음과 같다.
const [currentAccount, setCurrentAccount] = useState("");
//모든 wave를 담은 state 추가
const [allWaves, setAllWaves] = useState([]);
const contractAddress = "0x1AFE0dfFDa4057e64a5926725B97a55e72AEa89c";
//모든 wave를 가져오는 함수 추가
const getAllWaves = async () => {
try {
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const wavePortalContract = new ethers.Contract(contractAddress, contractABI, signer);
//컨트랙트에서 getAllWaves 메소드 실행
const waves = await wavePortalContract.getAllWaves();
//프론트엔드에서 사용할 것들만 가져오기
let wavesCleaned = [];
waves.forEach(wave => {
wavesCleaned.push({
address: wave.waver,
timestamp: new Date(wave.timestamp * 1000),
message: wave.message
});
});
//state 업데이트
setAllWaves(wavesCleaned);
} else {
console.log("Ethereum object doesn't exist!")
}
} catch (error) {
console.log(error);
}
}
return 부분에 아래 내용도 넣어준다.
{allWaves.map((wave, index) => {
return (
<div key={index} style={{ backgroundColor: "OldLace", marginTop: "16px", padding: "8px" }}>
<div>Address: {wave.address}</div>
<div>Time: {wave.timestamp.toString()}</div>
<div>Message: {wave.message}</div>
</div>)
})}
마지막으로 wave함수에 파라미터를 하드코딩해주면 끝.(안 그럼 에러가 난다..)
const waveTxn = await wavePortalContract.wave("this is a message")