반응형

🕸️ Web3에서 프론트엔드의 역할은 무엇일까?

Web3가 점차 주목받으면서 프론트엔드 개발자에게도 새로운 흐름이 요구되고 있습니다. 구조는 Web2와 크게 다르지 않지만, 기술적 환경이 변화하면서 프론트엔드가 맡는 역할도 달라지고 있죠. 이번 글에서는 Web2와 Web3의 차이점부터, 프론트엔드의 핵심 역할과 실제 코드 예시까지 정리해보겠습니다.


1. Web2 vs Web3

Web2와 Web3는 구조적으로 유사해 보일 수 있지만, 사용하는 기술 환경이 완전히 다르기 때문에 처음에는 진입 장벽을 느끼기 쉽습니다. 이를 아래 표로 정리하면 다음과 같습니다.

📊 Web2와 Web3 비교

구분 Web2 프론트엔드 Web3 프론트엔드
백엔드 REST API 서버 스마트 컨트랙트
인증 방식 JWT, 세션 등 지갑 로그인 (Metamask 등)
데이터 저장 DB (MySQL 등) 블록체인, IPFS 등
주요 연동 API, SDK Web3 라이브러리 (ethers.js, web3.js 등)

 

위 표를 보면 Web3에서도 프론트엔드-백엔드 구조는 여전하지만, 각 구성 요소의 성격이 완전히 다름을 알 수 있습니다. 특히 다양한 블록체인 네트워크(Ethereum, Solana, Polygon 등)의 등장으로 인해 각 생태계에 맞춘 개발이 중요해졌습니다.


2. Web3 프론트엔드 구성 요소 자세히 보기

✅ 스마트 컨트랙트

Web3에서 스마트 컨트랙트는 Web2의 서버(Service layer) 역할을 합니다. 비즈니스 로직을 담고 있으며, 블록체인 위에서 실행됩니다. Solidity 언어를 사용하며, 클래스 구조와 유사합니다.

📌 간단한 스마트 컨트랙트 예시 (Solidity)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Counter {
    uint256 public count;

    function increment() public {
        count += 1;
    }

    function getCount() public view returns (uint256) {
        return count;
    }
}
  • count: 상태 변수로 저장됨 (블록체인 상 저장됨)
  • increment(): 트랜잭션을 발생시켜 값을 1 증가시킴
  • getCount(): 현재 값을 조회하는 view 함수 (가스비 X)

✅ 지갑 로그인

Web2에서는 이메일·소셜 로그인 등으로 사용자를 인증했다면, Web3에서는 지갑을 통해 서명 기반 로그인 방식이 일반적입니다.

  • 대표 지갑: Metamask, WalletConnect, Kaikas 등
  • 최근에는 Google/Apple 계정 기반으로 지갑을 만들 수 있는 소셜 로그인 지갑도 많이 생겼습니다.

✅ 블록체인

Web2에서의 DB 역할을 합니다. 스마트 컨트랙트에서 실행된 데이터는 블록체인에 영구 저장되며, 누구나 열람 가능하고 위조가 불가능합니다.


✅ Web3 라이브러리

Web2에서 REST API를 호출했다면, Web3에서는 ethers.jsweb3.js를 통해 스마트 컨트랙트와 직접 상호작용합니다.

📌 ethers.js 예시: 프론트에서 스마트 컨트랙트와 통신

import { ethers } from "ethers";

// 1. 컨트랙트 주소와 ABI
const contractAddress = "0x1234567890abcdef1234567890abcdef12345678";
const contractABI = [
  "function getCount() view returns (uint256)",
  "function increment()"
];

// 2. provider와 signer 준비 (지갑 연결 필요)
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();

// 3. 컨트랙트 인스턴스 생성
const counterContract = new ethers.Contract(contractAddress, contractABI, signer);

// 4. 함수 호출
async function getCount() {
  const count = await counterContract.getCount();
  console.log("현재 count:", count.toString());
}

async function incrementCount() {
  const tx = await counterContract.increment(); // 트랜잭션 발생
  await tx.wait(); // 블록에 기록될 때까지 대기
  console.log("카운트 증가 완료");
}

🧠 getCount()는 읽기 함수로 가스비가 들지 않지만, increment()는 쓰기 함수로 서명과 트랜잭션 처리가 필요합니다.

3. Ethereum, Solana

구분 Ethereum Solana
스마트 컨트랙트 언어 Solidity Rust (Anchor 프레임워크 사용)
주요 라이브러리 ethers.js, web3.js @solana/web3.js, @project-serum/anchor
트랜잭션 처리 느리지만 탈중앙성 강함 (PoS/PoW → 현재 PoS) 빠른 처리 속도 (PoH + PoS)
지갑 MetaMask, WalletConnect 등 EVM 기반 Phantom, Solflare 등 Solana 전용 지갑
ABI 사용 여부 필수 (프론트에서 ABI로 함수 접근) X (Anchor IDL 또는 직접 구조화 필요)
컨트랙트 주소 접근 단일 주소 사용 프로그램 ID, 계정 주소 구분 필요
읽기/쓰기 구분 명확 (view/pure vs transaction) 계정 상태 조회와 서명 구조가 복잡함

✅ 예시 비교: 이더리움 vs 솔라나에서 getCount 호출

🔹 이더리움 (ethers.js)

const contract = new ethers.Contract(contractAddress, abi, signer);
const count = await contract.getCount(); // 간단한 함수 호출

🔹 솔라나 (web3.js + Anchor IDL)

const provider = new Anchor.AnchorProvider(connection, wallet, {});
const program = new Anchor.Program(idl, programId, provider);
const account = await program.account.counter.fetch(counterAccountAddress);
console.log("현재 count:", account.count.toNumber());

📌 Solana는 ‘프로그램’과 ‘계정’을 분리하여 접근하며, 태는 ‘계정’에 저장된다.

4. Web3 프론트엔드 프로젝트 폴더 구조

처음 웹 3 프로젝트를 빌딩하다보면 프로젝트 구조를 어떻게 세워야할지 난감한 때가 오게된다. 다음과 같이 폴더를 정리하면 좀 더 쉽게 정리가 가능하다.

📦 your-dapp/
├── 📁 public/
│   └── favicon.ico
├── 📁 src/
│   ├── 📁 abi/               # 스마트 컨트랙트 ABI JSON 파일 보관
│   ├── 📁 assets/            # 이미지, 폰트 등 정적 리소스
│   ├── 📁 components/        # 공통 UI 컴포넌트 (Button, Navbar 등)
│   ├── 📁 contracts/         # 스마트 컨트랙트 주소 등 관련 정보
│   ├── 📁 hooks/             # 커스텀 훅 (useWallet, useContract 등)
│   ├── 📁 pages/             # 각 페이지 단위 컴포넌트
│   ├── 📁 utils/             # 블록체인 유틸 함수 (주소 포맷팅, 트랜잭션 처리 등)
│   ├── 📁 contexts/          # 상태 공유용 Context (예: WalletContext)
│   ├── 📁 config/            # 네트워크 설정, 환경변수 관리
│   ├── 📁 styles/            # 전역 스타일, Tailwind, SCSS 등
│   ├── App.jsx              # 앱 라우팅 설정
│   ├── index.jsx            # 진입점 (ReactDOM 렌더링)
├── .env                     # 환경 변수 (RPC URL, Contract 주소 등)
├── package.json
└── README.md

🔍 각 폴더 설명

폴더/파일 설명
abi/ 컴파일된 스마트 컨트랙트 ABI를 JSON으로 저장해서 불러오기 용이
contracts/ 배포된 컨트랙트 주소, 네트워크 ID 등 정리
hooks/ useWallet(), useContract(), useBalance() 등의 커스텀 훅으로 Web3 로직 모듈화
contexts/ 지갑 상태, 로그인 여부, 현재 네트워크 등을 Context API로 전역 공유
utils/ 트랜잭션 헬퍼 함수, 토큰 포맷팅 등 유틸 함수
config/ 지원 네트워크, RPC URL, 체인 ID 등 환경설정 모음
.env 프로젝트 환경에 따라 컨트랙트 주소, RPC URL을 바꿀 수 있도록 외부에서 주입

다음에는 프론트엔드와 지갑 및 컨트랙트 연결 구조를 집중적으로 알아보겠습니다.

반응형

'BlockChain > Web3' 카테고리의 다른 글

[2] Web3 프론트엔드 연결 요약 (Wagmi + Reown)  (0) 2025.04.12
반응형

Reinventing the Layer 1 Blockchain

Feature of Monad

  1. 이더리움 보다 1000배 빠르다.
  2. 1센트 미만의 가스비
  3. 100% EVM 호환
  4. 적은 하드웨어 요구사항

블록체인의 트랜잭션들은 대부분 비효율적이다.

  • 모나드팀이 생각한 문제점은 위와 같다.

대부분의 블록체인은 블록을 생성하는데 많은 요구사항을 필요로 한다. 이러한 요구사항은 블록의 생성을 늦추고 비싸게 만들 뿐이다.

또한, 블록체인 업계가 가상 머신을 최적화하는데 집중하고 있지 않기에 전체 처리 성능이 개선되지 않았다.

탈중앙화는 많은 돈이 든다.

네트워크에 참여하려면 노드가 고성능 RAM 같은 값비싼 장비를 요구하기 때문에, 참여 시작 비용만 해도 수천 달러에 달한다. 이런 점에서 탈중앙화를 확장하는 것은 매우 어렵다.

스택 전반에 걸친 EVM 최적화가 필요

EVM 트랜잭션을 효율적으로 처리하기 위해서는, 데이터베이스, 가상 머신, 합의 및 네트워킹 계층 등 모든 단계에서 저수준 최적화가 필요하다.

이러한 최적화를 통해 고가의 하드웨어 없이도 소비자 수준의 장비로 뛰어난 성능을 구현하면서, 동시에 높은 탈중앙화를 유지할 수 있게 된다.

Monad(모나드) 의 해결 방안

1. Optimistic Parallel Execution (병렬 처리 최적화)

다수의 가상 머신을 이용해 트랜잭션을 병렬로 처리하고, 트랜잭션 간 의존성을 예측해 최적의 실행 순서를 계획한다. 이후 결과를 순차적으로 병합하여 실행 순서의 정확성을 유지하면서 자원 활용률을 극대화한다.

2. Asynchronous Execution (비동기 실행)

블록의 실행은 해당 블록에 대한 합의가 끝난 뒤 시작되며, 이 과정은 다음 블록의 합의 과정과 동시에(병렬로) 진행된다.

문제상황

합의와 실행이 섞인 방식

  • 합의와 실행은 서로 기다려야 하는 점에 있어 거래 실행이 느려진다.
    • 이더리움 블록 생성 시간은 12초지만, 실제 거래 실행에 쓰이는 시간은 0.1초

해결방법

비동기 실행

  • 합의 : 노드들이 트랜잭션의 순서만 결정
  • 실행 : 결정된 순서에 따라 실제 거래를 나중에 실행
  • 서로를 기다리지 않아도 된다.
  • 실행 속도가 매우 높아진다.

오류 가능성

트랜잭션 실패

  • 거래가 실패해도 결정된 거래 순서대로 처리 되므로, 모든 노드가 동일하게 실패를 처리

3. MonadBFT

HotStuff를 기반으로 설계된 커스텀 BFT 합의 메커니즘은, 기존의 3단계 통신을 2단계로 줄여 효율성을 높였다.

또한 낙관적 응답 설계를 채택하여 네트워크 지연 시간이 짧을 경우 라운드 진행 속도가 이에 맞춰 빨라져, 전반적인 성능 향상에 기여한다.

 

다른 BFT와의 차별점

  1. 정상 작동 시 : 메시지 통신 복잡도가 선형으로 낮다. (O(N), N : 참여하는 노드의 개수)
  2. 타임 아웃 발생 시 : 메시지 통신 복잡도가 Quadratic 으로 증가. (O(N2))
  3. 블록 제안과 응답 인증서를 파이프라인 형태로 처리 ⇒ 블록을 빠르게 제안
  4. 블록의 최종 확정에 걸리는 시간 1초

작동원리

  • 합의 과정은 매 라운드마다 리더가 바뀌며 반복적으로 진행
  1. 블록 제안
    • 리더가 새로운 블록을 만들어 모든 노드에게 보냄.
    • 이전 라운드의 결과가 담긴 인증서(QC or TC) 도 보냄.
  2. 투표
    • 노드들은 블록이 규칙에 맞으면 YES 투표
    • 이 투표는 다음 라운드의 리더에게 전달
  3. QC 만들기
    • 투표가 충분히 모이면 (2/3) 다음 라운드의 리더는 QC를 만듦.
  4. TC 만들기
    • 타임아웃 발생 시 (2/3) 의 투표를 통해 리더는 TC를 만듦
  5. 블록 확정하기
    • 두 라운드에 갈쳐 QC 가 제대로 전달된다면 블록확정

 

4. MonadDB

머클 패트리샤 트리(Merkle Patricia Trie) 데이터의 저장 및 접근 방식을 최적화하여 블록체인 성능을 크게 향상시키는 맞춤형 상태 데이터베이스. 이를 통해 고성능의 병렬 접근이 가능해지며, Monad의 병렬 실행 기능을 실현하는 데 필수적인 역할.

MonadDB를 사용하면 대부분의 상태 데이터를 RAM 대신 SSD에 저장할 수 있으므로, 메모리 요구량과 비용이 감소하고 일반 소비자급 하드웨어에서도 네트워크 참여가 가능하다.

Time ───────────────────────────────────▶

Block N         Block N+1         Block N+2
═════════       ══════════        ══════════
합의 진행 중     합의 진행 중        합의 진행 중
     │                │                 │
     ▼                ▼                 ▼
[ Block N 실행 시작 ] [ Block N+1 실행 시작 ] ...
반응형
반응형

Layout.tsx

import React, {FC} from "react";
import {Stack, Flex, Box, Text, Button} from '@chakra-ui/react';
import { Link } from "react-router-dom";

const Layout: FC = ({children}) =>{
    return( 
    
    <Stack h="100vh">
        <Flex bg="purple.200" p={4} justifyContent="space-around" alignItems="center">
            <Box>
                <Text fontWeight="bold">h662-Animals</Text>
            </Box>
            <Link to="/">
                <Button size="sm" colorScheme="blue">
                    Main
                </Button>
            </Link>
            <Link to="my-animal">
                <Button size="sm" colorScheme="red">
                    My Animal
                </Button>
            </Link>
            <Link to="sale-animal">
                <Button size="sm" colorScheme="green">
                    Sale Animal
                </Button>
            </Link>
        </Flex>
        <Flex direction="column" h="full" justifyContent="center" alignItems="center">
            {children}
        </Flex>
    </Stack>
    );
}

export default Layout

 

MyAnimalCard.tsx

 

import React , {FC, useState, ChangeEvent} from "react";
import {Box, Text, InputGroup, Input, InputRightAddon, Button} from "@chakra-ui/react"
import AnimalCard from "./AnimalCard";
import { saleAnimalTokenContract, web3 } from "../contracts";

export interface IMyanimalCard{
    animalTokenId : string;
    animalType : string;
    animalPrice : string;
}

interface MyAnimalCardProps extends IMyanimalCard {
    saleStatus:boolean;
    account:string;
}

const MyAnimalCard: FC<MyAnimalCardProps> = ({animalTokenId, animalType, animalPrice, saleStatus, account}) =>{
    const [sellPrice, setSellPrice] = useState<string>("");
    const [myAnimalPrice, setMyAnimalPrice] = useState<string>(animalPrice)

    const onChangeSellPrice = (e: ChangeEvent<HTMLInputElement>) =>{
        setSellPrice(e.target.value);
    }

    const onClickSell = async () =>{
        try{
            if(!account || !saleStatus) return;

            const response = await saleAnimalTokenContract.methods
            .setForSaleAnimalToken(animalTokenId, web3.utils.toWei(sellPrice, "ether"))
            .send({from:account});

            if(response.status) {
                setMyAnimalPrice(web3.utils.toWei(sellPrice, "ether"));
                console.log(response)
            }
        }catch(err){
            console.log(err);
        }
    }

    return(
        <Box textAlign="center" w={150}>
            <AnimalCard animalType={animalType} /><Box  mt={2}>
                {animalPrice === "0" ? (
                <>
                    <InputGroup>
                        <Input type='number' value={sellPrice} onChange={onChangeSellPrice}/>
                        <InputRightAddon children="Matic" />
                    </InputGroup>
                    <Button size="sm" colorScheme="green" mt={2} onClick={onClickSell}>
                        Sell
                    </Button>
                </>) : <Text d="inline-block">{web3.utils.fromWei(myAnimalPrice)} Matic</Text> }
            </Box>
        </Box>
    )
}

export default MyAnimalCard;

함수 설명

  1. onChangeSellPrice = (e: ChangeEvent<HTMLInputElement>)
    • Input 박스 내의 요소가 바뀔때마다 price 를 바꿔준다.
  2. onClickSell
    • 토큰을 판매 등록하는 함수

SaleAnimalCard.tsx

import React, {FC, useState, useEffect} from "react";
import AnimalCard from "./AnimalCard";
import {Box, Text, Button} from "@chakra-ui/react"
import { mintAnimalTokenContract, saleAnimalTokenContract, web3 } from "../contracts";

interface SaleAnimalCardProps{
    animalType:string;
    animalPrice:string;
    animalTokenId: string;
    account:string;
    getOnSaleAnimalTokens: () => Promise<void>;
}

const SaleAnimalCard:FC<SaleAnimalCardProps> =({animalType,animalPrice,animalTokenId, account,getOnSaleAnimalTokens}) =>{
    const [isBuyable, setIsBuyable] = useState<boolean>(false);

    const getAnimalTokenOwner = async () => {
        try{
            const response = await mintAnimalTokenContract.methods.ownerOf(animalTokenId).call();
            
            console.log(response)
            console.log(account);
            setIsBuyable(response.toLocaleLowerCase() === account.toLocaleLowerCase())
        } catch(err){
            console.log(err);
        }   
    } 

    const onClickBuy = async () =>{
        try{
            if(!account) return;
            const response = await saleAnimalTokenContract.methods.purchaseAnimalToken(animalTokenId).send({from: account, value:animalPrice});

            if(response.status){
                getOnSaleAnimalTokens();
            }
        } catch(err){
            console.log(err)
        }
    }

    useEffect(() => {
      
        getAnimalTokenOwner();
    }, [])
    

    return(
        <Box textAlign ="center" w={100}>
            <AnimalCard animalType={animalType} />
            <Box>
                <Text d="inline-block">
                    {web3.utils.fromWei(animalPrice)} Matic
                </Text>
                <Button size="sm" colorScheme="green" m={2} disabled={isBuyable} onClick={onClickBuy}>Buy</Button>
            </Box>
        </Box>
    )
}

export default SaleAnimalCard
반응형
반응형
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "MintAnimalToken.sol";

contract SaleAnimalToken {
    MintAnimalToken public mintAnimalTokenAddress;
    //디플로이 주소값을 담는다.

    constructor (address _mintAnimalTokenAddress) {
        mintAnimalTokenAddress = MintAnimalToken(_mintAnimalTokenAddress);
    }

    mapping(uint256 => uint256) public animalTokenPrices;
    //가격관리 맵핑

    uint256[] public onSaleAnimalTokenArray;
    //FE 에서 사용할 배열. 판매중이 토큰 확인용

    function setForSaleAnimalToken(uint256 _animalTokenId, uint256 _price) public {
        //판매 등록 함수

        address animalTokenOwner = mintAnimalTokenAddress.ownerOf(_animalTokenId);

        require(animalTokenOwner == msg.sender, "Caller is not animal token owner");
        //함수 실행 사람이 토큰의 주인이 맞는지
        require(_price > 0 , "Price is zero or lower");

        require(animalTokenPrices[_animalTokenId] == 0, "This animal token is already on sale.");

        require(mintAnimalTokenAddress.isApprovedForAll(animalTokenOwner, address(this)), "Animal token owner did not approve token.");
        //스마트컨트랙트 판매권한 여부를 확인
        
        animalTokenPrices[_animalTokenId] = _price;

        onSaleAnimalTokenArray.push(_animalTokenId);
    }
}

 

알고리즘

  • MintAnimalToken 장부를 먼저 Deploy 해야한다.
  • Deploy 된 장부의 주소값을 인자로 등록한다.
  • setForSaleAnimalToken 함수
    • 등록할 토큰의 ID 를 가진 주인의 주소를 불러온다.
    • 함수를 실행하는 사람이 msg.sender 가 주인이 맞는지 확인한다.
    • 가격이 0 초과인지 확인한다.
    • 판매등록이 되지 않은 상품인지 확인한다.
    • 해당 장부가 판매등록이 허가가 된 장부인지 확인한다.
    • 배열에 판매등록을 한다.

 

function purchaseAnimalToken(uint256 _animalTokenId) public payable {
        //payble 을 붙여야 matic 이 왔다갔다하는 함수를 실행할 수 있다.

        uint256 price = animalTokenPrices[_animalTokenId];
        address animalTokenOwner = mintAnimalTokenAddress.ownerOf(_animalTokenId);

        require(price > 0, "Animal Token not sale.");
        require(price <= msg.value, "Caller sent lower than price.");
        //함수를 실행할때 보내는 MATIC 의 양이 같거나 큰지 확인
        require(animalTokenOwner != msg.sender, "Caller is animal Token owner");

        payable(animalTokenOwner).transfer(msg.value);
        //가격만큼의 양이 돈의 주인으로 보내진다.

        mintAnimalTokenAddress.safeTransferFrom(animalTokenOwner, msg.sender, _animalTokenId);

        animalTokenPrices[_animalTokenId] = 0;

        for(uint256 i = 0; i<onSaleAnimalTokenArray.length;i++){
            if(animalTokenPrices[onSaleAnimalTokenArray[i]] == 0){
                onSaleAnimalTokenArray[i] = onSaleAnimalTokenArray[onSaleAnimalTokenArray.length -1];
                onSaleAnimalTokenArray.pop();
                //맨뒤랑 바꿔서 맨뒤 삭제
            }
        }
        
        
    }
    //읽기 전용 FE 함수
    //판매중인 리스트 확인용
    function getOnSaleAnimalTokenArrayLength() view public returns (uint256){
            return onSaleAnimalTokenArray.length;
    }

 

알고리즘

  • payable 을 사용해야 구입 판매 함수를 사용할 수 있다.
  • 구매할 토큰의 ID 와 토큰 주인의 주소를 불러온다.
  • 토큰의 판매여부를 확인
  • 구매자가 보낸 금액이 토큰 가격보다 크거나 같은지 확인
  • 구매자가 판매자와 동일한지 확인
  • payable 함수를 이용해 msg.value 에 저장된 금액을 판매자로 송금
  • 판매된 토큰을 배열에서 제거

후기

  • Solidity 언어를 많이 다룰줄 알았는데 아닌것같다. 그래도 대충 하는 법을 익힌듯 해서 얼른 만들어버리고 다른 강의 들어야겠다.

 

전체 알고리즘

MintAnimal Token Deploy
mint 주소를 이용해 Sale 장부 실행
Animal 민팅
Sale 장부 허가
판매 등록후 확인
다른 계정으로 구매후 구매확인

반응형

'BlockChain > Solidity' 카테고리의 다른 글

[zkSync] 간단하게 다뤄보기  (0) 2025.01.11
[DP/NFT] 4. 리액트 세팅  (0) 2022.09.05
[DP/NFT] 2. Minting contract 작성  (0) 2022.09.05
[DP/NFT] 1. Install Solidity & Metamask  (0) 2022.08.25

+ Recent posts