Web3 Wallet 연결을 사용하는 NextJS 13의 수화 오류 이해

문제
최근에 새로 베타 NextJS 애플리케이션과 함께 WAGMI 를 설치 하고 일부 기본 지갑 연결을 시도한 경우 Hydration 오류를 표시하는 오류가 발생했을 수 있습니다. 이것은 단순히 NextJS 13 문제가 아니라 NextJS 13이 이를 격리하고 해결하는 데 도움이 되도록 몇 가지 규칙을 변경하는 방법을 다룰 것입니다.
수화 란 무엇입니까?
수화는 클라이언트 측 JavaScript를 사용하여 서버 렌더링 HTML에 응용 프로그램 상태 및 상호 작용을 추가하는 프로세스입니다. Gatsby 프레임워크를 만드는 기본 도구 중 하나인 React의 기능입니다. Gatsby는 수화를 사용하여 빌드 시 생성된 정적 HTML을 React 애플리케이션으로 변환합니다.
- 반응 수화의 이해
무슨 일이야?
문제는 우리가 NextJS와 같은 SSR(Server-Side Rendered) React 프레임워크를 사용할 때 기술적으로 페이지를 특정 방식으로 렌더링한 다음 클라이언트(브라우저)가 무언가를 렌더링할 때 렌더링된 상태를 예상한다는 것입니다. 서버는 클라이언트 측에 있는 것과 일치하여 상태를 관리하는 방법을 알고 있는지 확인합니다.
서버 측 상태와 클라이언트 상태가 일치하지 않으면 수화 오류가 발생합니다.
이를 확인하는 가장 좋은 방법은 브라우저에서 JavaScrip을 비활성화하고 두 DOM 간의 차이점을 확인하는 것입니다.

수화에 대해 자세히 알아보려면 Josh Comeau 의 The Perils of Rehydration 블로그 게시물을 살펴보는 것이 좋습니다 .
해결책은 무엇입니까?
해결책은 서버에서 처리해야 하는 것과 클라이언트 측에서 처리해야 하는 것을 구분해야 한다는 것입니다. NextJS 13에 대한 일부 새로운 조정으로 인해 일부 문서 에서 서버와 클라이언트 간의 파일 분리가 명확하게 표시됩니다 . 아이디어일 뿐이지만 솔루션을 해결하는 방법을 보여주기 위해 시연할 수 있습니다.
요구 사항
시작하기 전에 다음 단계를 따르기 위해 컴퓨터에 다음이 설치되어 있는지 확인하십시오.
- NVM 또는 노드 v18.12.1
- pnpm v7.15.0
위에 표시된 문제를 재현한 다음 문제를 해결하는 방법에 대한 몇 가지 가능한 솔루션을 살펴보겠습니다.

오류를 재현하기 위한 초기 설정을 해봅시다.
pnpm create next-app --typescript next13-wagmi-hydration;
# Expected Prompts:
#? Would you like to use ESLint with this project? › No / Yes
# Creating a new Next.js app in /path/to/next13-wagmi-hydration.
#
# Using pnpm.
#
# Installing dependencies:
# - react
# - react-dom
# - next
# - typescript
# - @types/react
# - @types/node
# - @types/react-dom
# - eslint
# - eslint-config-next
pnpm add wagmi ethers;
최신 베타 NextJS 13 Docs 중 일부를 사용하여 NextJS 앱을 구성할 것입니다.
파일: ./next13-wagmi-hydration/next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
experimental: {
appDir: true,
},
};
module.exports = nextConfig;
# FROM ./next13-wagmi-hydration
mkdir ./app;
# FROM ./next13-wagmi-hydration
mv ./pages/index.tsx app/page.tsx
rm -rf pages;
# FROM ./next13-wagmi-hydration
touch ./pages/layout.text
// Imports
// ========================================================
import '../styles/globals.css';
// Layout
// ========================================================
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<head />
<body>
{children}
</body>
</html>
)
};
파일: ./next13-wagmi-hyration/app/page.tsx
// Imports
// ========================================================
import React from 'react';
// Page
// ========================================================
export default function Home() {
// Render
return (
<div>
<h1 className="text-2xl text-white font-medium mb-6">Wallet Connection</h1>
</div>
);
};

Tailwind 구성(선택 사항)
이 다음 단계는 선택 사항이지만 UI를 시연할 때 상황이 더 좋아 보이는 것이 좋습니다. 이를 위해 Tailwind 를 사용할 것 입니다.
# FROM ./next13-wagmi-hydration
pnpm add -D tailwindcss postcss autoprefixer;
pnpx tailwindcss init -p;
파일: ./next13-wagmi-hyration/tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
파일: ./next13-wagmi-hyration/styles/global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
파일: ./next13-wagmi-hyration/app/layout.tsx
// Imports
// ========================================================
import '../styles/globals.css';
// Layout
// ========================================================
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<head />
<body className="bg-zinc-900">
{children}
</body>
</html>
)
};
// Imports
// ========================================================
import React from 'react';
// Page
// ========================================================
export default function Home() {
// Render
return (
<div className="p-8">
<h1 className="text-2xl text-white font-medium mb-6">Wallet Connection</h1>
</div>
);
};

WAGMI 구성
다음으로 지갑 상호 작용을 허용하도록 WAGMI 설정을 가져옵니다.
# FROM ./next13-wagmi-hydration
pnpm add wagmi ethers;
# FROM ./next13-wagmi-hydration
mkdir ./providers;
mkdir ./providers/wagmi;
touch ./providers/wagmi/index.tsx;
touch ./app/providers.tsx;
파일: ./next13-wagmi-hyration/providers/wagmi/index.tsx
// Imports
// ========================================================
import React from 'react';
import { WagmiConfig, createClient } from "wagmi";
import { getDefaultProvider } from 'ethers';
// Config
// ========================================================
const client = createClient({
autoConnect: true,
provider: getDefaultProvider()
});
// Provider
// ========================================================
const WagmiProvider = ({ children }: { children: React.ReactNode }) => {
return <WagmiConfig client={client}>{children}</WagmiConfig>
};
// Exports
// ========================================================
export default WagmiProvider;
파일: ./next13-wagmi-hyration/app/providers.tsx
// Imports
// ========================================================
import React from 'react';
import WagmiProvider from "../providers/wagmi";
// Root Provider
// ========================================================
const RootProvider = ({ children }: { children: React.ReactNode }) => {
return <div>
<WagmiProvider>
{children}
</WagmiProvider>
</div>
};
// Exports
// ========================================================
export default RootProvider;
파일: ./next13-wagmi-hyration/app/layout.tsx
// Imports
// ========================================================
import RootProvider from "./providers";
import '../styles/globals.css';
// Layout
// ========================================================
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<head />
<body className="bg-zinc-900">
<RootProvider>
{children}
</RootProvider>
</body>
</html>
)
};
파일: ./next13-wagmi-hyration/app/page.tsx
// Imports
// ========================================================
import React from 'react';
import { useAccount, useConnect, useDisconnect } from "wagmi";
import { InjectedConnector } from "wagmi/connectors/injected";
// Page
// ========================================================
export default function Home() {
// State / Props
const { address, isConnected } = useAccount();
const { connect } = useConnect({
connector: new InjectedConnector(),
});
const { disconnect } = useDisconnect()
// Render
return (
<div className="p-8">
<h1 className="text-2xl text-white font-medium mb-6">Wallet Connection</h1>
{!isConnected
? <div>
<button className="h-10 bg-blue-600 text-white px-6 rounded-full hover:bg-blue-800 transition-colors ease-in-out duration-200" onClick={() => connect()}>Connect Wallet</button>
</div>
: <div>
<label className="text-zinc-400 block mb-2">Wallet Address Connected</label>
<code className="bg-zinc-700 text-zinc-200 p-4 rounded block mb-4"><pre>{address}</pre></code>
<button className="h-10 bg-red-600 text-white px-6 rounded-full hover:bg-red-800 transition-colors ease-in-out duration-200" onClick={() => disconnect()}>Disconnect Wallet</button>
</div>}
</div>
);
};
# when running pnpm run dev
wait - compiling...
error - ./node_modules/.pnpm/@[email protected]_fsy4krnncv4idvr4txy3aqiuqm/node_modules/@tanstack/react-query-persist-client/build/lib/PersistQueryClientProvider.mjs
Attempted import error: 'useState' is not exported from 'react' (imported as 'React').
NextJS 마이그레이션 페이지 에서 베타 문서를 확인하십시오 .
이 문제를 해결하려면 use client;
.
이것이 필요한 곳이 두 군데 있습니다. 첫 번째는 대부분의 공급자가 클라이언트 측에서 주로 사용되는 후크 를 provider.tsx
활용한다는 것을 알고 있기 때문입니다 . 두 번째 장소는 우리 의 이지만 이것은 일시적이며 그 이유를 설명합니다.useState
useEffect
page.tsx
파일: ./next13-wagmi-hyration/app/providers.tsx
'use client';
// Imports
// ========================================================
import React from 'react';
import WagmiProvider from "../providers/wagmi";
// Root Provider
// ========================================================
const RootProvider = ({ children }: { children: React.ReactNode }) => {
return <div>
<WagmiProvider>
{children}
</WagmiProvider>
</div>
};
// Exports
// ========================================================
export default RootProvider;
'use client';
// Imports
// ========================================================
import React from 'react';
import { useAccount, useConnect, useDisconnect } from "wagmi";
import { InjectedConnector } from "wagmi/connectors/injected";
// Page
// ========================================================
export default function Home() {
// State / Props
const { address, isConnected } = useAccount();
const { connect } = useConnect({
connector: new InjectedConnector(),
});
const { disconnect } = useDisconnect()
// Render
return (
<div className="p-8">
<h1 className="text-2xl text-white font-medium mb-6">Wallet Connection</h1>
{!isConnected
? <div>
<button className="h-10 bg-blue-600 text-white px-6 rounded-full hover:bg-blue-800 transition-colors ease-in-out duration-200" onClick={() => connect()}>Connect Wallet</button>
</div>
: <div>
<label className="text-zinc-400 block mb-2">Wallet Address Connected</label>
<code className="bg-zinc-700 text-zinc-200 p-4 rounded block mb-4"><pre>{address}</pre></code>
<button className="h-10 bg-red-600 text-white px-6 rounded-full hover:bg-red-800 transition-colors ease-in-out duration-200" onClick={() => disconnect()}>Disconnect Wallet</button>
</div>}
</div>
);
};

이제 사이트를 연결하려고 하면 여전히 문제가 표시되지 않습니다.

마지막으로 페이지를 새로고침하고 수화 오류를 확인합니다.

엄청난! 이제 문제가 생겼으니 해결 방법으로 넘어가겠습니다.
해결책
좀 더 잘 정리하는 방법에 대한 몇 가지 아이디어를 살펴보고 최적화된 방법으로 몇 가지 솔루션을 보여드리겠습니다.
먼저 클라이언트에 필요한 것과 서버에 필요한 것을 분리해 보겠습니다. 를 보면 클라이언트가 처리해야 하는 것이 변수 page.tsx
에만 있다는 것을 알 수 있습니다. isConnected
다른 모든 것은 서버에서 렌더링할 수 있습니다.
page.tsx
지갑 상호 작용을 제거 use client
하고 자체 구성 요소로 추상화하도록 리팩터링해 봅시다 .
파일: ./next13-wagmi-hyration/app/page.tsx
// Imports
// ========================================================
import React from 'react';
import ConnectWallet from './wallet';
// Page
// ========================================================
export default function Home() {
// Render
return (
<div className="p-8">
<h1 className="text-2xl text-white font-medium mb-6">Wallet Connection</h1>
<ConnectWallet />
</div>
);
};

이제 JavaScript를 다시 활성화하고 새 ConnectWallet
구성 요소를 생성해 보겠습니다.
# FROM ./next13-wagmi-hydration
touch ./app/wallet.tsx
파일: ./next13-wagmi-hyration/app/wallet.tsx
'use client';
// Imports
// ========================================================
import React from 'react';
import { useAccount, useConnect, useDisconnect } from "wagmi";
import { InjectedConnector } from "wagmi/connectors/injected";
// Page
// ========================================================
export default function Home() {
// State / Props
const { address, isConnected } = useAccount();
const { connect } = useConnect({
connector: new InjectedConnector(),
});
const { disconnect } = useDisconnect()
// Render
return (
<div>
{!isConnected
? <div>
<button className="h-10 bg-blue-600 text-white px-6 rounded-full hover:bg-blue-800 transition-colors ease-in-out duration-200" onClick={() => connect()}>Connect Wallet</button>
</div>
: <div>
<label className="text-zinc-400 block mb-2">Wallet Address Connected</label>
<code className="bg-zinc-700 text-zinc-200 p-4 rounded block mb-4"><pre>{address}</pre></code>
<button className="h-10 bg-red-600 text-white px-6 rounded-full hover:bg-red-800 transition-colors ease-in-out duration-200" onClick={() => disconnect()}>Connect Wallet</button>
</div>}
</div>
);
};

솔루션 크레딧
솔루션은 원래 Josh Comeau가 The Perils of Rehydration 에 대한 그의 블로그에서 만들었습니다 . Josh에게 많은 감사를 드립니다.
첫 번째 솔루션
구성 요소가 먼저 마운트되었는지 확인하고 마운트되지 않은 경우 구성 요소를 로드하지 않는 첫 번째 솔루션에 대해 작업해 보겠습니다.
이를 추적 useState
하려면 useEffect
. useRef
안타깝게도 구성 요소가 마운트된 경우 추적하는 데 사용할 수 없습니다 .
파일: ./next13-wagmi-hyration/app/wallet.tsx
'use client';
// Imports
// ========================================================
import React, { useState, useEffect } from 'react';
import { useAccount, useConnect, useDisconnect } from "wagmi";
import { InjectedConnector } from "wagmi/connectors/injected";
// Page
// ========================================================
export default function Wallet() {
// State / Props
const [hasMounted, setHasMounted] = useState(false);
const { address, isConnected } = useAccount();
const { connect } = useConnect({
connector: new InjectedConnector(),
});
const { disconnect } = useDisconnect()
// Hooks
useEffect(() => {
setHasMounted(true);
}, [])
// Render
if (!hasMounted) return null;
return (
<div>
{!isConnected
? <div>
<button className="h-10 bg-blue-600 text-white px-6 rounded-full hover:bg-blue-800 transition-colors ease-in-out duration-200" onClick={() => connect()}>Connect Wallet</button>
</div>
: <div>
<label className="text-zinc-400 block mb-2">Wallet Address Connected</label>
<code className="bg-zinc-700 text-zinc-200 p-4 rounded block mb-4"><pre>{address}</pre></code>
<button className="h-10 bg-red-600 text-white px-6 rounded-full hover:bg-red-800 transition-colors ease-in-out duration-200" onClick={() => disconnect()}>Connect Wallet</button>
</div>}
</div>
);
};

최적화된 솔루션
모든 구성 요소 에 추가하기는 하지만 약간 반복적 hasMounted
이므로 해당 기능을 자체 구성 요소로 추상화하여 한 단계 더 나아갈 수 있습니다.
# FROM ./next13-wagmi-hydration
touch ./app/clientOnly.tsx;
'use client';
// Imports
// ========================================================
import React, { useState, useEffect } from 'react';
// Page
// ========================================================
export default function ClientOnly({ children }: { children: React.ReactNode }) {
// State / Props
const [hasMounted, setHasMounted] = useState(false);
// Hooks
useEffect(() => {
setHasMounted(true);
}, [])
// Render
if (!hasMounted) return null;
return (
<div>
{children}
</div>
);
};
파일: ./next13-wagmi-hyration/app/wallet.tsx
'use client';
// Imports
// ========================================================
import React from 'react';
import ClientOnly from './clientOnly';
import { useAccount, useConnect, useDisconnect } from "wagmi";
import { InjectedConnector } from "wagmi/connectors/injected";
// Page
// ========================================================
export default function Wallet() {
// State / Props
const { address, isConnected } = useAccount();
const { connect } = useConnect({
connector: new InjectedConnector(),
});
const { disconnect } = useDisconnect()
// Render
return (
<div>
<ClientOnly>
{!isConnected
? <div>
<button className="h-10 bg-blue-600 text-white px-6 rounded-full hover:bg-blue-800 transition-colors ease-in-out duration-200" onClick={() => connect()}>Connect Wallet</button>
</div>
: <div>
<label className="text-zinc-400 block mb-2">Wallet Address Connected</label>
<code className="bg-zinc-700 text-zinc-200 p-4 rounded block mb-4"><pre>{address}</pre></code>
<button className="h-10 bg-red-600 text-white px-6 rounded-full hover:bg-red-800 transition-colors ease-in-out duration-200" onClick={() => disconnect()}>Connect Wallet</button>
</div>}
</ClientOnly>
</div>
);
};
NextJS Wallet Connection — Optimized Solution For Hydration Error Fixed
전체 코드 작동을 보려면 이 github 리포지토리를 확인하십시오.
무엇 향후 계획?
곧 출시될 NextJS와 함께 작동하는 Sign-In With Ethereum의 초기 구현에 대한 다른 기사를 찾아보십시오.
이것으로부터 가치를 얻었다면 트위터(내가 활발하게 활동하는 곳) @codingwithmannny 와 인스타그램 @codingwithmanny 에서 나를 팔로우해 주세요 .
