Ver3.0 ๐จ ์๋ฆฌ๋ํฐ๋ก NFT ๋ง์ผํ๋ ์ด์ค ๋ง๋ค๊ธฐ

๐จ ์๋ฆฌ๋ํฐ๋ก NFT ๋ง์ผํ๋ ์ด์ค ๋ง๋ค๊ธฐ
์๋ ! ์์ฆ NFT ์ดํ ์ ๋ง ๋๋จํ์ง? ๐ฅ ๋์งํธ ์ํธ๋ถํฐ ๊ฒ์ ์์ดํ , ์ฌ์ง์ด ํธ์๊น์ง NFT๋ก ๊ฑฐ๋๋๋ ์๋๊ฐ ์์ด. ๊ทธ๋ฐ๋ฐ ์ด๋ฐ NFT๋ค์ด ๊ฑฐ๋๋๋ ๋ง์ผํ๋ ์ด์ค๋ฅผ ์ง์ ๋ง๋ค ์ ์๋ค๋ฉด ์ด๋จ๊น? ์ค๋์ ์๋ฆฌ๋ํฐ ์ธ์ด๋ฅผ ์ฌ์ฉํด์ NFT ๋ง์ผํ๋ ์ด์ค๋ฅผ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ์น๊ตฌ์ฒ๋ผ ํธํ๊ฒ ์๋ ค์ค๊ฒ.
์ด ๊ธ์ ์ฝ๊ณ ๋๋ฉด ์ค๋งํธ ์ปจํธ๋ํธ์ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ถํฐ ์ค์ ์๋ํ๋ NFT ๊ฑฐ๋ ์์คํ ๊น์ง ๋ง๋ค ์ ์์ ๊ฑฐ์ผ. ๋ณต์กํด ๋ณด์ด์ง๋ง ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐ๋ผ์ค๋ฉด ์๊ฐ๋ณด๋ค ์ฌ๋ฏธ์์ด! ๐
๐ฏ NFT ๋ง์ผํ๋ ์ด์ค๋ ๋ฌด์์ธ๊ฐ?
NFT ๋ง์ผํ๋ ์ด์ค๋ ์ฝ๊ฒ ๋งํด์ ๋์งํธ ์์ฐ์ ์ฌ๊ณ ํ๋ ์จ๋ผ์ธ ์ฅํฐ์ผ. ์คํ์จ(OpenSea)๋ ๋ผ๋ฆฌ๋ธ(Rarible) ๊ฐ์ ํ๋ซํผ์ ์๊ฐํ๋ฉด ๋ผ. ํ์ง๋ง ์ฐ๋ฆฌ๊ฐ ๋ง๋ค ๊ฑด ๋จ์ํ ์น์ฌ์ดํธ๊ฐ ์๋๋ผ ๋ธ๋ก์ฒด์ธ ์์์ ์๋ํ๋ ํ์ค์ํ๋ ๊ฑฐ๋ ์์คํ ์ด์ผ.
NFT ๋ง์ผํ๋ ์ด์ค๋ ํฌ๊ฒ ์ธ ๊ฐ์ง ๊ธฐ๋ฅ์ ์ ๊ณตํด:
1. NFT ๋ฐํ(Minting) - ๋์งํธ ์์ฐ์ ๋ธ๋ก์ฒด์ธ์ ๋ฑ๋ก
2. NFT ํ๋งค(Listing) - ๊ฐ๊ฒฉ์ ์ ํ๊ณ ํ๋งค ๋ฑ๋ก
3. NFT ๊ตฌ๋งค(Purchase) - ์ํธํํ๋ก NFT ๊ตฌ๋งค
์ ํต์ ์ธ ์จ๋ผ์ธ ๋ง์ผ๊ณผ ๋ค๋ฅธ ์ ์ ์ค๊ฐ์๊ฐ ์๋ค๋ ๊ฑฐ์ผ. ์ค๋งํธ ์ปจํธ๋ํธ๊ฐ ๋ชจ๋ ๊ฑฐ๋๋ฅผ ์๋์ผ๋ก ์ฒ๋ฆฌํ๊ณ , ๊ฑฐ๋ ๋ด์ญ์ ๋ธ๋ก์ฒด์ธ์ ์๊ตฌ์ ์ผ๋ก ๊ธฐ๋ก๋ผ. ์ด๊ฒ ๋ฐ๋ก Web3์ ๋งค๋ ฅ์ด์ง! ๐
๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ ์ค๋นํ๊ธฐ
๋ณธ๊ฒฉ์ ์ผ๋ก ์ฝ๋ฉ์ ๋ค์ด๊ฐ๊ธฐ ์ ์ ํ์ํ ๋๊ตฌ๋ค์ ์ค๋นํด์ผ ํด. ๊ฑฑ์ ๋ง, ํ๋๋ ์ด๋ ต์ง ์์! ๐
์๋ฐ์คํฌ๋ฆฝํธ ๋ฐํ์ ํ๊ฒฝ์ด์ผ. ํจํค์ง ๊ด๋ฆฌ๋ฅผ ์ํด ํ์์ ์ด์ง. ์ต์ LTS ๋ฒ์ ์ ์ค์นํ๋ฉด ๋ผ.
์ด๋๋ฆฌ์ ๊ฐ๋ฐ ํ๊ฒฝ์ด์ผ. ์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ์ปดํ์ผํ๊ณ ํ ์คํธํ๊ณ ๋ฐฐํฌํ๋ ๋ชจ๋ ๊ฑธ ๋์์ค.
๋ธ๋ผ์ฐ์ ์ง๊ฐ์ด์ผ. ํ ์คํธ๋ท์ ์ฐ๊ฒฐํ๊ณ ๊ฑฐ๋๋ฅผ ์น์ธํ๋ ๋ฐ ์ฌ์ฉํด. ํฌ๋กฌ ํ์ฅ ํ๋ก๊ทธ๋จ์ผ๋ก ์ค์นํ๋ฉด ๋ผ.
ํฐ๋ฏธ๋์ ์ด๊ณ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์ ๋ ฅํด๋ณด์:
mkdir nft-marketplace
cd nft-marketplace
npm init -y
npm install --save-dev hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers
์ด ๋ช ๋ น์ด๋ค์ ํ๋ก์ ํธ ํด๋๋ฅผ ๋ง๋ค๊ณ ํ์ํ ํจํค์ง๋ค์ ์ค์นํด์ค. Hardhat์ ์ฐ๋ฆฌ์ ๋ฉ์ธ ๊ฐ๋ฐ ๋๊ตฌ๊ณ , ethers.js๋ ์ด๋๋ฆฌ์๊ณผ ์ํธ์์ฉํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ.
npx hardhat
์ด ๋ช ๋ น์ด๋ฅผ ์คํํ๋ฉด ํ๋ก์ ํธ ํ์ ์ ์ ํํ๋ผ๊ณ ๋์. "Create a basic sample project"๋ฅผ ์ ํํ๋ฉด ๊ธฐ๋ณธ ๊ตฌ์กฐ๊ฐ ์๋์ผ๋ก ์์ฑ๋ผ. ์ ๋ง ํธ๋ฆฌํ์ง? ๐
๊ฐ๋ฐ ์ค์๋ ๋ก์ปฌ ๋ธ๋ก์ฒด์ธ ๋คํธ์ํฌ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ์ข์. Hardhat์ด ์ ๊ณตํ๋ ๋ก์ปฌ ๋ ธ๋๋ ๋น ๋ฅด๊ณ ๋ฌด๋ฃ์ผ. ์ค์ ๋ฐฐํฌ๋ ํ ์คํธ๋ท(Goerli, Sepolia)์์ ์ถฉ๋ถํ ํ ์คํธํ ํ์ ๋ฉ์ธ๋ท์ผ๋ก ๊ฐ๋ ๊ฒ ์์ ํด!
๐ NFT ์ค๋งํธ ์ปจํธ๋ํธ ์์ฑํ๊ธฐ
์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์๋ฆฌ๋ํฐ ์ฝ๋๋ฅผ ์์ฑํด๋ณผ ์๊ฐ์ด์ผ! NFT ๋ง์ผํ๋ ์ด์ค๋ ํฌ๊ฒ ๋ ๊ฐ์ ์ปจํธ๋ํธ๋ก ๊ตฌ์ฑ๋ผ:
1. NFT ์ปจํธ๋ํธ - ERC-721 ํ์ค์ ๋ฐ๋ฅด๋ NFT ํ ํฐ
2. ๋ง์ผํ๋ ์ด์ค ์ปจํธ๋ํธ - NFT ๊ฑฐ๋๋ฅผ ์ฒ๋ฆฌํ๋ ๋ก์ง
๐จ NFT ์ปจํธ๋ํธ ๋ง๋ค๊ธฐ
๋จผ์ contracts ํด๋์ NFT.sol ํ์ผ์ ๋ง๋ค์ด๋ณด์:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract NFT is ERC721URIStorage {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
address contractAddress;
constructor(address marketplaceAddress) ERC721("Metaverse Tokens", "METT") {
contractAddress = marketplaceAddress;
}
function createToken(string memory tokenURI) public returns (uint) {
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(msg.sender, newItemId);
_setTokenURI(newItemId, tokenURI);
setApprovalForAll(contractAddress, true);
return newItemId;
}
}
์ฝ๋๋ฅผ ํ๋์ฉ ๋ฏ์ด๋ณด์! ๐
pragma solidity ^0.8.20 - ์๋ฆฌ๋ํฐ ๋ฒ์ ์ ๋ช ์ํด. 0.8.20 ์ด์์์ ์๋ํ๋ค๋ ๋ป์ด์ผ.
import ๋ฌธ๋ค - OpenZeppelin ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๊ฐ์ ธ์. ์ด๋ฏธ ๊ฒ์ฆ๋ ์์ ํ ์ฝ๋๋ฅผ ์ฌ์ฌ์ฉํ๋ ๊ฑฐ์ง. ๋ฐํด๋ฅผ ๋ค์ ๋ฐ๋ช ํ ํ์ ์์์? ๐
ERC721URIStorage - NFT์ ๋ฉํ๋ฐ์ดํฐ(์ด๋ฏธ์ง, ์ค๋ช ๋ฑ)๋ฅผ ์ ์ฅํ ์ ์๊ฒ ํด์ฃผ๋ ํ์ฅ ๊ธฐ๋ฅ์ด์ผ.
Counters - ํ ํฐ ID๋ฅผ ์๋์ผ๋ก ์ฆ๊ฐ์์ผ์ฃผ๋ ์ ํธ๋ฆฌํฐ์ผ. 1, 2, 3... ์ด๋ฐ ์์ผ๋ก ๊ณ ์ ํ ID๋ฅผ ๋ถ์ฌํด.
createToken ํจ์ - ์ค์ ๋ก NFT๋ฅผ ๋ฐํํ๋ ํจ์์ผ. tokenURI๋ IPFS๋ ๋ค๋ฅธ ์ ์ฅ์์ ์๋ ๋ฉํ๋ฐ์ดํฐ ์ฃผ์๋ฅผ ๊ฐ๋ฆฌ์ผ.
OpenZeppelin ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด ๋จผ์ ์ค์นํด์ผ ํด:
npm install @openzeppelin/contracts์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋ณด์ ๊ฐ์ฌ๋ฅผ ๊ฑฐ์น ํ์ค ๊ตฌํ์ฒด๋ผ์ ์ง์ ์์ฑํ๋ ๊ฒ๋ณด๋ค ํจ์ฌ ์์ ํด!
๐ช ๋ง์ผํ๋ ์ด์ค ์ปจํธ๋ํธ ๋ง๋ค๊ธฐ
์ด์ ์ง์ง ํต์ฌ์ธ ๋ง์ผํ๋ ์ด์ค ์ปจํธ๋ํธ๋ฅผ ๋ง๋ค์ด๋ณด์. NFTMarketplace.sol ํ์ผ์ ์์ฑํด:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract NFTMarketplace is ReentrancyGuard {
using Counters for Counters.Counter;
Counters.Counter private _itemIds;
Counters.Counter private _itemsSold;
address payable owner;
uint256 listingPrice = 0.025 ether;
constructor() {
owner = payable(msg.sender);
}
struct MarketItem {
uint itemId;
address nftContract;
uint256 tokenId;
address payable seller;
address payable owner;
uint256 price;
bool sold;
}
mapping(uint256 => MarketItem) private idToMarketItem;
event MarketItemCreated (
uint indexed itemId,
address indexed nftContract,
uint256 indexed tokenId,
address seller,
address owner,
uint256 price,
bool sold
);
function getListingPrice() public view returns (uint256) {
return listingPrice;
}
function createMarketItem(
address nftContract,
uint256 tokenId,
uint256 price
) public payable nonReentrant {
require(price > 0, "Price must be at least 1 wei");
require(msg.value == listingPrice, "Price must be equal to listing price");
_itemIds.increment();
uint256 itemId = _itemIds.current();
idToMarketItem[itemId] = MarketItem(
itemId,
nftContract,
tokenId,
payable(msg.sender),
payable(address(0)),
price,
false
);
IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
emit MarketItemCreated(
itemId,
nftContract,
tokenId,
msg.sender,
address(0),
price,
false
);
}
function createMarketSale(
address nftContract,
uint256 itemId
) public payable nonReentrant {
uint price = idToMarketItem[itemId].price;
uint tokenId = idToMarketItem[itemId].tokenId;
require(msg.value == price, "Please submit the asking price");
idToMarketItem[itemId].seller.transfer(msg.value);
IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
idToMarketItem[itemId].owner = payable(msg.sender);
idToMarketItem[itemId].sold = true;
_itemsSold.increment();
payable(owner).transfer(listingPrice);
}
function fetchMarketItems() public view returns (MarketItem[] memory) {
uint itemCount = _itemIds.current();
uint unsoldItemCount = _itemIds.current() - _itemsSold.current();
uint currentIndex = 0;
MarketItem[] memory items = new MarketItem[](unsoldItemCount);
for (uint i = 0; i < itemCount; i++) {
if (idToMarketItem[i + 1].owner == address(0)) {
uint currentId = i + 1;
MarketItem storage currentItem = idToMarketItem[currentId];
items[currentIndex] = currentItem;
currentIndex += 1;
}
}
return items;
}
function fetchMyNFTs() public view returns (MarketItem[] memory) {
uint totalItemCount = _itemIds.current();
uint itemCount = 0;
uint currentIndex = 0;
for (uint i = 0; i < totalItemCount; i++) {
if (idToMarketItem[i + 1].owner == msg.sender) {
itemCount += 1;
}
}
MarketItem[] memory items = new MarketItem[](itemCount);
for (uint i = 0; i < totalItemCount; i++) {
if (idToMarketItem[i + 1].owner == msg.sender) {
uint currentId = i + 1;
MarketItem storage currentItem = idToMarketItem[currentId];
items[currentIndex] = currentItem;
currentIndex += 1;
}
}
return items;
}
function fetchItemsCreated() public view returns (MarketItem[] memory) {
uint totalItemCount = _itemIds.current();
uint itemCount = 0;
uint currentIndex = 0;
for (uint i = 0; i < totalItemCount; i++) {
if (idToMarketItem[i + 1].seller == msg.sender) {
itemCount += 1;
}
}
MarketItem[] memory items = new MarketItem[](itemCount);
for (uint i = 0; i < totalItemCount; i++) {
if (idToMarketItem[i + 1].seller == msg.sender) {
uint currentId = i + 1;
MarketItem storage currentItem = idToMarketItem[currentId];
items[currentIndex] = currentItem;
currentIndex += 1;
}
}
return items;
}
}
์, ์ฝ๋๊ฐ ์ข ๊ธธ์ง? ํ์ง๋ง ๊ฑฑ์ ๋ง! ํ๋์ฉ ๋ฏ์ด๋ณด๋ฉด ์๊ฐ๋ณด๋ค ๋จ์ํด. ๐ค
MarketItem ๊ตฌ์กฐ์ฒด - ํ๋งค ์ค์ธ NFT์ ์ ๋ณด๋ฅผ ๋ด๋ ๋ฐ์ดํฐ ๊ตฌ์กฐ์ผ. ๋๊ฐ ํ๊ณ , ์ผ๋ง์ ํ๊ณ , ํ๋ ธ๋์ง ๋ฑ์ ์ ๋ณด๊ฐ ๋ค์ด๊ฐ.
mapping - ์์ดํ ID์ MarketItem์ ์ฐ๊ฒฐํ๋ ํด์๋งต์ด์ผ. ๋น ๋ฅธ ์กฐํ๋ฅผ ์ํด ์ฌ์ฉํด.
event - ๋ธ๋ก์ฒด์ธ์ ๋ก๊ทธ๋ฅผ ๋จ๊ธฐ๋ ๊ฑฐ์ผ. ํ๋ก ํธ์๋์์ ์ด ์ด๋ฒคํธ๋ฅผ ๊ฐ์งํด์ UI๋ฅผ ์ ๋ฐ์ดํธํ ์ ์์ด.
nonReentrant - ์ฌ์ง์ ๊ณต๊ฒฉ์ ๋ฐฉ์งํ๋ modifier์ผ. ๋ณด์์ ์ํด ๊ผญ ํ์ํด!
๐ ์ฃผ์ ํจ์ ์ค๋ช
์ด ํจ์๋ NFT๋ฅผ ๋ง์ผํ๋ ์ด์ค์ ๋ฑ๋กํ๋ ์ญํ ์ ํด. ํ๋งค์๋ listing fee๋ฅผ ์ง๋ถํ๊ณ NFT๋ฅผ ์ปจํธ๋ํธ์ ์ ์กํด. ๊ทธ๋ฌ๋ฉด ๋ง์ผํ๋ ์ด์ค๊ฐ NFT๋ฅผ ๋ณด๊ดํ๋ค๊ฐ ๊ตฌ๋งค์๊ฐ ๋ํ๋๋ฉด ์ ๋ฌํ๋ ๊ฑฐ์ง.
ํต์ฌ ๋ก์ง:
1. ๊ฐ๊ฒฉ์ด 0๋ณด๋ค ํฐ์ง ํ์ธ
2. listing fee๊ฐ ์ ํํ ์ง๋ถ๋์๋์ง ํ์ธ
3. ์๋ก์ด ๋ง์ผ ์์ดํ
์์ฑ
4. NFT๋ฅผ ํ๋งค์์๊ฒ์ ์ปจํธ๋ํธ๋ก ์ ์ก
5. ์ด๋ฒคํธ ๋ฐ์
์ค์ ๊ตฌ๋งค๊ฐ ์ผ์ด๋๋ ํจ์์ผ! ๊ตฌ๋งค์๊ฐ ์ ํํ ๊ธ์ก์ ๋ณด๋ด๋ฉด:
1. ํ๋งค์์๊ฒ ํ๋งค ๊ธ์ก ์ ์ก
2. NFT๋ฅผ ๊ตฌ๋งค์์๊ฒ ์ ์ก
3. ์์ ๊ถ ์
๋ฐ์ดํธ
4. ๋ง์ผํ๋ ์ด์ค ์ด์์์๊ฒ listing fee ์ ์ก
๋ชจ๋ ๊ฒ ์๋์ผ๋ก ์ฒ๋ฆฌ๋ผ. ์ค๊ฐ์๊ฐ ํ์ ์๋ ๊ฑฐ์ง! ์ด๊ฒ ๋ฐ๋ก ์ค๋งํธ ์ปจํธ๋ํธ์ ๋ง๋ฒ์ด์ผ. โจ
fetchMarketItems() - ํ์ฌ ํ๋งค ์ค์ธ ๋ชจ๋ NFT ๋ชฉ๋ก์ ๊ฐ์ ธ์
fetchMyNFTs() - ๋ด๊ฐ ๊ตฌ๋งคํ NFT ๋ชฉ๋ก
fetchItemsCreated() - ๋ด๊ฐ ํ๋งค ๋ฑ๋กํ NFT ๋ชฉ๋ก
์ด ํจ์๋ค์ ํ๋ก ํธ์๋์์ UI๋ฅผ ๊ตฌ์ฑํ ๋ ์ฌ์ฉ๋ผ. ์ฌ์ฉ์๊ฐ ์์ ์ ์ปฌ๋ ์ ์ ๋ณผ ์ ์๊ฒ ํด์ฃผ๋ ๊ฑฐ์ง.
๐งช ํ ์คํธ ์์ฑํ๊ธฐ
์ฝ๋๋ฅผ ์์ฑํ์ผ๋ฉด ์ ๋๋ก ์๋ํ๋์ง ํ ์คํธํด์ผ๊ฒ ์ง? ๋ธ๋ก์ฒด์ธ์์๋ ํ ๋ฒ ๋ฐฐํฌํ๋ฉด ์์ ์ด ์ด๋ ต๊ธฐ ๋๋ฌธ์ ํ ์คํธ๊ฐ ์ ๋ง ์ค์ํด! ๐ฏ
test ํด๋์ marketplace-test.js ํ์ผ์ ๋ง๋ค์ด๋ณด์:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("NFTMarketplace", function () {
let nft;
let marketplace;
let owner;
let buyer;
let listingPrice;
beforeEach(async function () {
[owner, buyer] = await ethers.getSigners();
const NFT = await ethers.getContractFactory("NFT");
const Marketplace = await ethers.getContractFactory("NFTMarketplace");
marketplace = await Marketplace.deploy();
await marketplace.deployed();
nft = await NFT.deploy(marketplace.address);
await nft.deployed();
listingPrice = await marketplace.getListingPrice();
});
it("Should create and execute market sales", async function () {
const tokenURI = "https://ipfs.io/ipfs/QmTest123";
await nft.createToken(tokenURI);
await nft.createToken(tokenURI);
const auctionPrice = ethers.utils.parseUnits("1", "ether");
await marketplace.createMarketItem(nft.address, 1, auctionPrice, {
value: listingPrice,
});
await marketplace.createMarketItem(nft.address, 2, auctionPrice, {
value: listingPrice,
});
await marketplace
.connect(buyer)
.createMarketSale(nft.address, 1, { value: auctionPrice });
let items = await marketplace.fetchMarketItems();
expect(items.length).to.equal(1);
items = await marketplace.connect(buyer).fetchMyNFTs();
expect(items.length).to.equal(1);
});
it("Should return correct listing price", async function () {
const price = await marketplace.getListingPrice();
expect(price).to.equal(ethers.utils.parseEther("0.025"));
});
it("Should fail if price is not met", async function () {
const tokenURI = "https://ipfs.io/ipfs/QmTest456";
await nft.createToken(tokenURI);
const auctionPrice = ethers.utils.parseUnits("1", "ether");
await marketplace.createMarketItem(nft.address, 1, auctionPrice, {
value: listingPrice,
});
await expect(
marketplace
.connect(buyer)
.createMarketSale(nft.address, 1, {
value: ethers.utils.parseUnits("0.5", "ether")
})
).to.be.revertedWith("Please submit the asking price");
});
});
ํ ์คํธ ์ฝ๋๋ฅผ ์คํํ๋ ค๋ฉด:
npx hardhat test
๋ชจ๋ ํ ์คํธ๊ฐ ํต๊ณผํ๋ฉด ์ด๋ก์ ์ฒดํฌ๋งํฌ๊ฐ ๋ฐ ๊ฑฐ์ผ! โ ๋ง์ฝ ์คํจํ๋ฉด ์ด๋๊ฐ ๋ฌธ์ ์ธ์ง ํ์ธํ๊ณ ์์ ํด์ผ ํด.
ํ ์คํธ๋ ๋ค์ํ ์๋๋ฆฌ์ค๋ฅผ ์ปค๋ฒํด์ผ ํด:
- ์ ์์ ์ธ ๊ฑฐ๋ ํ๋ฆ
- ์๋ชป๋ ๊ฐ๊ฒฉ ์ ๋ ฅ
- ๊ถํ ์๋ ์ ๊ทผ ์๋
- ๊ฒฝ๊ณ๊ฐ ํ ์คํธ(0 ETH, ๋งค์ฐ ํฐ ๊ธ์ก ๋ฑ)
์ค์ ํ๋ก์ ํธ์์๋ ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ 80% ์ด์ ์ ์งํ๋ ๊ฒ ์ข์!
๐ ๋ฐฐํฌํ๊ธฐ
ํ ์คํธ๊น์ง ์๋ฃํ์ผ๋ฉด ์ด์ ์ค์ ๋ธ๋ก์ฒด์ธ์ ๋ฐฐํฌํ ์ฐจ๋ก์ผ! ๋จผ์ ํ ์คํธ๋ท์ ๋ฐฐํฌํด์ ์ค์ ํ๊ฒฝ์์ ํ ์คํธํด๋ณด์. ๐
๐ ๋ฐฐํฌ ์คํฌ๋ฆฝํธ ์์ฑ
scripts ํด๋์ deploy.js ํ์ผ์ ๋ง๋ค์ด:
const hre = require("hardhat");
async function main() {
const NFTMarketplace = await hre.ethers.getContractFactory("NFTMarketplace");
const marketplace = await NFTMarketplace.deploy();
await marketplace.deployed();
console.log("NFTMarketplace deployed to:", marketplace.address);
const NFT = await hre.ethers.getContractFactory("NFT");
const nft = await NFT.deploy(marketplace.address);
await nft.deployed();
console.log("NFT contract deployed to:", nft.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
โ๏ธ Hardhat ์ค์ ํ์ผ ์์
hardhat.config.js ํ์ผ์ ์ด์ด์ ๋คํธ์ํฌ ์ค์ ์ ์ถ๊ฐํด:
require("@nomiclabs/hardhat-waffle");
require("dotenv").config();
module.exports = {
solidity: "0.8.20",
networks: {
hardhat: {
chainId: 1337
},
sepolia: {
url: process.env.SEPOLIA_RPC_URL,
accounts: [process.env.PRIVATE_KEY]
},
polygon: {
url: process.env.POLYGON_RPC_URL,
accounts: [process.env.PRIVATE_KEY]
}
}
};
์ ๋๋ก private key๋ฅผ ์ฝ๋์ ์ง์ ๋ฃ์ผ๋ฉด ์ ๋ผ! .env ํ์ผ์ ๋ง๋ค์ด์ ํ๊ฒฝ ๋ณ์๋ก ๊ด๋ฆฌํด์ผ ํด:
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_PROJECT_IDPRIVATE_KEY=your_private_key_here๊ทธ๋ฆฌ๊ณ .gitignore์ .env๋ฅผ ๊ผญ ์ถ๊ฐํด์ GitHub์ ์ฌ๋ผ๊ฐ์ง ์๊ฒ ํด!
๐ฌ ์ค์ ๋ฐฐํฌ ์คํ
Sepolia ํ ์คํธ๋ท์ ๋ฐฐํฌํ๋ ค๋ฉด:
npx hardhat run scripts/deploy.js --network sepolia
๋ฐฐํฌ๊ฐ ์๋ฃ๋๋ฉด ์ปจํธ๋ํธ ์ฃผ์๊ฐ ์ถ๋ ฅ๋ผ. ์ด ์ฃผ์๋ฅผ ์ ์ ์ฅํด๋ฌ์ผ ํด! ํ๋ก ํธ์๋์์ ์ด ์ฃผ์๋ก ์ปจํธ๋ํธ์ ํต์ ํ๊ฑฐ๋ . ๐
ํ ์คํธ๋ท์ ๋ฐฐํฌํ๋ ค๋ฉด ํ ์คํธ์ฉ ์ด๋๊ฐ ํ์ํด. ๋ค์ faucet์์ ๋ฌด๋ฃ๋ก ๋ฐ์ ์ ์์ด:
- Sepolia Faucet: https://sepoliafaucet.com/
- Goerli Faucet: https://goerlifaucet.com/
MetaMask ์ฃผ์๋ฅผ ์ ๋ ฅํ๋ฉด ํ ์คํธ ์ด๋๋ฅผ ๋ณด๋ด์ค. ๋ณดํต 0.5 ETH ์ ๋ ๋ฐ์ ์ ์์ด!
๐จ ํ๋ก ํธ์๋ ์ฐ๋ํ๊ธฐ
์ค๋งํธ ์ปจํธ๋ํธ๋ง ์์ผ๋ฉด ๋ญํด? ์ฌ์ฉ์๊ฐ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๋ ์ธํฐํ์ด์ค๊ฐ ํ์ํ์ง! React๋ฅผ ์ฌ์ฉํด์ ๊ฐ๋จํ ํ๋ก ํธ์๋๋ฅผ ๋ง๋ค์ด๋ณด์. ๐ผ๏ธ
๐ฆ ํ์ํ ํจํค์ง ์ค์น
npm install ethers@5.7.2 axios ipfs-http-client
npx create-react-app client
cd client
npm install ethers@5.7.2 axios
๐ Web3 ์ฐ๊ฒฐ ์ค์
๋จผ์ MetaMask์ ์ฐ๊ฒฐํ๋ ๊ธฐ๋ฅ์ ๋ง๋ค์ด์ผ ํด. src/utils/web3.js ํ์ผ์ ๋ง๋ค์ด:
import { ethers } from 'ethers';
import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json';
import NFT from '../artifacts/contracts/NFT.sol/NFT.json';
const marketplaceAddress = "YOUR_MARKETPLACE_CONTRACT_ADDRESS";
const nftAddress = "YOUR_NFT_CONTRACT_ADDRESS";
export async function connectWallet() {
if (typeof window.ethereum !== 'undefined') {
try {
await window.ethereum.request({ method: 'eth_requestAccounts' });
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
return { provider, signer };
} catch (error) {
console.error("User denied account access");
throw error;
}
} else {
alert('Please install MetaMask!');
throw new Error('MetaMask not installed');
}
}
export async function getMarketplaceContract(signer) {
return new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer);
}
export async function getNFTContract(signer) {
return new ethers.Contract(nftAddress, NFT.abi, signer);
}
๐ผ๏ธ NFT ๋ชฉ๋ก ํ์ํ๊ธฐ
ํ๋งค ์ค์ธ NFT ๋ชฉ๋ก์ ๋ณด์ฌ์ฃผ๋ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค์ด๋ณด์:
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import { connectWallet, getMarketplaceContract, getNFTContract } from './utils/web3';
function Marketplace() {
const [nfts, setNfts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadNFTs();
}, []);
async function loadNFTs() {
try {
const { provider, signer } = await connectWallet();
const marketplaceContract = await getMarketplaceContract(signer);
const nftContract = await getNFTContract(signer);
const data = await marketplaceContract.fetchMarketItems();
const items = await Promise.all(data.map(async i => {
const tokenUri = await nftContract.tokenURI(i.tokenId);
const meta = await fetch(tokenUri).then(res => res.json());
let price = ethers.utils.formatUnits(i.price.toString(), 'ether');
return {
price,
tokenId: i.tokenId.toNumber(),
seller: i.seller,
owner: i.owner,
image: meta.image,
name: meta.name,
description: meta.description,
};
}));
setNfts(items);
setLoading(false);
} catch (error) {
console.error("Error loading NFTs:", error);
setLoading(false);
}
}
async function buyNft(nft) {
try {
const { signer } = await connectWallet();
const marketplaceContract = await getMarketplaceContract(signer);
const price = ethers.utils.parseUnits(nft.price.toString(), 'ether');
const transaction = await marketplaceContract.createMarketSale(
nftAddress,
nft.tokenId,
{ value: price }
);
await transaction.wait();
alert('NFT purchased successfully!');
loadNFTs();
} catch (error) {
console.error("Error buying NFT:", error);
alert('Purchase failed!');
}
}
if (loading) return <div>Loading...</div>;
return (
<div className="marketplace">
<h1>NFT Marketplace</h1>
<div className="nft-grid">
{nfts.map((nft, i) => (
<div key={i} className="nft-card">
<img src={nft.image} alt={nft.name} />
<h3>{nft.name}</h3>
<p>{nft.description}</p>
<p className="price">{nft.price} ETH</p>
<button onClick={() => buyNft(nft)}>Buy Now</button>
</div>
))}
</div>
</div>
);
}
export default Marketplace;
์ด ์ฝ๋๋ ๋ง์ผํ๋ ์ด์ค์์ ํ๋งค ์ค์ธ ๋ชจ๋ NFT๋ฅผ ๊ฐ์ ธ์์ ์นด๋ ํํ๋ก ๋ณด์ฌ์ค. ๊ฐ ์นด๋์๋ ๊ตฌ๋งค ๋ฒํผ์ด ์์ด์ ํด๋ฆญํ๋ฉด MetaMask๊ฐ ์ด๋ฆฌ๊ณ ๊ฑฐ๋๋ฅผ ์น์ธํ ์ ์์ด! ๐
์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํ๋ ค๋ฉด:
- ๋ก๋ฉ ์ํ๋ฅผ ๋ช ํํ ํ์
- ๊ฑฐ๋ ์งํ ์ํฉ์ ์ค์๊ฐ์ผ๋ก ๋ณด์ฌ์ฃผ๊ธฐ
- ์๋ฌ ๋ฉ์์ง๋ฅผ ์น์ ํ๊ฒ ์์ฑ
- ๋ชจ๋ฐ์ผ ๋ฐ์ํ ๋์์ธ ์ ์ฉ
- ์ด๋ฏธ์ง ๋ก๋ฉ ์ต์ ํ
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์ฌ๋ฅ์ ๊ณต์ ํ ๋๋ ์ด๋ฐ ์ธ์ฌํ ๋ฐฐ๋ ค๊ฐ ์ค์ํด!
๐ธ IPFS๋ก ๋ฉํ๋ฐ์ดํฐ ์ ์ฅํ๊ธฐ
NFT์ ์ด๋ฏธ์ง์ ๋ฉํ๋ฐ์ดํฐ๋ ์ด๋์ ์ ์ฅํด์ผ ํ ๊น? ๋ธ๋ก์ฒด์ธ์ ์ง์ ์ ์ฅํ๋ฉด ๊ฐ์ค๋น๊ฐ ์์ฒญ๋๊ฒ ๋น์ธ์ ธ. ๊ทธ๋์ ๋ณดํต IPFS(InterPlanetary File System)๋ฅผ ์ฌ์ฉํด! ๐
๐ง IPFS ํด๋ผ์ด์ธํธ ์ค์
import { create } from 'ipfs-http-client';
const projectId = 'YOUR_INFURA_PROJECT_ID';
const projectSecret = 'YOUR_INFURA_PROJECT_SECRET';
const auth = 'Basic ' + Buffer.from(projectId + ':' + projectSecret).toString('base64');
const client = create({
host: 'ipfs.infura.io',
port: 5001,
protocol: 'https',
headers: {
authorization: auth,
},
});
export async function uploadToIPFS(file) {
try {
const added = await client.add(file);
const url = `https://ipfs.io/ipfs/${added.path}`;
return url;
} catch (error) {
console.error('Error uploading file:', error);
throw error;
}
}
export async function uploadMetadata(metadata) {
try {
const added = await client.add(JSON.stringify(metadata));
const url = `https://ipfs.io/ipfs/${added.path}`;
return url;
} catch (error) {
console.error('Error uploading metadata:', error);
throw error;
}
}
๐จ NFT ์์ฑ ์ปดํฌ๋ํธ
์ด์ ์ฌ์ฉ์๊ฐ ์์ ์ ์ํ์ NFT๋ก ๋ง๋ค ์ ์๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํด๋ณด์:
import React, { useState } from 'react';
import { uploadToIPFS, uploadMetadata } from './utils/ipfs';
import { connectWallet, getNFTContract, getMarketplaceContract } from './utils/web3';
import { ethers } from 'ethers';
function CreateNFT() {
const [fileUrl, setFileUrl] = useState(null);
const [formInput, updateFormInput] = useState({
price: '',
name: '',
description: ''
});
async function onChange(e) {
const file = e.target.files[0];
try {
const url = await uploadToIPFS(file);
setFileUrl(url);
} catch (error) {
console.error('Error uploading file:', error);
}
}
async function createMarket() {
const { name, description, price } = formInput;
if (!name || !description || !price || !fileUrl) {
alert('Please fill all fields!');
return;
}
try {
const metadata = {
name,
description,
image: fileUrl,
};
const metadataUrl = await uploadMetadata(metadata);
await mintAndList(metadataUrl, price);
} catch (error) {
console.error('Error creating NFT:', error);
}
}
async function mintAndList(tokenURI, price) {
try {
const { signer } = await connectWallet();
const nftContract = await getNFTContract(signer);
const marketplaceContract = await getMarketplaceContract(signer);
let transaction = await nftContract.createToken(tokenURI);
let tx = await transaction.wait();
let event = tx.events[0];
let value = event.args[2];
let tokenId = value.toNumber();
const priceInWei = ethers.utils.parseUnits(price, 'ether');
const listingPrice = await marketplaceContract.getListingPrice();
transaction = await marketplaceContract.createMarketItem(
nftContract.address,
tokenId,
priceInWei,
{ value: listingPrice }
);
await transaction.wait();
alert('NFT created and listed successfully!');
} catch (error) {
console.error('Error minting NFT:', error);
alert('Failed to create NFT!');
}
}
return (
<div className="create-nft">
<h1>Create New NFT</h1>
<input
placeholder="Asset Name"
onChange={e => updateFormInput({ ...formInput, name: e.target.value })}
/>
<textarea
placeholder="Asset Description"
onChange={e => updateFormInput({ ...formInput, description: e.target.value })}
/>
<input
placeholder="Asset Price in ETH"
type="number"
onChange={e => updateFormInput({ ...formInput, price: e.target.value })}
/>
<input
type="file"
name="Asset"
onChange={onChange}
/>
{fileUrl && (
<img src={fileUrl} alt="Preview" style={{ width: '300px' }} />
)}
<button onClick={createMarket}>Create NFT</button>
</div>
);
}
export default CreateNFT;
์ด ์ปดํฌ๋ํธ๋ ์ฌ์ฉ์๊ฐ ์ด๋ฏธ์ง๋ฅผ ์ ๋ก๋ํ๊ณ , ์ด๋ฆ๊ณผ ์ค๋ช ์ ์ ๋ ฅํ๊ณ , ๊ฐ๊ฒฉ์ ์ค์ ํ ์ ์๊ฒ ํด์ค. ๋ชจ๋ ์ ๋ณด๊ฐ ์ ๋ ฅ๋๋ฉด IPFS์ ์ ๋ก๋ํ๊ณ NFT๋ฅผ ๋ฐํํ ๋ค์ ๋ง์ผํ๋ ์ด์ค์ ๋ฑ๋กํ๋ ์ ์ฒด ๊ณผ์ ์ ์๋์ผ๋ก ์ฒ๋ฆฌํด! ๐
IPFS ์ธ์๋ ๋ค๋ฅธ ํ์ค์ํ ์ ์ฅ์๋ฅผ ์ฌ์ฉํ ์ ์์ด:
- Arweave: ์๊ตฌ ์ ์ฅ์ ๋ณด์ฅํ๋ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ฐ ์คํ ๋ฆฌ์ง
- Filecoin: IPFS์ ์ธ์ผํฐ๋ธ ๋ ์ด์ด
- Pinata: IPFS ํธ์คํ ์๋น์ค (์ฌ์ฉํ๊ธฐ ์ฌ์)
๊ฐ๊ฐ ์ฅ๋จ์ ์ด ์์ผ๋ ํ๋ก์ ํธ ์๊ตฌ์ฌํญ์ ๋ง๊ฒ ์ ํํ๋ฉด ๋ผ!
๐ ๋ณด์ ๊ณ ๋ ค์ฌํญ
NFT ๋ง์ผํ๋ ์ด์ค๋ ์ค์ ๋์ด ์ค๊ฐ๋ ์์คํ ์ด์ผ. ๊ทธ๋์ ๋ณด์์ด ์ ๋ง ์ค์ํด! ํด์ปค๋ค์ ํญ์ ์ทจ์ฝ์ ์ ์ฐพ๊ณ ์๊ฑฐ๋ . ๐ฑ
ReentrancyGuard๋ฅผ ์ฌ์ฉํด์ ํจ์๊ฐ ์คํ ์ค์ผ ๋ ๋ค์ ํธ์ถ๋๋ ๊ฑธ ๋ง์์ผ ํด. ์ด๊ฑด ๊ฐ์ฅ ํํ ๊ณต๊ฒฉ ๋ฐฉ๋ฒ ์ค ํ๋์ผ.
Solidity 0.8.0 ์ด์์์๋ ์๋์ผ๋ก ์ฒดํฌ๋์ง๋ง, ๊ทธ๋๋ SafeMath ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ์์ ํด.
onlyOwner ๊ฐ์ modifier๋ฅผ ์ฌ์ฉํด์ ์ค์ํ ํจ์๋ ๊ถํ์ด ์๋ ์ฌ๋๋ง ์คํํ ์ ์๊ฒ ํด์ผ ํด.
๋ชจ๋ ์ ๋ ฅ๊ฐ์ require ๋ฌธ์ผ๋ก ๊ฒ์ฆํด์ผ ํด. ๊ฐ๊ฒฉ์ด 0๋ณด๋ค ํฐ์ง, ์ฃผ์๊ฐ ์ ํจํ์ง ๋ฑ์ ์ฒดํฌํ๋ ๊ฑฐ์ง.
๐ ๋ณด์ ์ฒดํฌ๋ฆฌ์คํธ
โ ์ฌ์ง์
๊ณต๊ฒฉ ๋ฐฉ์ด
๋ชจ๋ ์ํ ๋ณ๊ฒฝ์ ์ธ๋ถ ํธ์ถ ์ ์ ์๋ฃํด์ผ ํด. Checks-Effects-Interactions ํจํด์ ๋ฐ๋ฅด๋ ๊ฑฐ์ง.
function createMarketSale(address nftContract, uint256 itemId)
public payable nonReentrant {
// 1. Checks
require(msg.value == price, "Incorrect price");
// 2. Effects (์ํ ๋ณ๊ฒฝ)
idToMarketItem[itemId].sold = true;
idToMarketItem[itemId].owner = payable(msg.sender);
_itemsSold.increment();
// 3. Interactions (์ธ๋ถ ํธ์ถ)
seller.transfer(msg.value);
IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
}
โ ๊ฐ์ค ์ต์ ํ
๋ถํ์ํ ์ ์ฅ์ ์ฝ๊ธฐ/์ฐ๊ธฐ๋ฅผ ์ค์ด๊ณ , ๋ฃจํ๋ฅผ ์ต์ํํด์ผ ํด. ๊ฐ์ค๋น๊ฐ ๋๋ฌด ๋น์ธ๋ฉด ์ฌ์ฉ์๋ค์ด ๋ ๋๊ฑฐ๋ !
โ ์ด๋ฒคํธ ๋ก๊น
๋ชจ๋ ์ค์ํ ์ํ ๋ณ๊ฒฝ์ ์ด๋ฒคํธ๋ก ๊ธฐ๋กํด์ผ ํด. ํ๋ก ํธ์๋์์ ์ถ์ ํ๊ธฐ๋ ์ฝ๊ณ , ํฌ๋ช
์ฑ๋ ๋์์ ธ.
โ ์
๊ทธ๋ ์ด๋ ๊ฐ๋ฅ์ฑ
ํ๋ก์ ํจํด์ ์ฌ์ฉํ๋ฉด ์ปจํธ๋ํธ๋ฅผ ์
๊ทธ๋ ์ด๋ํ ์ ์์ด. ํ์ง๋ง ๋ณต์กํ๋๊น ์ฒ์์๋ ๊ฐ๋จํ๊ฒ ์์ํ๋ ๊ฒ ์ข์.
์ค์ ๋ฉ์ธ๋ท์ ๋ฐฐํฌํ๊ธฐ ์ ์๋ ๋ฐ๋์ ์ ๋ฌธ๊ฐ์ ๋ณด์ ๊ฐ์ฌ๋ฅผ ๋ฐ์์ผ ํด. CertiK, OpenZeppelin, Trail of Bits ๊ฐ์ ํ์ฌ๋ค์ด ์ ๋ช ํด. ๋น์ฉ์ด ๋ค์ง๋ง ํดํน๋นํ๋ ๊ฒ๋ณด๋ค๋ ํจ์ฌ ์ ๋ ดํ์ง!
โก ๊ฐ์ค ์ต์ ํ ๊ธฐ๋ฒ
์ด๋๋ฆฌ์์ ๊ฐ์ค๋น๋ ์ ๋ง ๋น์ธ. ํนํ ๋คํธ์ํฌ๊ฐ ํผ์กํ ๋๋ ๋์ฑ ๊ทธ๋. ๊ทธ๋์ ๊ฐ์ค ์ต์ ํ๋ ํ์์ผ! ๐ธ
๐ ๊ฐ์ค๋ฅผ ๋ง์ด ์ฐ๋ ์์ ๋ค
1. ์ ์ฅ์(Storage) ์ฐ๊ธฐ - 20,000 ๊ฐ์ค
2. ์ ์ฅ์ ์ฝ๊ธฐ - 2,100 ๊ฐ์ค
3. ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ - 3 ๊ฐ์ค
4. ๊ณ์ฐ ์์ - 3-5 ๊ฐ์ค
์ ์ฅ์ ์์ ์ด ์๋์ ์ผ๋ก ๋น์ธ๋ค๋ ๊ฑธ ์ ์ ์์ง? ๊ทธ๋์ ์ ์ฅ์ ์ฌ์ฉ์ ์ต์ํํ๋ ๊ฒ ํต์ฌ์ด์ผ!
๐ฏ ์ต์ ํ ๊ธฐ๋ฒ๋ค
Solidity๋ 32๋ฐ์ดํธ ๋จ์๋ก ์ ์ฅ์๋ฅผ ์ฌ์ฉํด. ๊ทธ๋์ ์์ ๋ณ์๋ค์ ๋ฌถ์ด์ ์ ์ธํ๋ฉด ๊ฐ์ค๋ฅผ ์ ์ฝํ ์ ์์ด:
// ๋์ ์ - 3๊ฐ์ ์ฌ๋กฏ ์ฌ์ฉ
uint256 a; // 32 bytes
uint128 b; // 16 bytes
uint128 c; // 16 bytes
// ์ข์ ์ - 2๊ฐ์ ์ฌ๋กฏ ์ฌ์ฉ
uint256 a; // 32 bytes
uint128 b; // 16 bytes
uint128 c; // 16 bytes (๊ฐ์ ์ฌ๋กฏ์ ์ ์ฅ๋จ)
๊ฐ๋ฅํ๋ฉด memory๋ฅผ ์ฌ์ฉํ๊ณ , ๊ผญ ํ์ํ ๋๋ง storage๋ฅผ ์ฌ์ฉํด:
// ๋นํจ์จ์
function getItems() public view returns (MarketItem[] memory) {
MarketItem[] storage items = new MarketItem[](itemCount);
// ...
}
// ํจ์จ์
function getItems() public view returns (MarketItem[] memory) {
MarketItem[] memory items = new MarketItem[](itemCount);
// ...
}
๋ฃจํ ์์์ ์ ์ฅ์๋ฅผ ์ฝ์ง ๋ง๊ณ , ํ ๋ฒ๋ง ์ฝ์ด์ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅํด:
// ๋นํจ์จ์ - ๋งค๋ฒ ์ ์ฅ์ ์ฝ๊ธฐ
for (uint i = 0; i < items.length; i++) {
if (items[i].sold) { ... }
}
// ํจ์จ์ - ํ ๋ฒ๋ง ์ฝ๊ธฐ
uint length = items.length;
for (uint i = 0; i < length; i++) {
MarketItem memory item = items[i];
if (item.sold) { ... }
}
๊ฐ์ค๋น๊ฐ ๋๋ฌด ๋ถ๋ด๋๋ค๋ฉด Layer 2 ์๋ฃจ์ ์ ๊ณ ๋ คํด๋ด:
- Polygon (Matic): ์ด๋๋ฆฌ์๊ณผ ํธํ๋๋ฉด์ ๊ฐ์ค๋น๊ฐ ๊ฑฐ์ ๋ฌด๋ฃ
- Arbitrum: Optimistic Rollup ๊ธฐ์ ์ฌ์ฉ
- Optimism: ๋ ๋ค๋ฅธ Optimistic Rollup
- zkSync: Zero-Knowledge Rollup
ํนํ Polygon์ NFT ํ๋ก์ ํธ์์ ๋ง์ด ์ฌ์ฉํด. OpenSea๋ ์ง์ํ๊ณ !
๐ญ ๊ณ ๊ธ ๊ธฐ๋ฅ ์ถ๊ฐํ๊ธฐ
๊ธฐ๋ณธ ๋ง์ผํ๋ ์ด์ค๊ฐ ์์ฑ๋๋ค๋ฉด ์ด์ ๋ ์ฌ๋ฏธ์๋ ๊ธฐ๋ฅ๋ค์ ์ถ๊ฐํด๋ณผ ์ฐจ๋ก์ผ! ๐
๐จ ๊ฒฝ๋งค ์์คํ
๊ณ ์ ๊ฐ๊ฒฉ ํ๋งค๋ง ์์ผ๋ฉด ์ฌ๋ฏธ์์์? ๊ฒฝ๋งค ๊ธฐ๋ฅ์ ์ถ๊ฐํด๋ณด์:
struct Auction {
uint256 tokenId;
address payable seller;
uint256 startPrice;
uint256 highestBid;
address payable highestBidder;
uint256 endTime;
bool ended;
}
mapping(uint256 => Auction) public auctions;
function createAuction(
address nftContract,
uint256 tokenId,
uint256 startPrice,
uint256 duration
) public {
require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "Not owner");
IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
auctions[tokenId] = Auction({
tokenId: tokenId,
seller: payable(msg.sender),
startPrice: startPrice,
highestBid: 0,
highestBidder: payable(address(0)),
endTime: block.timestamp + duration,
ended: false
});
}
function placeBid(uint256 tokenId) public payable {
Auction storage auction = auctions[tokenId];
require(block.timestamp < auction.endTime, "Auction ended");
require(msg.value > auction.highestBid, "Bid too low");
require(msg.value >= auction.startPrice, "Below start price");
if (auction.highestBidder != address(0)) {
auction.highestBidder.transfer(auction.highestBid);
}
auction.highestBid = msg.value;
auction.highestBidder = payable(msg.sender);
}
function endAuction(address nftContract, uint256 tokenId) public {
Auction storage auction = auctions[tokenId];
require(block.timestamp >= auction.endTime, "Auction not ended");
require(!auction.ended, "Already ended");
auction.ended = true;
if (auction.highestBidder != address(0)) {
IERC721(nftContract).transferFrom(
address(this),
auction.highestBidder,
tokenId
);
auction.seller.transfer(auction.highestBid);
} else {
IERC721(nftContract).transferFrom(
address(this),
auction.seller,
tokenId
);
}
}
๊ฒฝ๋งค ์์คํ ์ ์ข ๋ ๋ณต์กํ์ง๋ง ์ฌ์ฉ์๋ค์๊ฒ ์ธ๊ธฐ๊ฐ ๋ง์. ํนํ ํฌ๊ทํ NFT๋ ๊ฒฝ๋งค๋ก ํ๋งคํ๋ฉด ๋ ๋์ ๊ฐ๊ฒฉ์ ๋ฐ์ ์ ์๊ฑฐ๋ ! ๐ฐ
๐ ๋ก์ดํฐ ์์คํ
ํฌ๋ฆฌ์์ดํฐ๊ฐ 2์ฐจ ํ๋งค์์๋ ์์ต์ ์ป์ ์ ์๊ฒ ๋ก์ดํฐ ๊ธฐ๋ฅ์ ์ถ๊ฐํด๋ณด์:
mapping(uint256 => address) public creators;
mapping(uint256 => uint256) public royaltyPercentage;
function setRoyalty(uint256 tokenId, uint256 percentage) public {
require(creators[tokenId] == msg.sender, "Not creator");
require(percentage <= 10, "Max 10%");
royaltyPercentage[tokenId] = percentage;
}
function createMarketSaleWithRoyalty(
address nftContract,
uint256 itemId
) public payable nonReentrant {
uint price = idToMarketItem[itemId].price;
uint tokenId = idToMarketItem[itemId].tokenId;
require(msg.value == price, "Incorrect price");
uint royalty = (price * royaltyPercentage[tokenId]) / 100;
uint sellerAmount = price - royalty;
if (royalty > 0 && creators[tokenId] != address(0)) {
payable(creators[tokenId]).transfer(royalty);
}
idToMarketItem[itemId].seller.transfer(sellerAmount);
IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
idToMarketItem[itemId].owner = payable(msg.sender);
idToMarketItem[itemId].sold = true;
_itemsSold.increment();
}
๋ก์ดํฐ ์์คํ ์ ํฌ๋ฆฌ์์ดํฐ๋ค์๊ฒ ์ ๋ง ์ค์ํด. ์์ ์ ์ํ์ด ๊ณ์ ๊ฑฐ๋๋ ๋๋ง๋ค ์์ต์ ์ป์ ์ ์์ผ๋๊น! ๋ณดํต 5-10% ์ ๋๋ก ์ค์ ํด. ๐จ
๐ ์ปฌ๋ ์ ๊ธฐ๋ฅ
NFT๋ฅผ ์ปฌ๋ ์ ๋ณ๋ก ๋ฌถ์ด์ ๊ด๋ฆฌํ ์ ์๊ฒ ํด๋ณด์:
struct Collection {
string name;
string symbol;
address creator;
uint256[] tokenIds;
}
mapping(uint256 => Collection) public collections;
uint256 public collectionCount;
function createCollection(
string memory name,
string memory symbol
) public returns (uint256) {
collectionCount++;
collections[collectionCount] = Collection({
name: name,
symbol: symbol,
creator: msg.sender,
tokenIds: new uint256[](0)
});
return collectionCount;
}
function addToCollection(
uint256 collectionId,
uint256 tokenId
) public {
require(
collections[collectionId].creator == msg.sender,
"Not collection owner"
);
collections[collectionId].tokenIds.push(tokenId);
}
์ปฌ๋ ์ ๊ธฐ๋ฅ์ด ์์ผ๋ฉด ์ฌ์ฉ์๋ค์ด ๊ด๋ จ๋ NFT๋ค์ ํ๋์ ๋ณผ ์ ์์ด์ ์ข์. ์๋ฅผ ๋ค์ด "Bored Ape Yacht Club" ๊ฐ์ ์ ๋ช ์ปฌ๋ ์ ๋ค์ ์๊ฐํด๋ด! ๐ต
๐ฑ ๋ชจ๋ฐ์ผ ์ง์
์์ฆ์ ๋ชจ๋ฐ์ผ์์ NFT๋ฅผ ๊ฑฐ๋ํ๋ ์ฌ๋๋ค์ด ์ ๋ง ๋ง์. ๊ทธ๋์ ๋ชจ๋ฐ์ผ ์ง์์ ํ์์ผ! ๐ฑ
- WalletConnect: ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ๋๋ ํ๋กํ ์ฝ
- MetaMask Mobile: ๋ชจ๋ฐ์ผ ์ฑ ์ง์
- Coinbase Wallet: ์ฌ์ฉํ๊ธฐ ์ฌ์ด ์ธํฐํ์ด์ค
- Trust Wallet: ๋ค์ํ ์ฒด์ธ ์ง์
๐ WalletConnect ํตํฉ
import WalletConnect from "@walletconnect/client";
import QRCodeModal from "@walletconnect/qrcode-modal";
const connector = new WalletConnect({
bridge: "https://bridge.walletconnect.org",
qrcodeModal: QRCodeModal,
});
async function connectMobile() {
if (!connector.connected) {
await connector.createSession();
}
connector.on("connect", (error, payload) => {
if (error) {
throw error;
}
const { accounts, chainId } = payload.params[0];
console.log("Connected:", accounts[0]);
});
}
async function sendTransaction(tx) {
if (!connector.connected) {
await connectMobile();
}
const result = await connector.sendTransaction(tx);
return result;
}
WalletConnect๋ฅผ ์ฌ์ฉํ๋ฉด QR ์ฝ๋๋ฅผ ์ค์บํด์ ๋ชจ๋ฐ์ผ ์ง๊ฐ๊ณผ ์ฐ๊ฒฐํ ์ ์์ด. ์ ๋ง ํธ๋ฆฌํ์ง? ๐
- ํฐ์น ์์ญ์ ์ถฉ๋ถํ ํฌ๊ฒ (์ต์ 44x44px)
- ์คํฌ๋กค ์ฑ๋ฅ ์ต์ ํ
- ์ด๋ฏธ์ง lazy loading
- ๋ชจ๋ฐ์ผ ๋ค๋น๊ฒ์ด์ ๋ฉ๋ด
- ๊ฐ๋ก/์ธ๋ก ๋ชจ๋ ๋ชจ๋ ์ง์
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ๋ชจ๋ฐ์ผ UX๋ ์ ๋ง ์ค์ํด!
๐ ๊ฒ์ ๋ฐ ํํฐ๋ง
NFT๊ฐ ๋ง์์ง๋ฉด ์ํ๋ ๊ฑธ ์ฐพ๊ธฐ ์ด๋ ค์์ ธ. ๊ทธ๋์ ๊ฒ์๊ณผ ํํฐ๋ง ๊ธฐ๋ฅ์ด ํ์ํด! ๐
๐ฏ ์ค๋งํธ ์ปจํธ๋ํธ์์ ํํฐ๋ง
function fetchItemsByPriceRange(
uint256 minPrice,
uint256 maxPrice
) public view returns (MarketItem[] memory) {
uint totalItemCount = _itemIds.current();
uint itemCount = 0;
for (uint i = 0; i < totalItemCount; i++) {
if (
idToMarketItem[i + 1].owner == address(0) &&
idToMarketItem[i + 1].price >= minPrice &&
idToMarketItem[i + 1].price <= maxPrice
) {
itemCount += 1;
}
}
MarketItem[] memory items = new MarketItem[](itemCount);
uint currentIndex = 0;
for (uint i = 0; i < totalItemCount; i++) {
if (
idToMarketItem[i + 1].owner == address(0) &&
idToMarketItem[i + 1].price >= minPrice &&
idToMarketItem[i + 1].price <= maxPrice
) {
items[currentIndex] = idToMarketItem[i + 1];
currentIndex += 1;
}
}
return items;
}
function fetchItemsBySeller(address seller)
public view returns (MarketItem[] memory) {
uint totalItemCount = _itemIds.current();
uint itemCount = 0;
for (uint i = 0; i < totalItemCount; i++) {
if (idToMarketItem[i + 1].seller == seller) {
itemCount += 1;
}
}
MarketItem[] memory items = new MarketItem[](itemCount);
uint currentIndex = 0;
for (uint i = 0; i < totalItemCount; i++) {
if (idToMarketItem[i + 1].seller == seller) {
items[currentIndex] = idToMarketItem[i + 1];
currentIndex += 1;
}
}
return items;
}
๐ฅ๏ธ ํ๋ก ํธ์๋ ๊ฒ์ UI
function SearchAndFilter() {
const [nfts, setNfts] = useState([]);
const [filteredNfts, setFilteredNfts] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [priceRange, setPriceRange] = useState({ min: 0, max: 100 });
const [sortBy, setSortBy] = useState('newest');
useEffect(() => {
filterAndSort();
}, [searchTerm, priceRange, sortBy, nfts]);
function filterAndSort() {
let filtered = nfts.filter(nft => {
const matchesSearch = nft.name.toLowerCase()
.includes(searchTerm.toLowerCase());
const matchesPrice = nft.price >= priceRange.min &&
nft.price <= priceRange.max;
return matchesSearch && matchesPrice;
});
if (sortBy === 'price-low') {
filtered.sort((a, b) => a.price - b.price);
} else if (sortBy === 'price-high') {
filtered.sort((a, b) => b.price - a.price);
} else if (sortBy === 'newest') {
filtered.sort((a, b) => b.tokenId - a.tokenId);
}
setFilteredNfts(filtered);
}
return (
<div className="search-filter">
<input
type="text"
placeholder="Search NFTs..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<div className="price-filter">
<label>Price Range:</label>
<input
type="number"
placeholder="Min"
value={priceRange.min}
onChange={(e) => setPriceRange({
...priceRange,
min: parseFloat(e.target.value)
})}
/>
<input
type="number"
placeholder="Max"
value={priceRange.max}
onChange={(e) => setPriceRange({
...priceRange,
max: parseFloat(e.target.value)
})}
/>
</div>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="newest">Newest First</option>
<option value="price-low">Price: Low to High</option>
<option value="price-high">Price: High to Low</option>
</select>
<div className="nft-grid">
{filteredNfts.map((nft, i) => (
<NFTCard key={i} nft={nft} />
))}
</div>
</div>
);
}
๊ฒ์๊ณผ ํํฐ๋ง์ด ์์ผ๋ฉด ์ฌ์ฉ์ ๊ฒฝํ์ด ํจ์ฌ ์ข์์ ธ. ํนํ NFT๊ฐ ์์ฒ ๊ฐ๊ฐ ๋๋ฉด ํ์ ๊ธฐ๋ฅ์ด์ผ! ๐ฏ
๐ ๋ถ์ ๋ฐ ํต๊ณ
๋ง์ผํ๋ ์ด์ค๋ฅผ ์ด์ํ๋ค ๋ณด๋ฉด ์ฌ๋ฌ ๊ฐ์ง ํต๊ณ๊ฐ ํ์ํด. ๊ฑฐ๋๋, ์ธ๊ธฐ NFT, ํ์ฑ ์ฌ์ฉ์ ๋ฑ์ ์ถ์ ํด์ผ ํ๊ฑฐ๋ ! ๐
๐ ์จ์ฒด์ธ ํต๊ณ
struct Statistics {
uint256 totalVolume;
uint256 totalSales;
uint256 totalListings;
uint256 activeUsers;
}
Statistics public stats;
function updateStats(uint256 salePrice) internal {
stats.totalVolume += salePrice;
stats.totalSales += 1;
}
function getMarketStats() public view returns (
uint256 totalVolume,
uint256 totalSales,
uint256 totalListings,
uint256 averagePrice
) {
totalVolume = stats.totalVolume;
totalSales = stats.totalSales;
totalListings = _itemIds.current() - _itemsSold.current();
if (stats.totalSales > 0) {
averagePrice = stats.totalVolume / stats.totalSales;
} else {
averagePrice = 0;
}
return (totalVolume, totalSales, totalListings, averagePrice);
}
function getTopSellers(uint256 limit)
public view returns (address[] memory, uint256[] memory) {
// ํ๋งค๋ ๊ธฐ์ค ์์ ํ๋งค์ ๋ฐํ
// ์ค์ ๊ตฌํ์ ๋ ๋ณต์กํ์ง๋ง ๊ฐ๋
์ ์ดํดํ์ ๊ฑฐ์ผ!
}
๐ ํ๋ก ํธ์๋ ๋์๋ณด๋
function Dashboard() {
const [stats, setStats] = useState({
totalVolume: 0,
totalSales: 0,
totalListings: 0,
averagePrice: 0
});
useEffect(() => {
loadStats();
}, []);
async function loadStats() {
const { signer } = await connectWallet();
const marketplace = await getMarketplaceContract(signer);
const data = await marketplace.getMarketStats();
setStats({
totalVolume: ethers.utils.formatEther(data.totalVolume),
totalSales: data.totalSales.toNumber(),
totalListings: data.totalListings.toNumber(),
averagePrice: ethers.utils.formatEther(data.averagePrice)
});
}
return (
<div className="dashboard">
<h1>Marketplace Statistics</h1>
<div className="stats-grid">
<div className="stat-card">
<h3>Total Volume</h3>
<p>{stats.totalVolume} ETH</p>
</div>
<div className="stat-card">
<h3>Total Sales</h3>
<p>{stats.totalSales}</p>
</div>
<div className="stat-card">
<h3>Active Listings</h3>
<p>{stats.totalListings}</p>
</div>
<div className="stat-card">
<h3>Average Price</h3>
<p>{stats.averagePrice} ETH</p>
</div>
</div>
</div>
);
}
ํต๊ณ ๋์๋ณด๋๊ฐ ์์ผ๋ฉด ๋ง์ผํ๋ ์ด์ค์ ๊ฑด๊ฐ ์ํ๋ฅผ ํ๋์ ํ์ ํ ์ ์์ด. ํฌ์์๋ค์๊ฒ ๋ณด์ฌ์ฃผ๊ธฐ๋ ์ข๊ณ ! ๐ผ
๐ ์ค์ ํ๊ณผ ๋ฒ ์คํธ ํ๋ํฐ์ค
์ด์ ๊ฑฐ์ ๋ค ์์ด! ๋ง์ง๋ง์ผ๋ก ์ค์ ์์ ์ ์ฉํ ํ๋ค์ ๊ณต์ ํ ๊ฒ. ๐
๋ฉ์ธ๋ท ๋ฐฐํฌ ์ ์ ํ ์คํธ๋ท์์ ์ต์ 2์ฃผ๋ ํ ์คํธํด๋ด. ์ค์ ์ฌ์ฉ์๋ค์ ์ด๋ํด์ ํผ๋๋ฐฑ์ ๋ฐ๋ ๊ฒ๋ ์ข์.
์ฝ๋ ์ฃผ์๊ณผ README๋ฅผ ์ ์์ฑํด. ๋์ค์ ๋ค๋ฅธ ๊ฐ๋ฐ์๊ฐ ์ฐธ์ฌํ๊ฑฐ๋, ๋ฏธ๋์ ๋ด๊ฐ ๋ณผ ๋ ๋์์ด ๋ผ.
Git์ ์ฌ์ฉํด์ ์ฝ๋๋ฅผ ๊ด๋ฆฌํ๊ณ , ์๋ฏธ ์๋ ์ปค๋ฐ ๋ฉ์์ง๋ฅผ ์์ฑํด. ๋์ค์ ๋ฒ๊ทธ๋ฅผ ์ฐพ์ ๋ ์ ๋ง ์ ์ฉํด!
Discord๋ Telegram ์ปค๋ฎค๋ํฐ๋ฅผ ๋ง๋ค์ด. ์ฌ์ฉ์๋ค์ ํผ๋๋ฐฑ์ ๋ฃ๊ณ ๊ฐ์ ํ๋ ๊ฒ ์ฑ๊ณต์ ์ด์ ์ผ.
๐ ๋ฐฐํฌ ํ ์ฒดํฌ๋ฆฌ์คํธ
โ ์ค๋งํธ ์ปจํธ๋ํธ
- ๋ณด์ ๊ฐ์ฌ ์๋ฃ
- ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง 80% ์ด์
- ๊ฐ์ค ์ต์ ํ ์๋ฃ
- ์ด๋ฒคํธ ๋ก๊น
ํ์ธ
โ ํ๋ก ํธ์๋
- ๋ชจ๋ ๊ธฐ๋ฅ ๋์ ํ์ธ
- ๋ชจ๋ฐ์ผ ๋ฐ์ํ ํ
์คํธ
- ์๋ฌ ์ฒ๋ฆฌ ๊ตฌํ
- ๋ก๋ฉ ์ํ ํ์
โ ์ธํ๋ผ
- IPFS ๋ฐฑ์
์ค์
- RPC ์๋ํฌ์ธํธ ์ด์คํ
- ๋ชจ๋ํฐ๋ง ์์คํ
๊ตฌ์ถ
- ๋ฐฑ์
๊ณํ ์๋ฆฝ
โ ๋ฒ๋ฅ ๋ฐ ๊ท์
- ์ด์ฉ์ฝ๊ด ์์ฑ
- ๊ฐ์ธ์ ๋ณด ์ฒ๋ฆฌ๋ฐฉ์นจ
- ์ ์๊ถ ์ ์ฑ
- ์ธ๊ธ ๊ด๋ จ ๊ณ ๋ ค์ฌํญ
- Twitter, Discord์์ ์ปค๋ฎค๋ํฐ ๊ตฌ์ถ
- ์ธํ๋ฃจ์ธ์์ ํ์
- ์ด๊ธฐ ์ฌ์ฉ์์๊ฒ ์ธ์ผํฐ๋ธ ์ ๊ณต
- ํ๋ ์ค ๋ฆด๋ฆฌ์ค ๋ฐฐํฌ
- ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๊ฐ๋ฐ ์ฌ๋ฅ ๊ณต์
๊ธฐ์ ๋งํผ ๋ง์ผํ ๋ ์ค์ํด. ์ข์ ์ ํ์ ๋ง๋ค์๋ค๋ฉด ์ฌ๋๋ค์๊ฒ ์๋ ค์ผ์ง!
๐ ํ์ต ๋ฆฌ์์ค
NFT ๋ง์ผํ๋ ์ด์ค ๊ฐ๋ฐ์ ๊ณ์ ๋ฐฐ์์ผ ํ๋ ๋ถ์ผ์ผ. ์ฌ๊ธฐ ์ ์ฉํ ๋ฆฌ์์ค๋ค์ ์ ๋ฆฌํด๋ดค์ด! ๐
๊ณต์ ๋ฌธ์
- Solidity ๊ณต์ ๋ฌธ์: https://docs.soliditylang.org/
- Hardhat ๋ฌธ์: https://hardhat.org/docs
- OpenZeppelin ๋ฌธ์: https://docs.openzeppelin.com/
- Ethers.js ๋ฌธ์: https://docs.ethers.io/
์จ๋ผ์ธ ๊ฐ์
- CryptoZombies: ๊ฒ์์ผ๋ก ๋ฐฐ์ฐ๋ ์๋ฆฌ๋ํฐ
- Buildspace: ์ค์ ํ๋ก์ ํธ ์ค์ฌ
- Alchemy University: ๋ฌด๋ฃ ๋ธ๋ก์ฒด์ธ ๊ต์ก
์ปค๋ฎค๋ํฐ
- Ethereum Stack Exchange
- Reddit r/ethdev
- Discord ๊ฐ๋ฐ์ ์ปค๋ฎค๋ํฐ๋ค
๐ ๏ธ ์ ์ฉํ ๋๊ตฌ๋ค
๋ธ๋ผ์ฐ์ ์์ ๋ฐ๋ก ์๋ฆฌ๋ํฐ๋ฅผ ์์ฑํ๊ณ ํ ์คํธํ ์ ์๋ ์จ๋ผ์ธ IDE์ผ. ์ด๋ณด์์๊ฒ ์ต๊ณ !
์ค๋งํธ ์ปจํธ๋ํธ ๋ชจ๋ํฐ๋ง๊ณผ ๋๋ฒ๊น ๋๊ตฌ. ํธ๋์ญ์ ์ ์๊ฐ์ ์ผ๋ก ๋ถ์ํ ์ ์์ด.
๋ธ๋ก์ฒด์ธ ์ต์คํ๋ก๋ฌ. ๋ฐฐํฌ๋ ์ปจํธ๋ํธ๋ฅผ ํ์ธํ๊ณ ๊ฒ์ฆํ ์ ์์ด.
๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ๋ฅผ ์ฟผ๋ฆฌํ๋ ์ธ๋ฑ์ฑ ํ๋กํ ์ฝ. ๋ณต์กํ ๋ฐ์ดํฐ ์กฐํ์ ์ ์ฉํด.
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์, ์ ๋ง ๊ธด ์ฌ์ ์ด์์ง? ๐ ์ฐ๋ฆฌ๋ NFT ๋ง์ผํ๋ ์ด์ค๋ฅผ ์ฒ์๋ถํฐ ๋๊น์ง ๋ง๋ค์ด๋ดค์ด. ์ค๋งํธ ์ปจํธ๋ํธ ์์ฑ๋ถํฐ ํ๋ก ํธ์๋ ์ฐ๋, ๋ณด์, ์ต์ ํ๊น์ง ์ ๋ง ๋ง์ ๊ฑธ ๋ฐฐ์ ์ด!
์๋ฆฌ๋ํฐ๋ก NFT ๋ง์ผํ๋ ์ด์ค๋ฅผ ๋ง๋๋ ๊ฑด ์ฝ์ง ์์ ๋์ ์ด์ผ. ํ์ง๋ง ํ ๋ฒ ๋ง๋ค๊ณ ๋๋ฉด ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ์ ํต์ฌ์ ์ดํดํ๊ฒ ๋ผ. ์ด ์ง์์ ๋ค๋ฅธ DApp์ ๋ง๋ค ๋๋ ์ ๋ง ์ ์ฉํ ๊ฑฐ์ผ. ๐ช
์ด์ ๊ธฐ๋ณธ์ ๋ง์คํฐํ์ผ๋ ๋ค์ ๋จ๊ณ๋ก ๋์๊ฐ๋ณด์:
- ๋ค๋ฅธ ๋ธ๋ก์ฒด์ธ(Polygon, Solana ๋ฑ)์ ๋ฐฐํฌํด๋ณด๊ธฐ
- NFT ๋์ฌ ๊ธฐ๋ฅ ์ถ๊ฐ
- DAO ๊ฑฐ๋ฒ๋์ค ํตํฉ
- ํฌ๋ก์ค์ฒด์ธ ๋ธ๋ฆฟ์ง ๊ตฌํ
- AI ๊ธฐ๋ฐ NFT ์ถ์ฒ ์์คํ
๊ฐ๋ฅ์ฑ์ ๋ฌด๊ถ๋ฌด์งํด! ๐
๊ธฐ์ตํด, ์๋ฒฝํ ์ฝ๋๋ ์์ด. ๊ณ์ ๊ฐ์ ํ๊ณ ๋ฐฐ์ฐ๋ฉด์ ๋ฐ์ ํ๋ ๊ฑฐ์ผ. ์ปค๋ฎค๋ํฐ์ ์ฐธ์ฌํ๊ณ , ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๊ณผ ์ํตํ๊ณ , ์์ ์ ํ๋ก์ ํธ๋ฅผ ๊ณต์ ํด๋ด. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์์ ์ ๊ฐ๋ฐ ๊ฒฝํ์ ๋๋๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ!
๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ์ ์ ๋ง ํฅ๋ฏธ์ง์งํ ๋ถ์ผ์ผ. ๊ธฐ์ ์ด ๋น ๋ฅด๊ฒ ๋ฐ์ ํ๊ณ ์๊ณ , ์๋ก์ด ๊ธฐํ๋ค์ด ๊ณ์ ์๊ฒจ๋๊ณ ์์ด. ์ง๊ธ ์์ํ๋ ๊ฒ ์๋ฒฝํ ํ์ด๋ฐ์ด์ผ! โฐ
- ์๊ฒ ์์ํด์ ์ ์ง์ ์ผ๋ก ํ์ฅํด
- ์ฌ์ฉ์ ํผ๋๋ฐฑ์ ์ ๊ทน์ ์ผ๋ก ์์ฉํด
- ๋ณด์์ ์ ๋ ํํํ์ง ๋ง
- ์ปค๋ฎค๋ํฐ์ ํจ๊ป ์ฑ์ฅํด
- ์คํจ๋ฅผ ๋๋ ค์ํ์ง ๋ง. ์คํจ์์ ๋ฐฐ์ฐ๋ ๊ฒ ๊ฐ์ฅ ๋ง์!
ํ์ดํ ! ๋ ํ ์ ์์ด! ๐ชโจ
์ด ๊ธ์ด ๋์์ด ๋๋ค๋ฉด ๋ค๋ฅธ ์ฌ๋๋ค๊ณผ๋ ๊ณต์ ํด์ค. ํจ๊ป ๋ฐฐ์ฐ๊ณ ์ฑ์ฅํ๋ ๊ฒ Web3์ ์ ์ ์ด๋๊น! ๊ถ๊ธํ ์ ์ด ์์ผ๋ฉด ์ธ์ ๋ ์ปค๋ฎค๋ํฐ์ ๋ฌผ์ด๋ด. ์ฐ๋ฆฌ ๋ชจ๋ ์ฒ์์ ์ด๋ณด์์ด. ๐
๊ทธ๋ผ ํ์ด์ ๋น์ด! ๋ฉ์ง NFT ๋ง์ผํ๋ ์ด์ค๋ฅผ ๋ง๋ค์ด๋ด. ์ธ์ ๊ฐ ๋ค๊ฐ ๋ง๋ ํ๋ซํผ์์ ๊ฑฐ๋ํ๋ ๋ ์ ๊ธฐ๋ํ ๊ฒ! ๐จ๐
Happy Coding! ๐
Build the future of digital ownership
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.