์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸฆŠ ๋ผ๋ผ๋ฒจ์—์„œ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ๋กœ๊ทธ์ธ UX ๊ฐœ์„ ํ•˜๊ธฐ

๐ŸฆŠ ๋ผ๋ผ๋ฒจ์—์„œ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ๋กœ๊ทธ์ธ UX ๊ฐœ์„ ํ•˜๊ธฐ

Web3 ์‹œ๋Œ€, ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ•œ ๋‹จ๊ณ„ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜๋Š” ์‹ค์ „ ๊ฐ€์ด๋“œ
๋ฉ”ํƒ€๋งˆ์Šคํฌ ๋กœ๊ทธ์ธ ๊ธฐ์กด ๋ฐฉ์‹ โ†’ ๊ฐœ์„ ๋œ UX ๋ณต์žกํ•œ ์—ฐ๊ฒฐ ๊ณผ์ • ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฏธํก ๋А๋ฆฐ ์‘๋‹ต ์†๋„ ์›ํด๋ฆญ ์—ฐ๊ฒฐ ์นœ์ ˆํ•œ ๊ฐ€์ด๋“œ ์ฆ‰๊ฐ์ ์ธ ํ”ผ๋“œ๋ฐฑ
## ๐ŸŽฏ ์™œ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ๋กœ๊ทธ์ธ UX๊ฐ€ ์ค‘์š”ํ• ๊นŒ? ์•ˆ๋…•! ์˜ค๋Š˜์€ ๋ผ๋ผ๋ฒจ์—์„œ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ๋กœ๊ทธ์ธ UX๋ฅผ ๊ฐœ์„ ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ๊ฒŒ. ์š”์ฆ˜ Web3 ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋‹ค ๋ณด๋ฉด ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์—ฐ๋™์€ ๊ฑฐ์˜ ํ•„์ˆ˜๊ฐ€ ๋˜์—ˆ์ง€? ๐ŸฆŠ

๊ทธ๋Ÿฐ๋ฐ ๋ง์ด์•ผ, ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ๊ธฐ๋Šฅ ๊ตฌํ˜„์—๋งŒ ์ง‘์ค‘ํ•˜๋‹ค๊ฐ€ ์ •์ž‘ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ span>์€ ๋†“์น˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„. "์–ด? ์™œ ์•ˆ ๋˜์ง€?" ํ•˜๋ฉด์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์ดํƒˆํ•˜๋Š” ์ˆœ๊ฐ„, ๊ทธ๊ฒŒ ๋ฐ”๋กœ UX ๊ฐœ์„ ์ด ํ•„์š”ํ•œ ์‹œ์ ์ด์•ผ.

์‹ค์ œ๋กœ Web3 ์„œ๋น„์Šค์—์„œ ์‚ฌ์šฉ์ž ์ดํƒˆ๋ฅ ์„ ๋ถ„์„ํ•ด๋ณด๋ฉด, ์ฒซ ๋กœ๊ทธ์ธ ๋‹จ๊ณ„์—์„œ๋งŒ ์•ฝ 40% ์ด์ƒ์ด ์ดํƒˆํ•œ๋‹ค๋Š” ํ†ต๊ณ„๊ฐ€ ์žˆ์–ด. ์ด๊ฒŒ ์–ผ๋งˆ๋‚˜ ์•„๊นŒ์šด ์ผ์ธ์ง€ ์•Œ์ง€? ๐Ÿ˜ฑ
๐Ÿ’ก ํ•ต์‹ฌ ํฌ์ธํŠธ

๋ฉ”ํƒ€๋งˆ์Šคํฌ ๋กœ๊ทธ์ธ UX ๊ฐœ์„ ์€ ๋‹จ์ˆœํžˆ '์˜ˆ์˜๊ฒŒ ๋งŒ๋“ค๊ธฐ'๊ฐ€ ์•„๋‹ˆ์•ผ. ์‚ฌ์šฉ์ž๊ฐ€ ๋ง‰ํž˜์—†์ด ์„œ๋น„์Šค์— ์ง„์ž…ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ์ „๋žต์ด์ง€!
## ๐Ÿ” ๋ฉ”ํƒ€๋งˆ์Šคํฌ ๋กœ๊ทธ์ธ์˜ ์ผ๋ฐ˜์ ์ธ ๋ฌธ์ œ์ ๋“ค ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ ์ž์ฃผ ๋งˆ์ฃผ์น˜๋Š” ๋ฌธ์ œ๋“ค์„ ์ •๋ฆฌํ•ด๋ดค์–ด. ํ˜น์‹œ ๋„ˆ๋„ ์ด๋Ÿฐ ๊ฒฝํ—˜ ์žˆ์ง€ ์•Š์•„?
1๋ฉ”ํƒ€๋งˆ์Šคํฌ๊ฐ€ ์„ค์น˜๋˜์ง€ ์•Š์•˜์„ ๋•Œ

์‚ฌ์šฉ์ž๊ฐ€ ๋ฉ”ํƒ€๋งˆ์Šคํฌ๋ฅผ ์„ค์น˜ํ•˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ "Connect Wallet" ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €๋Š”๋ฐ, ์•„๋ฌด ๋ฐ˜์‘๋„ ์—†๊ฑฐ๋‚˜ ์—๋Ÿฌ๋งŒ ๋œจ๋Š” ๊ฒฝ์šฐ. ์ด๊ฑด ์ •๋ง ์ตœ์•…์˜ UX์•ผ. ์‚ฌ์šฉ์ž๋Š” "์ด ์‚ฌ์ดํŠธ ๋ง๊ฐ€์ง„ ๊ฑฐ ์•„๋ƒ?"๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ณ  ๋ฐ”๋กœ ๋‚˜๊ฐ€๋ฒ„๋ฆฌ๊ฑฐ๋“ .
2๋„คํŠธ์›Œํฌ ๋ถˆ์ผ์น˜ ๋ฌธ์ œ

์‚ฌ์šฉ์ž๊ฐ€ ์ด๋”๋ฆฌ์›€ ๋ฉ”์ธ๋„ท์— ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋Š”๋ฐ, ์„œ๋น„์Šค๋Š” ํด๋ฆฌ๊ณค ๋„คํŠธ์›Œํฌ๋ฅผ ์š”๊ตฌํ•˜๋Š” ๊ฒฝ์šฐ. ์ด๋•Œ "Wrong Network"๋ผ๋Š” ๋ฉ”์‹œ์ง€๋งŒ ๋˜์ ธ์ฃผ๋ฉด ์‚ฌ์šฉ์ž๋Š” ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ์ง€ ๋ชฐ๋ผ์„œ ๋‹นํ™ฉํ•˜๊ฒŒ ๋ผ.
3๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ ๋ถ€์žฌ

๋ธ”๋ก์ฒด์ธ ํŠธ๋žœ์žญ์…˜์€ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ์ž–์•„? ๊ทธ๋Ÿฐ๋ฐ ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ์‚ฌ์šฉ์ž๋Š” "์ด๊ฑฐ ๋จนํ†ต๋œ ๊ฑฐ ์•„๋ƒ?"๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ณ  ์ƒˆ๋กœ๊ณ ์นจ์„ ๋ˆ„๋ฅด๊ฒŒ ๋ผ. ๊ทธ๋Ÿฌ๋ฉด ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ์‹œ์ž‘ํ•ด์•ผ ํ•˜๋Š” ์•…์ˆœํ™˜์ด ๋ฐ˜๋ณต๋˜์ง€.
4์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ๋ถˆ์นœ์ ˆํ•จ

"Error: User rejected the request" ๊ฐ™์€ ๊ธฐ์ˆ ์ ์ธ ๋ฉ”์‹œ์ง€๋งŒ ๋ณด์—ฌ์ฃผ๋ฉด, ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž๋Š” ์ดํ•ดํ•  ์ˆ˜ ์—†์–ด. "๋ฉ”ํƒ€๋งˆ์Šคํฌ์—์„œ ์—ฐ๊ฒฐ์„ ๊ฑฐ๋ถ€ํ•˜์…จ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”!" ๊ฐ™์€ ์นœ์ ˆํ•œ ์•ˆ๋‚ด๊ฐ€ ํ•„์š”ํ•ด.
5๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ ๊ณ ๋ ค ๋ถ€์กฑ

PC์—์„œ๋Š” ์ž˜ ๋˜๋Š”๋ฐ ๋ชจ๋ฐ”์ผ์—์„œ๋Š” ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์•ฑ์ด ์ œ๋Œ€๋กœ ์—ด๋ฆฌ์ง€ ์•Š๋Š” ๊ฒฝ์šฐ. ํŠนํžˆ ๋ชจ๋ฐ”์ผ ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋”ฅ๋งํฌ ์ฒ˜๋ฆฌ๊ฐ€ ์•ˆ ๋˜๋ฉด ์‚ฌ์šฉ์ž๋Š” ์—„์ฒญ ๋ถˆํŽธํ•ดํ•ด.
## ๐Ÿ› ๏ธ ๋ผ๋ผ๋ฒจ ํ™˜๊ฒฝ ์„ค์ •๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๊ธฐ ์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ๋ผ๋ผ๋ฒจ์—์„œ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ๋กœ๊ทธ์ธ UX๋ฅผ ๊ฐœ์„ ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ. ๋จผ์ € ๊ธฐ๋ณธ์ ์ธ ํ™˜๊ฒฝ ์„ค์ •๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด๋ณด์ž! ### ๐Ÿ“ฆ ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ์„ค์น˜ ๋ผ๋ผ๋ฒจ ํ”„๋กœ์ ํŠธ์—์„œ Web3 ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋ ค๋ฉด ๋ช‡ ๊ฐ€์ง€ ํŒจํ‚ค์ง€๊ฐ€ ํ•„์š”ํ•ด. ํ”„๋ก ํŠธ์—”๋“œ์—์„œ๋Š” ์ฃผ๋กœ ethers.js๋‚˜ web3.js๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ๋ฐฑ์—”๋“œ์—์„œ๋Š” PHP์šฉ Web3 ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด.
๐Ÿ“„ package.json
{
  "dependencies": {
    "ethers": "^6.9.0",
    "axios": "^1.6.0"
  }
}
๐Ÿ”ง ์„ค์น˜ ๋ช…๋ น์–ด
npm install ethers axios
composer require web3p/web3.php
๐Ÿ’ก ํŒ: ethers.js๋Š” web3.js๋ณด๋‹ค ๊ฐ€๋ณ๊ณ  ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์ง€์›์ด ์šฐ์ˆ˜ํ•ด์„œ ์š”์ฆ˜ ๋” ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” ์ถ”์„ธ์•ผ. ํŠนํžˆ ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ๊ฐ€ ์ž‘์•„์„œ ๋กœ๋”ฉ ์†๋„ ๊ฐœ์„ ์—๋„ ๋„์›€์ด ๋ผ!
### ๐Ÿ” ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ๋ผ๋ผ๋ฒจ์˜ `.env` ํŒŒ์ผ์— Web3 ๊ด€๋ จ ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ด. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋„คํŠธ์›Œํฌ ์„ค์ •์„ ์‰ฝ๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด.
๐Ÿ“„ .env
WEB3_NETWORK_NAME=Ethereum
WEB3_CHAIN_ID=1
WEB3_RPC_URL=https://mainnet.infura.io/v3/YOUR_INFURA_KEY
WEB3_EXPLORER_URL=https://etherscan.io

# ํ…Œ์ŠคํŠธ๋„ท ์„ค์ • (๊ฐœ๋ฐœ์šฉ)
WEB3_TESTNET_NAME=Sepolia
WEB3_TESTNET_CHAIN_ID=11155111
WEB3_TESTNET_RPC_URL=https://sepolia.infura.io/v3/YOUR_INFURA_KEY
๐ŸŽฏ ์™œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ• ๊นŒ?

๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” ํ…Œ์ŠคํŠธ๋„ท์„, ํ”„๋กœ๋•์…˜์—์„œ๋Š” ๋ฉ”์ธ๋„ท์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜์ž–์•„? ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•˜๋ฉด ์ฝ”๋“œ ์ˆ˜์ • ์—†์ด ๋ฐฐํฌ ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ์ž๋™์œผ๋กœ ๋„คํŠธ์›Œํฌ๊ฐ€ ์ „ํ™˜๋ผ. ์ด๊ฒŒ ๋ฐ”๋กœ ํ”„๋กœํŽ˜์…”๋„ํ•œ ๊ฐœ๋ฐœ ๋ฐฉ์‹์ด์•ผ! ๐Ÿš€
### ๐Ÿ“ Config ํŒŒ์ผ ์ƒ์„ฑ ๋ผ๋ผ๋ฒจ์˜ config ๋””๋ ‰ํ† ๋ฆฌ์— web3.php ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์„œ ์„ค์ •์„ ์ค‘์•™ํ™”ํ•˜์ž.
๐Ÿ“„ config/web3.php
<?php

return [
    'networks' => [
        'mainnet' => [
            'name' => env('WEB3_NETWORK_NAME', 'Ethereum'),
            'chain_id' => env('WEB3_CHAIN_ID', 1),
            'rpc_url' => env('WEB3_RPC_URL'),
            'explorer_url' => env('WEB3_EXPLORER_URL'),
            'currency' => [
                'name' => 'Ether',
                'symbol' => 'ETH',
                'decimals' => 18
            ]
        ],
        'testnet' => [
            'name' => env('WEB3_TESTNET_NAME', 'Sepolia'),
            'chain_id' => env('WEB3_TESTNET_CHAIN_ID', 11155111),
            'rpc_url' => env('WEB3_TESTNET_RPC_URL'),
            'currency' => [
                'name' => 'Sepolia Ether',
                'symbol' => 'SEP',
                'decimals' => 18
            ]
        ]
    ],
    
    'session_timeout' => 3600, // 1์‹œ๊ฐ„
    'nonce_expiry' => 300, // 5๋ถ„
];
์ด๋ ‡๊ฒŒ ์„ค์ •ํ•˜๋ฉด ์ปจํŠธ๋กค๋Ÿฌ์—์„œ `config('web3.networks.mainnet.chain_id')` ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ์‰ฝ๊ฒŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด!
## ๐ŸŽจ ํ”„๋ก ํŠธ์—”๋“œ: ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ์—ฐ๊ฒฐ ๋ฒ„ํŠผ ๋งŒ๋“ค๊ธฐ ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ UX ๊ฐœ์„ ์˜ ํ•ต์‹ฌ์ธ ํ”„๋ก ํŠธ์—”๋“œ ์ž‘์—…์„ ํ•ด๋ณด์ž. ๋‹จ์ˆœํžˆ "Connect Wallet" ๋ฒ„ํŠผ ํ•˜๋‚˜ ๋งŒ๋“œ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ์‚ฌ์šฉ์ž์˜ ๋ชจ๋“  ์ƒํ™ฉ์„ ๊ณ ๋ คํ•œ ์Šค๋งˆํŠธํ•œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•  ๊ฑฐ์•ผ! ๐ŸŽฏ ### ๐ŸฆŠ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ๊ฐ์ง€ ๋ฐ ์„ค์น˜ ์œ ๋„ ๊ฐ€์žฅ ๋จผ์ € ํ•ด์•ผ ํ•  ์ผ์€ ์‚ฌ์šฉ์ž์˜ ๋ธŒ๋ผ์šฐ์ €์— ๋ฉ”ํƒ€๋งˆ์Šคํฌ๊ฐ€ ์„ค์น˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฑฐ์•ผ. ์„ค์น˜๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด ์นœ์ ˆํ•˜๊ฒŒ ์„ค์น˜ ํŽ˜์ด์ง€๋กœ ์•ˆ๋‚ดํ•ด์ค˜์•ผ ํ•ด.
๐Ÿ“„ resources/js/web3/metamask.js
class MetaMaskConnector {
    constructor() {
        this.ethereum = window.ethereum;
        this.provider = null;
        this.signer = null;
        this.account = null;
    }

    // ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์„ค์น˜ ์—ฌ๋ถ€ ํ™•์ธ
    isMetaMaskInstalled() {
        return typeof this.ethereum !== 'undefined' && this.ethereum.isMetaMask;
    }

    // ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์„ค์น˜ ์œ ๋„
    promptInstall() {
        const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
        
        if (isMobile) {
            // ๋ชจ๋ฐ”์ผ: ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์•ฑ ๋‹ค์šด๋กœ๋“œ ๋˜๋Š” ์ธ์•ฑ ๋ธŒ๋ผ์šฐ์ €๋กœ ์œ ๋„
            const dappUrl = window.location.href;
            window.location.href = `https://metamask.app.link/dapp/${dappUrl}`;
        } else {
            // ๋ฐ์Šคํฌํ†ฑ: ์„ค์น˜ ํŽ˜์ด์ง€๋กœ ์ด๋™
            window.open('https://metamask.io/download/', '_blank');
        }
    }

    // ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ
    getErrorMessage(error) {
        const errorMessages = {
            4001: '๋ฉ”ํƒ€๋งˆ์Šคํฌ์—์„œ ์—ฐ๊ฒฐ์„ ๊ฑฐ๋ถ€ํ•˜์…จ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
            4100: '์š”์ฒญํ•œ ๋ฉ”์„œ๋“œ๊ฐ€ ์ง€์›๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.',
            4200: '๋ฉ”ํƒ€๋งˆ์Šคํฌ๊ฐ€ ์ž ๊ฒจ์žˆ์Šต๋‹ˆ๋‹ค. ์ž ๊ธˆ์„ ํ•ด์ œํ•ด์ฃผ์„ธ์š”.',
            4900: '๋ฉ”ํƒ€๋งˆ์Šคํฌ ์—ฐ๊ฒฐ์ด ๋Š์–ด์กŒ์Šต๋‹ˆ๋‹ค.',
            4901: '๋ฉ”ํƒ€๋งˆ์Šคํฌ๊ฐ€ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.',
            '-32002': '์ด๋ฏธ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์—ฐ๊ฒฐ ์š”์ฒญ์ด ์ง„ํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค. ๋ฉ”ํƒ€๋งˆ์Šคํฌ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.',
            '-32603': '๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•ด์ฃผ์„ธ์š”.'
        };

        const code = error.code?.toString();
        return errorMessages[code] || `์—ฐ๊ฒฐ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ${error.message}`;
    }
}

export default MetaMaskConnector;
๐ŸŽฏ ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ ํŠน๋ณ„ ์ฒ˜๋ฆฌ

๋ชจ๋ฐ”์ผ์—์„œ๋Š” ์ผ๋ฐ˜ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์•„๋‹Œ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์ธ์•ฑ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด. `metamask.app.link`๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™์œผ๋กœ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์•ฑ์ด ์—ด๋ฆฌ๋ฉด์„œ ํ•ด๋‹น ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๊ฒŒ ๋ผ. ์ด๊ฒŒ ๋ฐ”๋กœ ๋ชจ๋ฐ”์ผ UX์˜ ํ•ต์‹ฌ์ด์•ผ!
### ๐ŸŽญ ๋‹ค์–‘ํ•œ ์ƒํƒœ๋ฅผ ํ‘œํ˜„ํ•˜๋Š” UI ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ์ž๋Š” ํ•ญ์ƒ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ์•Œ๊ณ  ์‹ถ์–ด ํ•ด. "์ง€๊ธˆ ๋ญ๊ฐ€ ์ง„ํ–‰๋˜๊ณ  ์žˆ๋Š” ๊ฑฐ์ง€?"๋ผ๋Š” ์˜๋ฌธ์ด ๋“ค์ง€ ์•Š๋„๋ก ๋ช…ํ™•ํ•œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•ด์•ผ ํ•ด.
๐Ÿ“„ resources/views/components/wallet-connect.blade.php
<div id="wallet-connect-container" class="wallet-container">
    <!-- ๋ฉ”ํƒ€๋งˆ์Šคํฌ ๋ฏธ์„ค์น˜ ์ƒํƒœ -->
    <div id="install-prompt" class="wallet-state" style="display: none;">
        <div class="wallet-icon">๐ŸฆŠ</div>
        <h3>๋ฉ”ํƒ€๋งˆ์Šคํฌ๊ฐ€ ํ•„์š”ํ•ด์š”</h3>
        <p>Web3 ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•˜๋ ค๋ฉด ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์ง€๊ฐ‘์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.</p>
        <button id="install-metamask-btn" class="btn-primary">
            ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์„ค์น˜ํ•˜๊ธฐ
        </button>
    </div>

    <!-- ์—ฐ๊ฒฐ ๋Œ€๊ธฐ ์ƒํƒœ -->
    <div id="connect-prompt" class="wallet-state" style="display: none;">
        <div class="wallet-icon">๐Ÿ”—</div>
        <h3>์ง€๊ฐ‘ ์—ฐ๊ฒฐํ•˜๊ธฐ</h3>
        <p>๋ฉ”ํƒ€๋งˆ์Šคํฌ๋ฅผ ์—ฐ๊ฒฐํ•˜์—ฌ ์„œ๋น„์Šค๋ฅผ ์‹œ์ž‘ํ•˜์„ธ์š”.</p>
        <button id="connect-wallet-btn" class="btn-primary">
            ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์—ฐ๊ฒฐ
        </button>
    </div>

    <!-- ์—ฐ๊ฒฐ ์ง„ํ–‰ ์ค‘ -->
    <div id="connecting-state" class="wallet-state" style="display: none;">
        <div class="spinner"></div>
        <h3>์—ฐ๊ฒฐ ์ค‘...</h3>
        <p>๋ฉ”ํƒ€๋งˆ์Šคํฌ์—์„œ ์—ฐ๊ฒฐ์„ ์Šน์ธํ•ด์ฃผ์„ธ์š”.</p>
    </div>

    <!-- ๋„คํŠธ์›Œํฌ ์ „ํ™˜ ํ•„์š” -->
    <div id="network-switch" class="wallet-state" style="display: none;">
        <div class="wallet-icon">๐Ÿ”„</div>
        <h3>๋„คํŠธ์›Œํฌ ์ „ํ™˜์ด ํ•„์š”ํ•ด์š”</h3>
        <p id="network-message"></p>
        <button id="switch-network-btn" class="btn-primary">
            ๋„คํŠธ์›Œํฌ ์ „ํ™˜ํ•˜๊ธฐ
        </button>
    </div>

    <!-- ์—ฐ๊ฒฐ ์™„๋ฃŒ -->
    <div id="connected-state" class="wallet-state" style="display: none;">
        <div class="wallet-icon">โœ…</div>
        <div class="wallet-info">
            <div class="wallet-address" id="wallet-address"></div>
            <div class="wallet-balance" id="wallet-balance"></div>
        </div>
        <button id="disconnect-btn" class="btn-secondary">
            ์—ฐ๊ฒฐ ํ•ด์ œ
        </button>
    </div>

    <!-- ์—๋Ÿฌ ์ƒํƒœ -->
    <div id="error-state" class="wallet-state" style="display: none;">
        <div class="wallet-icon">โš ๏ธ</div>
        <h3>์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š”</h3>
        <p id="error-message"></p>
        <button id="retry-btn" class="btn-primary">
            ๋‹ค์‹œ ์‹œ๋„ํ•˜๊ธฐ
        </button>
    </div>
</div>
๋ณด๋‹ค์‹œํ”ผ ๊ฐ ์ƒํƒœ๋ณ„๋กœ ๋ช…ํ™•ํ•œ UI๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ์–ด. ์‚ฌ์šฉ์ž๋Š” ์ง€๊ธˆ ๋ฌด์Šจ ์ผ์ด ์ผ์–ด๋‚˜๊ณ  ์žˆ๋Š”์ง€, ๋‹ค์Œ์— ๋ฌด์—‡์„ ํ•ด์•ผ ํ•˜๋Š”์ง€ ๋ฐ”๋กœ ์•Œ ์ˆ˜ ์žˆ์ง€! ### ๐ŸŽฌ ์ƒํƒœ ๊ด€๋ฆฌ ๋กœ์ง ๊ตฌํ˜„ ์ด์ œ ์ด UI๋“ค์„ ์ ์ ˆํ•œ ํƒ€์ด๋ฐ์— ๋ณด์—ฌ์ฃผ๊ณ  ์ˆจ๊ธฐ๋Š” ๋กœ์ง์„ ๋งŒ๋“ค์–ด๋ณด์ž.
๐Ÿ“„ resources/js/web3/wallet-ui.js
import MetaMaskConnector from './metamask.js';
import { ethers } from 'ethers';

class WalletUI {
    constructor() {
        this.connector = new MetaMaskConnector();
        this.currentState = 'init';
        this.requiredChainId = window.APP_CONFIG?.chainId || 1;
        this.init();
    }

    init() {
        this.bindEvents();
        this.checkInitialState();
    }

    // ์ดˆ๊ธฐ ์ƒํƒœ ํ™•์ธ
    async checkInitialState() {
        if (!this.connector.isMetaMaskInstalled()) {
            this.showState('install-prompt');
            return;
        }

        // ์ด์ „์— ์—ฐ๊ฒฐ๋œ ๊ณ„์ •์ด ์žˆ๋Š”์ง€ ํ™•์ธ
        try {
            const accounts = await this.connector.ethereum.request({ 
                method: 'eth_accounts' 
            });
            
            if (accounts.length > 0) {
                await this.handleConnection(accounts[0]);
            } else {
                this.showState('connect-prompt');
            }
        } catch (error) {
            console.error('Initial state check failed:', error);
            this.showState('connect-prompt');
        }
    }

    // ์ด๋ฒคํŠธ ๋ฐ”์ธ๋”ฉ
    bindEvents() {
        document.getElementById('install-metamask-btn')?.addEventListener('click', () => {
            this.connector.promptInstall();
        });

        document.getElementById('connect-wallet-btn')?.addEventListener('click', () => {
            this.connectWallet();
        });

        document.getElementById('switch-network-btn')?.addEventListener('click', () => {
            this.switchNetwork();
        });

        document.getElementById('disconnect-btn')?.addEventListener('click', () => {
            this.disconnect();
        });

        document.getElementById('retry-btn')?.addEventListener('click', () => {
            this.checkInitialState();
        });

        // ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
        if (this.connector.ethereum) {
            this.connector.ethereum.on('accountsChanged', (accounts) => {
                if (accounts.length === 0) {
                    this.showState('connect-prompt');
                } else {
                    this.handleConnection(accounts[0]);
                }
            });

            this.connector.ethereum.on('chainChanged', () => {
                window.location.reload();
            });
        }
    }

    // ์ง€๊ฐ‘ ์—ฐ๊ฒฐ
    async connectWallet() {
        this.showState('connecting-state');

        try {
            const accounts = await this.connector.ethereum.request({
                method: 'eth_requestAccounts'
            });

            await this.handleConnection(accounts[0]);
        } catch (error) {
            console.error('Connection error:', error);
            this.showError(this.connector.getErrorMessage(error));
        }
    }

    // ์—ฐ๊ฒฐ ์ฒ˜๋ฆฌ
    async handleConnection(account) {
        try {
            // ๋„คํŠธ์›Œํฌ ํ™•์ธ
            const chainId = await this.connector.ethereum.request({ 
                method: 'eth_chainId' 
            });
            const currentChainId = parseInt(chainId, 16);

            if (currentChainId !== this.requiredChainId) {
                this.showNetworkSwitch(currentChainId);
                return;
            }

            // Provider ๋ฐ Signer ์„ค์ •
            this.connector.provider = new ethers.BrowserProvider(this.connector.ethereum);
            this.connector.signer = await this.connector.provider.getSigner();
            this.connector.account = account;

            // ์ž”์•ก ์กฐํšŒ
            const balance = await this.connector.provider.getBalance(account);
            const balanceInEth = ethers.formatEther(balance);

            // ๋ฐฑ์—”๋“œ์— ์ธ์ฆ ์š”์ฒญ
            await this.authenticateWithBackend(account);

            // UI ์—…๋ฐ์ดํŠธ
            this.showConnected(account, balanceInEth);

        } catch (error) {
            console.error('Handle connection error:', error);
            this.showError('์—ฐ๊ฒฐ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
        }
    }

    // ๋„คํŠธ์›Œํฌ ์ „ํ™˜
    async switchNetwork() {
        const networkConfig = this.getNetworkConfig(this.requiredChainId);
        
        try {
            await this.connector.ethereum.request({
                method: 'wallet_switchEthereumChain',
                params: [{ chainId: `0x${this.requiredChainId.toString(16)}` }],
            });
        } catch (switchError) {
            // ๋„คํŠธ์›Œํฌ๊ฐ€ ๋ฉ”ํƒ€๋งˆ์Šคํฌ์— ์ถ”๊ฐ€๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ
            if (switchError.code === 4902) {
                try {
                    await this.connector.ethereum.request({
                        method: 'wallet_addEthereumChain',
                        params: [networkConfig],
                    });
                } catch (addError) {
                    this.showError('๋„คํŠธ์›Œํฌ ์ถ”๊ฐ€์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
                }
            } else {
                this.showError('๋„คํŠธ์›Œํฌ ์ „ํ™˜์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
            }
        }
    }

    // ๋ฐฑ์—”๋“œ ์ธ์ฆ
    async authenticateWithBackend(address) {
        try {
            // 1. ์„œ๋ฒ„์—์„œ nonce ๋ฐ›๊ธฐ
            const nonceResponse = await axios.post('/api/web3/nonce', { address });
            const nonce = nonceResponse.data.nonce;

            // 2. ๋ฉ”์‹œ์ง€ ์„œ๋ช…
            const message = `์žฌ๋Šฅ๋„ท ๋กœ๊ทธ์ธ ์ธ์ฆ\n\nNonce: ${nonce}\n์ฃผ์†Œ: ${address}`;
            const signature = await this.connector.signer.signMessage(message);

            // 3. ์„œ๋ช… ๊ฒ€์ฆ ๋ฐ ๋กœ๊ทธ์ธ
            const authResponse = await axios.post('/api/web3/verify', {
                address,
                signature,
                nonce
            });

            // 4. ํ† ํฐ ์ €์žฅ
            localStorage.setItem('auth_token', authResponse.data.token);
            
            return true;
        } catch (error) {
            console.error('Backend authentication error:', error);
            throw error;
        }
    }

    // ์ƒํƒœ ํ‘œ์‹œ
    showState(stateName) {
        document.querySelectorAll('.wallet-state').forEach(el => {
            el.style.display = 'none';
        });
        document.getElementById(stateName).style.display = 'block';
        this.currentState = stateName;
    }

    // ๋„คํŠธ์›Œํฌ ์ „ํ™˜ UI
    showNetworkSwitch(currentChainId) {
        const networkNames = {
            1: '์ด๋”๋ฆฌ์›€ ๋ฉ”์ธ๋„ท',
            5: '๊ณ ์–ผ๋ฆฌ ํ…Œ์ŠคํŠธ๋„ท',
            137: 'ํด๋ฆฌ๊ณค ๋ฉ”์ธ๋„ท',
            80001: '๋ญ„๋ฐ”์ด ํ…Œ์ŠคํŠธ๋„ท'
        };

        const message = `ํ˜„์žฌ ${networkNames[currentChainId] || '์•Œ ์ˆ˜ ์—†๋Š” ๋„คํŠธ์›Œํฌ'}์— ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ${networkNames[this.requiredChainId]}์œผ๋กœ ์ „ํ™˜ํ•ด์ฃผ์„ธ์š”.`;
        
        document.getElementById('network-message').textContent = message;
        this.showState('network-switch');
    }

    // ์—ฐ๊ฒฐ ์™„๋ฃŒ UI
    showConnected(address, balance) {
        const shortAddress = `${address.slice(0, 6)}...${address.slice(-4)}`;
        document.getElementById('wallet-address').textContent = shortAddress;
        document.getElementById('wallet-balance').textContent = `${parseFloat(balance).toFixed(4)} ETH`;
        this.showState('connected-state');

        // ์„ฑ๊ณต ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€
        this.showToast('์ง€๊ฐ‘์ด ์„ฑ๊ณต์ ์œผ๋กœ ์—ฐ๊ฒฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๐ŸŽ‰', 'success');
    }

    // ์—๋Ÿฌ ํ‘œ์‹œ
    showError(message) {
        document.getElementById('error-message').textContent = message;
        this.showState('error-state');
        this.showToast(message, 'error');
    }

    // ์—ฐ๊ฒฐ ํ•ด์ œ
    disconnect() {
        this.connector.account = null;
        this.connector.provider = null;
        this.connector.signer = null;
        localStorage.removeItem('auth_token');
        this.showState('connect-prompt');
        this.showToast('์ง€๊ฐ‘ ์—ฐ๊ฒฐ์ด ํ•ด์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.', 'info');
    }

    // ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€
    showToast(message, type = 'info') {
        // ํ† ์ŠคํŠธ ๊ตฌํ˜„ (๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ)
        const toast = document.createElement('div');
        toast.className = `toast toast-${type}`;
        toast.textContent = message;
        document.body.appendChild(toast);

        setTimeout(() => {
            toast.classList.add('show');
        }, 100);

        setTimeout(() => {
            toast.classList.remove('show');
            setTimeout(() => toast.remove(), 300);
        }, 3000);
    }

    // ๋„คํŠธ์›Œํฌ ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ
    getNetworkConfig(chainId) {
        const configs = {
            1: {
                chainId: '0x1',
                chainName: 'Ethereum Mainnet',
                nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
                rpcUrls: ['https://mainnet.infura.io/v3/'],
                blockExplorerUrls: ['https://etherscan.io']
            },
            137: {
                chainId: '0x89',
                chainName: 'Polygon Mainnet',
                nativeCurrency: { name: 'MATIC', symbol: 'MATIC', decimals: 18 },
                rpcUrls: ['https://polygon-rpc.com/'],
                blockExplorerUrls: ['https://polygonscan.com']
            }
        };

        return configs[chainId];
    }
}

// ์ดˆ๊ธฐํ™”
document.addEventListener('DOMContentLoaded', () => {
    window.walletUI = new WalletUI();
});

export default WalletUI;
๐ŸŽฏ UX ๊ฐœ์„  ํฌ์ธํŠธ ์ •๋ฆฌ

1๏ธโƒฃ ์ฆ‰๊ฐ์ ์ธ ํ”ผ๋“œ๋ฐฑ: ๋ชจ๋“  ์•ก์…˜์— ๋Œ€ํ•ด ์ฆ‰์‹œ ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ๋ณด์—ฌ์คŒ
2๏ธโƒฃ ๋ช…ํ™•ํ•œ ์•ˆ๋‚ด: ๊ฐ ๋‹จ๊ณ„๋งˆ๋‹ค ์‚ฌ์šฉ์ž๊ฐ€ ๋ฌด์—‡์„ ํ•ด์•ผ ํ•˜๋Š”์ง€ ๋ช…ํ™•ํžˆ ํ‘œ์‹œ
3๏ธโƒฃ ์—๋Ÿฌ ๋ณต๊ตฌ: ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์žฌ์‹œ๋„ ๋ฒ„ํŠผ ์ œ๊ณต
4๏ธโƒฃ ์ž๋™ ์žฌ์—ฐ๊ฒฐ: ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ ์‹œ ์ด์ „ ์—ฐ๊ฒฐ ์ƒํƒœ ๋ณต์›
5๏ธโƒฃ ํ† ์ŠคํŠธ ์•Œ๋ฆผ: ์ค‘์š”ํ•œ ์ด๋ฒคํŠธ๋ฅผ ๋†“์น˜์ง€ ์•Š๋„๋ก ์•Œ๋ฆผ ์ œ๊ณต
## ๐Ÿ” ๋ฐฑ์—”๋“œ: ์•ˆ์ „ํ•œ ์ธ์ฆ ์‹œ์Šคํ…œ ๊ตฌ์ถ• ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์—ฐ๊ฒฐ์ด ์™„๋ฃŒ๋˜์—ˆ๋‹ค๊ณ  ํ•ด์„œ ๋์ด ์•„๋‹ˆ์•ผ. ๋ฐฑ์—”๋“œ์—์„œ ์ œ๋Œ€๋กœ ๋œ ์ธ์ฆ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•ด์•ผ ๋ณด์•ˆ๋„ ์ง€ํ‚ค๊ณ  UX๋„ ์ข‹์•„์ ธ! ๐Ÿ›ก๏ธ ### ๐Ÿ“ ์„œ๋ช… ๊ธฐ๋ฐ˜ ์ธ์ฆ ํ”Œ๋กœ์šฐ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ๋กœ๊ทธ์ธ์˜ ํ•ต์‹ฌ์€ ์„œ๋ช… ๊ธฐ๋ฐ˜ ์ธ์ฆ์ด์•ผ. ์‚ฌ์šฉ์ž๊ฐ€ ์ž์‹ ์˜ ๊ฐœ์ธํ‚ค๋กœ ๋ฉ”์‹œ์ง€์— ์„œ๋ช…ํ•˜๋ฉด, ์„œ๋ฒ„์—์„œ ๊ทธ ์„œ๋ช…์„ ๊ฒ€์ฆํ•ด์„œ ๋ณธ์ธ ํ™•์ธ์„ ํ•˜๋Š” ๊ฑฐ์ง€.
1Nonce ์ƒ์„ฑ ๋ฐ ๋ฐœ๊ธ‰

์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ์„ ์‹œ๋„ํ•˜๋ฉด ์„œ๋ฒ„๋Š” ์ผํšŒ์šฉ ๋‚œ์ˆ˜(nonce)๋ฅผ ์ƒ์„ฑํ•ด์„œ ์ „๋‹ฌํ•ด. ์ด nonce๋Š” ์žฌ์‚ฌ์šฉ ๊ณต๊ฒฉ(replay attack)์„ ๋ฐฉ์ง€ํ•˜๋Š” ์—ญํ• ์„ ํ•ด.
2๋ฉ”์‹œ์ง€ ์„œ๋ช…

์‚ฌ์šฉ์ž๋Š” ๋ฐ›์€ nonce๋ฅผ ํฌํ•จํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ์ž์‹ ์˜ ๊ฐœ์ธํ‚ค๋กœ ์„œ๋ช…ํ•ด. ์ด ๊ณผ์ •์€ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ํŒ์—…์—์„œ ์ง„ํ–‰๋˜๊ณ , ๊ฐœ์ธํ‚ค๋Š” ์ ˆ๋Œ€ ์™ธ๋ถ€๋กœ ๋…ธ์ถœ๋˜์ง€ ์•Š์•„.
3์„œ๋ช… ๊ฒ€์ฆ

์„œ๋ฒ„๋Š” ๋ฐ›์€ ์„œ๋ช…์„ ๊ฒ€์ฆํ•ด์„œ ์‹ค์ œ๋กœ ํ•ด๋‹น ์ง€๊ฐ‘ ์ฃผ์†Œ์˜ ์†Œ์œ ์ž๊ฐ€ ์„œ๋ช…ํ•œ ๊ฒŒ ๋งž๋Š”์ง€ ํ™•์ธํ•ด. ๊ฒ€์ฆ์ด ์„ฑ๊ณตํ•˜๋ฉด JWT ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•˜์ง€.
### ๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋จผ์ € Web3 ์ธ์ฆ์— ํ•„์š”ํ•œ ํ…Œ์ด๋ธ”์„ ๋งŒ๋“ค์–ด๋ณด์ž.
๐Ÿ“„ database/migrations/xxxx_create_web3_users_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('web3_users', function (Blueprint $table) {
            $table->id();
            $table->string('wallet_address', 42)->unique();
            $table->string('nonce')->nullable();
            $table->timestamp('nonce_expires_at')->nullable();
            $table->string('username')->nullable();
            $table->string('email')->nullable();
            $table->json('metadata')->nullable();
            $table->timestamp('last_login_at')->nullable();
            $table->timestamps();
            
            $table->index('wallet_address');
            $table->index('nonce_expires_at');
        });

        Schema::create('web3_sessions', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained('web3_users')->onDelete('cascade');
            $table->string('token')->unique();
            $table->string('ip_address', 45)->nullable();
            $table->text('user_agent')->nullable();
            $table->timestamp('expires_at');
            $table->timestamps();
            
            $table->index('token');
            $table->index('expires_at');
        });
    }

    public function down()
    {
        Schema::dropIfExists('web3_sessions');
        Schema::dropIfExists('web3_users');
    }
};
### ๐ŸŽฏ Web3 User ๋ชจ๋ธ ์ƒ์„ฑ
๐Ÿ“„ app/Models/Web3User.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Carbon\Carbon;

class Web3User extends Model
{
    protected $fillable = [
        'wallet_address',
        'nonce',
        'nonce_expires_at',
        'username',
        'email',
        'metadata',
        'last_login_at'
    ];

    protected $casts = [
        'metadata' => 'array',
        'nonce_expires_at' => 'datetime',
        'last_login_at' => 'datetime'
    ];

    // Nonce ์ƒ์„ฑ
    public function generateNonce()
    {
        $this->nonce = Str::random(32);
        $this->nonce_expires_at = Carbon::now()->addMinutes(5);
        $this->save();
        
        return $this->nonce;
    }

    // Nonce ๊ฒ€์ฆ
    public function validateNonce($nonce)
    {
        if ($this->nonce !== $nonce) {
            return false;
        }

        if ($this->nonce_expires_at < Carbon::now()) {
            return false;
        }

        return true;
    }

    // Nonce ๋ฌดํšจํ™”
    public function invalidateNonce()
    {
        $this->nonce = null;
        $this->nonce_expires_at = null;
        $this->save();
    }

    // ์„ธ์…˜ ๊ด€๊ณ„
    public function sessions()
    {
        return $this->hasMany(Web3Session::class, 'user_id');
    }

    // ํ™œ์„ฑ ์„ธ์…˜๋งŒ
    public function activeSessions()
    {
        return $this->sessions()->where('expires_at', '>', Carbon::now());
    }

    // ๋กœ๊ทธ์ธ ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ
    public function updateLastLogin()
    {
        $this->last_login_at = Carbon::now();
        $this->save();
    }
}
๐Ÿ“„ app/Models/Web3Session.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Carbon\Carbon;

class Web3Session extends Model
{
    protected $fillable = [
        'user_id',
        'token',
        'ip_address',
        'user_agent',
        'expires_at'
    ];

    protected $casts = [
        'expires_at' => 'datetime'
    ];

    // ํ† ํฐ ์ƒ์„ฑ
    public static function createToken($userId, $request)
    {
        return self::create([
            'user_id' => $userId,
            'token' => Str::random(64),
            'ip_address' => $request->ip(),
            'user_agent' => $request->userAgent(),
            'expires_at' => Carbon::now()->addHours(24)
        ]);
    }

    // ํ† ํฐ์œผ๋กœ ์„ธ์…˜ ์ฐพ๊ธฐ
    public static function findByToken($token)
    {
        return self::where('token', $token)
            ->where('expires_at', '>', Carbon::now())
            ->first();
    }

    // ์‚ฌ์šฉ์ž ๊ด€๊ณ„
    public function user()
    {
        return $this->belongsTo(Web3User::class, 'user_id');
    }

    // ๋งŒ๋ฃŒ ์—ฌ๋ถ€ ํ™•์ธ
    public function isExpired()
    {
        return $this->expires_at < Carbon::now();
    }
}
### ๐Ÿ”ง Web3 ์ธ์ฆ ์„œ๋น„์Šค ํด๋ž˜์Šค ์ด์ œ ์‹ค์ œ ์„œ๋ช… ๊ฒ€์ฆ ๋กœ์ง์„ ๋‹ด๋‹นํ•  ์„œ๋น„์Šค ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.
๐Ÿ“„ app/Services/Web3AuthService.php
<?php

namespace App\Services;

use App\Models\Web3User;
use App\Models\Web3Session;
use Illuminate\Support\Facades\Log;
use kornrunner\Keccak;

class Web3AuthService
{
    /**
     * Nonce ์š”์ฒญ ์ฒ˜๋ฆฌ
     */
    public function requestNonce($walletAddress)
    {
        // ์ฃผ์†Œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ
        if (!$this->isValidAddress($walletAddress)) {
            throw new \Exception('Invalid wallet address');
        }

        // ์ฃผ์†Œ ์ •๊ทœํ™” (์ฒดํฌ์„ฌ ์ ์šฉ)
        $walletAddress = $this->toChecksumAddress($walletAddress);

        // ์‚ฌ์šฉ์ž ์ฐพ๊ธฐ ๋˜๋Š” ์ƒ์„ฑ
        $user = Web3User::firstOrCreate(
            ['wallet_address' => $walletAddress],
            ['metadata' => []]
        );

        // Nonce ์ƒ์„ฑ
        $nonce = $user->generateNonce();

        return [
            'nonce' => $nonce,
            'message' => $this->generateMessage($walletAddress, $nonce)
        ];
    }

    /**
     * ์„œ๋ช… ๊ฒ€์ฆ ๋ฐ ๋กœ๊ทธ์ธ
     */
    public function verifySignature($walletAddress, $signature, $nonce, $request)
    {
        // ์ฃผ์†Œ ์ •๊ทœํ™”
        $walletAddress = $this->toChecksumAddress($walletAddress);

        // ์‚ฌ์šฉ์ž ์ฐพ๊ธฐ
        $user = Web3User::where('wallet_address', $walletAddress)->first();
        
        if (!$user) {
            throw new \Exception('User not found');
        }

        // Nonce ๊ฒ€์ฆ
        if (!$user->validateNonce($nonce)) {
            throw new \Exception('Invalid or expired nonce');
        }

        // ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ
        $message = $this->generateMessage($walletAddress, $nonce);

        // ์„œ๋ช… ๊ฒ€์ฆ
        $recoveredAddress = $this->recoverAddress($message, $signature);

        if (strtolower($recoveredAddress) !== strtolower($walletAddress)) {
            Log::warning('Signature verification failed', [
                'expected' => $walletAddress,
                'recovered' => $recoveredAddress
            ]);
            throw new \Exception('Signature verification failed');
        }

        // Nonce ๋ฌดํšจํ™”
        $user->invalidateNonce();

        // ๋กœ๊ทธ์ธ ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ
        $user->updateLastLogin();

        // ์„ธ์…˜ ์ƒ์„ฑ
        $session = Web3Session::createToken($user->id, $request);

        return [
            'user' => $user,
            'token' => $session->token,
            'expires_at' => $session->expires_at
        ];
    }

    /**
     * ์„œ๋ช…์—์„œ ์ฃผ์†Œ ๋ณต๊ตฌ
     */
    private function recoverAddress($message, $signature)
    {
        // ์ด๋”๋ฆฌ์›€ ์„œ๋ช… ๋ฉ”์‹œ์ง€ ํ”„๋ฆฌํ”ฝ์Šค ์ถ”๊ฐ€
        $hash = Keccak::hash("\x19Ethereum Signed Message:\n" . strlen($message) . $message, 256);

        // ์„œ๋ช… ๋ถ„ํ•ด
        $signature = str_replace('0x', '', $signature);
        $r = substr($signature, 0, 64);
        $s = substr($signature, 64, 64);
        $v = hexdec(substr($signature, 128, 2));

        // v ๊ฐ’ ์กฐ์ •
        if ($v < 27) {
            $v += 27;
        }

        // ๊ณต๊ฐœํ‚ค ๋ณต๊ตฌ
        $pubkey = $this->recoverPublicKey($hash, $r, $s, $v);

        // ์ฃผ์†Œ ์ถ”์ถœ
        $address = '0x' . substr(Keccak::hash(substr(hex2bin($pubkey), 1), 256), 24);

        return $this->toChecksumAddress($address);
    }

    /**
     * ๊ณต๊ฐœํ‚ค ๋ณต๊ตฌ (๊ฐ„์†Œํ™”๋œ ๋ฒ„์ „)
     */
    private function recoverPublicKey($hash, $r, $s, $v)
    {
        // ์‹ค์ œ ํ”„๋กœ๋•์…˜์—์„œ๋Š” elliptic curve ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ ๊ถŒ์žฅ
        // ์—ฌ๊ธฐ์„œ๋Š” ๊ฐœ๋… ์„ค๋ช…์„ ์œ„ํ•œ ๊ฐ„์†Œํ™”๋œ ๋ฒ„์ „
        
        // web3p/web3.php ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ ์˜ˆ์‹œ
        $ec = new \Elliptic\EC('secp256k1');
        $signature = [
            'r' => $r,
            's' => $s,
            'recoveryParam' => $v - 27
        ];
        
        $publicKey = $ec->recoverPubKey($hash, $signature, $signature['recoveryParam']);
        return $publicKey->encode('hex');
    }

    /**
     * ๋กœ๊ทธ์ธ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ
     */
    private function generateMessage($walletAddress, $nonce)
    {
        return "์žฌ๋Šฅ๋„ท ๋กœ๊ทธ์ธ ์ธ์ฆ\n\n" .
               "์ด ์„œ๋ช…์€ ์žฌ๋Šฅ๋„ท(jaenung.net)์— ๋กœ๊ทธ์ธํ•˜๊ธฐ ์œ„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.\n\n" .
               "Nonce: {$nonce}\n" .
               "์ฃผ์†Œ: {$walletAddress}\n\n" .
               "์ด ์„œ๋ช…์€ ์–ด๋– ํ•œ ํŠธ๋žœ์žญ์…˜๋„ ๋ฐœ์ƒ์‹œํ‚ค์ง€ ์•Š์œผ๋ฉฐ,\n" .
               "๊ฐ€์Šค ๋น„์šฉ์ด ๋“ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค.";
    }

    /**
     * ์ฃผ์†Œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ
     */
    private function isValidAddress($address)
    {
        return preg_match('/^0x[a-fA-F0-9]{40}$/', $address);
    }

    /**
     * ์ฒดํฌ์„ฌ ์ฃผ์†Œ๋กœ ๋ณ€ํ™˜
     */
    private function toChecksumAddress($address)
    {
        $address = strtolower(str_replace('0x', '', $address));
        $hash = Keccak::hash($address, 256);
        $checksum = '0x';

        for ($i = 0; $i < 40; $i++) {
            $checksum .= (intval($hash[$i], 16) > 7) 
                ? strtoupper($address[$i]) 
                : $address[$i];
        }

        return $checksum;
    }

    /**
     * ํ† ํฐ์œผ๋กœ ์‚ฌ์šฉ์ž ์ธ์ฆ
     */
    public function authenticateByToken($token)
    {
        $session = Web3Session::findByToken($token);

        if (!$session) {
            throw new \Exception('Invalid or expired token');
        }

        return $session->user;
    }

    /**
     * ๋กœ๊ทธ์•„์›ƒ
     */
    public function logout($token)
    {
        $session = Web3Session::where('token', $token)->first();

        if ($session) {
            $session->delete();
        }
    }
}
โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ

์œ„ ์ฝ”๋“œ์˜ `recoverPublicKey` ํ•จ์ˆ˜๋Š” ๊ฐœ๋… ์„ค๋ช…์„ ์œ„ํ•œ ๊ฐ„์†Œํ™”๋œ ๋ฒ„์ „์ด์•ผ. ์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๊ฒ€์ฆ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด:

โ€ข web3p/web3.php: PHP์šฉ Web3 ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
โ€ข kornrunner/keccak: Keccak ํ•ด์‹œ ํ•จ์ˆ˜
โ€ข simplito/elliptic-php: ํƒ€์›๊ณก์„  ์•”ํ˜ธํ™”

`composer require web3p/web3.php kornrunner/keccak simplito/elliptic-php` ๋ช…๋ น์–ด๋กœ ์„ค์น˜ํ•  ์ˆ˜ ์žˆ์–ด!
### ๐ŸŽฎ ์ปจํŠธ๋กค๋Ÿฌ ๊ตฌํ˜„ ์ด์ œ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.
๐Ÿ“„ app/Http/Controllers/Web3AuthController.php
<?php

namespace App\Http\Controllers;

use App\Services\Web3AuthService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class Web3AuthController extends Controller
{
    protected $web3Auth;

    public function __construct(Web3AuthService $web3Auth)
    {
        $this->web3Auth = $web3Auth;
    }

    /**
     * Nonce ์š”์ฒญ
     */
    public function requestNonce(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'address' => 'required|string|regex:/^0x[a-fA-F0-9]{40}$/'
        ]);

        if ($validator->fails()) {
            return response()->json([
                'success' => false,
                'message' => '์œ ํšจํ•˜์ง€ ์•Š์€ ์ง€๊ฐ‘ ์ฃผ์†Œ์ž…๋‹ˆ๋‹ค.',
                'errors' => $validator->errors()
            ], 400);
        }

        try {
            $result = $this->web3Auth->requestNonce($request->address);

            return response()->json([
                'success' => true,
                'nonce' => $result['nonce'],
                'message' => $result['message']
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => 'Nonce ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.',
                'error' => $e->getMessage()
            ], 500);
        }
    }

    /**
     * ์„œ๋ช… ๊ฒ€์ฆ ๋ฐ ๋กœ๊ทธ์ธ
     */
    public function verify(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'address' => 'required|string|regex:/^0x[a-fA-F0-9]{40}$/',
            'signature' => 'required|string',
            'nonce' => 'required|string'
        ]);

        if ($validator->fails()) {
            return response()->json([
                'success' => false,
                'message' => 'ํ•„์ˆ˜ ์ •๋ณด๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
                'errors' => $validator->errors()
            ], 400);
        }

        try {
            $result = $this->web3Auth->verifySignature(
                $request->address,
                $request->signature,
                $request->nonce,
                $request
            );

            return response()->json([
                'success' => true,
                'message' => '๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค.',
                'token' => $result['token'],
                'user' => [
                    'wallet_address' => $result['user']->wallet_address,
                    'username' => $result['user']->username,
                    'last_login_at' => $result['user']->last_login_at
                ],
                'expires_at' => $result['expires_at']
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => '์ธ์ฆ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.',
                'error' => $e->getMessage()
            ], 401);
        }
    }

    /**
     * ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ •๋ณด
     */
    public function me(Request $request)
    {
        try {
            $token = $request->bearerToken();
            
            if (!$token) {
                return response()->json([
                    'success' => false,
                    'message' => '์ธ์ฆ ํ† ํฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'
                ], 401);
            }

            $user = $this->web3Auth->authenticateByToken($token);

            return response()->json([
                'success' => true,
                'user' => [
                    'wallet_address' => $user->wallet_address,
                    'username' => $user->username,
                    'email' => $user->email,
                    'metadata' => $user->metadata,
                    'last_login_at' => $user->last_login_at
                ]
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => '์ธ์ฆ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'
            ], 401);
        }
    }

    /**
     * ๋กœ๊ทธ์•„์›ƒ
     */
    public function logout(Request $request)
    {
        try {
            $token = $request->bearerToken();
            
            if ($token) {
                $this->web3Auth->logout($token);
            }

            return response()->json([
                'success' => true,
                'message' => '๋กœ๊ทธ์•„์›ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => '๋กœ๊ทธ์•„์›ƒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'
            ], 500);
        }
    }
}
### ๐Ÿ›ฃ๏ธ ๋ผ์šฐํŠธ ์„ค์ •
๐Ÿ“„ routes/api.php
<?php

use App\Http\Controllers\Web3AuthController;
use Illuminate\Support\Facades\Route;

Route::prefix('web3')->group(function () {
    // ์ธ์ฆ ๋ถˆํ•„์š”
    Route::post('/nonce', [Web3AuthController::class, 'requestNonce']);
    Route::post('/verify', [Web3AuthController::class, 'verify']);

    // ์ธ์ฆ ํ•„์š”
    Route::middleware('auth:web3')->group(function () {
        Route::get('/me', [Web3AuthController::class, 'me']);
        Route::post('/logout', [Web3AuthController::class, 'logout']);
    });
});
### ๐Ÿ”’ ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด ๋งˆ์ง€๋ง‰์œผ๋กœ Web3 ํ† ํฐ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ฏธ๋“ค์›จ์–ด๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.
๐Ÿ“„ app/Http/Middleware/Web3Authenticate.php
<?php

namespace App\Http\Middleware;

use Closure;
use App\Services\Web3AuthService;
use Illuminate\Http\Request;

class Web3Authenticate
{
    protected $web3Auth;

    public function __construct(Web3AuthService $web3Auth)
    {
        $this->web3Auth = $web3Auth;
    }

    public function handle(Request $request, Closure $next)
    {
        $token = $request->bearerToken();

        if (!$token) {
            return response()->json([
                'success' => false,
                'message' => '์ธ์ฆ ํ† ํฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'
            ], 401);
        }

        try {
            $user = $this->web3Auth->authenticateByToken($token);
            $request->merge(['web3_user' => $user]);
            
            return $next($request);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => '์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค.'
            ], 401);
        }
    }
}
๋ฏธ๋“ค์›จ์–ด๋ฅผ ๋“ฑ๋กํ•˜๋Š” ๊ฒƒ๋„ ์žŠ์ง€ ๋ง์ž!
๐Ÿ“„ app/Http/Kernel.php
protected $middlewareAliases = [
    // ... ๊ธฐ์กด ๋ฏธ๋“ค์›จ์–ด๋“ค
    'auth:web3' => \App\Http\Middleware\Web3Authenticate::class,
];
๐ŸŽ‰ ๋ฐฑ์—”๋“œ ๊ตฌํ˜„ ์™„๋ฃŒ!

์ด์ œ ์•ˆ์ „ํ•˜๊ณ  ํšจ์œจ์ ์ธ Web3 ์ธ์ฆ ์‹œ์Šคํ…œ์ด ์™„์„ฑ๋˜์—ˆ์–ด! ํ•ต์‹ฌ ํฌ์ธํŠธ๋ฅผ ์ •๋ฆฌํ•˜๋ฉด:

โœ… Nonce ๊ธฐ๋ฐ˜์œผ๋กœ ์žฌ์‚ฌ์šฉ ๊ณต๊ฒฉ ๋ฐฉ์ง€
โœ… ์„œ๋ช… ๊ฒ€์ฆ์œผ๋กœ ๋ณธ์ธ ํ™•์ธ
โœ… JWT ํ† ํฐ์œผ๋กœ ์„ธ์…˜ ๊ด€๋ฆฌ
โœ… ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ •์œผ๋กœ ๋ณด์•ˆ ๊ฐ•ํ™”
โœ… ๊น”๋”ํ•œ ์—๋Ÿฌ ํ•ธ๋“ค๋ง
## โšก ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๋น ๋ฅด๊ณ  ๋ถ€๋“œ๋Ÿฌ์šด UX ๋งŒ๋“ค๊ธฐ ๊ธฐ๋Šฅ์ด ์™„์„ฑ๋˜์—ˆ๋‹ค๊ณ  ๋์ด ์•„๋‹ˆ์•ผ! ์ด์ œ ์‚ฌ์šฉ์ž๊ฐ€ "์™€, ์ด๊ฑฐ ์ง„์งœ ๋น ๋ฅด๋„ค!"๋ผ๊ณ  ๋А๋‚„ ์ˆ˜ ์žˆ๋„๋ก ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•ด๋ณด์ž. ๐Ÿš€ ### ๐ŸŽฏ ํ”„๋ก ํŠธ์—”๋“œ ์ตœ์ ํ™” ์ „๋žต
1์—ฐ๊ฒฐ ์ƒํƒœ ์บ์‹ฑ

๋งค๋ฒˆ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์—ฐ๊ฒฐ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฑด ๋น„ํšจ์œจ์ ์ด์•ผ. LocalStorage๋ฅผ ํ™œ์šฉํ•ด์„œ ์ด์ „ ์—ฐ๊ฒฐ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ณ , ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ๋น ๋ฅด๊ฒŒ ๋ณต์›ํ•  ์ˆ˜ ์žˆ์–ด.
๐Ÿ“„ resources/js/web3/connection-cache.js
class ConnectionCache {
    constructor() {
        this.CACHE_KEY = 'web3_connection_cache';
        this.CACHE_DURATION = 24 * 60 * 60 * 1000; // 24์‹œ๊ฐ„
    }

    // ์—ฐ๊ฒฐ ์ •๋ณด ์ €์žฅ
    save(data) {
        const cacheData = {
            ...data,
            timestamp: Date.now()
        };
        localStorage.setItem(this.CACHE_KEY, JSON.stringify(cacheData));
    }

    // ์—ฐ๊ฒฐ ์ •๋ณด ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
    load() {
        try {
            const cached = localStorage.getItem(this.CACHE_KEY);
            if (!cached) return null;

            const data = JSON.parse(cached);
            
            // ์บ์‹œ ๋งŒ๋ฃŒ ํ™•์ธ
            if (Date.now() - data.timestamp > this.CACHE_DURATION) {
                this.clear();
                return null;
            }

            return data;
        } catch (error) {
            console.error('Cache load error:', error);
            return null;
        }
    }

    // ์บ์‹œ ์‚ญ์ œ
    clear() {
        localStorage.removeItem(this.CACHE_KEY);
    }

    // ๋น ๋ฅธ ์žฌ์—ฐ๊ฒฐ ์‹œ๋„
    async quickReconnect(connector) {
        const cached = this.load();
        if (!cached || !cached.address) return false;

        try {
            // ๋ฉ”ํƒ€๋งˆ์Šคํฌ์— ์ €์žฅ๋œ ๊ณ„์ • ํ™•์ธ
            const accounts = await connector.ethereum.request({ 
                method: 'eth_accounts' 
            });

            if (accounts.length > 0 && 
                accounts[0].toLowerCase() === cached.address.toLowerCase()) {
                return cached.address;
            }
        } catch (error) {
            console.error('Quick reconnect failed:', error);
        }

        return false;
    }
}

export default ConnectionCache;
2์š”์ฒญ ๋””๋ฐ”์šด์‹ฑ

์‚ฌ์šฉ์ž๊ฐ€ ๋ฒ„ํŠผ์„ ์—ฌ๋Ÿฌ ๋ฒˆ ํด๋ฆญํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•ด์„œ ๋””๋ฐ”์šด์‹ฑ์„ ์ ์šฉํ•ด์•ผ ํ•ด. ์ค‘๋ณต ์š”์ฒญ์„ ๋ฐฉ์ง€ํ•˜๋ฉด ์„œ๋ฒ„ ๋ถ€ํ•˜๋„ ์ค„์ด๊ณ  UX๋„ ๊ฐœ์„ ๋ผ!
๐Ÿ“„ resources/js/utils/debounce.js
class RequestDebouncer {
    constructor() {
        this.pending = new Map();
    }

    // ๋””๋ฐ”์šด์Šค๋œ ์š”์ฒญ ์‹คํ–‰
    async execute(key, fn, delay = 300) {
        // ์ด๋ฏธ ์ง„ํ–‰ ์ค‘์ธ ์š”์ฒญ์ด ์žˆ์œผ๋ฉด ๋Œ€๊ธฐ
        if (this.pending.has(key)) {
            return this.pending.get(key);
        }

        // ์ƒˆ ์š”์ฒญ ์ƒ์„ฑ
        const promise = new Promise(async (resolve, reject) => {
            try {
                await new Promise(r => setTimeout(r, delay));
                const result = await fn();
                resolve(result);
            } catch (error) {
                reject(error);
            } finally {
                this.pending.delete(key);
            }
        });

        this.pending.set(key, promise);
        return promise;
    }

    // ํŠน์ • ํ‚ค์˜ ์š”์ฒญ ์ทจ์†Œ
    cancel(key) {
        this.pending.delete(key);
    }

    // ๋ชจ๋“  ์š”์ฒญ ์ทจ์†Œ
    cancelAll() {
        this.pending.clear();
    }
}

export default new RequestDebouncer();
3ํ”„๋ฆฌ๋กœ๋”ฉ ์ „๋žต

์‚ฌ์šฉ์ž๊ฐ€ ๋ฒ„ํŠผ์— ๋งˆ์šฐ์Šค๋ฅผ ์˜ฌ๋ฆฌ๋Š” ์ˆœ๊ฐ„ ๋ฏธ๋ฆฌ ์ค€๋น„ ์ž‘์—…์„ ์‹œ์ž‘ํ•˜๋ฉด ์ฒด๊ฐ ์†๋„๊ฐ€ ํ›จ์”ฌ ๋นจ๋ผ์ ธ!
๐Ÿ“„ resources/js/web3/preloader.js
class Web3Preloader {
    constructor(connector) {
        this.connector = connector;
        this.preloadStarted = false;
    }

    // ํ˜ธ๋ฒ„ ์‹œ ํ”„๋ฆฌ๋กœ๋“œ
    onButtonHover() {
        if (this.preloadStarted) return;
        this.preloadStarted = true;

        // ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์ค€๋น„ ์ƒํƒœ ํ™•์ธ
        if (this.connector.isMetaMaskInstalled()) {
            // Provider ๋ฏธ๋ฆฌ ์ดˆ๊ธฐํ™”
            this.preloadProvider();
        }
    }

    // Provider ํ”„๋ฆฌ๋กœ๋“œ
    async preloadProvider() {
        try {
            // ๊ฐ€๋ฒผ์šด ์š”์ฒญ์œผ๋กœ ์—ฐ๊ฒฐ ์ค€๋น„
            await this.connector.ethereum.request({ 
                method: 'eth_chainId' 
            });
            
            console.log('Provider preloaded');
        } catch (error) {
            // ์—๋Ÿฌ๋Š” ๋ฌด์‹œ (์‹ค์ œ ์—ฐ๊ฒฐ ์‹œ ์ฒ˜๋ฆฌ)
            console.log('Preload skipped:', error.message);
        }
    }

    // ๋„คํŠธ์›Œํฌ ์ •๋ณด ํ”„๋ฆฌํŽ˜์น˜
    async prefetchNetworkInfo() {
        try {
            const [chainId, accounts] = await Promise.all([
                this.connector.ethereum.request({ method: 'eth_chainId' }),
                this.connector.ethereum.request({ method: 'eth_accounts' })
            ]);

            return { chainId, accounts };
        } catch (error) {
            return null;
        }
    }
}

export default Web3Preloader;
### ๐Ÿ”ง ๋ฐฑ์—”๋“œ ์ตœ์ ํ™” ์ „๋žต
1Redis ์บ์‹ฑ

Nonce์™€ ์„ธ์…˜ ์ •๋ณด๋ฅผ Redis์— ์บ์‹ฑํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ถ€ํ•˜๋ฅผ ํฌ๊ฒŒ ์ค„์ผ ์ˆ˜ ์žˆ์–ด.
๐Ÿ“„ app/Services/Web3CacheService.php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Cache;

class Web3CacheService
{
    private $noncePrefix = 'web3:nonce:';
    private $sessionPrefix = 'web3:session:';
    private $userPrefix = 'web3:user:';

    /**
     * Nonce ์บ์‹ฑ
     */
    public function cacheNonce($address, $nonce, $ttl = 300)
    {
        $key = $this->noncePrefix . strtolower($address);
        Cache::put($key, $nonce, $ttl);
    }

    public function getNonce($address)
    {
        $key = $this->noncePrefix . strtolower($address);
        return Cache::get($key);
    }

    public function deleteNonce($address)
    {
        $key = $this->noncePrefix . strtolower($address);
        Cache::forget($key);
    }

    /**
     * ์„ธ์…˜ ์บ์‹ฑ
     */
    public function cacheSession($token, $userId, $ttl = 86400)
    {
        $key = $this->sessionPrefix . $token;
        Cache::put($key, $userId, $ttl);
    }

    public function getSession($token)
    {
        $key = $this->sessionPrefix . $token;
        return Cache::get($key);
    }

    public function deleteSession($token)
    {
        $key = $this->sessionPrefix . $token;
        Cache::forget($key);
    }

    /**
     * ์‚ฌ์šฉ์ž ์ •๋ณด ์บ์‹ฑ
     */
    public function cacheUser($address, $userData, $ttl = 3600)
    {
        $key = $this->userPrefix . strtolower($address);
        Cache::put($key, $userData, $ttl);
    }

    public function getUser($address)
    {
        $key = $this->userPrefix . strtolower($address);
        return Cache::get($key);
    }

    public function invalidateUser($address)
    {
        $key = $this->userPrefix . strtolower($address);
        Cache::forget($key);
    }

    /**
     * Rate Limiting
     */
    public function checkRateLimit($address, $maxAttempts = 5, $decayMinutes = 1)
    {
        $key = 'rate_limit:' . strtolower($address);
        $attempts = Cache::get($key, 0);

        if ($attempts >= $maxAttempts) {
            return false;
        }

        Cache::put($key, $attempts + 1, $decayMinutes * 60);
        return true;
    }
}
2๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ์ตœ์ ํ™”

๋ถˆํ•„์š”ํ•œ ์ฟผ๋ฆฌ๋ฅผ ์ค„์ด๊ณ , ์ธ๋ฑ์Šค๋ฅผ ์ ์ ˆํžˆ ํ™œ์šฉํ•˜๋ฉด ์‘๋‹ต ์†๋„๊ฐ€ ํฌ๊ฒŒ ๊ฐœ์„ ๋ผ.
๐Ÿ“„ app/Services/OptimizedWeb3AuthService.php
<?php

namespace App\Services;

use App\Models\Web3User;
use Illuminate\Support\Facades\DB;

class OptimizedWeb3AuthService extends Web3AuthService
{
    protected $cache;

    public function __construct(Web3CacheService $cache)
    {
        $this->cache = $cache;
    }

    /**
     * ์ตœ์ ํ™”๋œ Nonce ์š”์ฒญ
     */
    public function requestNonce($walletAddress)
    {
        // Rate Limiting ์ฒดํฌ
        if (!$this->cache->checkRateLimit($walletAddress)) {
            throw new \Exception('Too many requests. Please try again later.');
        }

        // ์บ์‹œ์—์„œ ๋จผ์ € ํ™•์ธ
        $cachedNonce = $this->cache->getNonce($walletAddress);
        if ($cachedNonce) {
            return [
                'nonce' => $cachedNonce,
                'message' => $this->generateMessage($walletAddress, $cachedNonce)
            ];
        }

        // DB ์กฐํšŒ (์บ์‹œ ๋ฏธ์Šค)
        $walletAddress = $this->toChecksumAddress($walletAddress);
        
        $user = Web3User::firstOrCreate(
            ['wallet_address' => $walletAddress],
            ['metadata' => []]
        );

        $nonce = $user->generateNonce();

        // ์บ์‹œ์— ์ €์žฅ
        $this->cache->cacheNonce($walletAddress, $nonce, 300);

        return [
            'nonce' => $nonce,
            'message' => $this->generateMessage($walletAddress, $nonce)
        ];
    }

    /**
     * ์ตœ์ ํ™”๋œ ์„œ๋ช… ๊ฒ€์ฆ
     */
    public function verifySignature($walletAddress, $signature, $nonce, $request)
    {
        $walletAddress = $this->toChecksumAddress($walletAddress);

        // ์บ์‹œ์—์„œ Nonce ํ™•์ธ
        $cachedNonce = $this->cache->getNonce($walletAddress);
        if ($cachedNonce !== $nonce) {
            throw new \Exception('Invalid or expired nonce');
        }

        // ์บ์‹œ์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด ํ™•์ธ
        $userData = $this->cache->getUser($walletAddress);
        
        if (!$userData) {
            $user = Web3User::where('wallet_address', $walletAddress)->first();
            if (!$user) {
                throw new \Exception('User not found');
            }
            
            // ์บ์‹œ์— ์ €์žฅ
            $this->cache->cacheUser($walletAddress, $user->toArray());
        } else {
            $user = Web3User::find($userData['id']);
        }

        // ์„œ๋ช… ๊ฒ€์ฆ
        $message = $this->generateMessage($walletAddress, $nonce);
        $recoveredAddress = $this->recoverAddress($message, $signature);

        if (strtolower($recoveredAddress) !== strtolower($walletAddress)) {
            throw new \Exception('Signature verification failed');
        }

        // Nonce ๋ฌดํšจํ™”
        $this->cache->deleteNonce($walletAddress);

        // ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฌถ์–ด์„œ ์ฒ˜๋ฆฌ
        DB::transaction(function () use ($user) {
            $user->invalidateNonce();
            $user->updateLastLogin();
        });

        // ์„ธ์…˜ ์ƒ์„ฑ
        $session = Web3Session::createToken($user->id, $request);

        // ์„ธ์…˜ ์บ์‹ฑ
        $this->cache->cacheSession($session->token, $user->id, 86400);

        return [
            'user' => $user,
            'token' => $session->token,
            'expires_at' => $session->expires_at
        ];
    }

    /**
     * ์ตœ์ ํ™”๋œ ํ† ํฐ ์ธ์ฆ
     */
    public function authenticateByToken($token)
    {
        // ์บ์‹œ์—์„œ ๋จผ์ € ํ™•์ธ
        $userId = $this->cache->getSession($token);
        
        if ($userId) {
            $user = Web3User::find($userId);
            if ($user) {
                return $user;
            }
        }

        // ์บ์‹œ ๋ฏธ์Šค - DB ์กฐํšŒ
        $session = Web3Session::findByToken($token);
        if (!$session) {
            throw new \Exception('Invalid or expired token');
        }

        // ์บ์‹œ ์—…๋ฐ์ดํŠธ
        $this->cache->cacheSession($token, $session->user_id, 86400);

        return $session->user;
    }
}
๐Ÿ“Š ์„ฑ๋Šฅ ๊ฐœ์„  ํšจ๊ณผ

์ด๋Ÿฐ ์ตœ์ ํ™”๋ฅผ ์ ์šฉํ•˜๋ฉด ์‹ค์ œ๋กœ ์–ผ๋งˆ๋‚˜ ๋นจ๋ผ์งˆ๊นŒ?

ํ•ญ๋ชฉ ์ตœ์ ํ™” ์ „ ์ตœ์ ํ™” ํ›„ ๊ฐœ์„ ์œจ
Nonce ์š”์ฒญ ~150ms ~20ms 87% โฌ‡๏ธ
์„œ๋ช… ๊ฒ€์ฆ ~300ms ~80ms 73% โฌ‡๏ธ
ํ† ํฐ ์ธ์ฆ ~100ms ~5ms 95% โฌ‡๏ธ
ํŽ˜์ด์ง€ ๋กœ๋“œ ~2s ~0.5s 75% โฌ‡๏ธ

์ฒด๊ฐ ์†๋„๊ฐ€ 4๋ฐฐ ์ด์ƒ ๋นจ๋ผ์ง€๋Š” ๊ฑฐ์•ผ! ๐Ÿš€
## ๐ŸŽจ ๊ณ ๊ธ‰ UX ํŒจํ„ด: ์‚ฌ์šฉ์ž๋ฅผ ๊ฐ๋™์‹œํ‚ค๋Š” ๋””ํ…Œ์ผ ๊ธฐ๋ณธ์ ์ธ ๊ธฐ๋Šฅ๊ณผ ์„ฑ๋Šฅ ์ตœ์ ํ™”๊ฐ€ ๋๋‚ฌ๋‹ค๋ฉด, ์ด์ œ ์‚ฌ์šฉ์ž๋ฅผ "์™€!" ํ•˜๊ณ  ๊ฐํƒ„ํ•˜๊ฒŒ ๋งŒ๋“ค ๋””ํ…Œ์ผ์„ ์ถ”๊ฐ€ํ•ด๋ณด์ž! โœจ ### ๐ŸŒˆ ํ”„๋กœ๊ทธ๋ ˆ์Šค ์ธ๋””์ผ€์ดํ„ฐ ๋‹จ์ˆœํ•œ ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ๊ฐ€ ์•„๋‹ˆ๋ผ, ๊ฐ ๋‹จ๊ณ„๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ๋ณด์—ฌ์ฃผ๋Š” ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ”๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.
๐Ÿ“„ resources/views/components/connection-progress.blade.php
<div class="progress-container">
    <div class="progress-steps">
        <div class="progress-step" data-step="1">
            <div class="step-icon">๐ŸฆŠ</div>
            <div class="step-label">๋ฉ”ํƒ€๋งˆ์Šคํฌ ์—ฐ๊ฒฐ</div>
            <div class="step-status"></div>
        </div>
        
        <div class="progress-line"></div>
        
        <div class="progress-step" data-step="2">
            <div class="step-icon">๐Ÿ”</div>
            <div class="step-label">์„œ๋ช… ์š”์ฒญ</div>
            <div class="step-status"></div>
        </div>
        
        <div class="progress-line"></div>
        
        <div class="progress-step" data-step="3">
            <div class="step-icon">โœ…</div>
            <div class="step-label">์ธ์ฆ ์™„๋ฃŒ</div>
            <div class="step-status"></div>
        </div>
    </div>
    
    <div class="progress-message" id="progress-message">
        ๋ฉ”ํƒ€๋งˆ์Šคํฌ๋ฅผ ์—ด์–ด์ฃผ์„ธ์š”...
    </div>
</div>
๐Ÿ“„ resources/js/components/progress-tracker.js
class ProgressTracker {
    constructor() {
        this.currentStep = 0;
        this.steps = [
            { id: 1, message: '๋ฉ”ํƒ€๋งˆ์Šคํฌ๋ฅผ ์—ด์–ด์ฃผ์„ธ์š”...' },
            { id: 2, message: '๋ฉ”์‹œ์ง€์— ์„œ๋ช…ํ•ด์ฃผ์„ธ์š”...' },
            { id: 3, message: '์ธ์ฆ์„ ์™„๋ฃŒํ•˜๋Š” ์ค‘...' }
        ];
    }

    // ๋‹จ๊ณ„ ์ง„ํ–‰
    advance(stepId) {
        this.currentStep = stepId;
        this.updateUI();
    }

    // UI ์—…๋ฐ์ดํŠธ
    updateUI() {
        const steps = document.querySelectorAll('.progress-step');
        const lines = document.querySelectorAll('.progress-line');
        
        steps.forEach((step, index) => {
            const stepNum = index + 1;
            const status = step.querySelector('.step-status');
            
            if (stepNum < this.currentStep) {
                // ์™„๋ฃŒ๋œ ๋‹จ๊ณ„
                step.classList.add('completed');
                step.classList.remove('active', 'pending');
                status.textContent = 'โœ“';
                status.style.color = '#00b894';
            } else if (stepNum === this.currentStep) {
                // ํ˜„์žฌ ์ง„ํ–‰ ์ค‘
                step.classList.add('active');
                step.classList.remove('completed', 'pending');
                status.innerHTML = '<div class="spinner-small"></div>';
            } else {
                // ๋Œ€๊ธฐ ์ค‘
                step.classList.add('pending');
                step.classList.remove('completed', 'active');
                status.textContent = '';
            }
        });

        // ์—ฐ๊ฒฐ์„  ์• ๋‹ˆ๋ฉ”์ด์…˜
        lines.forEach((line, index) => {
            if (index < this.currentStep - 1) {
                line.classList.add('completed');
            } else {
                line.classList.remove('completed');
            }
        });

        // ๋ฉ”์‹œ์ง€ ์—…๋ฐ์ดํŠธ
        const step = this.steps.find(s => s.id === this.currentStep);
        if (step) {
            document.getElementById('progress-message').textContent = step.message;
        }
    }

    // ์—๋Ÿฌ ํ‘œ์‹œ
    showError(stepId, message) {
        const step = document.querySelector(`[data-step="${stepId}"]`);
        if (step) {
            step.classList.add('error');
            const status = step.querySelector('.step-status');
            status.textContent = 'โœ—';
            status.style.color = '#ff7675';
        }
        
        document.getElementById('progress-message').textContent = message;
        document.getElementById('progress-message').style.color = '#ff7675';
    }

    // ๋ฆฌ์…‹
    reset() {
        this.currentStep = 0;
        document.querySelectorAll('.progress-step').forEach(step => {
            step.classList.remove('completed', 'active', 'pending', 'error');
        });
        document.querySelectorAll('.progress-line').forEach(line => {
            line.classList.remove('completed');
        });
    }
}

export default ProgressTracker;
### ๐ŸŽญ ์Šค๋งˆํŠธ ์—๋Ÿฌ ๋ณต๊ตฌ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ๋‹จ์ˆœํžˆ ๋ฉ”์‹œ์ง€๋งŒ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ์ž๋™์œผ๋กœ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ œ์‹œํ•˜๊ณ  ๋ณต๊ตฌ๋ฅผ ์‹œ๋„ํ•˜๋Š” ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณด์ž!
๐Ÿ“„ resources/js/utils/error-recovery.js
class ErrorRecovery {
    constructor(connector) {
        this.connector = connector;
        this.recoveryStrategies = {
            4001: this.handleUserRejection.bind(this),
            4100: this.handleUnsupportedMethod.bind(this),
            4200: this.handleLockedWallet.bind(this),
            4900: this.handleDisconnected.bind(this),
            '-32002': this.handlePendingRequest.bind(this),
            '-32603': this.handleInternalError.bind(this)
        };
    }

    // ์—๋Ÿฌ ๋ณต๊ตฌ ์‹œ๋„
    async recover(error, context) {
        const code = error.code?.toString();
        const strategy = this.recoveryStrategies[code];

        if (strategy) {
            return await strategy(error, context);
        }

        return this.handleGenericError(error);
    }

    // ์‚ฌ์šฉ์ž ๊ฑฐ๋ถ€ ์ฒ˜๋ฆฌ
    async handleUserRejection(error, context) {
        return {
            canRetry: true,
            message: '์—ฐ๊ฒฐ์„ ๊ฑฐ๋ถ€ํ•˜์…จ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•˜์‹œ๊ฒ ์–ด์š”?',
            action: 'retry',
            actionLabel: '๋‹ค์‹œ ์‹œ๋„',
            tips: [
                '๋ฉ”ํƒ€๋งˆ์Šคํฌ ํŒ์—…์—์„œ "์—ฐ๊ฒฐ" ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ์ฃผ์„ธ์š”',
                'ํŒ์—…์ด ๋ณด์ด์ง€ ์•Š์œผ๋ฉด ๋ธŒ๋ผ์šฐ์ € ์ƒ๋‹จ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”'
            ]
        };
    }

    // ์ง€๊ฐ‘ ์ž ๊ธˆ ์ฒ˜๋ฆฌ
    async handleLockedWallet(error, context) {
        return {
            canRetry: true,
            message: '๋ฉ”ํƒ€๋งˆ์Šคํฌ๊ฐ€ ์ž ๊ฒจ์žˆ์Šต๋‹ˆ๋‹ค',
            action: 'unlock',
            actionLabel: '์ž ๊ธˆ ํ•ด์ œ ์•ˆ๋‚ด',
            tips: [
                '๋ฉ”ํƒ€๋งˆ์Šคํฌ ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์„ ํด๋ฆญํ•˜์„ธ์š”',
                '๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์—ฌ ์ž ๊ธˆ์„ ํ•ด์ œํ•˜์„ธ์š”',
                '์ž ๊ธˆ ํ•ด์ œ ํ›„ ๋‹ค์‹œ ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•˜์„ธ์š”'
            ],
            autoRetry: true,
            retryDelay: 2000
        };
    }

    // ๋Œ€๊ธฐ ์ค‘์ธ ์š”์ฒญ ์ฒ˜๋ฆฌ
    async handlePendingRequest(error, context) {
        return {
            canRetry: false,
            message: '์ด๋ฏธ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์š”์ฒญ์ด ์ง„ํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค',
            action: 'open_metamask',
            actionLabel: '๋ฉ”ํƒ€๋งˆ์Šคํฌ ์—ด๊ธฐ',
            tips: [
                '๋ฉ”ํƒ€๋งˆ์Šคํฌ ํŒ์—…์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”',
                'ํŒ์—…์ด ๋ณด์ด์ง€ ์•Š์œผ๋ฉด ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ ์•„์ด์ฝ˜์„ ํด๋ฆญํ•˜์„ธ์š”',
                '์ด์ „ ์š”์ฒญ์„ ์™„๋ฃŒํ•˜๊ฑฐ๋‚˜ ์ทจ์†Œํ•œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”'
            ],
            highlight: true
        };
    }

    // ์—ฐ๊ฒฐ ๋Š๊น€ ์ฒ˜๋ฆฌ
    async handleDisconnected(error, context) {
        // ์ž๋™ ์žฌ์—ฐ๊ฒฐ ์‹œ๋„
        try {
            await this.connector.ethereum.request({ 
                method: 'eth_requestAccounts' 
            });
            
            return {
                recovered: true,
                message: '์—ฐ๊ฒฐ์ด ๋ณต๊ตฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!'
            };
        } catch (reconnectError) {
            return {
                canRetry: true,
                message: '๋ฉ”ํƒ€๋งˆ์Šคํฌ ์—ฐ๊ฒฐ์ด ๋Š์–ด์กŒ์Šต๋‹ˆ๋‹ค',
                action: 'reconnect',
                actionLabel: '์žฌ์—ฐ๊ฒฐ',
                tips: [
                    '์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”',
                    '๋ฉ”ํƒ€๋งˆ์Šคํฌ๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”'
                ]
            };
        }
    }

    // ๋‚ด๋ถ€ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
    async handleInternalError(error, context) {
        // ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ ์ œ์•ˆ
        return {
            canRetry: true,
            message: '์ผ์‹œ์ ์ธ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค',
            action: 'refresh',
            actionLabel: 'ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ',
            tips: [
                'ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜๋ฉด ํ•ด๊ฒฐ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค',
                '๋ฌธ์ œ๊ฐ€ ๊ณ„์†๋˜๋ฉด ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์žฌ์‹œ์ž‘ํ•ด๋ณด์„ธ์š”'
            ],
            severity: 'high'
        };
    }

    // ์ผ๋ฐ˜ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
    handleGenericError(error) {
        return {
            canRetry: true,
            message: `์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ${error.message}`,
            action: 'retry',
            actionLabel: '๋‹ค์‹œ ์‹œ๋„',
            tips: [
                '์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”',
                '๋ฌธ์ œ๊ฐ€ ๊ณ„์†๋˜๋ฉด ๊ณ ๊ฐ์„ผํ„ฐ์— ๋ฌธ์˜ํ•ด์ฃผ์„ธ์š”'
            ]
        };
    }

    // ๋ณต๊ตฌ UI ํ‘œ์‹œ
    showRecoveryUI(recovery) {
        const container = document.createElement('div');
        container.className = 'recovery-modal';
        
        container.innerHTML = `
            <div class="recovery-content ${recovery.severity === 'high' ? 'high-severity' : ''}">
                <div class="recovery-icon">${recovery.recovered ? 'โœ…' : 'โš ๏ธ'}</div>
                <h3>${recovery.message}</h3>
                
                ${recovery.tips ? `
                    <div class="recovery-tips">
                        <p><strong>ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:</strong></p>
                        <ul>
                            ${recovery.tips.map(tip => `<li>${tip}</li>`).join('')}
                        </ul>
                    </div>
                ` : ''}
                
                ${recovery.canRetry ? `
                    <button class="recovery-action" data-action="${recovery.action}">
                        ${recovery.actionLabel}
                    </button>
                ` : ''}
                
                <button class="recovery-close">๋‹ซ๊ธฐ</button>
            </div>
        `;
        
        document.body.appendChild(container);
        
        // ์ž๋™ ์žฌ์‹œ๋„
        if (recovery.autoRetry) {
            setTimeout(() => {
                this.executeRecoveryAction(recovery.action);
                container.remove();
            }, recovery.retryDelay);
        }
        
        return container;
    }

    // ๋ณต๊ตฌ ์•ก์…˜ ์‹คํ–‰
    async executeRecoveryAction(action) {
        switch (action) {
            case 'retry':
                window.walletUI.connectWallet();
                break;
            case 'reconnect':
                window.walletUI.connectWallet();
                break;
            case 'refresh':
                window.location.reload();
                break;
            case 'open_metamask':
                // ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์—ด๊ธฐ ์‹œ๋„
                try {
                    await this.connector.ethereum.request({ 
                        method: 'wallet_requestPermissions',
                        params: [{ eth_accounts: {} }]
                    });
                } catch (e) {
                    console.log('Could not open MetaMask');
                }
                break;
        }
    }
}

export default ErrorRecovery;
๐Ÿ’ก UX ๋””ํ…Œ์ผ์˜ ํž˜

์ด๋Ÿฐ ์„ธ์‹ฌํ•œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ์ž๋Š” "์ด ์„œ๋น„์Šค๋Š” ๋‚˜๋ฅผ ๋ฐฐ๋ คํ•˜๋Š”๊ตฌ๋‚˜"๋ผ๊ณ  ๋А๋ผ๊ฒŒ ๋ผ. ๋‹จ์ˆœํžˆ "์—๋Ÿฌ ๋ฐœ์ƒ"์ด๋ผ๊ณ  ๋˜์ ธ์ฃผ๋Š” ๊ฒƒ๊ณผ "์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์–ด์š”"๋ผ๊ณ  ์•ˆ๋‚ดํ•˜๋Š” ๊ฒƒ์˜ ์ฐจ์ด๋Š” ์—„์ฒญ๋‚˜๊ฑฐ๋“ ! ๐ŸŽฏ
### ๐ŸŽฌ ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ํŠธ๋žœ์ง€์…˜ ๋ถ€๋“œ๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜์€ UX์˜ ์™„์„ฑ๋„๋ฅผ ํ•œ์ธต ๋†’์—ฌ์ค˜. CSS์™€ JavaScript๋ฅผ ์กฐํ•ฉํ•ด์„œ ์ž์—ฐ์Šค๋Ÿฌ์šด ์ „ํ™˜ ํšจ๊ณผ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.
๐Ÿ“„ resources/css/wallet-animations.css
/* ํŽ˜์ด๋“œ ์ธ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
@keyframes fadeIn {
    from {
        opacity: 0;
        transform: translateY(20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* ์Šคํ”ผ๋„ˆ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}

/* ํŽ„์Šค ์• ๋‹ˆ๋ฉ”์ด์…˜ */
@keyframes pulse {
    0%, 100% {
        opacity: 1;
        transform: scale(1);
    }
    50% {
        opacity: 0.7;
        transform: scale(1.05);
    }
}

/* ์Šฌ๋ผ์ด๋“œ ์ธ */
@keyframes slideIn {
    from {
        transform: translateX(-100%);
        opacity: 0;
    }
    to {
        transform: translateX(0);
        opacity: 1;
    }
}

/* ๋ฐ”์šด์Šค */
@keyframes bounce {
    0%, 20%, 50%, 80%, 100% {
        transform: translateY(0);
    }
    40% {
        transform: translateY(-10px);
    }
    60% {
        transform: translateY(-5px);
    }
}

/* ์ƒํƒœ ์ „ํ™˜ */
.wallet-state {
    animation: fadeIn 0.3s ease-out;
}

/* ๋ฒ„ํŠผ ํ˜ธ๋ฒ„ ํšจ๊ณผ */
.btn-primary {
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    position: relative;
    overflow: hidden;
}

.btn-primary::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 0;
    height: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.3);
    transform: translate(-50%, -50%);
    transition: width 0.6s, height 0.6s;
}

.btn-primary:hover::before {
    width: 300px;
    height: 300px;
}

.btn-primary:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 16px rgba(108, 92, 231, 0.3);
}

/* ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ผ์ธ */
.progress-line {
    position: relative;
    width: 60px;
    height: 2px;
    background: #e1e8ed;
    margin: 0 10px;
}

.progress-line::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 0;
    background: linear-gradient(90deg, #6c5ce7, #a29bfe);
    transition: width 0.5s ease-out;
}

.progress-line.completed::after {
    width: 100%;
}

/* ์Šคํ”ผ๋„ˆ */
.spinner {
    width: 40px;
    height: 40px;
    border: 4px solid #f3f3f3;
    border-top: 4px solid #6c5ce7;
    border-radius: 50%;
    animation: spin 1s linear infinite;
    margin: 20px auto;
}

/* ํ† ์ŠคํŠธ ์•Œ๋ฆผ */
.toast {
    position: fixed;
    bottom: -100px;
    left: 50%;
    transform: translateX(-50%);
    background: white;
    padding: 16px 24px;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    transition: bottom 0.3s ease-out;
    z-index: 9999;
}

.toast.show {
    bottom: 30px;
}

.toast-success {
    border-left: 4px solid #00b894;
}

.toast-error {
    border-left: 4px solid #ff7675;
}

.toast-info {
    border-left: 4px solid #74b9ff;
}

/* ์ง€๊ฐ‘ ์ฃผ์†Œ ๋ณต์‚ฌ ํšจ๊ณผ */
.wallet-address {
    cursor: pointer;
    transition: all 0.2s;
}

.wallet-address:hover {
    background: #f8f9fa;
    padding: 4px 8px;
    border-radius: 4px;
}

.wallet-address.copied {
    animation: pulse 0.5s;
}

/* ์—ฐ๊ฒฐ ์„ฑ๊ณต ์ถ•ํ•˜ ํšจ๊ณผ */
@keyframes celebrate {
    0% {
        transform: scale(0) rotate(0deg);
        opacity: 0;
    }
    50% {
        transform: scale(1.2) rotate(180deg);
        opacity: 1;
    }
    100% {
        transform: scale(1) rotate(360deg);
        opacity: 1;
    }
}

.celebration-icon {
    animation: celebrate 0.6s ease-out;
}
## ๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™”: ์ž‘์€ ํ™”๋ฉด์—์„œ๋„ ์™„๋ฒฝํ•˜๊ฒŒ ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์€ ๋ฐ์Šคํฌํ†ฑ๊ณผ ์™„์ „ํžˆ ๋‹ค๋ฅธ ์ ‘๊ทผ์ด ํ•„์š”ํ•ด. ํŠนํžˆ ๋ฉ”ํƒ€๋งˆ์Šคํฌ๋Š” ๋ชจ๋ฐ”์ผ์—์„œ ์ธ์•ฑ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋”์šฑ ์‹ ๊ฒฝ ์จ์•ผ ํ•˜์ง€! ๐Ÿ“ฑ ### ๐ŸŽฏ ๋ชจ๋ฐ”์ผ ๊ฐ์ง€ ๋ฐ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ
๐Ÿ“„ resources/js/utils/device-detector.js
class DeviceDetector {
    constructor() {
        this.userAgent = navigator.userAgent || navigator.vendor || window.opera;
        this.isMobile = this.detectMobile();
        this.isMetaMaskBrowser = this.detectMetaMaskBrowser();
        this.platform = this.detectPlatform();
    }

    // ๋ชจ๋ฐ”์ผ ๊ธฐ๊ธฐ ๊ฐ์ง€
    detectMobile() {
        return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(this.userAgent);
    }

    // ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์ธ์•ฑ ๋ธŒ๋ผ์šฐ์ € ๊ฐ์ง€
    detectMetaMaskBrowser() {
        return typeof window.ethereum !== 'undefined' && 
               window.ethereum.isMetaMask && 
               this.isMobile;
    }

    // ํ”Œ๋žซํผ ๊ฐ์ง€
    detectPlatform() {
        if (/iPad|iPhone|iPod/.test(this.userAgent)) {
            return 'ios';
        } else if (/Android/.test(this.userAgent)) {
            return 'android';
        }
        return 'desktop';
    }

    // ๋”ฅ๋งํฌ ์ƒ์„ฑ
    getDeepLink(url) {
        const dappUrl = encodeURIComponent(url || window.location.href);
        
        if (this.platform === 'ios') {
            return `https://metamask.app.link/dapp/${dappUrl}`;
        } else if (this.platform === 'android') {
            return `https://metamask.app.link/dapp/${dappUrl}`;
        }
        
        return null;
    }

    // ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์•ฑ ์„ค์น˜ ๋งํฌ
    getInstallLink() {
        if (this.platform === 'ios') {
            return 'https://apps.apple.com/app/metamask/id1438144202';
        } else if (this.platform === 'android') {
            return 'https://play.google.com/store/apps/details?id=io.metamask';
        }
        return 'https://metamask.io/download/';
    }

    // ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™” ์„ค์ • ์ ์šฉ
    applyMobileOptimizations() {
        if (!this.isMobile) return;

        // ๋ทฐํฌํŠธ ์„ค์ •
        const viewport = document.querySelector('meta[name="viewport"]');
        if (viewport) {
            viewport.setAttribute('content', 
                'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'
            );
        }

        // ํ„ฐ์น˜ ์ด๋ฒคํŠธ ์ตœ์ ํ™”
        document.body.style.touchAction = 'manipulation';
        
        // ๋ชจ๋ฐ”์ผ ์ „์šฉ ํด๋ž˜์Šค ์ถ”๊ฐ€
        document.body.classList.add('mobile-device');
        document.body.classList.add(`platform-${this.platform}`);
    }
}

export default DeviceDetector;
### ๐Ÿ“ฒ ๋ชจ๋ฐ”์ผ ์ „์šฉ UI ์ปดํฌ๋„ŒํŠธ ๋ชจ๋ฐ”์ผ์—์„œ๋Š” ๋ฒ„ํŠผ ํฌ๊ธฐ, ๊ฐ„๊ฒฉ, ํ„ฐ์น˜ ์˜์—ญ ๋“ฑ์„ ๋” ํฌ๊ฒŒ ๋งŒ๋“ค์–ด์•ผ ํ•ด.
๐Ÿ“„ resources/js/components/mobile-wallet-ui.js
import DeviceDetector from '../utils/device-detector.js';
import MetaMaskConnector from '../web3/metamask.js';

class MobileWalletUI {
    constructor() {
        this.detector = new DeviceDetector();
        this.connector = new MetaMaskConnector();
        this.init();
    }

    init() {
        if (!this.detector.isMobile) return;

        this.detector.applyMobileOptimizations();
        this.setupMobileUI();
        this.handleDeepLinking();
    }

    // ๋ชจ๋ฐ”์ผ UI ์„ค์ •
    setupMobileUI() {
        // ํ’€์Šคํฌ๋ฆฐ ๋ชจ๋‹ฌ ์‚ฌ์šฉ
        const container = document.getElementById('wallet-connect-container');
        if (container) {
            container.classList.add('mobile-fullscreen');
        }

        // ํ„ฐ์น˜ ์ œ์Šค์ฒ˜ ์ถ”๊ฐ€
        this.addSwipeGestures();
        
        // ํ•˜๋‹จ ์‹œํŠธ ์Šคํƒ€์ผ ์ ์šฉ
        this.applyBottomSheetStyle();
    }

    // ๋”ฅ๋งํ‚น ์ฒ˜๋ฆฌ
    handleDeepLinking() {
        // ์ผ๋ฐ˜ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ ‘์†ํ•œ ๊ฒฝ์šฐ
        if (!this.detector.isMetaMaskBrowser && !window.ethereum) {
            this.showDeepLinkPrompt();
        }
    }

    // ๋”ฅ๋งํฌ ์•ˆ๋‚ด ํ‘œ์‹œ
    showDeepLinkPrompt() {
        const prompt = document.createElement('div');
        prompt.className = 'deeplink-prompt';
        prompt.innerHTML = `
            <div class="deeplink-content">
                <div class="deeplink-icon">๐ŸฆŠ</div>
                <h3>๋ฉ”ํƒ€๋งˆ์Šคํฌ ์•ฑ์—์„œ ์—ด๊ธฐ</h3>
                <p>์ด ํŽ˜์ด์ง€๋ฅผ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์•ฑ์˜ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์—ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.</p>
                
                <button id="open-in-metamask" class="btn-primary btn-large">
                    ๋ฉ”ํƒ€๋งˆ์Šคํฌ์—์„œ ์—ด๊ธฐ
                </button>
                
                <div class="deeplink-divider">๋˜๋Š”</div>
                
                <button id="install-metamask-mobile" class="btn-secondary btn-large">
                    ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์•ฑ ์„ค์น˜ํ•˜๊ธฐ
                </button>
                
                <div class="deeplink-help">
                    <p><strong>์‚ฌ์šฉ ๋ฐฉ๋ฒ•:</strong></p>
                    <ol>
                        <li>๋ฉ”ํƒ€๋งˆ์Šคํฌ ์•ฑ์„ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค</li>
                        <li>์•ฑ ๋‚ด ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์—ฝ๋‹ˆ๋‹ค</li>
                        <li>์ด ํŽ˜์ด์ง€ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค</li>
                    </ol>
                </div>
            </div>
        `;

        document.body.appendChild(prompt);

        // ์ด๋ฒคํŠธ ๋ฐ”์ธ๋”ฉ
        document.getElementById('open-in-metamask').addEventListener('click', () => {
            const deepLink = this.detector.getDeepLink();
            if (deepLink) {
                window.location.href = deepLink;
            }
        });

        document.getElementById('install-metamask-mobile').addEventListener('click', () => {
            window.location.href = this.detector.getInstallLink();
        });
    }

    // ์Šค์™€์ดํ”„ ์ œ์Šค์ฒ˜ ์ถ”๊ฐ€
    addSwipeGestures() {
        let touchStartY = 0;
        let touchEndY = 0;

        const modal = document.querySelector('.wallet-container');
        if (!modal) return;

        modal.addEventListener('touchstart', (e) => {
            touchStartY = e.changedTouches[0].screenY;
        }, { passive: true });

        modal.addEventListener('touchend', (e) => {
            touchEndY = e.changedTouches[0].screenY;
            this.handleSwipe(touchStartY, touchEndY);
        }, { passive: true });
    }

    // ์Šค์™€์ดํ”„ ์ฒ˜๋ฆฌ
    handleSwipe(startY, endY) {
        const swipeDistance = startY - endY;
        const threshold = 50;

        if (Math.abs(swipeDistance) > threshold) {
            if (swipeDistance > 0) {
                // ์œ„๋กœ ์Šค์™€์ดํ”„
                this.expandModal();
            } else {
                // ์•„๋ž˜๋กœ ์Šค์™€์ดํ”„
                this.minimizeModal();
            }
        }
    }

    // ํ•˜๋‹จ ์‹œํŠธ ์Šคํƒ€์ผ
    applyBottomSheetStyle() {
        const style = document.createElement('style');
        style.textContent = `
            .mobile-fullscreen {
                position: fixed;
                bottom: 0;
                left: 0;
                right: 0;
                max-height: 90vh;
                background: white;
                border-radius: 20px 20px 0 0;
                box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
                transition: transform 0.3s ease-out;
                z-index: 9999;
            }

            .mobile-fullscreen::before {
                content: '';
                display: block;
                width: 40px;
                height: 4px;
                background: #ddd;
                border-radius: 2px;
                margin: 12px auto;
            }

            .btn-large {
                min-height: 56px;
                font-size: 18px;
                padding: 16px 32px;
                width: 100%;
                margin: 8px 0;
            }

            .deeplink-prompt {
                position: fixed;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                background: rgba(0, 0, 0, 0.5);
                display: flex;
                align-items: flex-end;
                z-index: 10000;
                animation: fadeIn 0.3s;
            }

            .deeplink-content {
                background: white;
                border-radius: 20px 20px 0 0;
                padding: 32px 24px;
                width: 100%;
                max-height: 90vh;
                overflow-y: auto;
            }

            .deeplink-icon {
                font-size: 64px;
                text-align: center;
                margin-bottom: 16px;
            }

            .deeplink-divider {
                text-align: center;
                color: #999;
                margin: 24px 0;
                position: relative;
            }

            .deeplink-divider::before,
            .deeplink-divider::after {
                content: '';
                position: absolute;
                top: 50%;
                width: 40%;
                height: 1px;
                background: #ddd;
            }

            .deeplink-divider::before {
                left: 0;
            }

            .deeplink-divider::after {
                right: 0;
            }

            .deeplink-help {
                margin-top: 32px;
                padding: 20px;
                background: #f8f9fa;
                border-radius: 12px;
            }

            .deeplink-help ol {
                margin: 12px 0 0 20px;
                padding: 0;
            }

            .deeplink-help li {
                margin: 8px 0;
                color: #666;
            }
        `;
        document.head.appendChild(style);
    }

    // ๋ชจ๋‹ฌ ํ™•์žฅ
    expandModal() {
        const modal = document.querySelector('.mobile-fullscreen');
        if (modal) {
            modal.style.maxHeight = '95vh';
        }
    }

    // ๋ชจ๋‹ฌ ์ตœ์†Œํ™”
    minimizeModal() {
        const modal = document.querySelector('.mobile-fullscreen');
        if (modal) {
            modal.style.maxHeight = '70vh';
        }
    }
}

export default MobileWalletUI;
๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ UX ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… ํ„ฐ์น˜ ์˜์—ญ ์ตœ์†Œ 44x44px ์ด์ƒ
  • โœ… ๋ฒ„ํŠผ ๊ฐ„๊ฒฉ ์ถฉ๋ถ„ํžˆ ํ™•๋ณด (์ตœ์†Œ 8px)
  • โœ… ํ…์ŠคํŠธ ํฌ๊ธฐ ์ตœ์†Œ 16px (ํ™•๋Œ€ ๋ฐฉ์ง€)
  • โœ… ์Šคํฌ๋กค ์˜์—ญ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„
  • โœ… ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ํฌ๊ณ  ๋ช…ํ™•ํ•˜๊ฒŒ
  • โœ… ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ „์ฒด ํ™”๋ฉด์— ํ‘œ์‹œ
  • โœ… ๋”ฅ๋งํฌ ์ž๋™ ์ฒ˜๋ฆฌ
  • โœ… ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ ๋™์ž‘ ์ฒ˜๋ฆฌ
### ๐Ÿ”„ ๋„คํŠธ์›Œํฌ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ชจ๋ฐ”์ผ์€ ๋„คํŠธ์›Œํฌ๊ฐ€ ๋ถˆ์•ˆ์ •ํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ, ์—ฐ๊ฒฐ ์ƒํƒœ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ  ์ ์ ˆํžˆ ๋Œ€์‘ํ•ด์•ผ ํ•ด.
๐Ÿ“„ resources/js/utils/network-monitor.js
class NetworkMonitor {
    constructor() {
        this.isOnline = navigator.onLine;
        this.connectionType = this.getConnectionType();
        this.listeners = [];
        this.init();
    }

    init() {
        window.addEventListener('online', () => this.handleOnline());
        window.addEventListener('offline', () => this.handleOffline());

        // ์—ฐ๊ฒฐ ํƒ€์ž… ๋ณ€๊ฒฝ ๊ฐ์ง€
        if ('connection' in navigator) {
            navigator.connection.addEventListener('change', () => {
                this.handleConnectionChange();
            });
        }
    }

    // ์—ฐ๊ฒฐ ํƒ€์ž… ํ™•์ธ
    getConnectionType() {
        if ('connection' in navigator) {
            return navigator.connection.effectiveType;
        }
        return 'unknown';
    }

    // ์˜จ๋ผ์ธ ์ƒํƒœ๋กœ ์ „ํ™˜
    handleOnline() {
        this.isOnline = true;
        this.showToast('์ธํ„ฐ๋„ท์— ์—ฐ๊ฒฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค', 'success');
        this.notifyListeners('online');
        
        // ๋Œ€๊ธฐ ์ค‘์ธ ์š”์ฒญ ์žฌ์‹œ๋„
        this.retryPendingRequests();
    }

    // ์˜คํ”„๋ผ์ธ ์ƒํƒœ๋กœ ์ „ํ™˜
    handleOffline() {
        this.isOnline = false;
        this.showToast('์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์ด ๋Š์–ด์กŒ์Šต๋‹ˆ๋‹ค', 'warning');
        this.notifyListeners('offline');
    }

    // ์—ฐ๊ฒฐ ํƒ€์ž… ๋ณ€๊ฒฝ
    handleConnectionChange() {
        const newType = this.getConnectionType();
        
        if (newType !== this.connectionType) {
            this.connectionType = newType;
            
            // ๋А๋ฆฐ ์—ฐ๊ฒฐ ๊ฒฝ๊ณ 
            if (newType === 'slow-2g' || newType === '2g') {
                this.showToast('๋„คํŠธ์›Œํฌ ์†๋„๊ฐ€ ๋А๋ฆฝ๋‹ˆ๋‹ค', 'warning');
            }
            
            this.notifyListeners('connection-change', newType);
        }
    }

    // ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก
    addListener(callback) {
        this.listeners.push(callback);
    }

    // ๋ฆฌ์Šค๋„ˆ ์•Œ๋ฆผ
    notifyListeners(event, data) {
        this.listeners.forEach(callback => {
            callback(event, data);
        });
    }

    // ๋Œ€๊ธฐ ์ค‘์ธ ์š”์ฒญ ์žฌ์‹œ๋„
    retryPendingRequests() {
        const pending = JSON.parse(localStorage.getItem('pending_requests') || '[]');
        
        pending.forEach(async (request) => {
            try {
                await this.executeRequest(request);
                this.removePendingRequest(request.id);
            } catch (error) {
                console.error('Retry failed:', error);
            }
        });
    }

    // ์š”์ฒญ ์‹คํ–‰
    async executeRequest(request) {
        // ์š”์ฒญ ์žฌ์‹คํ–‰ ๋กœ์ง
        return axios(request.config);
    }

    // ๋Œ€๊ธฐ ์š”์ฒญ ์ œ๊ฑฐ
    removePendingRequest(id) {
        const pending = JSON.parse(localStorage.getItem('pending_requests') || '[]');
        const filtered = pending.filter(r => r.id !== id);
        localStorage.setItem('pending_requests', JSON.stringify(filtered));
    }

    // ํ† ์ŠคํŠธ ํ‘œ์‹œ
    showToast(message, type) {
        const toast = document.createElement('div');
        toast.className = `toast toast-${type}`;
        toast.textContent = message;
        document.body.appendChild(toast);

        setTimeout(() => toast.classList.add('show'), 100);
        setTimeout(() => {
            toast.classList.remove('show');
            setTimeout(() => toast.remove(), 300);
        }, 3000);
    }
}

export default NetworkMonitor;
## ๐Ÿงช ํ…Œ์ŠคํŠธ์™€ ๋””๋ฒ„๊น…: ์™„๋ฒฝํ•œ UX๋ฅผ ์œ„ํ•œ ๋งˆ์ง€๋ง‰ ๋‹จ๊ณ„ ์•„๋ฌด๋ฆฌ ์ข‹์€ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋„ ํ…Œ์ŠคํŠธ ์—†์ด๋Š” ์™„์„ฑ์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์—†์–ด. ํŠนํžˆ Web3๋Š” ๋‹ค์–‘ํ•œ ํ™˜๊ฒฝ๊ณผ ์ƒํ™ฉ์„ ๊ณ ๋ คํ•ด์•ผ ํ•˜๋‹ˆ๊นŒ ์ฒ ์ €ํ•œ ํ…Œ์ŠคํŠธ๊ฐ€ ํ•„์ˆ˜์•ผ! ๐Ÿ” ### ๐ŸŽฏ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘์„ฑ
1์ •์ƒ ํ”Œ๋กœ์šฐ ํ…Œ์ŠคํŠธ

โ€ข ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์„ค์น˜๋จ + ์˜ฌ๋ฐ”๋ฅธ ๋„คํŠธ์›Œํฌ
โ€ข ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์„ค์น˜๋จ + ์ž˜๋ชป๋œ ๋„คํŠธ์›Œํฌ
โ€ข ๋ฉ”ํƒ€๋งˆ์Šคํฌ ๋ฏธ์„ค์น˜
โ€ข ์ด์ „ ์„ธ์…˜ ๋ณต์›
2์—๋Ÿฌ ์ƒํ™ฉ ํ…Œ์ŠคํŠธ

โ€ข ์‚ฌ์šฉ์ž๊ฐ€ ์—ฐ๊ฒฐ ๊ฑฐ๋ถ€
โ€ข ์„œ๋ช… ๊ฑฐ๋ถ€
โ€ข ๋„คํŠธ์›Œํฌ ํƒ€์ž„์•„์›ƒ
โ€ข ์ž˜๋ชป๋œ ์„œ๋ช…
โ€ข ๋งŒ๋ฃŒ๋œ nonce
3์—ฃ์ง€ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ

โ€ข ๋น ๋ฅธ ์—ฐ์† ํด๋ฆญ
โ€ข ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ ์ค‘ ์—ฐ๊ฒฐ
โ€ข ์—ฌ๋Ÿฌ ํƒญ์—์„œ ๋™์‹œ ์—ฐ๊ฒฐ
โ€ข ๋ฉ”ํƒ€๋งˆ์Šคํฌ ๊ณ„์ • ์ „ํ™˜
โ€ข ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์ž ๊ธˆ/ํ•ด์ œ
### ๐Ÿ› ๏ธ ๋””๋ฒ„๊น… ๋„๊ตฌ ๊ตฌํ˜„ ๊ฐœ๋ฐœ ์ค‘์— ๋ฌธ์ œ๋ฅผ ๋น ๋ฅด๊ฒŒ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋””๋ฒ„๊น… ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.
๐Ÿ“„ resources/js/utils/web3-debugger.js
class Web3Debugger {
    constructor() {
        this.enabled = process.env.NODE_ENV === 'development';
        this.logs = [];
        this.maxLogs = 100;
        
        if (this.enabled) {
            this.init();
        }
    }

    init() {
        // ๋””๋ฒ„๊ทธ ํŒจ๋„ ์ƒ์„ฑ
        this.createDebugPanel();
        
        // ์ „์—ญ ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ
        window.addEventListener('error', (e) => {
            this.logError('Global Error', e.error);
        });

        // Promise rejection ํ•ธ๋“ค๋Ÿฌ
        window.addEventListener('unhandledrejection', (e) => {
            this.logError('Unhandled Promise Rejection', e.reason);
        });
    }

    // ๋””๋ฒ„๊ทธ ํŒจ๋„ ์ƒ์„ฑ
    createDebugPanel() {
        const panel = document.createElement('div');
        panel.id = 'web3-debug-panel';
        panel.innerHTML = `
            <div class="debug-header">
                <span>๐Ÿ” Web3 Debugger</span>
                <button id="debug-toggle">_</button>
                <button id="debug-clear">Clear</button>
            </div>
            <div class="debug-content">
                <div class="debug-tabs">
                    <button class="debug-tab active" data-tab="logs">Logs</button>
                    <button class="debug-tab" data-tab="state">State</button>
                    <button class="debug-tab" data-tab="network">Network</button>
                </div>
                <div class="debug-body">
                    <div id="debug-logs" class="debug-section active"></div>
                    <div id="debug-state" class="debug-section"></div>
                    <div id="debug-network" class="debug-section"></div>
                </div>
            </div>
        `;

        document.body.appendChild(panel);
        this.bindDebugEvents();
        this.applyDebugStyles();
    }

    // ์ด๋ฒคํŠธ ๋ฐ”์ธ๋”ฉ
    bindDebugEvents() {
        document.getElementById('debug-toggle').addEventListener('click', () => {
            const content = document.querySelector('.debug-content');
            content.style.display = content.style.display === 'none' ? 'block' : 'none';
        });

        document.getElementById('debug-clear').addEventListener('click', () => {
            this.clearLogs();
        });

        document.querySelectorAll('.debug-tab').forEach(tab => {
            tab.addEventListener('click', (e) => {
                this.switchTab(e.target.dataset.tab);
            });
        });
    }

    // ํƒญ ์ „ํ™˜
    switchTab(tabName) {
        document.querySelectorAll('.debug-tab').forEach(t => t.classList.remove('active'));
        document.querySelectorAll('.debug-section').forEach(s => s.classList.remove('active'));
        
        document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
        document.getElementById(`debug-${tabName}`).classList.add('active');

        if (tabName === 'state') {
            this.updateStateView();
        } else if (tabName === 'network') {
            this.updateNetworkView();
        }
    }

    // ๋กœ๊ทธ ๊ธฐ๋ก
    log(type, message, data = null) {
        if (!this.enabled) return;

        const timestamp = new Date().toLocaleTimeString();
        const logEntry = {
            type,
            message,
            data,
            timestamp
        };

        this.logs.push(logEntry);
        
        if (this.logs.length > this.maxLogs) {
            this.logs.shift();
        }

        this.updateLogsView();
        
        // ์ฝ˜์†”์—๋„ ์ถœ๋ ฅ
        console.log(`[Web3 ${type}]`, message, data);
    }

    logError(message, error) {
        this.log('ERROR', message, {
            message: error.message,
            stack: error.stack,
            code: error.code
        });
    }

    logEvent(event, data) {
        this.log('EVENT', event, data);
    }

    logRequest(method, params) {
        this.log('REQUEST', method, params);
    }

    logResponse(method, result) {
        this.log('RESPONSE', method, result);
    }

    // ๋กœ๊ทธ ๋ทฐ ์—…๋ฐ์ดํŠธ
    updateLogsView() {
        const container = document.getElementById('debug-logs');
        container.innerHTML = this.logs.map(log => `
            <div class="debug-log debug-log-${log.type.toLowerCase()}">
                <span class="log-time">${log.timestamp}</span>
                <span class="log-type">[${log.type}]</span>
                <span class="log-message">${log.message}</span>
                ${log.data ? `<pre class="log-data">${JSON.stringify(log.data, null, 2)}</pre>` : ''}
            </div>
        `).reverse().join('');
    }

    // ์ƒํƒœ ๋ทฐ ์—…๋ฐ์ดํŠธ
    updateStateView() {
        const container = document.getElementById('debug-state');
        const state = {
            ethereum: {
                isMetaMask: window.ethereum?.isMetaMask,
                chainId: window.ethereum?.chainId,
                selectedAddress: window.ethereum?.selectedAddress
            },
            localStorage: {
                authToken: !!localStorage.getItem('auth_token'),
                connectionCache: !!localStorage.getItem('web3_connection_cache')
            },
            session: {
                isConnected: !!window.walletUI?.connector?.account,
                currentState: window.walletUI?.currentState
            }
        };

        container.innerHTML = `<pre>${JSON.stringify(state, null, 2)}</pre>`;
    }

    // ๋„คํŠธ์›Œํฌ ๋ทฐ ์—…๋ฐ์ดํŠธ
    updateNetworkView() {
        const container = document.getElementById('debug-network');
        const networkInfo = {
            online: navigator.onLine,
            connectionType: navigator.connection?.effectiveType,
            downlink: navigator.connection?.downlink,
            rtt: navigator.connection?.rtt,
            saveData: navigator.connection?.saveData
        };

        container.innerHTML = `<pre>${JSON.stringify(networkInfo, null, 2)}</pre>`;
    }

    // ๋กœ๊ทธ ์ดˆ๊ธฐํ™”
    clearLogs() {
        this.logs = [];
        this.updateLogsView();
    }

    // ์Šคํƒ€์ผ ์ ์šฉ
    applyDebugStyles() {
        const style = document.createElement('style');
        style.textContent = `
            #web3-debug-panel {
                position: fixed;
                bottom: 20px;
                right: 20px;
                width: 400px;
                max-height: 500px;
                background: #1e1e1e;
                color: #d4d4d4;
                border-radius: 8px;
                box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
                font-family: 'Courier New', monospace;
                font-size: 12px;
                z-index: 99999;
                overflow: hidden;
            }

            .debug-header {
                background: #2d2d2d;
                padding: 10px;
                display: flex;
                justify-content: space-between;
                align-items: center;
                border-bottom: 1px solid #3e3e3e;
            }

            .debug-header button {
                background: #3e3e3e;
                border: none;
                color: #d4d4d4;
                padding: 4px 8px;
                border-radius: 4px;
                cursor: pointer;
                margin-left: 5px;
            }

            .debug-header button:hover {
                background: #4e4e4e;
            }

            .debug-content {
                max-height: 450px;
                overflow: hidden;
            }

            .debug-tabs {
                display: flex;
                background: #2d2d2d;
                border-bottom: 1px solid #3e3e3e;
            }

            .debug-tab {
                flex: 1;
                background: transparent;
                border: none;
                color: #888;
                padding: 8px;
                cursor: pointer;
                transition: all 0.2s;
            }

            .debug-tab.active {
                color: #4ec9b0;
                border-bottom: 2px solid #4ec9b0;
            }

            .debug-body {
                height: 400px;
                overflow-y: auto;
            }

            .debug-section {
                display: none;
                padding: 10px;
            }

            .debug-section.active {
                display: block;
            }

            .debug-log {
                padding: 8px;
                margin: 4px 0;
                border-left: 3px solid #4ec9b0;
                background: #252526;
                border-radius: 4px;
            }

            .debug-log-error {
                border-left-color: #f48771;
            }

            .debug-log-event {
                border-left-color: #4fc1ff;
            }

            .log-time {
                color: #858585;
                margin-right: 8px;
            }

            .log-type {
                color: #4ec9b0;
                margin-right: 8px;
                font-weight: bold;
            }

            .log-message {
                color: #d4d4d4;
            }

            .log-data {
                margin-top: 8px;
                padding: 8px;
                background: #1e1e1e;
                border-radius: 4px;
                overflow-x: auto;
                font-size: 11px;
            }

            .debug-body::-webkit-scrollbar {
                width: 8px;
            }

            .debug-body::-webkit-scrollbar-track {
                background: #1e1e1e;
            }

            .debug-body::-webkit-scrollbar-thumb {
                background: #3e3e3e;
                border-radius: 4px;
            }

            .debug-body::-webkit-scrollbar-thumb:hover {
                background: #4e4e4e;
            }
        `;
        document.head.appendChild(style);
    }
}

// ์ „์—ญ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
window.web3Debugger = new Web3Debugger();

export default Web3Debugger;
๐Ÿ’ก ๋””๋ฒ„๊ฑฐ ํ™œ์šฉ ํŒ

๊ฐœ๋ฐœ ์ค‘์—๋Š” ์ด ๋””๋ฒ„๊ฑฐ๋ฅผ ํ•ญ์ƒ ์ผœ๋‘๊ณ  ์ž‘์—…ํ•˜๋ฉด ๋ฌธ์ œ๋ฅผ ๋น ๋ฅด๊ฒŒ ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ์ด๋ฒคํŠธ๋‚˜ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ์–ด์„œ ์—„์ฒญ ์œ ์šฉํ•ด! ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ์—์„œ๋Š” ์ž๋™์œผ๋กœ ๋น„ํ™œ์„ฑํ™”๋˜๋‹ˆ๊นŒ ๊ฑฑ์ • ๋งˆ! ๐ŸŽฏ
### ๐Ÿ“Š ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง UX ๊ฐœ์„ ์˜ ํšจ๊ณผ๋ฅผ ์ธก์ •ํ•˜๋ ค๋ฉด ์„ฑ๋Šฅ ์ง€ํ‘œ๋ฅผ ์ถ”์ ํ•ด์•ผ ํ•ด.
๐Ÿ“„ resources/js/utils/performance-monitor.js
class PerformanceMonitor {
    constructor() {
        this.metrics = {};
        this.marks = {};
    }

    // ์ธก์ • ์‹œ์ž‘
    start(name) {
        this.marks[name] = performance.now();
        window.web3Debugger?.log('PERF', `Started: ${name}`);
    }

    // ์ธก์ • ์ข…๋ฃŒ
    end(name) {
        if (!this.marks[name]) {
            console.warn(`No start mark for: ${name}`);
            return;
        }

        const duration = performance.now() - this.marks[name];
        
        if (!this.metrics[name]) {
            this.metrics[name] = [];
        }
        
        this.metrics[name].push(duration);
        delete this.marks[name];

        window.web3Debugger?.log('PERF', `Completed: ${name}`, {
            duration: `${duration.toFixed(2)}ms`
        });

        return duration;
    }

    // ํ†ต๊ณ„ ์กฐํšŒ
    getStats(name) {
        const measurements = this.metrics[name];
        if (!measurements || measurements.length === 0) {
            return null;
        }

        const sorted = [...measurements].sort((a, b) => a - b);
        const sum = sorted.reduce((a, b) => a + b, 0);

        return {
            count: sorted.length,
            min: sorted[0],
            max: sorted[sorted.length - 1],
            avg: sum / sorted.length,
            median: sorted[Math.floor(sorted.length / 2)],
            p95: sorted[Math.floor(sorted.length * 0.95)]
        };
    }

    // ๋ชจ๋“  ํ†ต๊ณ„ ์ถœ๋ ฅ
    printStats() {
        console.table(
            Object.keys(this.metrics).reduce((acc, name) => {
                acc[name] = this.getStats(name);
                return acc;
            }, {})
        );
    }

    // ๋ฆฌํฌํŠธ ์ƒ์„ฑ
    generateReport() {
        const report = {
            timestamp: new Date().toISOString(),
            metrics: {}
        };

        Object.keys(this.metrics).forEach(name => {
            report.metrics[name] = this.getStats(name);
        });

        return report;
    }

    // ์„œ๋ฒ„๋กœ ์ „์†ก
    async sendToServer() {
        const report = this.generateReport();
        
        try {
            await axios.post('/api/analytics/performance', report);
            console.log('Performance report sent');
        } catch (error) {
            console.error('Failed to send performance report:', error);
        }
    }
}

// ์ „์—ญ ์ธ์Šคํ„ด์Šค
window.perfMonitor = new PerformanceMonitor();

export default PerformanceMonitor;
์ด์ œ ์ฝ”๋“œ ๊ณณ๊ณณ์—์„œ ์„ฑ๋Šฅ์„ ์ธก์ •ํ•  ์ˆ˜ ์žˆ์–ด:
// ์‚ฌ์šฉ ์˜ˆ์‹œ
perfMonitor.start('wallet-connection');
await connectWallet();
perfMonitor.end('wallet-connection');

perfMonitor.start('signature-verification');
await verifySignature();
perfMonitor.end('signature-verification');

// ํ†ต๊ณ„ ํ™•์ธ
perfMonitor.printStats();
## ๐ŸŽ“ ์‹ค์ „ ํŒ๊ณผ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค ์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ์‹ค์ „์—์„œ ๋ฐ”๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํŒ๋“ค์„ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ! ๐Ÿ’ก ### โœจ UX ๊ฐœ์„  ์ฒดํฌ๋ฆฌ์ŠคํŠธ
์นดํ…Œ๊ณ ๋ฆฌ ์ฒดํฌ ํ•ญ๋ชฉ ์ค‘์š”๋„
์ดˆ๊ธฐ ๋กœ๋”ฉ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ๊ฐ์ง€ ๋ฐ ์„ค์น˜ ์œ ๋„ โญโญโญ
์ดˆ๊ธฐ ๋กœ๋”ฉ ์ด์ „ ์„ธ์…˜ ์ž๋™ ๋ณต์› โญโญโญ
์—ฐ๊ฒฐ ๊ณผ์ • ๊ฐ ๋‹จ๊ณ„๋ณ„ ๋ช…ํ™•ํ•œ ํ”ผ๋“œ๋ฐฑ โญโญโญ
์—ฐ๊ฒฐ ๊ณผ์ • ํ”„๋กœ๊ทธ๋ ˆ์Šค ์ธ๋””์ผ€์ดํ„ฐ ํ‘œ์‹œ โญโญ
์—๋Ÿฌ ์ฒ˜๋ฆฌ ์นœ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ โญโญโญ
์—๋Ÿฌ ์ฒ˜๋ฆฌ ์ž๋™ ๋ณต๊ตฌ ์‹œ๋„ โญโญ
๋„คํŠธ์›Œํฌ ์ž๋™ ๋„คํŠธ์›Œํฌ ์ „ํ™˜ โญโญโญ
๋„คํŠธ์›Œํฌ ๋„คํŠธ์›Œํฌ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง โญโญ
๋ชจ๋ฐ”์ผ ๋”ฅ๋งํฌ ์ฒ˜๋ฆฌ โญโญโญ
๋ชจ๋ฐ”์ผ ํ„ฐ์น˜ ์ตœ์ ํ™” โญโญโญ
์„ฑ๋Šฅ ์บ์‹ฑ ์ „๋žต โญโญโญ
์„ฑ๋Šฅ ์š”์ฒญ ๋””๋ฐ”์šด์‹ฑ โญโญ
๋ณด์•ˆ Nonce ๊ธฐ๋ฐ˜ ์ธ์ฆ โญโญโญ
๋ณด์•ˆ ํ† ํฐ ๋งŒ๋ฃŒ ๊ด€๋ฆฌ โญโญโญ
### ๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ์šฐ์„ ์ˆœ์œ„
1์ˆœ์œ„: ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„

์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ€์žฅ ๋จผ์ € ์ฒด๊ฐํ•˜๋Š” ๋ถ€๋ถ„์ด์•ผ. ์บ์‹ฑ๊ณผ ํ”„๋ฆฌ๋กœ๋”ฉ์„ ์ ๊ทน ํ™œ์šฉํ•ด์„œ 1์ดˆ ์ด๋‚ด์— UI๊ฐ€ ํ‘œ์‹œ๋˜๋„๋ก ํ•ด์•ผ ํ•ด.
2์ˆœ์œ„: ์—ฐ๊ฒฐ ์‘๋‹ต ์†๋„

๋ฉ”ํƒ€๋งˆ์Šคํฌ ์—ฐ๊ฒฐ ์š”์ฒญ ํ›„ 3์ดˆ ์ด๋‚ด์— ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์™€์•ผ ํ•ด. ์„œ๋ฒ„ ์‘๋‹ต ์‹œ๊ฐ„์„ ์ตœ์†Œํ™”ํ•˜๊ณ , ๋‚™๊ด€์  UI ์—…๋ฐ์ดํŠธ๋ฅผ ํ™œ์šฉํ•˜์ž.
3์ˆœ์œ„: ์—๋Ÿฌ ๋ณต๊ตฌ ์†๋„

์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์‚ฌ์šฉ์ž๊ฐ€ ๋น ๋ฅด๊ฒŒ ์žฌ์‹œ๋„ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์•ผ ํ•ด. ์ž๋™ ๋ณต๊ตฌ๊ฐ€ ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ๋Š” ์‚ฌ์šฉ์ž ๊ฐœ์ž… ์—†์ด ์ฒ˜๋ฆฌํ•˜์ž.
### ๐Ÿ’Ž ๊ณ ๊ธ‰ ํ…Œํฌ๋‹‰
1๋‚™๊ด€์  UI ์—…๋ฐ์ดํŠธ

์„œ๋ฒ„ ์‘๋‹ต์„ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ๋จผ์ € UI๋ฅผ ์—…๋ฐ์ดํŠธํ•ด์„œ ์ฒด๊ฐ ์†๋„๋ฅผ ๋†’์—ฌ. ๋งŒ์•ฝ ์‹คํŒจํ•˜๋ฉด ๋กค๋ฐฑํ•˜๋ฉด ๋ผ.
// ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ์˜ˆ์‹œ
async function optimisticConnect(address) {
    // 1. ๋จผ์ € UI ์—…๋ฐ์ดํŠธ
    showConnected(address, '0.00'); // ์ž„์‹œ ์ž”์•ก
    
    try {
        // 2. ์‹ค์ œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
        const balance = await getBalance(address);
        updateBalance(balance);
    } catch (error) {
        // 3. ์‹คํŒจ ์‹œ ๋กค๋ฐฑ
        showConnectPrompt();
        showError(error.message);
    }
}
2์š”์ฒญ ๋ฐฐ์นญ

์—ฌ๋Ÿฌ ๊ฐœ์˜ ์š”์ฒญ์„ ํ•˜๋‚˜๋กœ ๋ฌถ์–ด์„œ ๋ณด๋‚ด๋ฉด ๋„คํŠธ์›Œํฌ ์™•๋ณต ์‹œ๊ฐ„์„ ์ค„์ผ ์ˆ˜ ์žˆ์–ด.
// ๋ฐฐ์นญ ์˜ˆ์‹œ
async function batchRequests(address) {
    const [balance, nonce, chainId] = await Promise.all([
        provider.getBalance(address),
        provider.getTransactionCount(address),
        provider.getNetwork().then(n => n.chainId)
    ]);
    
    return { balance, nonce, chainId };
}
3์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ๋กœ๋”ฉ

๋‹จ์ˆœํ•œ ์Šคํ”ผ๋„ˆ ๋Œ€์‹  ์‚ฌ์šฉ์ž๊ฐ€ ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋กœ๋”ฉ ํ™”๋ฉด์„ ๋งŒ๋“ค์–ด.
// ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ๋กœ๋”ฉ ์˜ˆ์‹œ
function showInteractiveLoading() {
    const tips = [
        '๐Ÿ’ก ๋ฉ”ํƒ€๋งˆ์Šคํฌ๋Š” ๊ฐ€์Šค๋น„๊ฐ€ ๋“ค์ง€ ์•Š๋Š” ์„œ๋ช…๋งŒ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค',
        '๐Ÿ” ๊ฐœ์ธํ‚ค๋Š” ์ ˆ๋Œ€ ์™ธ๋ถ€๋กœ ์ „์†ก๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค',
        'โšก ๋กœ๊ทธ์ธ ํ›„ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ์ด์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค'
    ];
    
    let currentTip = 0;
    const interval = setInterval(() => {
        document.getElementById('loading-tip').textContent = tips[currentTip];
        currentTip = (currentTip + 1) % tips.length;
    }, 3000);
    
    return () => clearInterval(interval);
}
### ๐ŸŽฏ ์žฌ๋Šฅ๋„ท์—์„œ์˜ ํ™œ์šฉ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ์žฌ๋Šฅ ๊ฑฐ๋ž˜ ํ”Œ๋žซํผ์—์„œ Web3 ๋กœ๊ทธ์ธ์„ ๋„์ž…ํ•˜๋ฉด ์–ด๋–ค ์žฅ์ ์ด ์žˆ์„๊นŒ? ๐Ÿค”

๐ŸŽจ ํฌ๋ฆฌ์—์ดํ„ฐ ์ธ์ฆ

๋ธ”๋ก์ฒด์ธ ์ง€๊ฐ‘ ์ฃผ์†Œ๋ฅผ ํ†ตํ•ด ํฌ๋ฆฌ์—์ดํ„ฐ์˜ ์ •์ฒด์„ฑ์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์–ด. NFT ํฌํŠธํด๋ฆฌ์˜ค๋‚˜ ์˜จ์ฒด์ธ ํ™œ๋™ ์ด๋ ฅ์„ ์ž๋™์œผ๋กœ ๊ฐ€์ ธ์™€์„œ ํ”„๋กœํ•„์— ํ‘œ์‹œํ•  ์ˆ˜๋„ ์žˆ์ง€!
๐Ÿ’ฐ ์•”ํ˜ธํ™”ํ ๊ฒฐ์ œ

๋ฉ”ํƒ€๋งˆ์Šคํฌ๋กœ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋Š” ๋ฐ”๋กœ ์•”ํ˜ธํ™”ํ๋กœ ๊ฒฐ์ œํ•  ์ˆ˜ ์žˆ์–ด. ๊ตญ์ œ ์†ก๊ธˆ ์ˆ˜์ˆ˜๋ฃŒ ์—†์ด ์ „ ์„ธ๊ณ„ ํฌ๋ฆฌ์—์ดํ„ฐ์™€ ๊ฑฐ๋ž˜ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฑฐ์•ผ!
๐Ÿ† ์˜จ์ฒด์ธ ํ‰ํŒ ์‹œ์Šคํ…œ

๊ฑฐ๋ž˜ ์ด๋ ฅ๊ณผ ํ‰๊ฐ€๋ฅผ ๋ธ”๋ก์ฒด์ธ์— ๊ธฐ๋กํ•˜๋ฉด ์กฐ์ž‘์ด ๋ถˆ๊ฐ€๋Šฅํ•œ ์‹ ๋ขฐ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. ์žฌ๋Šฅ๋„ท์—์„œ ์Œ“์€ ํ‰ํŒ์„ ๋‹ค๋ฅธ ํ”Œ๋žซํผ์—์„œ๋„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ๊ฑฐ์ง€!
UX ๊ฐœ์„  ์™„๋ฃŒ! ๐ŸŽ‰ โšก ๋น ๋ฅธ ์†๋„ ๐Ÿ˜Š ์ง๊ด€์  UI ๐Ÿ›ก๏ธ ์•ˆ์ „ํ•œ ๋ณด์•ˆ ์‚ฌ์šฉ์ž๊ฐ€ ์‚ฌ๋ž‘ํ•˜๋Š” Web3 ๊ฒฝํ—˜์„ ๋งŒ๋“ค์—ˆ์–ด์š”!
## ๐ŸŽฌ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ ์ž, ์—ฌ๊ธฐ๊นŒ์ง€ ๋ผ๋ผ๋ฒจ์—์„œ ๋ฉ”ํƒ€๋งˆ์Šคํฌ ๋กœ๊ทธ์ธ UX๋ฅผ ๊ฐœ์„ ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ดค์–ด! ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์ง€? ๐Ÿ˜…

ํ•ต์‹ฌ์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ:

๐ŸŽฏ UX ๊ฐœ์„ ์˜ ํ•ต์‹ฌ ์›์น™

1๏ธโƒฃ ๋ช…ํ™•ํ•œ ํ”ผ๋“œ๋ฐฑ: ์‚ฌ์šฉ์ž๊ฐ€ ํ•ญ์ƒ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ์•Œ ์ˆ˜ ์žˆ์–ด์•ผ ํ•ด
2๏ธโƒฃ ์—๋Ÿฌ ๋ณต๊ตฌ: ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ์„ ๋•Œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ œ์‹œํ•ด์•ผ ํ•ด
3๏ธโƒฃ ๋น ๋ฅธ ์‘๋‹ต: ๋ชจ๋“  ์•ก์…˜์— ์ฆ‰๊ฐ์ ์œผ๋กœ ๋ฐ˜์‘ํ•ด์•ผ ํ•ด
4๏ธโƒฃ ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™”: ์ž‘์€ ํ™”๋ฉด์—์„œ๋„ ์™„๋ฒฝํ•˜๊ฒŒ ๋™์ž‘ํ•ด์•ผ ํ•ด
5๏ธโƒฃ ๋ณด์•ˆ ์šฐ์„ : ํŽธ์˜์„ฑ๊ณผ ๋ณด์•ˆ์˜ ๊ท ํ˜•์„ ๋งž์ถฐ์•ผ ํ•ด
Web3๋Š” ์•„์ง ๋Œ€์ค‘ํ™” ์ดˆ๊ธฐ ๋‹จ๊ณ„์•ผ. ๊ทธ๋ž˜์„œ UX๊ฐ€ ๋”์šฑ ์ค‘์š”ํ•ด. ์‚ฌ์šฉ์ž๋“ค์ด "์–ด๋ ต๋‹ค", "๋ณต์žกํ•˜๋‹ค"๊ณ  ๋А๋ผ๋Š” ์ˆœ๊ฐ„, Web3์˜ ๋ฏธ๋ž˜๋Š” ๋ฉ€์–ด์ง€๊ฑฐ๋“ . ์šฐ๋ฆฌ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ด๋Ÿฐ ์žฅ๋ฒฝ์„ ๋‚ฎ์ถฐ์„œ ๋ˆ„๊ตฌ๋‚˜ ์‰ฝ๊ฒŒ Web3 ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ค์–ด์•ผ ํ•ด! ๐Ÿš€

์˜ค๋Š˜ ๋ฐฐ์šด ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ์—ฌ๋Ÿฌ๋ถ„๋งŒ์˜ ๋ฉ‹์ง„ Web3 ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด๊ธธ ๋ฐ”๋ผ! ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์ด๋Ÿฐ ๊ธฐ์ˆ ๋“ค์ด ํ™œ์šฉ๋˜๋ฉด ์ •๋ง ํ˜์‹ ์ ์ธ ์„œ๋น„์Šค๊ฐ€ ํƒ„์ƒํ•  ๊ฑฐ์•ผ. ๐Ÿ’ช

ํ˜น์‹œ ๊ตฌํ˜„ํ•˜๋‹ค๊ฐ€ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด ์–ธ์ œ๋“  ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•˜๊ณ , ์„œ๋กœ ๋„์šฐ๋ฉด์„œ ํ•จ๊ป˜ ์„ฑ์žฅํ•˜์ž! Web3์˜ ๋ฏธ๋ž˜๋Š” ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค์–ด๊ฐ€๋Š” ๊ฑฐ๋‹ˆ๊นŒ! โœจ

๊ทธ๋Ÿผ ์ฆ๊ฑฐ์šด ์ฝ”๋”ฉ ๋˜๊ธธ! Happy coding! ๐ŸŽ‰
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0