일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 컨트렉트 배포 자동화
- 러스트기초
- ethers
- 계정추상화
- erc4337 contract
- ethers v6
- 러스트 기초 학습
- 스마트컨트렉트 함수이름 중복 호출
- chainlink 설명
- multicall
- rust 기초
- 스마트컨트렉트테스트
- Vue
- 러스트 기초
- 스마트 컨트렉트 함수이름 중복
- git rebase
- erc4337
- ethers websocket
- ambiguous function description
- 머신러닝기초
- SBT표준
- 체인의정석
- 오블완
- ethers typescript
- 컨트렉트 동일한 함수이름 호출
- vue기초
- Vue.js
- 티스토리챌린지
- 스마트컨트렉트 예약어 함수이름 중복
- ethers type
- Today
- Total
체인의정석
React + Next.js 에서 블록체인 지갑 기반 로그인 시스템 만들기 (프론트엔드) 본문
이전 게시글
https://it-timehacker.tistory.com/537
[React+Next+typescript+rainbow wallet] 관리자 페이지 만들고 rainbow wallet 붙이기 (블록체인 앱 관리자 페이
1. 관리자 페이지 기본 템플릿다운로드 순서1. 템플릿https://tailadmin.com/download Download Free Tailwind Admin Template - TailAdminDownload TailAdmin Now Select your preferred option below to start Download and Kickstart your journey.ta
it-timehacker.tistory.com
위의 게시글을 통해 기본 설치를 한 후
메타마스크를 이용한 로그인 시스템을 만들때는 다음 단계를 거쳐야한다.
1. 지갑 로그인
2. 로그인 후 메세지 서명
3. 서명한 메세지에 대한 원본 메세지, 서명 값, 지갑 주소를 백엔드로 전송
4. 백엔드에서 서명 검증 후 유효할 경우 JWT 발급
여기서 1~3까지는 프론트엔드이고 4번은 백엔드의 영역이다.
백엔드에서 JWT를 발급해야 하는 이유는 클라이언트 단에서는 메타마스크에 거짓된 요청을 바로 보낼 수 있기 때문이다.
일반적인 경우로는 연결된 지갑 주소만 판단해도 된다고 생각하지만 지갑을 통해 로그인을 하고 해당 로그인을 통해 별도의 화면이나 접근 제어가 필요한 경우에는 이처럼 지갑 인증과 동시에 서명을 받고 서명에 통과한 경우 JWT를 발급해주는 구조를 사용하여야 한다.
1. 서버사이드 렌더링 되는 app 경로의 page
import type { Metadata } from "next";
import React from "react";
import {base_metadata} from "@/libs/header";
import LoginPage from "@/components/web3/MetaMaskLogIn";
export const metadata: Metadata = base_metadata
export default function LogIn() {
return (
<LoginPage></LoginPage>
);
}
이런식으로 클라이언트에서 렌더링되는 부분을 제외한 부분을 여기에 작성하면 된다. 여기에 클라이언트 사이드 렌더링에 대한 로직을 넣는다면 hydrate 에러가 발생하기 때문에 여기서는 간단하게 필요한 고정 값만 넣어두었다.
2. MetaMaskLogIn 컴포넌트
실제 서버사이드 렌더링이 되는 부분이 여기에 모두 포함되어 있다.
"use client";
import { useAccount } from 'wagmi';
import { useRouter } from 'next/navigation';
import {useEffect, useState} from 'react';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import Image from "next/image";
import useSignature from "@/hooks/useSignature";
import {loginBySignature} from "@/services/login.service";
export default function LoginPage() {
const { isConnected, address } = useAccount();
const { signAndSendRequest } = useSignature();
const router = useRouter();
const [isSigning, setIsSigning] = useState(false); // ✅ 중복 방지 상태
useEffect(() => {
if (isConnected && address && !isSigning) {
setIsSigning(true); // ✅ 서명 요청 시작
(async () => {
try {
const { message, signature, address } = await signAndSendRequest();
console.log("📨 서명 요청 전송:", { message, signature, address });
// TODO: 실제 로그인 API 호출
const jwtRes = await loginBySignature(address, message, signature);
if(jwtRes) {
router.push("/");
} else {
Error("유효한 서명이 아닙니다.")
}
} catch (err) {
if(err !="UserRejectedRequestError: User rejected the request.") {
throw Error(`❌ 서명 처리 오류: ${err}`);
}
} finally {
setIsSigning(false); // ✅ 요청 완료 후 해제
}
})();
}
}, [isConnected, address, signAndSendRequest]);
return (
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-700 relative">
{/* 위쪽 절반 배경 */}
<div className="absolute top-0 left-0 w-full h-85/100 opacity-70 overflow-hidden">
<Image
src="/images/logo/backgroundWhite.png"
alt="Caliverse Logo"
fill
className="object-center opacity-20"
/>
</div>
{/* 로그인 카드 */}
<div className="z-10 bg-white/90 dark:bg-gray-900/90 backdrop-blur-md rounded-xl shadow-2xl p-10 max-w-md w-full text-center border border-white/10">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Caliverse Quest
</h1>
<h2 className="text-lg font-medium text-gray-500 dark:text-gray-400 mb-6">
Admin Dashboard Login
</h2>
<div className="flex justify-center mt-4">
<ConnectButton />
</div>
</div>
</div>
);
}
여기서는 useEffect를 써서 밑에 화면이 렌더링이 모두 되고 난 후에 값이 들어가도록 설정하였다.
지갑의 경우 레인보우 월렛의 ConnectButton 컴포넌트만 쓰면 작성이 되기 때문에 로그인 된 후의 서명 로직만 자체적으로 만들면 된다.
핵심이 되는 부분은 다음 코드이다.
const { isConnected, address } = useAccount();
const { signAndSendRequest } = useSignature();
const router = useRouter();
const [isSigning, setIsSigning] = useState(false); // ✅ 중복 방지 상태
useEffect(() => {
if (isConnected && address && !isSigning) {
setIsSigning(true); // ✅ 서명 요청 시작
(async () => {
try {
const { message, signature, address } = await signAndSendRequest();
console.log("📨 서명 요청 전송:", { message, signature, address });
// TODO: 실제 로그인 API 호출
const jwtRes = await loginBySignature(address, message, signature);
if(jwtRes) {
router.push("/");
} else {
Error("유효한 서명이 아닙니다.")
}
} catch (err) {
if(err !="UserRejectedRequestError: User rejected the request.") {
throw Error(`❌ 서명 처리 오류: ${err}`);
}
} finally {
setIsSigning(false); // ✅ 요청 완료 후 해제
}
})();
}
}, [isConnected, address, signAndSendRequest]);
먼저 서명 요청을 무한으로 보내지 않고 딱 한번만 진행되게 하기 위해서 자제적인 isSigning이라는 state를 두었으며, 마지막에 [isConnected, address, signAndRequest] 의 경우 의존성 배열에 해당된다.
✅ 의존성 배열이란?
useEffect는 컴포넌트가 렌더링될 때 실행되는데, 의존성 배열을 통해 "이 값들이 바뀌었을 때만" 다시 실행되도록 제어할 수 있어요.
즉, 위 코드는 다음을 의미합니다:
isConnected, address, signAndSendRequest 중 하나라도 값이 바뀌면, useEffect 내부 코드를 다시 실행해라.
이처럼 의존성 배열을 설정해 두어서 만약 연결상태나, 주소 등이 서명 중간에 변경되면 재실행이 되도록 만들었다.
이후 서명을 하는 signAndRequest는 hook으로 분리하고
로그인 API를 호출하는 부분은 service 영역으로 따로 분리해두어서 useEffect안에는 각 hook과 API응답 값을 통해서 각각 지갑과 백엔드 서버와의 통신을 담당한다.
2. Hook
hook의 경우에는 서명을 하는 부분을 넣어두었다. 이처럼 hook으로 뺀 이유는 추후 해당 로직을 다른곳에서도 재사용 할 수 있기 때문이다. 서명의 함수는 wagmi를 통해서 진행하였으며 서명은 기본적으로 제공되는 eip191타입의 서명을 넣었다.
서명하는 메세지는 고유의 값이 들어가야 하기 때문에 Date를 넣어두었으며, 여기에 랜덤값을 임의로 추가적으로 넣어도 괜찮을거 같다는 생각은 있다. 아무튼 서명값의 검증을 백엔드에서 하기위해서는 서명한 주소, 서명, 서명한 메세지 3개가 있으면 되기 때문에 3개의 값을 모두 구할 수 있게 리턴을 해준다. 이렇게 하면 다른곳에서의 서명 로직이 붙더라도 똑같이 사용이 가능하다.
또 하나 signAndSendRequest의 경우 async가 걸려있다. 이러한 경우에는 hook으로 만들 때 아래처럼 함수 안에 함수를 만든 후에 이를 리턴해와서 사용하는 방법이 있기 때문에 이렇게 사용하였다.
"use client";
import { useAccount, useSignMessage } from 'wagmi';
export default function useSignature() {
const { address, isConnected } = useAccount();
const { signMessageAsync } = useSignMessage();
const signAndSendRequest = async (): Promise<{ message: string; signature: string, address: string }> => {
if (!address || !isConnected) {
throw new Error("지갑이 연결되지 않았습니다.");
}
const message = `관리자 페이지에 로그인 합니다: ${new Date().toISOString()}`;
try {
// ✅ 서명 요청
const signature = await signMessageAsync({ message });
alert(`✅ 서명 성공 signature : ${signature} message : ${message}`);
return {message, signature, address};
} catch (error) {
console.error("❌ 서명 요청 오류:", error);
throw new Error("서명이 되지 않았습니다.");
}
};
return {
signAndSendRequest // 동기화를 위해 hook 안에서 정리를 한 후 return 하여 사용하는 방식
};
}
3. Service
hook에서 서명이 끝나면 서명값을 서버로 전달해 주어야 한다.
아직 프론트엔드 부분만 만들기 때문에 이부분은 임의로 주석처리를 해두었다.
이렇게 API로 호출되는 부분은 따로 모아서 services에 관리중이다.
// import axios from 'axios';
export async function loginBySignature(address: string, message: string, signature: string): Promise<boolean> {
console.log(address, message, signature);
// TODO: Call POST
// // ✅ API 요청
// const response = await axios.post('/api/auth/verify-signature', {
// address,
// message,
// signature
// });
// if (response.data.success) {
// console.log("🔥 인증된 사용자:", response.data.user);
// } else {
// alert("❌ 인증 실패");
// }
return true;
}
이제 위의 내용으로 코드를 띄우게 되면 메타마스크 기반의 로그인 페이지를 띄우고 서명값 까지 사용할 수 있다.
'개발 > frontend' 카테고리의 다른 글
Next.js "Use client" 사용법 (1) | 2025.04.02 |
---|---|
Next.js Hydration 에러 처리 (Rainbow Wallet 사용 시 적용 필요) (0) | 2025.04.02 |
[React+Next+typescript+rainbow wallet] 관리자 페이지 만들고 rainbow wallet 붙이기 (블록체인 앱 관리자 페이지 만들기) (0) | 2025.03.27 |
Metamask + Next.js 적용해서 프론트 페이지 만들어보기 (0) | 2024.06.13 |
텔레그램 봇 기본 틀 만들어 보기 (기본 템플릿, 깃허브 소스코드 및 사용방법 포함, javascript & typescript) (0) | 2023.07.29 |