DAO, DApp과 같은 Web3 애플리케이션이 점점 더 많은 인기를 얻고 있습니다. Web3의 기본 전제에 따르면, 이러한 플랫폼은 사용자의 신원을 타인에게 비공개로 유지하거나 심지어 본인조차 알 수 없게 하면서도, 더 개인화되고 맞춤화된 경험을 제공해야 합니다.
이 글에서는 이러한 상황에서 사용자 경험을 한층 향상시키는 방법을 살펴보겠습니다.
지갑을 통한 인증 및 신원 확인
많은 애플리케이션에서 MetaMask와 같은 블록체인 지갑은 ETH 등 암호화폐 자산을 관리하는 데 사용됩니다. 이러한 지갑을 통해 사용자는 여러 체인에서 계정을 생성할 수 있습니다. 계정은 개인 키(private key)로 생성되지만, 각 계정에는 이를 대표하는 공개 주소(public address)가 존재합니다.
공개 주소는 누구나 열람할 수 있지만, 해당 주소와 관련된 트랜잭션은 오직 소유자만 관리할 수 있습니다. 따라서 지갑 연결은 이런 애플리케이션에 매우 견고한 인증 메커니즘을 제공합니다.
프로젝트 소개
이 프로젝트에서는 Next.js를 활용해 사용자가 자신에게 제공되는 인터페이스를 직접 커스터마이징할 수 있는 환경설정(Preference) 저장 시스템을 구현합니다. 즉, 사용자가 사이트를 다시 방문하면 언제나 자신이 의도한 대로의 UI를 보게 됩니다.
더 나아가 여러 회사의 하위·크로스 플랫폼 간에 사용자 인터페이스를 정규화하는 것까지 확장할 수 있습니다. 이렇게 하면 사용자가 다른 곳에서 설정해 둔 환경설정을 지원하는 사이트에 방문할 때마다 일관된 경험을 누릴 수 있습니다.
또한 이 프로젝트는 커스터마이징 파라미터의 개수를 자유롭게 조정할 수 있어, 원하는 어떤 구조로든 응용이 가능합니다.
이 샘플 프로젝트에서는 단 두 가지 파라미터만 사용합니다:
- 웹사이트 테마
- 사용자를 부를 때 사용할 이름(인사말)
시작하기
Upstash Redis 환경 변수 설정
- Upstash에서 무료 Redis 데이터베이스를 생성합니다.
.env.local.example파일을.env.local(Git에서 무시됨)로 복사합니다.UPSTASH_REDIS_REST_URL과UPSTASH_REDIS_REST_TOKEN은 Upstash 콘솔의 데이터베이스 상세 페이지에서 확인할 수 있습니다.
의존성 설치
다음 명령어로 필요한 패키지를 설치합니다:
npm i @upstash/redis @metamask/detect-provider @mui/material @emotion/react @emotion/styled
api/store.js 파일 생성
Redis SDK를 설정하고 핸들러 함수를 export합니다.
import { Redis } from "@upstash/redis"
const redis = Redis.fromEnv()
export default async function handler(req, res) {
const accountID = await JSON.parse(req.body).accountID
const body = req.body
const setResult = await redis.set(accountID, body);
res.status(200).json({ result: setResult })
}
api/[accountAddress].js 파일 생성
마찬가지로 Redis SDK를 설정하고 핸들러를 export합니다.
import { Redis } from "@upstash/redis"
const redis = Redis.fromEnv()
export default async function handler(req, res) {
const accountID = req.query.accountAddress
const getResult = await redis.get(accountID)
res.status(200).json({ result: getResult })
}
index.js 파일 설정
프로젝트 변수 설정
const [accountAddress, setAccountAddress] = useState(null)
const [themePreference, setThemePreference] = useState("light")
const [greetingMessage, setGreetingMessage] = useState("Anonymous Person")
const [userSettings, setUserSettings] = useState(null)
// 이미 연결이 설정되어 있는지 확인하고, 연결되어 있다면 데이터를 가져옵니다.
useEffect(() => {
checkConnection()
getPreferences()
}, [accountAddress])
MetaMask 연결하기
async function connect() {
const provider = await detectEthereumProvider()
console.log("provider:", provider)
if (provider) {
console.log('Ethereum successfully detected!')
provider.request({ method: "eth_requestAccounts" }).then((accounts) => {
if (!accountAddress) {
setAccountAddress(accounts[0])
}
}).catch((err) => console.log(err))
console.log("window.ethereum:", window.ethereum)
getPreferences()
} else {
alert('Please install MetaMask!')
}
}
새로고침 또는 일정 시간 후 재방문 시 연결 상태 확인
async function checkConnection() {
const provider = await detectEthereumProvider()
if (provider) {
provider
.request({ method: 'eth_accounts' })
.then(accounts => {
setAccountAddress(accounts[0])
})
.catch(console.log)
} else {
console.log("Not connected, window.ethereum not found")
}
}
환경설정 저장하기
async function setPreferences(themePreference, greetingMessage) {
if (accountAddress) {
const res = await fetch(`/api/store`, {
method: "POST",
body: JSON.stringify({
accountID: accountAddress,
themePreference: themePreference,
greetingMessage: greetingMessage,
})
})
const data = await res.json()
}
else {
alert("No account address detected")
}
}
환경설정 불러오기
async function getPreferences(e) {
if (accountAddress) {
console.log("Fetching user preferences...")
const res = await fetch(`/api/${accountAddress}`, { method: "GET" })
const data = await res.json()
setUserSettings(data.result)
if (data.result) {
setThemePreference(data.result.themePreference)
setGreetingMessage(data.result.greetingMessage)
}
}
else {
console.log("No account connected yet!")
}
}
변수 상태 변경
아래 함수들을 입력 필드나 버튼 등에 바인딩하세요.
async function handleDarkMode(e) {
console.log("themePreference:", themePreference)
const newPreference = themePreference == "light" ? "dark" : "light"
setThemePreference(newPreference)
await setPreferences(newPreference, greetingMessage)
await getPreferences()
}
async function takeGreetingMessage(e) {
// Enter 키로 제출
if (e.keyCode == 13) {
const message = e.target.value
setGreetingMessage(message)
console.log(message)
await setPreferences(themePreference, message)
await getPreferences()
e.target.value = ""
}
}
위 함수들을 활용한 간단한 UI 만들기
관련 Material-UI 의존성을 import했는지 먼저 확인하세요.
return (
<div className={styles.container}>
<h2>Web3 Preferences Holder</h2>
<Button variant="contained" onClick={connect}>Connect Metamask</Button>
<p>
Lets you keep user preferences on cross-websites
</p>
<br />
<p>For example, take a greeter message from user.</p>
<TextField label="Call me..." variant="outlined" size="small" onKeyDown={takeGreetingMessage} />
<br />
<br />
<Button onClick={handleDarkMode} variant="contained" size="small" style={{ backgroundColor: "#3D3B3B" }}> Switch Dark Mode </Button>
<p>Sample Component/Page:</p>
<Showcase userSettings={userSettings} />
</div>
)
components 디렉터리에 sampleComponent.jsx 생성
이 컴포넌트는 웹 인터페이스나 애플리케이션을 나타내는 메인 컴포넌트입니다. 시스템이 작동하는 방식과 핵심 목표를 시각적으로 보여주는 역할을 하며, 여기서 파라미터를 원하는 방식으로 파싱해 샘플 인터페이스를 만들면 됩니다.
이 예제에서 프로젝트는 다음과 같이 구성되어 있습니다:
import React from "react";
import { useState} from 'react'
import { ThemeProvider, createTheme } from '@mui/material/styles'
import { orange, grey } from '@mui/material/colors'
const lightTheme = createTheme({
palette: {
primary: {
main: grey[400],
},
}
})
const darkTheme = createTheme({
palette: {
primary: {
main: orange[400],
},
}
})
export default function Showcase(parameters) {
const userSettings = parameters.userSettings
const [theme, setTheme] = useState("light")
const [greetingMessage, setGreetingMessage] = useState("Anonymous Person")
const items = []
if (userSettings) {
const obj = userSettings[0]
for (const key in userSettings) {
items.push(<li key={key}> {key}: {userSettings[key]} </li>)
}
if (userSettings["themePreference"] != theme) {
setTheme(userSettings["themePreference"] == "light" ? "light" : "dark")
}
if (!greetingMessage || userSettings["greetingMessage"] != greetingMessage) {
if (userSettings["greetingMessage"]) {
setGreetingMessage(userSettings["greetingMessage"])
}
else {
setGreetingMessage("not the same message")
}
}
}
return (
<div>
<div style={{
padding: 10,
margin: 10,
backgroundColor: theme == "light" ? "grey" : "orange",
border: "solid",
borderWidth: "30px",
borderColor: theme == "light" ? "#B2B2B2" : "black"
}}>
<ThemeProvider
theme={theme == "light" ? lightTheme : darkTheme}
>
<h2>Hi, {greetingMessage}!</h2>
<p>User and their preferences:</p>
{items}
</ThemeProvider>
</div>
</div>
);
}
워크플로우
MetaMask로 아직 연결하지 않은 사용자 — 기본 템플릿

MetaMask에 처음으로 연결한 사용자 — 기본 템플릿

- 사용자가 플랫폼에서 인사받을 때 보고 싶은 이름을 입력합니다.
- 사용자가 라이트 테마와 다크 테마 중 하나를 선택합니다.
환경설정 저장 완료 — 이후부터는 해당 환경설정 인터페이스를 지원하는 모든 플랫폼에서 동일한 인터페이스 표시 가능 (사용자 맞춤형 템플릿)

컴포넌트는 사용자가 설정한 환경설정 값을 텍스트 형식으로 보여주며, 원하는 방식으로 자유롭게 파싱해 활용할 수 있습니다.
이 간단한 예제에서 확인할 수 있듯이, 사용자에게 보여주는 모든 컴포넌트를 파라미터화할 수 있습니다. 디자인을 개선하고 파라미터 수를 늘려가며 프로젝트를 계속 발전시킬 수 있지만, 근본적인 로직은 언제나 동일하게 유지됩니다.
마무리
프로젝트 데모는 여기에서 확인할 수 있습니다.
완성된 프로젝트는 GitHub 저장소에서 확인하세요.
저장소에는 Vercel 빠른 배포 버튼이 포함되어 있어, 프로젝트를 손쉽게 배포하면서 Upstash Redis 통합까지 자동으로 생성할 수 있습니다.