지난 한 주 동안 .night 도메인이 실제로 어떻게 작동하는지 이해하려고 노력했습니다. 명세서를 보는 대신, 체인에 직접 닿는 무언가를 직접 만들어 보았습니다. 그 결과물은 작은 프로필 뷰어입니다. tomin.night와 같은 이름을 입력하면 블록체인에서 프로필을 즉시 찾아냅니다. 등록기관(registrar) API도, 인증 장벽도 없습니다. 오직 스마트 컨트랙트와 약간의 JavaScript뿐입니다.
이어지는 내용은 작동 방식과 제가 배운 점, 그리고 제 오후 시간을 통째로 앗아간 두 가지 구체적인 함정에 대한 이야기입니다.
왜 온체인 이름이 중요한가
Midnight에서 .night 도메인은 Midnames에 의해 관리됩니다. 기업의 중앙 서버에 쿼리를 보내는 대신, 체인에 존재하는 스마트 컨트랙트에서 직접 데이터를 읽어옵니다. 레지스트리 자체가 도메인, 소유자, 그리고 소유자가 연결한 프로필 필드를 보유합니다. 데이터가 온체인에 있기 때문에 신원은 휴대 가능합니다. 약관을 변경하거나 서비스를 중단할 수 있는 플랫폼으로부터 도메인을 빌리는 것이 아닙니다. 키를 제어하면 이름도 제어할 수 있습니다.
이러한 변화는 개발자에게도 중요합니다. 전통적인 DNS 시스템을 대상으로 구축할 때는 속도 제한(rate limits), API 키, 가동 시간(uptime) 보장 등을 신경 써야 합니다. 하지만 여기서는 컨트랙트 상태가 신뢰할 수 있는 유일한 원천(source of truth)입니다. 여러분의 애플리케이션은 다른 모든 애플리케이션과 동일한 방식으로 데이터를 읽습니다. 특권적인 API 계층 같은 것은 존재하지 않습니다.
코드가 너무 단순할 정도입니다
@midnames/sdk가 복잡한 작업을 처리합니다. 도메인을 주소와 프로필로 해석(resolve)하는 데는 정확히 두 줄이면 충분합니다.
const provider = createDefaultProvider({ networkId: "mainnet" });
const result = await resolveDomain(provider, "tomin.night");
그게 전부입니다. provider가 네트워크를 타겟팅하고, resolver가 컨트랙트와 통신합니다. SDK는 성공 여부 플래그를 포함한 결과 객체를 반환합니다. 도메인이 존재하지 않더라도 RPC 오류에 대한 복잡한 에러 핸들링이나 try-catch 블록을 겹겹이 쌓을 필요가 없습니다. SDK가 아무것도 없다는 사실을 깔끔하게 알려주기 때문입니다. 덕분에 UI를 구축하는 과정이 놀라울 정도로 즐겁습니다. 성공 플래그에 따라 분기하여, 실패 원인이 이름이 없는 것인지 노드가 죽은 것인지 추측할 필요 없이 "찾을 수 없음" 상태를 바로 보여줄 수 있습니다.
반환되는 데이터
조회가 성공하면 페이로드에는 중요한 두 가지 요소가 포함됩니다.
Target은 도메인이 가리키는 지갑 주소입니다. 이것이 핵심적인 유틸리티입니다. 긴 16진수(hex) 주소를 사람이 읽고, 입력하고, 기억할 수 있는 형태로 바꿔줍니다.
Fields에는 프로필 세부 정보가 담겨 있습니다. 소유자가 도메인에 연결한 소셜 링크, 아바타, 텍스트 레코드 등 무엇이든 이 구조 안에 들어 있습니다. 이 필드들은 어떤 회사의 MongoDB 클러스터에 저장되는 것이 아닙니다. 컨트랙트 상태의 필드이므로, 레지스트리를 읽는 방법을 아는 모든 앱이 동일한 프로필을 렌더링할 수 있습니다. 별도의 데이터베이스 동기화가 필요 없습니다.
저를 느리게 만든 두 가지 함정
구축 과정의 모든 것이 단 두 줄의 코드와 성공 플래그로 끝난 것은 아니었습니다. 여러분이 같은 실수를 반복하지 않도록, 제가 겪었던 두 가지 구체적인 장애물을 설명하겠습니다.
네트워크 불일치
SDK는 mainnet과 preprod 환경을 모두 지원합니다. 저는 "존재하지 않는" 도메인을 디버깅하며 꽤 많은 시간을 허비했습니다. 이름은 정확했고, 코드도 맞았으며, provider도 실행 중이었습니다. 문제는 제 스크립트가 preprod를 쿼리하고 있었는데, 정작 도메인은 mainnet에 등록되어 있었다는 점이었습니다. 에러는 도메인이 없는 것처럼 보였지만, 실제로는 네트워크 컨텍스트가 맞지 않았던 것입니다.
이름을 해석하는 과정에서 실패가 발생한다면, 다른 것을 디버깅하기 전에 provider 설정을 먼저 확인하십시오. networkId가 도메인이 실제로 민팅된 네트워크와 일치하는지 확인해야 합니다. 이는 나중에 돌이켜보면 당연해 보이지만, 컨트랙트 로직에 문제가 있다고 가정하고 있을 때는 정말 찾아내기 어려운 실수입니다.
직렬화(Serialization) 문제
SDK에서 반환되는 데이터는 표준 JavaScript 형식이 아닙니다. BigInt 값과 Map 객체를 포함하고 있습니다. 이를 브라우저로 보내기 위해 JSON.stringify에 그대로 넣으려고 하면 에러가 발생하거나 데이터가 조용히 누락됩니다. BigInt는 JSON에서 기본적으로 지원하는 표현 방식이 없으며, Map은 일반 객체처럼 직렬화되지 않습니다.
결국 저는 커스텀 직렬화기(serializer)를 작성했습니다. 이 직렬화기는 결과 객체를 순회하며 BigInt 값을 문자열로 변환하고, 응답이 서버를 떠나기 전에 Map 인스턴스를 일반 객체로 변환합니다. Midnight 데이터를 프론트엔드에 제공하는 API를 구축 중이라면, 이 단계를 미리 계획하십시오. SDK 출력이 JavaScript라고 해서 즉시 프론트엔드에서 사용하기 편할 것이라고 가정해서는 안 됩니다.
아키텍처
스택을 의도적으로 단순하게 유지했습니다. 백엔드는 Express를 실행하는 Node 서버입니다. @midnames/sdk를 임포트하여 이름 해석(resolution) 로직을 실행하고, 복잡한 직렬화(serialization) 과정을 처리한 뒤 깔끔한 JSON을 제공합니다. 프론트엔드는 순수 HTML과 바닐라 자바스크립트(vanilla JavaScript)로 구성했습니다. 빌드 단계도, 프레임워크도, 지갑 어댑터도 없습니다.
브라우저보다는 백엔드에서 SDK를 실행하기로 선택한 데에는 몇 가지 실질적인 이유가 있습니다. 프로바이더(provider) 설정을 클라이언트에 노출하지 않아도 되고, 직렬화 문제를 한 곳에서 해결할 수 있으며, 프론트엔드는 단순히 데이터를 가져와서 렌더링하기만 하면 되기 때문입니다.
가장 놀라웠던 부분은 이것입니다. 이름을 해석하는 것은 공개 읽기(public read) 작업이라는 점입니다. 지갑 연결도, 서명도, 사용자의 로그인도 필요하지 않습니다. 도메인이 존재한다면, 컨트랙트 상태는 요청하는 누구에게나 공개됩니다. 이는 모든 상호작용이 "지갑 연결"로 시작되는 일반적인 Web3 흐름과는 유의미하게 다른 점입니다. Midnight에서 신원(identity)을 읽는 것은 공개 웹사이트를 읽는 것과 마찬가지로 누구나 접근 가능한(permissionless) 방식입니다.
핵심 요약
이 뷰어를 만들면서 블록체인 개발에서 가장 어려운 부분은 블록체인 그 자체가 아닌 경우가 많다는 것을 다시 한번 깨달았습니다. Midnight은 중앙 데이터베이스 없이도 사람들이 자신의 이름과 프로필을 소유할 수 있게 하는 어려운 문제를 이미 해결했습니다. 개발자 입장에서 어려운 부분은 내가 어떤 네트워크를 가리키고 있는지 기억하는 것과 데이터 타입을 정리하기 위한 헬퍼 함수를 작성하는 것이었습니다.
이 프로토콜은 휴대 가능한 신원(portable identity)을 제공합니다. 개발자의 역할은 단순히 이를 올바르게 읽고 사용자의 흐름을 방해하지 않는 것입니다. 아키텍처를 단순하게 유지하고, 체인과 직접 상호작용하는 로직을 UI와 분리하며, 공개 읽기 작업을 있는 그대로—그저 분산 원장에 저장되어 있을 뿐인 일반적인 데이터베이스 쿼리처럼—취급하십시오.
코드를 확인하거나 직접 실행해보고 싶다면, 전체 소스 코드는 https://github.com/tomiin/midnames-profile-viewer 에서 확인할 수 있습니다.
