diff --git a/week-05/dev/frontend/.gitignore b/week-05/dev/frontend/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/week-05/dev/frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/week-05/dev/frontend/README.md b/week-05/dev/frontend/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/week-05/dev/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/week-05/dev/frontend/app/components/BalanceCard.tsx b/week-05/dev/frontend/app/components/BalanceCard.tsx new file mode 100644 index 0000000..8821863 --- /dev/null +++ b/week-05/dev/frontend/app/components/BalanceCard.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { useAccount, useBalance } from "wagmi"; + +export function BalanceCard() { + const { address, isConnected } = useAccount(); + const { data: balance } = useBalance({ address }); + + return ( +
+

내 잔액

+

+ {isConnected && balance + ? `${parseFloat(balance.formatted).toFixed(4)} ${balance.symbol}` + : "— ETH"} +

+

+ {isConnected && address ? address : "지갑을 연결해주세요"} +

+
+ ); +} diff --git a/week-05/dev/frontend/app/components/CustomConnectButton.tsx b/week-05/dev/frontend/app/components/CustomConnectButton.tsx new file mode 100644 index 0000000..add84cb --- /dev/null +++ b/week-05/dev/frontend/app/components/CustomConnectButton.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { ConnectButton } from "@rainbow-me/rainbowkit"; +import { useDisconnect } from "wagmi"; + +export function CustomConnectButton() { + const { disconnect } = useDisconnect(); + + return ( + + {({ account, chain, openConnectModal, openChainModal, mounted }) => { + const connected = mounted && account && chain; + + return ( +
+ {connected ? ( + <> + + + + + ) : ( + + )} +
+ ); + }} +
+ ); +} diff --git a/week-05/dev/frontend/app/components/Footer.tsx b/week-05/dev/frontend/app/components/Footer.tsx new file mode 100644 index 0000000..9f3d0e7 --- /dev/null +++ b/week-05/dev/frontend/app/components/Footer.tsx @@ -0,0 +1,11 @@ +export function Footer() { + return ( + + ); +} diff --git a/week-05/dev/frontend/app/components/Header.tsx b/week-05/dev/frontend/app/components/Header.tsx new file mode 100644 index 0000000..f8c8dd6 --- /dev/null +++ b/week-05/dev/frontend/app/components/Header.tsx @@ -0,0 +1,14 @@ +import { CustomConnectButton } from "./CustomConnectButton"; + +export function Header() { + return ( +
+
+

+ Week 5 dApp +

+ +
+
+ ); +} diff --git a/week-05/dev/frontend/app/components/SendEth.tsx b/week-05/dev/frontend/app/components/SendEth.tsx new file mode 100644 index 0000000..5fdae66 --- /dev/null +++ b/week-05/dev/frontend/app/components/SendEth.tsx @@ -0,0 +1,98 @@ +'use client'; + +import { useState } from 'react'; +import { useSendTransaction, useWaitForTransactionReceipt } from 'wagmi'; +import { parseEther } from 'viem'; + +export function SendEth() { + const [to, setTo] = useState(''); + const [amount, setAmount] = useState(''); + + const { sendTransaction, data: hash, isPending } = useSendTransaction(); + + const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + sendTransaction({ to: to as `0x${string}`, value: parseEther(amount) }); + }; + + return ( +
+

+ ETH 전송 +

+ +
+ {/* 받는 주소 */} +
+ + setTo(e.target.value)} + className="w-full rounded-lg border border-zinc-300 bg-zinc-50 px-4 py-2.5 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-500" + /> +
+ + {/* 금액 */} +
+ + setAmount(e.target.value)} + className="w-full rounded-lg border border-zinc-300 bg-zinc-50 px-4 py-2.5 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-500" + /> +
+ + {/* 전송 버튼 */} + +
+ + {/* 트랜잭션 상태 표시 */} +
+ {hash && ( +
+

트랜잭션 해시

+

+ {hash} +

+
+ )} + + {isConfirming && ( +
+ 트랜잭션 확인 중... +
+ )} + + {isSuccess && ( +
+ 트랜잭션 완료! +
+ )} +
+
+ ); +} diff --git a/week-05/dev/frontend/app/favicon.ico b/week-05/dev/frontend/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/week-05/dev/frontend/app/favicon.ico differ diff --git a/week-05/dev/frontend/app/globals.css b/week-05/dev/frontend/app/globals.css new file mode 100644 index 0000000..a2dc41e --- /dev/null +++ b/week-05/dev/frontend/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/week-05/dev/frontend/app/layout.tsx b/week-05/dev/frontend/app/layout.tsx new file mode 100644 index 0000000..91ac395 --- /dev/null +++ b/week-05/dev/frontend/app/layout.tsx @@ -0,0 +1,35 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import { Providers } from "./providers"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Week 5 dApp", + description: "RainbowKit 지갑 연결 실습", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/week-05/dev/frontend/app/page.tsx b/week-05/dev/frontend/app/page.tsx new file mode 100644 index 0000000..23d4dc8 --- /dev/null +++ b/week-05/dev/frontend/app/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import Image from "next/image"; +import { Header } from "./components/Header"; +import { BalanceCard } from "./components/BalanceCard"; +import { SendEth } from "./components/SendEth"; +import { Footer } from "./components/Footer"; + +export default function Home() { + return ( +
+
+
+ Pelican +
+
+ + +
+
+ ); +} diff --git a/week-05/dev/frontend/app/providers.tsx b/week-05/dev/frontend/app/providers.tsx new file mode 100644 index 0000000..171129a --- /dev/null +++ b/week-05/dev/frontend/app/providers.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { WagmiProvider } from "wagmi"; +import { RainbowKitProvider } from "@rainbow-me/rainbowkit"; +import { config } from "@/config/wagmi"; +import "@rainbow-me/rainbowkit/styles.css"; + +const queryClient = new QueryClient(); + +export function Providers({ children }: { children: React.ReactNode }) { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setMounted(true); + }, []); + + if (!mounted) return null; + + return ( + + + {children} + + + ); +} diff --git a/week-05/dev/frontend/config/wagmi.ts b/week-05/dev/frontend/config/wagmi.ts new file mode 100644 index 0000000..bfb8e97 --- /dev/null +++ b/week-05/dev/frontend/config/wagmi.ts @@ -0,0 +1,12 @@ +import { getDefaultConfig } from '@rainbow-me/rainbowkit'; +import { sepolia } from 'wagmi/chains'; + +// WalletConnect Cloud에서 발급받은 Project ID로 교체하세요 +// https://cloud.walletconnect.com +const WALLETCONNECT_PROJECT_ID = 'YOUR_PROJECT_ID'; + +export const config = getDefaultConfig({ + appName: 'Week 5 dApp', + projectId: WALLETCONNECT_PROJECT_ID, + chains: [sepolia], +}); diff --git a/week-05/dev/frontend/eslint.config.mjs b/week-05/dev/frontend/eslint.config.mjs new file mode 100644 index 0000000..05e726d --- /dev/null +++ b/week-05/dev/frontend/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/week-05/dev/frontend/next.config.ts b/week-05/dev/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/week-05/dev/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/week-05/dev/frontend/package.json b/week-05/dev/frontend/package.json new file mode 100644 index 0000000..1b5afe8 --- /dev/null +++ b/week-05/dev/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "@rainbow-me/rainbowkit": "^2.2.10", + "@tanstack/react-query": "^5.90.21", + "next": "16.1.6", + "react": "19.2.3", + "react-dom": "19.2.3", + "viem": "^2.47.0", + "wagmi": "^2.19.5" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.1.6", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/week-05/dev/frontend/postcss.config.mjs b/week-05/dev/frontend/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/week-05/dev/frontend/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/week-05/dev/frontend/public/Pelican.png b/week-05/dev/frontend/public/Pelican.png new file mode 100644 index 0000000..35e628a Binary files /dev/null and b/week-05/dev/frontend/public/Pelican.png differ diff --git a/week-05/dev/frontend/public/file.svg b/week-05/dev/frontend/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/week-05/dev/frontend/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/week-05/dev/frontend/public/globe.svg b/week-05/dev/frontend/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/week-05/dev/frontend/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/week-05/dev/frontend/public/next.svg b/week-05/dev/frontend/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/week-05/dev/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/week-05/dev/frontend/public/vercel.svg b/week-05/dev/frontend/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/week-05/dev/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/week-05/dev/frontend/public/window.svg b/week-05/dev/frontend/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/week-05/dev/frontend/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/week-05/dev/frontend/tsconfig.json b/week-05/dev/frontend/tsconfig.json new file mode 100644 index 0000000..3a13f90 --- /dev/null +++ b/week-05/dev/frontend/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/week-05/dev/frontend/types/css.d.ts b/week-05/dev/frontend/types/css.d.ts new file mode 100644 index 0000000..35306c6 --- /dev/null +++ b/week-05/dev/frontend/types/css.d.ts @@ -0,0 +1 @@ +declare module '*.css'; diff --git a/week-05/theory/quiz-05-solution.md b/week-05/theory/quiz-05-solution.md new file mode 100644 index 0000000..0991878 --- /dev/null +++ b/week-05/theory/quiz-05-solution.md @@ -0,0 +1,491 @@ +# Week 5 Quiz: PoS/Consensus + RainbowKit + +> **제출 방법:** 이 파일을 복사하여 답변을 작성한 후, PR로 제출하세요. +> **평가 기준:** 개념 이해도 중심 - 문법 오류보다 논리적 설명을 중시합니다. + +--- + +## 문제 1: PoS 개념 (객관식) + +이더리움이 PoW(작업 증명)에서 PoS(지분 증명)로 전환한 **가장 주요한 이유**는 무엇인가요? + +**보기:** +A) 트랜잭션 처리 속도를 10배 이상 높이기 위해 +B) 에너지 소비를 99.95% 이상 줄이고 환경 친화적으로 만들기 위해 +C) 블록 크기를 늘려서 더 많은 데이터를 저장하기 위해 +D) 채굴 장비 없이도 누구나 블록을 생성할 수 있게 하기 위해 + +**답변:** + + + +B, POW에서 블록을 생성하기 위해 채굴자들이 해시 연산 경쟁을 해야하며, 이 과정에서 막대한 컴퓨팅 파워와 전력을 소비한다. 그래서 POS를 선택하여 검증자는 자신의 ETH 를 예치하고 검증자가 블록을 만들고 보상을 받아가는 구조로 바꿨다. + +--- + +## 문제 2: 검증자 역할 (객관식) + +이더리움 PoS에서 검증자(Validator)가 수행하는 **두 가지 주요 역할**은 무엇인가요? + +**보기:** +A) 블록 채굴(Mining)과 가스 가격 결정 +B) 블록 제안(Proposing)과 블록 증명(Attesting) +C) 트랜잭션 전송과 수수료 수집 +D) 스마트 컨트랙트 배포와 실행 + +**답변:** + + + +B, validator은 블록 제안자로 선정되면 원하는 트랜잭션을 블록에 넣어 네트워크에 전파하고 +다른 validator들은 해당 블록에 투표를 한다. 이를통해 블록을 justified 한 후에 finalized한다. + +--- + +## 문제 3: 왜 PoW에서 PoS로? (단답형) + +PoW(작업 증명)와 PoS(지분 증명)의 **핵심 차이점**은 무엇인가요? +"자격 증명 방식"과 "보안 보장 방식" 두 관점에서 각각 비교하세요. + +**답변:** + + + +자격증명 방식: + +- PoW: 블록을 생성할 자격은 해시 연산을 통해 타겟넘버보다 낮은 값을 얻은 채굴자에게 주어진다. +- PoS: 블록을 생성한 자격은 네트워크에 ETH를 스테이킹한 검증자 중에서 프로토콜에 의해 선택된 벨리데이터에게 주어진다. + +보안 보장 방식 + +- Pow: CanonicalChain을 따르므로 네트워크를 공격하려면 전체 해시 파워의 과반수를 확보해야한다. 따라서 막대한 컴퓨팅 파워가 필요하다. 이 비용이 안전을 보장한다. +- PoS: 검증자는 자신의 ETH를 스테이킹하여 잘못된 블록을 제안하거나 부정한 투표를 하면 슬레싱으로 스테이킹한 ETH를 잃게된다. 따라서 경제적 패널티구조로 네트워크 보안을 보장한다. + +--- + +## 문제 4: 슬래싱의 목적 (단답형) + +슬래싱(Slashing)은 검증자의 스테이킹된 ETH를 **강제로 소각**하는 패널티입니다. + +1. 슬래싱이 발동되는 **두 가지 조건**은 무엇인가요? +2. **왜** 이런 처벌이 필요한가요? 없다면 어떤 문제가 생길 수 있나요? + +**답변:** + + + +1. 슬래싱 조건 + +- Double Voting: 동일한 epoch에서 하나의 validator가 서로 다른 두 블록에 대해 투표하는 경우 +- Surround Voting: validator가 이전에 한 투표를 포함하거나 감싸는 형태의 새로운 투표를 하는경우 + +2. 슬래싱이 필요한 이유 + 슬래싱이 없으면 validator은 여러 체인에 동시 투표하는 행동을 할 수 있는데 + 그러면 여러 포크체인이 생길 수 도있고 무엇보다 finalized가 보장되지 않는다. + +--- + +## 문제 5: 체인 선택 규칙 (단답형) + +여러 유효한 블록이 동시에 제안되면 **포크(Fork)**가 발생합니다. +이더리움의 LMD-GHOST(Latest Message Driven GHOST) 규칙은 어떻게 "정규 체인"을 선택하나요? + +1. LMD-GHOST의 기본 원리는 무엇인가요? +2. **왜** "가장 최근 메시지"를 사용하나요? (오래된 메시지를 사용하면 어떤 문제가?) + +**답변:** + + + +1. 루트 블록에서 시작하여 각 포크 지점마다 가장 많은 validator의 투표가 모인 자식 블록을 선택하여 내려간다. 이 과정을 반복하여 가장 많은 지분의 지지를 받은 체인 경로를 정규 체인으로 선택한다. + +--- + +## 문제 6: RainbowKit Provider 계층 (빈칸 채우기) + +다음 코드의 빈칸을 채워서 RainbowKit을 올바르게 설정하세요. +**Provider 순서가 중요합니다!** + +```typescript +'use client'; + +// TODO: 필요한 스타일 import +_________________________________________ + +import { RainbowKitProvider } from '@rainbow-me/rainbowkit'; +import { WagmiProvider } from 'wagmi'; +import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; +import { config } from '@/config/wagmi'; + +const queryClient = new QueryClient(); + +export default function RootLayout({ children }) { + return ( + + + {/* TODO: Provider를 올바른 순서로 중첩하세요 */} + <_________________ config={config}> + <_________________ client={queryClient}> + <_________________> + {children} + + + + + + ); +} +``` + +**답변:** + +```typescript +// 완성된 코드를 여기에 작성하세요 +// TODO: 필요한 스타일 import +_________________________________________ + +import { RainbowKitProvider } from '@rainbow-me/rainbowkit'; +import { WagmiProvider } from 'wagmi'; +import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; +import { config } from '@/config/wagmi'; + +const queryClient = new QueryClient(); + +export default function RootLayout({ children }) { + return ( + + + {/* TODO: Provider를 올바른 순서로 중첩하세요 */} + + + + {children} + + + + + + ); +} +``` + +**왜 이 순서인가요:** + + + +wagmiProvider은 블록체인 네트워크 연결, wallet 상태, signer, provider등을 관리해야한다. 따라서 가장 밖에서 이것을 받아와야한다. +QueryClientProvider은 wagmi에서 사용하는 비동기 데이터(fetch,caChing)을 관리한다. +wagmi와 rainbowkit 모두 리액트 쿼리를 통해 데이터를 캐싱하기 때분에 wagmi 내부에서 제공된다. +RainbowKit은 지갑 연결 UI를 제공하는 라이브러리이며 wagmi의 상태와 리액트 쿼리를 사용한다. 따라서 가장 안쪽에있어야한다. + +순서가 잘못되면 필요한 context를 찾지못해 오류가 발생한다. + +--- + +## 문제 7: Provider 순서 버그 (취약점 찾기) + +다음 코드에서 **문제점**을 찾고 수정하세요: + +```typescript +// BAD CODE - 문제점 찾기 +'use client'; + +import '@rainbow-me/rainbowkit/styles.css'; +import { RainbowKitProvider } from '@rainbow-me/rainbowkit'; +import { WagmiProvider } from 'wagmi'; +import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; +import { config } from '@/config/wagmi'; + +const queryClient = new QueryClient(); + +export default function Providers({ children }) { + return ( + // 문제가 있는 Provider 순서! + + + + {children} + + + + ); +} +``` + +**1) 발견한 문제점:** + + + +Provider의 순서가 잘못되었다. +Wagmi->QueryClient->RainbowKit 순으로 가야한다. + +**2) 왜 이것이 문제인가:** + + + +RainbowKit가 Wagmi 바깥에 있으므로 wagmi의 context (지갑 연결 상태, 체인정보 등)에 접근할 수 없다. +실행시 런타임 에러가 발생한다. + +**3) 올바른 수정 방법:** + +```typescript +// GOOD CODE - 수정된 버전을 작성하세요 +'use client'; + +import '@rainbow-me/rainbowkit/styles.css'; +import { RainbowKitProvider } from '@rainbow-me/rainbowkit'; +import { WagmiProvider } from 'wagmi'; +import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; +import { config } from '@/config/wagmi'; + +const queryClient = new QueryClient(); + +export default function Providers({ children }) { + return ( + // 문제가 있는 Provider 순서! + + + + {children} + + + + ); +} + +``` + +--- + +## 문제 8: 트랜잭션 상태 처리 (빈칸 채우기) + +다음 코드의 빈칸을 채워서 트랜잭션 전송 후 **확인 상태를 추적**하세요: + +```typescript +'use client'; + +import { useWriteContract, _________________ } from 'wagmi'; + +const abi = [ + { + name: 'increment', + type: 'function', + stateMutability: 'nonpayable', + inputs: [], + outputs: [], + }, +] as const; + +function IncrementButton() { + const { writeContract, data: hash, isPending } = useWriteContract(); + + // TODO: 트랜잭션 확인 상태를 추적하는 hook + const { isLoading: isConfirming, isSuccess } = _________________({ + _________________, + }); + + return ( +
+ + + {isSuccess &&

트랜잭션 성공!

} +
+ ); +} +``` + +**답변:** + +```typescript +// 완성된 코드를 여기에 작성하세요 +use client'; + +import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'; + +const abi = [ + { + name: 'increment', + type: 'function', + stateMutability: 'nonpayable', + inputs: [], + outputs: [], + }, +] as const; + +function IncrementButton() { + const { writeContract, data: hash, isPending } = useWriteContract(); + + // TODO: 트랜잭션 확인 상태를 추적하는 hook + const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ + hash, + }); + + return ( +
+ + + {isSuccess &&

트랜잭션 성공!

} +
+ ); +} + +``` + +**트랜잭션 상태 흐름을 설명하세요:** + + + +1. isPending 상태: 지갑서명을 대기하거나 트랜잭션을 전송하는 단계 +2. isConfirming 상태: 트랜잭션이 브로드 캐스트 된 후 블록에 포함되길 기다리고 있는지 판단하는 상태 +3. isSuccess 상태: 트랜잭션이 블록에 포함되어 receipt가 반환되었는지 판단하는 상태 + +--- + +## 문제 9: 검증자 생애주기 (다이어그램 해석) + +다음 다이어그램은 이더리움 검증자의 생애주기를 보여줍니다: + +```mermaid +stateDiagram-v2 + [*] --> Pending: 32 ETH 입금 + Pending --> Active: 활성화 큐 대기 + Active --> Slashed: 규칙 위반 + Active --> Exiting: 자발적 종료 + Exiting --> Exited: 출금 대기 + Slashed --> Exited: 강제 퇴장 + Exited --> [*]: ETH 출금 +``` + +**질문:** + +1. **Active** 상태에서 검증자가 수행하는 주요 활동은 무엇인가요? + +블록 제안자로 선정되어 트랜잭션을 담은 블록을 제안하거나, 제안된 블록이 옳은지 투표를 한다. + +2. Active에서 **Slashed**로 전이되는 조건은 무엇인가요? 이 경우 검증자에게 어떤 일이 발생하나요? + +Double Voting을 하거나 Surround Voting을 하면 Slashed로 전이된다. +이렇게되는 경우 검증자는 예치해놓은 ETH의 일부를 잃게된다. + +3. 검증자가 자발적으로 종료(**Exiting**)하려면 왜 바로 ETH를 출금할 수 없고 대기 기간이 필요한가요? + +바로 종료가 가능하면 네트워크가 위반을 발견하기 전에 ETH를 인출하게 되어 처벌을 회피할 수 있게된다. + +그리고 갑자기 많은 validator이 한번에 나가게되면 체인이 불안정해질 수 있다. + +--- + +## 문제 10: Provider 계층 구조 (다이어그램 해석) + +다음 다이어그램은 RainbowKit/wagmi 앱의 Provider 구조를 보여줍니다: + +```mermaid +graph TD + subgraph App["React App"] + WP["WagmiProvider
config 제공"] + QP["QueryClientProvider
캐싱/상태관리"] + RP["RainbowKitProvider
지갑 UI"] + COMP["Components
useAccount, useWriteContract 등"] + end + + WP --> QP --> RP --> COMP + + subgraph Deps["의존성"] + CONFIG["wagmi config"] + QC["QueryClient"] + WALLET["지갑 연결 상태"] + end + + CONFIG -.-> WP + QC -.-> QP + WP -.-> RP + QP -.-> COMP +``` + +**질문:** + +1. **WagmiProvider**가 가장 바깥에 있어야 하는 이유는 무엇인가요? + +2. **QueryClientProvider**의 역할은 무엇인가요? 없다면 어떤 문제가 발생하나요? + +3. 아래 코드에서 `useAccount()` hook이 **"Cannot find WagmiContext"** 오류를 발생시키는 이유는 무엇인가요? + +```typescript +// 오류 발생 코드 + + + {/* WagmiProvider가 안쪽에 있음 */} + {/* useAccount() 호출 */} + + + +``` + +--- + +## 제출 전 체크리스트 + +- [ ] 모든 문제에 답변을 작성했는가? +- [ ] 객관식 문제: 정답 선택 **이유**를 설명했는가? +- [ ] 단답형 문제: 2-3문장 이상으로 충분히 설명했는가? +- [ ] 코드 문제: 완성된 코드와 **왜 그렇게 작성했는지** 설명했는가? +- [ ] 다이어그램 문제: 각 질문에 논리적으로 답변했는가?