<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>na_treeeum.log</title>
        <link>https://velog.io/</link>
        <description>BlockChain DEV</description>
        <lastBuildDate>Sat, 19 Sep 2026 08:30:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>na_treeeum.log</title>
            <url>https://velog.velcdn.com/images/na_treeeum/profile/7477c3f5-894f-475f-8558-5d1296030c48/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. na_treeeum.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/na_treeeum" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[홈 서버에 OpenClaw 개인비서 만들기 3편 --- OpenAI Embedding 429를 버리고 로컬 Memory Indexing 구성하기]]></title>
            <link>https://velog.io/@na_treeeum/%ED%99%88-%EC%84%9C%EB%B2%84%EC%97%90-OpenClaw-%EA%B0%9C%EC%9D%B8%EB%B9%84%EC%84%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-3%ED%8E%B8-OpenAI-Embedding-429%EB%A5%BC-%EB%B2%84%EB%A6%AC%EA%B3%A0-%EB%A1%9C%EC%BB%AC-Memory-Indexing-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@na_treeeum/%ED%99%88-%EC%84%9C%EB%B2%84%EC%97%90-OpenClaw-%EA%B0%9C%EC%9D%B8%EB%B9%84%EC%84%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-3%ED%8E%B8-OpenAI-Embedding-429%EB%A5%BC-%EB%B2%84%EB%A6%AC%EA%B3%A0-%EB%A1%9C%EC%BB%AC-Memory-Indexing-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 19 Sep 2026 08:30:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 편에서 <code>김비서(main)</code>와 <code>박실장(calendar)</code>까지 만들고, 박실장에게
Google Calendar도 연결했다.
이제 슬슬 &quot;비서&quot;라고 부르려면 <strong>대화했던 내용을 기억하고 필요할 때
찾아오는 기능</strong>도 제대로 만들어줘야 할 것 같았다.</p>
<p>그런데 메모리 인덱싱을 켜자마자...</p>
<p><strong>OpenAI Embeddings 429.</strong></p>
<p>또 시작이다.</p>
</blockquote>
<p>홈 서버에 OpenClaw를 올려놓고 처음에는 Discord에서 대화하고, Google Calendar 일정을 관리하는 정도까지 구성했다.</p>
<p>현재 구조는 대충 이런 느낌이다.</p>
<pre><code class="language-text">                      👤 나
                        │
                        ▼
                    📎 김비서
               Chief Secretary Agent
                        │
                        ▼
                    📅 박실장
                  Calendar Agent</code></pre>
<p>앞으로 시장 분석 담당, 서버 관리 담당 같은 Agent도 하나씩 추가할
생각인데, Agent가 늘어나기 전에 <strong>Memory Search부터 제대로 잡아두는 게 좋겠다</strong>고 생각했다.</p>
<p>OpenClaw에는 대화나 Memory 파일을 검색할 때 단순 문자열 검색뿐 아니라 <strong>Embedding 기반 Semantic Search</strong>를 사용할 수 있다.</p>
<p>쉽게 말하면 이런 거다.</p>
<pre><code class="language-text">&quot;지난번에 홈 서버 메모리 얘기했던 거 뭐였지?&quot;</code></pre>
<p>라는 질문을 했을 때 정확히 같은 문장이 저장되어 있지 않아도,</p>
<pre><code class="language-text">RAM
N100
로컬 모델
Embedding
메모리 사용량</code></pre>
<p>같이 <strong>의미가 비슷한 내용을 찾아올 수 있게 만드는 것</strong>이다.</p>
<p>이번 3편에서는 이 Memory Indexing을 OpenAI Embedding API 대신 <strong>내 홈서버에서 llama.cpp + EmbeddingGemma로 로컬 처리</strong>하도록 구성해본다.</p>
<hr>
<h2 id="1-시작부터-429">1. 시작부터 429</h2>
<p>우선 현재 Memory 상태를 확인하면서 인덱싱을 시도했다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  memory status --index</code></pre>
<p>그런데 바로 이런 오류가 발생했다.</p>
<pre><code class="language-text">openai embeddings failed: 429
&quot;You have no credits remaining.&quot;</code></pre>
<p>main Agent 상태를 보면 대략 이런 상태였다.</p>
<pre><code class="language-text">Memory Search (main)
Provider: openai (requested: openai)
Model: text-embedding-3-small

Indexed: 3/3 files · 28 chunks
Dirty: yes

Embeddings: unavailable

Index identity:
index was built for model fts-only,
expected text-embedding-3-small

Vector search: paused until memory is rebuilt</code></pre>
<p>박실장도 비슷했다.</p>
<pre><code class="language-text">Memory Search (calendar)
Provider: openai
Model: text-embedding-3-small

Indexed: 0/3 files · 0 chunks
Dirty: yes
Embeddings: unavailable</code></pre>
<p>처음에는</p>
<blockquote>
<p>&quot;어? ChatGPT/OpenAI 모델 잘 쓰고 있는데 왜?&quot;</p>
</blockquote>
<p>싶었는데, <strong>Chat 모델 인증과 OpenAI Embedding API 사용량은 별개</strong>였다.</p>
<p>현재 OpenClaw는 Memory Search Provider를 별도로 지정하지 않으면 OpenAI
Embedding을 사용하고 있었고, 내가 사용하는 API 계정에는 Embedding API를
호출할 크레딧이 없었다.</p>
<p>즉 구조가 이랬다.</p>
<pre><code class="language-text">OpenClaw Agent
     │
     ├── Chat
     │     └── OpenAI GPT
     │
     └── Memory Search
           └── OpenAI Embeddings
                  │
                  └── 429 💥</code></pre>
<p>그렇다고 Memory Search 때문에 OpenAI API 크레딧을 따로 충전하고 싶지는
않았다.</p>
<p>홈 서버가 있는데?</p>
<p><strong>로컬에서 돌려보자.</strong></p>
<hr>
<h1 id="2-홈-서버에서-embedding-모델이-돌아갈까">2. 홈 서버에서 Embedding 모델이 돌아갈까?</h1>
<p>현재 홈 서버는 대략 이런 사양이다.</p>
<pre><code class="language-text">CPU
Intel N100
4 cores

RAM
7.5 GiB

Swap
4 GiB

Disk
98 GB
약 51 GB 여유</code></pre>
<p>처음에는 <code>local model</code>이라고 해서 살짝 긴장했다.</p>
<pre><code class="language-text">&quot;설마 LLM 하나 더 띄워야 하나?&quot;</code></pre>
<p>하지만 이번에 필요한 건 ChatGPT 같은 Chat LLM이 아니다.</p>
<p><strong>Embedding 전용 모델</strong>이다.</p>
<p>이번에 OpenClaw이 사용하는 기본 로컬 Embedding 모델은:</p>
<pre><code class="language-text">EmbeddingGemma 300M</code></pre>
<p>정도이고 실제 GGUF 파일도 약 300MB 수준이다.</p>
<p>그래서 대략:</p>
<pre><code class="language-text">Chat LLM
→ 수 GB ~ 수십 GB
→ N100 + 8GB RAM에서는 부담

Embedding Model
→ 약 300MB
→ N100에서도 충분히 가능</code></pre>
<p>이라고 판단했다.</p>
<p>특히 Embedding은 계속 문장을 생성하는 Chat 모델과 달리, Memory를 벡터로
변환할 때 CPU를 사용하는 형태라 내 홈 서버 용도로는 충분해 보였다.</p>
<hr>
<h1 id="3-llamacpp-provider-설치">3. llama.cpp Provider 설치</h1>
<p>OpenClaw에는 공식 <code>llama.cpp Provider</code> 플러그인이 있다.</p>
<p>설치한다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  plugins install @openclaw/llama-cpp-provider</code></pre>
<p>설치 후 확인해보니:</p>
<pre><code class="language-text">llama.cpp Provider
id: llama-cpp

Status: loaded
Version: 2026.8.2

Capabilities:
text-inference: llama-cpp
embedding: local</code></pre>
<p>굿.</p>
<p>이제 Memory Search Provider를 <code>local</code>로 바꾼다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  config set memory.search \
  &#39;{&quot;provider&quot;:&quot;local&quot;}&#39; \
  --strict-json</code></pre>
<p>나중에는 사용할 모델도 명시했다.</p>
<pre><code class="language-json">{
  &quot;provider&quot;: &quot;local&quot;,
  &quot;local&quot;: {
    &quot;modelPath&quot;: &quot;hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf&quot;
  }
}</code></pre>
<p>확인은:</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  config get memory.search</code></pre>
<p>그런데...</p>
<hr>
<h1 id="4-provider를-local로-바꿨는데-또-안-된다">4. Provider를 local로 바꿨는데 또 안 된다</h1>
<p>상태를 다시 확인했다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  memory status --deep --agent main</code></pre>
<p>이번에는 OpenAI 429 대신 새로운 오류가 나왔다.</p>
<pre><code class="language-text">Managed local embeddings are unavailable.

Run `openclaw configure`,
choose llama.cpp,
and retry.

Local embeddings need the managed llama.cpp server config.</code></pre>
<p>뭐지?</p>
<p><code>llama.cpp</code> 플러그인도 설치했고 <code>memory.search.provider=local</code>도
넣었는데 왜 안 될까.</p>
<p>여기서 중요한 걸 하나 알게 됐다.</p>
<p>OpenClaw의 로컬 Memory Embedding은 그냥 GGUF 파일만 지정한다고 되는
구조가 아니었다.</p>
<pre><code class="language-text">memory.search.provider = local
             │
             ▼
      llama.cpp Provider
             │
             ▼
   Managed llama-server
             │
             ▼
      EmbeddingGemma</code></pre>
<p>즉 OpenClaw이 직접 <code>llama-server</code> 프로세스를 관리할 수 있도록</p>
<pre><code class="language-text">models.providers.llama-cpp.localService</code></pre>
<p>설정까지 있어야 했다.</p>
<hr>
<h1 id="5-openclaw-onboarding으로-managed-llamacpp-구성">5. OpenClaw onboarding으로 Managed llama.cpp 구성</h1>
<p>공식적으로는 onboarding/configure 과정에서 설정하도록 되어 있었다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm -it openclaw-cli \
  onboard</code></pre>
<p>대략 다음 순서로 들어갔다.</p>
<pre><code class="language-text">Ask first
→ No — I&#39;ll configure it manually
→ More…
→ Local llama.cpp
→ Managed local server</code></pre>
<p>그러면 이런 안내가 나온다.</p>
<pre><code class="language-text">OpenClaw can install a verified llama.cpp server
and download only the local embedding model
(about 0.3 GB).

This will not install or change your chat model.
Continue?</code></pre>
<p>Yes.</p>
<p>이게 내가 원했던 구조다.</p>
<pre><code class="language-text">GPT-5.6
→ 기존대로 Chat 용도

EmbeddingGemma
→ Memory Search만 로컬 처리</code></pre>
<p>그런데...</p>
<p>역시 한 번에 될 리가 없다.</p>
<hr>
<h1 id="6-첫-번째-삽질-----libgompso1이-없다">6. 첫 번째 삽질 --- libgomp.so.1이 없다</h1>
<p>설치 중 <code>llama-server</code> 실행이 실패했다.</p>
<pre><code class="language-text">llama-server: error while loading shared libraries:
libgomp.so.1: cannot open shared object file</code></pre>
<p><code>libgomp</code>는 GCC의 OpenMP runtime이다.</p>
<p>llama.cpp CPU 빌드가 이 라이브러리를 필요로 하는데 현재 OpenClaw Docker
image에는 없었던 것 같다.</p>
<p>그래서 기존에 만들어둔 <code>Dockerfile.gog</code>에 <code>libgomp1</code>을 추가했다.</p>
<pre><code class="language-dockerfile">FROM ghcr.io/openclaw/openclaw:latest

USER root

ARG GOG_VERSION=&quot;0.38.2&quot;

RUN apt-get update \
    &amp;&amp; apt-get install -y --no-install-recommends libgomp1 \
    &amp;&amp; rm -rf /var/lib/apt/lists/*

RUN set -eux; \
    curl -fsSL \
      &quot;https://github.com/openclaw/gogcli/releases/download/v${GOG_VERSION}/gogcli_${GOG_VERSION}_linux_amd64.tar.gz&quot; \
      -o /tmp/gogcli.tar.gz; \
    tar -xzf /tmp/gogcli.tar.gz -C /tmp; \
    install -m 0755 /tmp/gog /usr/local/bin/gog; \
    rm -f /tmp/gogcli.tar.gz /tmp/gog; \
    gog --version

RUN chmod 1777 /tmp

USER node</code></pre>
<p>이미지는 다시 빌드하고 Gateway를 올렸다.</p>
<p>설치 확인:</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  exec openclaw-gateway \
  sh -lc &#39;dpkg -s libgomp1 | grep -E &quot;Status|Version&quot;; \
  find /usr/lib -name &quot;libgomp.so*&quot; -print&#39;</code></pre>
<p>정상적으로:</p>
<pre><code class="language-text">Status: install ok installed
Version: 12.2.0-14+deb12u1

/usr/lib/x86_64-linux-gnu/libgomp.so.1
/usr/lib/x86_64-linux-gnu/libgomp.so.1.0.0</code></pre>
<p>가 확인됐다.</p>
<p>오케이. 다시 onboarding.</p>
<hr>
<h1 id="7-두-번째-삽질-----managed-server-prepared인데-실패">7. 두 번째 삽질 --- &quot;Managed server prepared&quot;인데 실패?</h1>
<p>이번에는 llama-server 설치도 되고 EmbeddingGemma도 받아졌다.</p>
<p>그런데 onboarding 마지막에 이상한 메시지가 나왔다.</p>
<pre><code class="language-text">Managed llama.cpp server prepared</code></pre>
<p>오?</p>
<p>성공인가?</p>
<p>바로 이어서:</p>
<pre><code class="language-text">Managed local server:
This AI route is not available right now.

llama.cpp setup completed,
but no compatible model was found.
Add a compatible model and try again.</code></pre>
<p>뭐여.</p>
<pre><code class="language-text">Managed server prepared
→ 성공한 것 같음

no compatible model
→ 실패한 것 같음</code></pre>
<p>상태를 확인하면 여전히:</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  config get models.providers.llama-cpp</code></pre>
<pre><code class="language-text">Config path is valid but unset:
models.providers.llama-cpp</code></pre>
<p>였다.</p>
<p>즉 llama.cpp 서버와 Embedding 모델은 준비했는데 <strong>설정이 저장되지
않았다.</strong></p>
<hr>
<h1 id="8-실제로-파일은-다-받아졌는지-확인">8. 실제로 파일은 다 받아졌는지 확인</h1>
<p>먼저 llama-server부터 찾았다.</p>
<pre><code class="language-text">/home/node/.openclaw/tools/llama.cpp/b10534/linux-x64/llama-server</code></pre>
<p>EmbeddingGemma도 있었다.</p>
<pre><code class="language-text">/home/node/.openclaw/models/llama.cpp/
hf_ggml-org_embeddinggemma-300m-qat-Q8_0.gguf</code></pre>
<p>llama-server 자체도 정상 실행됐다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  exec openclaw-gateway \
  sh -lc &#39;/home/node/.openclaw/tools/llama.cpp/b10534/linux-x64/llama-server \
  --help | grep -E &quot;embedding|embeddings|router|model-dir|host|port&quot; | head -80&#39;</code></pre>
<p>출력에는:</p>
<pre><code class="language-text">--host HOST
--port PORT
--embedding, --embeddings
--models-dir PATH
--models-preset PATH
--models-max N</code></pre>
<p>등이 정상적으로 존재했다.</p>
<p>즉:</p>
<pre><code class="language-text">llama.cpp plugin      ✅
llama-server binary   ✅
libgomp               ✅
EmbeddingGemma GGUF   ✅
memory provider local ✅

Managed config        ❌</code></pre>
<p>딱 이 상태였다.</p>
<hr>
<h1 id="9-안-되면-코드를-까보자">9. 안 되면 코드를 까보자</h1>
<p>여기서 onboarding을 계속 반복하는 건 의미가 없어 보였다.</p>
<p>그래서 설치된 OpenClaw llama.cpp Provider 코드를 직접 확인했다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  exec openclaw-gateway \
  sh -lc &#39;grep -R -n -E \
  &quot;localService|models-preset|Managed llama.cpp server prepared|no compatible model was found&quot; \
  /home/node/.openclaw/npm/projects/openclaw-llama-cpp-provider-*/node_modules/@openclaw/llama-cpp-provider/dist \
  2&gt;/dev/null | head -80&#39;</code></pre>
<p>여기서 <code>runLlamaCppSetup()</code>을 찾았다.</p>
<p>핵심 로직을 보면 embedding-only 모드에서는 Chat model을 설치하지 않는다.</p>
<pre><code class="language-javascript">if (localMemoryIntent) {
    if (await ctx.prompter.confirm({
        message: `OpenClaw can install a verified llama.cpp server and download only ${describeEmbeddingDownload(embeddingModelIsDefault)}. This will not install or change your chat model. Continue?`,
        initialValue: false
    })) return { kind: &quot;embedding-only&quot; };
}</code></pre>
<p>그리고 실제 setup에서는:</p>
<pre><code class="language-javascript">if (plan.kind === &quot;chat&quot;) {
    // chat model 설정
} else {
    chatModel = { mode: &quot;remove&quot; };
}</code></pre>
<p>즉 <strong>Embedding-only는 공식적으로 지원되는 정상적인 setup path</strong>였다.</p>
<p>그다음:</p>
<pre><code class="language-javascript">const managed = await prepareManagedLlamaServer({
    chatModel,
    embeddingModelIsDefault: embeddingModel.isDefault,
    embeddingModelPath,
    port: readConfiguredPort(managedExisting)
});

progress.stop(&quot;Managed llama.cpp server prepared&quot;);</code></pre>
<p>여기까지 성공하고 있었다.</p>
<p>만약 llama-server나 모델 준비 자체가 실패했다면:</p>
<pre><code class="language-text">llama.cpp setup failed</code></pre>
<p>가 나왔어야 한다.</p>
<p>그런데 나는:</p>
<pre><code class="language-text">Managed llama.cpp server prepared</code></pre>
<p>까지 봤다.</p>
<p>따라서 문제는 <strong>그 이후 onboarding 공통 단계에서 Chat용 compatible model을 찾으려다가 embedding-only 설정 patch가 저장되지 않는 쪽</strong>으로 보였다.</p>
<hr>
<h1 id="10-openclaw이-만들려던-preset-확인">10. OpenClaw이 만들려던 preset 확인</h1>
<p><code>prepareManagedLlamaServer()</code> 코드를 보면 <code>models.ini</code>라는 preset을
만든다.</p>
<p>그래서 확인했다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  exec openclaw-gateway \
  cat /home/node/.openclaw/tools/llama.cpp/models.ini</code></pre>
<p>짜라란~</p>
<pre><code class="language-ini">version = 1

[embeddinggemma-300m-qat-q8_0]
model = /home/node/.openclaw/models/llama.cpp/hf_ggml-org_embeddinggemma-300m-qat-Q8_0.gguf
ubatch-size = 2048
embedding = true</code></pre>
<p>완벽하다.</p>
<p>Chat model은 없고 EmbeddingGemma만 등록되어 있다.</p>
<pre><code class="language-text">models.ini
   │
   └── embeddinggemma-300m-qat-q8_0
         │
         ├── GGUF
         ├── ubatch-size = 2048
         └── embedding = true</code></pre>
<p>즉 우리가 원했던 <strong>Embedding-only llama-server preset은 이미 제대로 만들어져 있었다.</strong></p>
<hr>
<h1 id="11-managed-llamacpp-설정값까지-코드에서-찾기">11. Managed llama.cpp 설정값까지 코드에서 찾기</h1>
<p>이제 OpenClaw이 원래 저장했어야 할 설정을 직접 찾아봤다.</p>
<p><code>prepareManagedLlamaServer()</code>는 다음처럼 되어 있었다.</p>
<pre><code class="language-javascript">const LLAMA_CPP_DEFAULT_PORT = 19432;</code></pre>
<p>Managed server 실행 인자는:</p>
<pre><code class="language-javascript">args: [
    &quot;--host&quot;,
    &quot;127.0.0.1&quot;,
    &quot;--port&quot;,
    String(port),
    &quot;--models-preset&quot;,
    presetPath,
    &quot;--models-max&quot;,
    &quot;2&quot;,
    &quot;--metrics&quot;,
    &quot;--no-ui&quot;
]</code></pre>
<p>그리고 provider config는:</p>
<pre><code class="language-javascript">return {
    ...existing,
    baseUrl: managed?.baseUrl ?? existing?.baseUrl ?? `http://127.0.0.1:19432/v1`,
    apiKey: existing?.apiKey ?? resolveLlamaCppSyntheticApiKey(),
    api: &quot;openai-completions&quot;,
    timeoutSeconds: existing?.timeoutSeconds ?? 600,

    ...managed ? {
        localService: {
            command: managed.command,
            args: managed.args,
            healthUrl: managed.healthUrl,
            readyTimeoutMs: 30000,
            idleStopMs: 600000
        }
    } : {},

    models
};</code></pre>
<p>그리고 제일 중요한 부분.</p>
<p>Embedding-only setup에서는:</p>
<pre><code class="language-javascript">modelInventory: []</code></pre>
<p>를 전달하고 있었다.</p>
<p>즉:</p>
<pre><code class="language-json">&quot;models&quot;: []</code></pre>
<p>는 오류가 아니다.</p>
<p><strong>Chat model을 로컬에서 사용하지 않는 Embedding-only 구성에서는 의도된
값이다.</strong></p>
<hr>
<h1 id="12-결국-managed-provider-설정을-직접-넣었다">12. 결국 Managed Provider 설정을 직접 넣었다</h1>
<p>OpenClaw 2026.8.2 코드에서 정확한 값을 확인했으니 그대로 넣었다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  config set models.providers.llama-cpp \
  &#39;{
    &quot;baseUrl&quot;: &quot;http://127.0.0.1:19432/v1&quot;,
    &quot;apiKey&quot;: &quot;llama-cpp-local&quot;,
    &quot;api&quot;: &quot;openai-completions&quot;,
    &quot;timeoutSeconds&quot;: 600,
    &quot;localService&quot;: {
      &quot;command&quot;: &quot;/home/node/.openclaw/tools/llama.cpp/b10534/linux-x64/llama-server&quot;,
      &quot;args&quot;: [
        &quot;--host&quot;,
        &quot;127.0.0.1&quot;,
        &quot;--port&quot;,
        &quot;19432&quot;,
        &quot;--models-preset&quot;,
        &quot;/home/node/.openclaw/tools/llama.cpp/models.ini&quot;,
        &quot;--models-max&quot;,
        &quot;2&quot;,
        &quot;--metrics&quot;,
        &quot;--no-ui&quot;
      ],
      &quot;healthUrl&quot;: &quot;http://127.0.0.1:19432/health&quot;,
      &quot;readyTimeoutMs&quot;: 30000,
      &quot;idleStopMs&quot;: 600000
    },
    &quot;models&quot;: []
  }&#39; \
  --strict-json</code></pre>
<p>설정 확인:</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  config get models.providers.llama-cpp</code></pre>
<p>그리고 Gateway를 재시작했다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  restart openclaw-gateway</code></pre>
<p>이제 다시 Memory 상태를 확인해본다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  memory status --deep --agent main</code></pre>
<p>드디어.</p>
<pre><code class="language-text">[provider-local-service]
starting llama-cpp local service

[provider-local-service]
llama-cpp local service ready</code></pre>
<p>그리고:</p>
<pre><code class="language-text">Memory Search (main)

Provider: local (requested: local)

Embeddings: ready

llama.cpp server: cpu
Server model: embeddinggemma-300m-qat-q8_0

Endpoints:
health=ready
models=ready
props=ready
metrics=ready

Vector store: ready
Semantic vectors: ready
FTS: ready</code></pre>
<p>오케이 굿.</p>
<p><strong>로컬 Embedding 서버가 제대로 붙었다.</strong></p>
<hr>
<h1 id="13-기존-fts-only-index를-다시-만들어야-한다">13. 기존 FTS-only Index를 다시 만들어야 한다</h1>
<p>그런데 아직 하나 남았다.</p>
<pre><code class="language-text">Index identity:
index was built for model fts-only

expected:
hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf

Vector search:
paused until memory is rebuilt</code></pre>
<p>이건 당연하다.</p>
<p>기존 Memory Index는 Embedding이 없던 시절에 만들어진 <code>fts-only</code> 인덱스다.</p>
<p>이제 EmbeddingGemma를 붙였으니 기존 문서들을 다시 Embedding해서 Vector
Index를 만들어줘야 한다.</p>
<p>OpenClaw이 친절하게 명령어까지 알려준다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  memory status --index --agent main</code></pre>
<p>인덱싱 후 다시 확인한다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  memory status --deep --agent main</code></pre>
<p>결과:</p>
<pre><code class="language-text">Memory Search (main)

Provider: local
Indexed: 3/3 files · 68 chunks
Dirty: no

Embeddings: ready

llama.cpp server: cpu
Server model: embeddinggemma-300m-qat-q8_0

Vector store: ready
Semantic vectors: ready
Vector dims: 768

FTS: ready

Embedding cache:
enabled (68 entries)</code></pre>
<p>드디어 <code>Dirty: no</code>.</p>
<p>그리고 기존에 있던:</p>
<pre><code class="language-text">Vector search: paused until memory is rebuilt</code></pre>
<p>문구도 사라졌다.</p>
<p>즉 <strong>김비서 Memory Indexing 완료.</strong></p>
<hr>
<h1 id="14-박실장도-인덱싱">14. 박실장도 인덱싱</h1>
<p>김비서만 기억력이 좋아지면 섭섭하다.</p>
<p>박실장도 같은 로컬 Embedding Provider를 사용하도록 인덱싱한다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  memory status --index --agent calendar</code></pre>
<p>그리고 확인.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  memory status --deep --agent calendar</code></pre>
<p>결과:</p>
<pre><code class="language-text">Memory Search (calendar)

Provider: local
Indexed: 3/3 files · 13 chunks
Dirty: no

Embeddings: ready

llama.cpp server: cpu
Server model: embeddinggemma-300m-qat-q8_0

Vector store: ready
Semantic vectors: ready
Vector dims: 768

FTS: ready

Embedding cache:
enabled (13 entries)</code></pre>
<p>짜라란~</p>
<pre><code class="language-text">📎 김비서
 └── Memory Search
      ├── FTS              ✅
      ├── Semantic Search  ✅
      └── Local Embedding  ✅

📅 박실장
 └── Memory Search
      ├── FTS              ✅
      ├── Semantic Search  ✅
      └── Local Embedding  ✅</code></pre>
<hr>
<h1 id="15-그런데-llama-server가-바로-종료되는데">15. 그런데 llama-server가 바로 종료되는데?</h1>
<p>상태 확인 마지막에 이런 로그가 나온다.</p>
<pre><code class="language-text">[provider-local-service]
stopping local model service:
reason=host-shutdown</code></pre>
<p>처음 보면:</p>
<pre><code class="language-text">&quot;어? 서버 죽은 거 아닌가?&quot;</code></pre>
<p>싶다.</p>
<p>하지만 정상이다.</p>
<p><code>docker compose run --rm openclaw-cli ...</code>로 실행한 CLI 프로세스가 Memory 작업을 위해 Managed llama-server를 띄우고, 작업이 끝나면서 자기 프로세스도 종료되기 때문에 local service도 같이 정리되는 것이다.</p>
<p>즉 테스트 명령에서는:</p>
<pre><code class="language-text">memory 명령 시작
      │
      ▼
llama-server 자동 실행
      │
      ▼
EmbeddingGemma load
      │
      ▼
Embedding / Search 작업
      │
      ▼
CLI 명령 종료
      │
      ▼
llama-server 종료</code></pre>
<p>형태로 동작한다.</p>
<p>OpenClaw에서 <code>localService</code>로 관리하고 있기 때문에 직접 systemd나 별도 Docker container로 llama-server를 항상 띄워둘 필요도 없다.</p>
<hr>
<h1 id="16-최종-구조">16. 최종 구조</h1>
<p>이번 작업이 끝난 현재 구조를 그려보면 이렇다.</p>
<pre><code class="language-text">                       👤 나
                         │
                         ▼
                     Discord
                         │
            ┌────────────┴────────────┐
            │                         │
            ▼                         ▼
        📎 김비서                  📅 박실장
          main                     calendar
            │                         │
            └────────────┬────────────┘
                         │
                         ▼
                  OpenClaw Memory
                         │
              ┌──────────┴──────────┐
              │                     │
              ▼                     ▼
             FTS              Semantic Search
                                    │
                                    ▼
                            llama.cpp Provider
                                    │
                                    ▼
                         Managed llama-server
                                    │
                                    ▼
                          EmbeddingGemma 300M
                                    │
                                    ▼
                           768-dim Vectors
                                    │
                                    ▼
                              sqlite-vec</code></pre>
<p>Chat 모델은 여전히 기존 OpenAI 모델을 사용한다.</p>
<pre><code class="language-text">김비서
→ GPT-5.6 Sol

박실장
→ GPT-5.6 Terra</code></pre>
<p>Memory Embedding만:</p>
<pre><code class="language-text">EmbeddingGemma 300M
→ 홈 서버에서 로컬 처리</code></pre>
<p>하도록 분리했다.</p>
<p>개인적으로 이 구성이 마음에 든다.</p>
<p>Chat 모델까지 N100에서 돌리는 건 무리지만, <strong>Memory Embedding처럼 작고 명확한 역할만 로컬로 가져오는 건 홈 서버를 굴리는 재미도 있고 API 비용에도 덜 의존한다.</strong></p>
<hr>
<h1 id="17-이번-삽질-정리">17. 이번 삽질 정리</h1>
<p>처음에는 단순히:</p>
<pre><code class="language-text">OpenAI Embedding 429</code></pre>
<p>하나 해결하려고 시작했다.</p>
<p>그런데 실제 여정은:</p>
<pre><code class="language-text">OpenAI Embedding quota 429
        │
        ▼
memory.search.provider = local
        │
        ▼
llama.cpp Provider 설치
        │
        ▼
Managed llama.cpp 필요
        │
        ▼
libgomp.so.1 없음
        │
        ▼
Docker image에 libgomp1 추가
        │
        ▼
EmbeddingGemma 다운로드
        │
        ▼
Managed llama.cpp server prepared
        │
        ▼
&quot;no compatible model found&quot; ???
        │
        ▼
OpenClaw plugin 코드 직접 확인
        │
        ▼
models.ini 정상 확인
        │
        ▼
embedding-only config 구조 확인
        │
        ▼
models.providers.llama-cpp 수동 설정
        │
        ▼
Memory Index rebuild
        │
        ├── 📎 main     ✅
        └── 📅 calendar ✅</code></pre>
<p>였다.</p>
<p>ㅋㅋㅋㅋㅋ</p>
<hr>
<h1 id="18-이번에-알게-된-것">18. 이번에 알게 된 것</h1>
<p>이번 작업을 하면서 몇 가지 구조가 꽤 명확해졌다.</p>
<h3 id="chat-model과-embedding-model은-완전히-별개다">Chat Model과 Embedding Model은 완전히 별개다</h3>
<p>처음에는 OpenAI Chat 모델을 사용하고 있으니 Memory도 당연히 같이 될 줄 알았다.</p>
<p>하지만:</p>
<pre><code class="language-text">Chat
→ GPT

Memory Semantic Search
→ Embedding Model</code></pre>
<p>은 별개의 영역이다.</p>
<p>그래서 지금처럼:</p>
<pre><code class="language-text">Chat
→ Cloud / OpenAI

Embedding
→ Local / EmbeddingGemma</code></pre>
<p>구성도 가능하다.</p>
<p>오히려 홈 서버에서는 이쪽이 꽤 합리적인 것 같다.</p>
<h3 id="semantic-search와-fts는-다르다">Semantic Search와 FTS는 다르다</h3>
<p>FTS는 단어 중심이다.</p>
<pre><code class="language-text">검색:
&quot;Google Calendar&quot;

→ Google Calendar라는 문자열이 있는 문서</code></pre>
<p>Semantic Search는 의미 중심이다.</p>
<pre><code class="language-text">검색:
&quot;내 일정 관리 설정 어떻게 했지?&quot;

→ Google Calendar
→ 박실장
→ gog
→ 일정관리실</code></pre>
<p>처럼 표현이 정확히 같지 않아도 관련 내용을 찾을 수 있다.</p>
<p>OpenClaw에서는 둘을 같이 사용할 수 있다.</p>
<pre><code class="language-text">FTS              → ready
Semantic vectors → ready</code></pre>
<h3 id="dirty-no를-확인하자"><code>Dirty: no</code>를 확인하자</h3>
<p>Embedding Provider가 <code>ready</code>라고 끝난 게 아니다.</p>
<p>기존 Index가 다른 모델이나 <code>fts-only</code> 기준으로 만들어져 있으면:</p>
<pre><code class="language-text">Dirty: yes
Vector search: paused</code></pre>
<p>상태가 될 수 있다.</p>
<p>그래서 Provider 변경 후에는 반드시 Index를 다시 만들어주는 게 좋다.</p>
<p>최종적으로 내가 확인한 정상 상태는:</p>
<pre><code class="language-text">Provider: local
Dirty: no

Embeddings: ready
Vector store: ready
Semantic vectors: ready
Vector dims: 768
FTS: ready</code></pre>
<p>였다.</p>
<hr>
<h1 id="마무리">마무리</h1>
<p>이제 김비서와 박실장은 단순히 Discord에서 명령을 받아 처리하는 Bot에서
조금 더 <strong>개인비서다운 구조</strong>가 됐다.</p>
<pre><code class="language-text">1편
홈 서버 + OpenClaw + Discord
        │
        ▼
📎 김비서

2편
Multi-Agent + Google Calendar
        │
        ▼
📅 박실장

3편
Local Memory Indexing
        │
        ▼
🧠 Semantic Memory</code></pre>
<p>특히 이번에는 OpenClaw 설정만 만진 게 아니라:</p>
<pre><code class="language-text">Docker
llama.cpp
GGUF
Embedding
Vector Search
sqlite-vec
Managed Process</code></pre>
<p>까지 한 번에 건드리게 됐다.</p>
<p>중간에 onboarding이</p>
<pre><code class="language-text">Managed llama.cpp server prepared</code></pre>
<p>라고 해놓고</p>
<pre><code class="language-text">no compatible model was found</code></pre>
<p>라고 하는 바람에 꽤 삽질했지만, 결국 플러그인 코드까지 까보고
<strong>Embedding-only Managed Provider 설정을 직접 만들어서 해결</strong>했다.</p>
<p>이 맛에 홈 서버 하는 거 아니겠는가.</p>
<p>다음에는 Agent들이 공통으로 따라야 할 규칙이나, 김비서가 다른 Agent에게 일을 분배하는 구조를 조금 더 손봐볼 생각이다.</p>
<p>과연 내 N100은 어디까지 버틸 것인가...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[홈 서버에 개인비서 만들기 2편 - OpenClaw에 두 번째 직원 `박실장` 채용하기]]></title>
            <link>https://velog.io/@na_treeeum/%ED%99%88-%EC%84%9C%EB%B2%84%EC%97%90-%EA%B0%9C%EC%9D%B8%EB%B9%84%EC%84%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-2%ED%8E%B8-OpenClaw%EC%97%90-%EB%91%90-%EB%B2%88%EC%A7%B8-%EC%A7%81%EC%9B%90-%EB%B0%95%EC%8B%A4%EC%9E%A5-%EC%B1%84%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@na_treeeum/%ED%99%88-%EC%84%9C%EB%B2%84%EC%97%90-%EA%B0%9C%EC%9D%B8%EB%B9%84%EC%84%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-2%ED%8E%B8-OpenClaw%EC%97%90-%EB%91%90-%EB%B2%88%EC%A7%B8-%EC%A7%81%EC%9B%90-%EB%B0%95%EC%8B%A4%EC%9E%A5-%EC%B1%84%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 09 Sep 2026 10:03:20 GMT</pubDate>
            <description><![CDATA[<p>지난 1편에서는 홈 서버에 OpenClaw를 올리고 Discord Bot인 <code>김비서</code>를
연결했습니다.</p>
<p>처음에는 그냥:</p>
<pre><code class="language-text">나
  ↓
Discord
  ↓
📎 김비서
  ↓
OpenClaw main agent</code></pre>
<p>정도였습니다.</p>
<p>일단 Discord에서 말을 걸면 대답하는 개인비서 하나가 생기긴 했습니다.</p>
<p>그런데 개인비서라고 만들어놓고 모든 일을 김비서 혼자 시키려니 뭔가
재미가 없었습니다.</p>
<p>일정도 관리하고, 시장도 보고, 홈 서버 상태도 확인하고, 나중에는 메일까지
정리하게 만들고 싶은데...</p>
<p>이걸 전부 하나의 Agent에게 몰아주는 것보다는 <strong>각 업무를 담당하는 직원을
따로 두고 김비서가 총괄하는 구조</strong>가 더 마음에 들었습니다.</p>
<p>그래서 2편부터는 본격적으로 OpenClaw를 <strong>멀티 에이전트 조직</strong>으로
만들어보려고 합니다.</p>
<p>최종적으로 만들고 싶은 그림은 이렇습니다.</p>
<pre><code class="language-text">                      👤 나
                        │
                        ▼
                    📎 김비서
               Chief Secretary Agent
                        │
          ┌─────────────┼─────────────┐
          │             │             │
          ▼             ▼             ▼
       📅 박실장      📈 한팀장      🖥️ 최과장
       Calendar        Market         Server
        Agent           Agent          Agent</code></pre>
<p>그리고 이번 2편에서 채용할 첫 번째 전문 직원은:</p>
<blockquote>
<p><strong>📅 박실장 - 일정관리실장</strong></p>
</blockquote>
<p>입니다.</p>
<p>박실장은 단순히 김비서가 일정관리 역할을 흉내 내는 구조가 아닙니다.</p>
<p>Discord Bot도 별도로 만들고, OpenClaw Agent도 별도로 만들고, Workspace와
성격도 따로 두고, Google Calendar까지 직접 사용할 수 있게 만들 생각입니다.</p>
<p>즉 이번 편의 목표는 정확히 이겁니다.</p>
<pre><code class="language-text">[1편에서 완성]

Discord
   ↓
📎 김비서
   ↓
main agent


[2편에서 추가]

Discord
   ├─ 📎 김비서
   │      ↓
   │   main agent
   │
   └─ 📅 박실장
          ↓
      calendar agent
          ↓
        gog CLI
          ↓
    Google Calendar</code></pre>
<p>김비서 혼자 있던 홈 서버에 <strong>두 번째 AI 직원이 들어오는 편</strong>이라고 보면
됩니다.</p>
<p>그리고 역시나...</p>
<pre><code class="language-text">Agent 하나 추가
+
Google Calendar 연결</code></pre>
<p>정도로 간단하게 끝날 줄 알았는데,</p>
<pre><code class="language-text">멀티 Agent routing
Discord Bot 분리
SecretRef
Docker image
OpenClaw 버전 충돌
/tmp 권한
Google OAuth
Calendar API
gog keyring
Skill allowlist</code></pre>
<p>까지 줄줄이 튀어나왔습니다.</p>
<p>이번에도 한 번에 되지는 않았습니다.</p>
<hr>
<h2 id="1-1편에서-만든-김비서부터-확인">1. 1편에서 만든 김비서부터 확인</h2>
<p>2편 작업을 시작하기 전에 <strong>1편에서 만든 김비서와 OpenClaw가 여전히
정상인지</strong> 먼저 확인했습니다.</p>
<p>OpenClaw는 Docker Compose로 실행하고 있습니다.</p>
<p>프로젝트 경로는 다음과 같습니다.</p>
<pre><code class="language-bash">cd /home/fcsd/openclaw</code></pre>
<p>먼저 Gateway 상태를 확인했습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  exec openclaw-gateway \
  sh -lc &#39;node dist/index.js gateway health --token &quot;$OPENCLAW_GATEWAY_TOKEN&quot;&#39;</code></pre>
<p>Gateway 자체의 health와 Discord 설정 여부를 확인하는 명령어입니다.</p>
<p>정상적으로 다음과 같이 나왔습니다.</p>
<pre><code class="language-text">Gateway Health
OK
Discord: configured</code></pre>
<p>Discord channel도 probe 해봤습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  channels status --probe</code></pre>
<p>결과:</p>
<pre><code class="language-text">Gateway reachable.
- Discord default: enabled, configured, running, connected,
  bot:@김비서, intents:content=limited, works, audit ok</code></pre>
<p>오케이 굿.</p>
<p>기존 <code>김비서</code>는 잘 살아있습니다.</p>
<hr>
<h1 id="2-박실장-calendar-agent-만들기">2. 박실장 Calendar Agent 만들기</h1>
<p>이번에 추가할 두 번째 agent는 <code>박실장</code>입니다.</p>
<p>역할은 오직 일정 관리입니다.</p>
<p>모델은 메인 비서인 김비서보다 조금 가벼운 <code>GPT-5.6 Terra</code>를 사용하기로
했습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  config set agents.entries.calendar.model \
  &#39;&quot;openai/gpt-5.6-terra&quot;&#39; \
  --strict-json</code></pre>
<p>Agent 구성은 대략 다음과 같습니다.</p>
<pre><code class="language-text">main
 ├─ 이름: 김비서
 ├─ 모델: openai/gpt-5.6-sol
 └─ Discord: default

calendar
 ├─ 이름: 박실장
 ├─ 모델: openai/gpt-5.6-terra
 └─ Discord: calendar</code></pre>
<p>Discord에서도 박실장용 Bot을 따로 만들었습니다.</p>
<p>즉 하나의 Bot이 내부적으로 역할극을 하는 게 아니라 진짜로:</p>
<pre><code class="language-text">Discord
 ├─ @김비서
 └─ @박실장</code></pre>
<p>두 Bot이 존재합니다.</p>
<hr>
<h1 id="3-discord-bot-token은-환경변수로-관리">3. Discord Bot Token은 환경변수로 관리</h1>
<p>박실장 Discord token을 OpenClaw config에 평문으로 박아 넣고 싶지는
않았습니다.</p>
<p>그래서 <code>.env</code>에 넣고 SecretRef 형태로 연결했습니다.</p>
<pre><code class="language-text">DISCORD_CALENDAR_BOT_TOKEN=...</code></pre>
<p>당연하지만 실제 token은 Git에 올리면 안 됩니다.</p>
<p>OpenClaw 설정에는 다음처럼 env reference만 저장했습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  config set channels.discord.accounts.calendar.token \
  &#39;{&quot;source&quot;:&quot;env&quot;,&quot;provider&quot;:&quot;default&quot;,&quot;id&quot;:&quot;DISCORD_CALENDAR_BOT_TOKEN&quot;}&#39; \
  --strict-json</code></pre>
<p>그리고 박실장 Discord account를 calendar agent에 binding했습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  agents bind \
  --agent calendar \
  --bind discord:calendar</code></pre>
<p>구조는 이렇게 됩니다.</p>
<pre><code class="language-text">@김비서
   ↓
Discord default
   ↓
main agent

@박실장
   ↓
Discord calendar
   ↓
calendar agent</code></pre>
<hr>
<h1 id="4-박실장은-일정관리실에서만-일하게-하기">4. 박실장은 일정관리실에서만 일하게 하기</h1>
<p>박실장에게 Discord Administrator 권한을 줬지만, OpenClaw에서는
일정관리실 channel만 처리하게 제한하고 싶었습니다.</p>
<p>그래서 account별 channel allowlist를 설정했습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  config set channels.discord.accounts.calendar.guilds.&lt;GUILD_ID&gt;.channels.&lt;CALENDAR_CHANNEL_ID&gt;.enabled \
  true \
  --strict-json</code></pre>
<p><code>&lt;GUILD_ID&gt;</code>와 <code>&lt;CALENDAR_CHANNEL_ID&gt;</code>에는 본인의 Discord 서버/채널 ID를
넣으면 됩니다.</p>
<p>이렇게 하면 Discord 권한상 Bot이 다른 채널을 볼 수 있더라도 <strong>OpenClaw는
허용한 채널의 요청만 처리</strong>합니다.</p>
<p>실제로 테스트해보니:</p>
<pre><code class="language-text">#일정관리실
나 → @박실장 안녕
박실장 → 응답 O

다른 채널
나 → @박실장 안녕
박실장 → 응답 X</code></pre>
<p>원하던 동작입니다.</p>
<hr>
<h1 id="5-박실장-성격도-만들어주기">5. 박실장 성격도 만들어주기</h1>
<p>그냥 Calendar API를 호출하는 Bot보다는 진짜 <code>실장</code>처럼 행동했으면 했습니다.</p>
<p>그래서 calendar workspace의 <code>SOUL.md</code>에 성격과 역할을 지정했습니다.</p>
<p>대략 이런 내용입니다.</p>
<pre><code class="language-markdown">## Identity

You are **박실장**, the manager of the user&#39;s **일정관리실**.

Your primary responsibility is calendar and schedule management.

### Personality

- Speak naturally in Korean.
- Address the user as Mr.Natreeum.
- Be concise, calm, and competent.
- Avoid excessive enthusiasm.
- When reporting schedules, prioritize date, time, title, and context.

### Role

- Checking schedules
- Creating calendar events
- Updating calendar events
- Deleting calendar events
- Finding available time
- Detecting schedule conflicts
- Reminding about important upcoming events</code></pre>
<p><code>AGENTS.md</code>에도 일정 관리 규칙을 추가했습니다.</p>
<p>예를 들면:</p>
<pre><code class="language-text">Timezone: Asia/Seoul

조회:
→ 바로 실행 가능

일정 생성:
→ 날짜/시간이 명확하면 생성
→ 애매하면 질문

수정:
→ 정확한 이벤트를 특정한 뒤 수정

삭제:
→ 대상이 명확하면 삭제
→ 여러 후보가 있으면 확인

충돌:
→ 기존 일정과 겹치면 먼저 경고</code></pre>
<p>이제 박실장의 캐릭터는 어느 정도 완성됐습니다.</p>
<p>문제는 아직 실제 Calendar가 없다는 겁니다.</p>
<hr>
<h1 id="6-google-calendar-연결하기">6. Google Calendar 연결하기</h1>
<p>OpenClaw에는 Google Workspace를 다루는 <code>gog</code> skill이 bundled 되어 있습니다.</p>
<p>Calendar뿐 아니라 Gmail, Drive, Contacts 등도 다룰 수 있는 CLI입니다.</p>
<p>그런데 확인해보니:</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  skills info gog --agent calendar</code></pre>
<p>결과가:</p>
<pre><code class="language-text">gog Disabled

Requirements:
  Binaries: ✗ gog</code></pre>
<p>였습니다.</p>
<p>Skill 정의는 OpenClaw 안에 있는데 정작 실행 파일인 <code>gog</code>가 Docker
image에 없습니다.</p>
<p>그래서 image에 직접 넣기로 했습니다.</p>
<hr>
<h1 id="7-첫-번째-대삽질---openclaw-버전이-내려가버렸다">7. 첫 번째 대삽질 - OpenClaw 버전이 내려가버렸다</h1>
<p>처음에는 기존 OpenClaw <code>Dockerfile</code>에 gog 설치 부분을 추가하고 그냥
build했습니다.</p>
<p>그런데 Gateway가 계속 재시작합니다.</p>
<pre><code class="language-bash">docker compose ps</code></pre>
<p>로그를 확인해보니:</p>
<pre><code class="language-text">Your OpenClaw config was written by version 2026.8.2,
but this command is running 2026.8.1.

Refusing to run automatic gateway startup migrations...</code></pre>
<p>엥?</p>
<p>원인은 이거였습니다.</p>
<pre><code class="language-text">기존 실행 image
OpenClaw 2026.8.2
       ↓
로컬 Dockerfile build
       ↓
내가 checkout 해둔 source는 2026.8.1
       ↓
2026.8.1 image 생성
       ↓
config는 이미 2026.8.2가 작성함
       ↓
구버전 binary가 신버전 config를 읽으려고 함
       ↓
Gateway: 응 안돼</code></pre>
<p>OpenClaw가 오히려 구버전 binary로 config를 망가뜨리지 않도록 막아준
셈입니다.</p>
<p>여기서:</p>
<pre><code class="language-text">OPENCLAW_ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS=1</code></pre>
<p>같은 옵션으로 억지로 내려가는 방법도 있었지만, 제가 원하는 건
downgrade가 아니므로 사용하지 않았습니다.</p>
<p>공식 image를 다시 pull해서 복구했습니다.</p>
<pre><code class="language-bash">cd /home/fcsd/openclaw

docker compose stop openclaw-gateway

docker pull ghcr.io/openclaw/openclaw:latest

docker compose up -d --force-recreate --no-build openclaw-gateway</code></pre>
<p>버전 확인:</p>
<pre><code class="language-bash">docker compose exec openclaw-gateway \
  node dist/index.js --version</code></pre>
<p>결과:</p>
<pre><code class="language-text">OpenClaw 2026.8.2 (0965053)</code></pre>
<p>살았습니다.</p>
<hr>
<h1 id="8-기존-dockerfile을-건드리지-말고-확장-image-만들기">8. 기존 Dockerfile을 건드리지 말고 확장 image 만들기</h1>
<p>교훈을 얻었습니다.</p>
<blockquote>
<p><strong>gog 하나 넣겠다고 OpenClaw source image를 다시 build하지 말자.</strong></p>
</blockquote>
<p>그래서 별도의 <code>Dockerfile.gog</code>를 만들었습니다.</p>
<pre><code class="language-dockerfile">FROM ghcr.io/openclaw/openclaw:latest

USER root

ARG GOG_VERSION=&quot;0.38.2&quot;

RUN set -eux; \
    curl -fsSL \
      &quot;https://github.com/openclaw/gogcli/releases/download/v${GOG_VERSION}/gogcli_${GOG_VERSION}_linux_amd64.tar.gz&quot; \
      -o /tmp/gogcli.tar.gz; \
    tar -xzf /tmp/gogcli.tar.gz -C /tmp; \
    install -m 0755 /tmp/gog /usr/local/bin/gog; \
    rm -f /tmp/gogcli.tar.gz /tmp/gog; \
    gog --version

RUN chmod 1777 /tmp

USER node</code></pre>
<p>Compose의:</p>
<pre><code class="language-yaml">build: .</code></pre>
<p>도 다음처럼 변경했습니다.</p>
<pre><code class="language-yaml">build:
  context: .
  dockerfile: Dockerfile.gog</code></pre>
<p>이제 구조는:</p>
<pre><code class="language-text">공식 OpenClaw image
       ↓
Dockerfile.gog
       ↓
gog만 추가
       ↓
내 OpenClaw image</code></pre>
<p>훨씬 깔끔합니다.</p>
<hr>
<h1 id="9-두-번째-대삽질---tmp-권한-때문에-gateway-무한-재시작">9. 두 번째 대삽질 - /tmp 권한 때문에 Gateway 무한 재시작</h1>
<p>새 image를 만들고 Gateway를 띄웠습니다.</p>
<pre><code class="language-bash">docker compose up -d --force-recreate openclaw-gateway</code></pre>
<p>그런데 또 restart loop.</p>
<p>ㅋㅋ</p>
<p>로그를 확인했습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  logs --tail=100 openclaw-gateway</code></pre>
<p>이번에는:</p>
<pre><code class="language-text">Unable to create fallback OpenClaw temp dir: /tmp/openclaw-1000</code></pre>
<p>권한을 확인해봤습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm --no-deps \
  --entrypoint sh \
  openclaw-gateway \
  -lc &#39;id; ls -ld /tmp; touch /tmp/test-file &amp;&amp; echo tmp-writable; mkdir -p /tmp/openclaw-1000 &amp;&amp; ls -ld /tmp/openclaw-1000&#39;</code></pre>
<p>결과:</p>
<pre><code class="language-text">uid=1000(node) gid=1000(node)
drwxr-xr-x ... /tmp

touch: cannot touch &#39;/tmp/test-file&#39;: Permission denied
mkdir: cannot create directory &#39;/tmp/openclaw-1000&#39;: Permission denied</code></pre>
<p>찾았습니다.</p>
<p>정상적인 <code>/tmp</code>는 보통:</p>
<pre><code class="language-text">drwxrwxrwt</code></pre>
<p>즉 <code>1777</code>이어야 합니다.</p>
<p>그래서 <code>Dockerfile.gog</code>에:</p>
<pre><code class="language-dockerfile">RUN chmod 1777 /tmp</code></pre>
<p>를 추가했습니다.</p>
<p>다시 build하고 확인했습니다.</p>
<pre><code class="language-bash">docker compose exec openclaw-gateway \
  sh -lc &#39;ls -ld /tmp; touch /tmp/test-file &amp;&amp; echo tmp-ok&#39;</code></pre>
<p>결과:</p>
<pre><code class="language-text">drwxrwxrwt ... /tmp
tmp-ok</code></pre>
<p>그리고:</p>
<pre><code class="language-bash">docker compose exec openclaw-gateway \
  node dist/index.js --version</code></pre>
<pre><code class="language-text">OpenClaw 2026.8.2 (0965053)</code></pre>
<p>gog도:</p>
<pre><code class="language-bash">docker compose exec openclaw-gateway \
  gog --version</code></pre>
<pre><code class="language-text">v0.38.2</code></pre>
<p>오케이 진짜 굿.</p>
<hr>
<h1 id="10-google-oauth-client-만들기">10. Google OAuth Client 만들기</h1>
<p>이제 Google Calendar와 실제로 연결해야 합니다.</p>
<p>Google Cloud에서 프로젝트를 만들고 OAuth 설정을 했습니다.</p>
<p>필요한 것은:</p>
<pre><code class="language-text">Google Cloud Project
       ↓
Google Auth Platform
       ↓
OAuth Client
       ↓
Desktop App
       ↓
client_secret.json</code></pre>
<p>그리고 <strong>Google Calendar API</strong>도 활성화해야 합니다.</p>
<p>이걸 빼먹으면 나중에 아주 친절하게 403을 맞습니다.</p>
<p>다운로드한 <code>client_secret.json</code>은 서버의 별도 디렉토리에 저장했습니다.</p>
<pre><code class="language-text">/home/fcsd/openclaw/googleSecret/client_secret.json</code></pre>
<p>당연히 이 파일도 Git에 올리면 안 됩니다.</p>
<hr>
<h1 id="11-docker-안에서는-호스트-경로가-안-보인다">11. Docker 안에서는 호스트 경로가 안 보인다</h1>
<p>처음에는 아무 생각 없이:</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  exec openclaw-gateway \
  gog auth credentials /home/fcsd/openclaw/googleSecret/client_secret.json</code></pre>
<p>을 실행했습니다.</p>
<p>결과:</p>
<pre><code class="language-text">no such file or directory</code></pre>
<p>파일이 있는데?</p>
<pre><code class="language-bash">ls -l</code></pre>
<pre><code class="language-text">client_secret.json</code></pre>
<p>당연한 이야기지만 <code>docker compose exec</code> 이후 명령은 <strong>컨테이너 내부에서
실행</strong>됩니다.</p>
<p>즉:</p>
<pre><code class="language-text">Host
/home/fcsd/openclaw/googleSecret/client_secret.json
       │
       X mount 안 됨
       │
Container
/home/fcsd/openclaw/googleSecret/client_secret.json
→ 없음</code></pre>
<p>그래서 임시로 container에 복사했습니다.</p>
<pre><code class="language-bash">docker cp \
  /home/fcsd/openclaw/googleSecret/client_secret.json \
  openclaw-openclaw-gateway-1:/tmp/client_secret.json</code></pre>
<p>그리고:</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  exec openclaw-gateway \
  gog auth credentials /tmp/client_secret.json</code></pre>
<p>성공했습니다.</p>
<pre><code class="language-text">client  default
client_secret_in_keyring  true</code></pre>
<hr>
<h1 id="12-google-oauth-403---test-user를-등록해야-한다">12. Google OAuth 403 - Test user를 등록해야 한다</h1>
<p>이제 Google 계정을 연결했습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  exec -it openclaw-gateway \
  gog auth add &lt;GOOGLE_ACCOUNT&gt; \
  --services calendar \
  --manual</code></pre>
<p>홈 서버에는 브라우저가 없으니 <code>--manual</code>을 사용합니다.</p>
<p>터미널에 출력되는 Google 로그인 URL을 내 PC에서 열었는데...</p>
<pre><code class="language-text">액세스 차단됨

앱은 현재 테스트 중이며
개발자가 승인한 테스터만 앱에 액세스할 수 있습니다.

403 오류: access_denied</code></pre>
<p>또 시작이군요.</p>
<p>원인은 OAuth 앱이 Testing 상태인데 로그인하려는 Google 계정을 Test
user로 등록하지 않았기 때문이었습니다.</p>
<p>Google Cloud에서:</p>
<pre><code class="language-text">Google Auth Platform
→ Audience
→ Test users
→ 내 Google 계정 추가</code></pre>
<p>후 다시 인증했습니다.</p>
<p>이번에는 성공했습니다.</p>
<pre><code class="language-text">email     &lt;GOOGLE_ACCOUNT&gt;
services  calendar
client    default</code></pre>
<hr>
<h1 id="13-google-calendar-api를-안-켜면-또-403">13. Google Calendar API를 안 켜면 또 403</h1>
<p>OAuth까지 성공했으니 신나게 조회했습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  exec -it openclaw-gateway \
  gog calendar events --today</code></pre>
<p>그런데:</p>
<pre><code class="language-text">Google API error (403 accessNotConfigured):
Google Calendar API ... is disabled.</code></pre>
<p>OAuth 권한을 받았다고 Calendar API가 자동으로 활성화되는 것은 아니었습니다.</p>
<p>Google Cloud에서:</p>
<pre><code class="language-text">APIs &amp; Services
→ Library
→ Google Calendar API
→ Enable</code></pre>
<p>활성화했습니다.</p>
<p>잠깐 기다렸다가 다시:</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  exec -it openclaw-gateway \
  gog calendar events --today</code></pre>
<p>결과:</p>
<pre><code class="language-text">No events</code></pre>
<p>짜라란~</p>
<p><code>No events</code>가 이렇게 반가울 줄이야.</p>
<hr>
<h1 id="14-그런데-passphrase를-사람이-입력하면-ai-비서가-아니잖아">14. 그런데 passphrase를 사람이 입력하면 AI 비서가 아니잖아?</h1>
<p>gog는 keyring을 사용할 때 계속:</p>
<pre><code class="language-text">Enter passphrase to unlock ...</code></pre>
<p>를 요구했습니다.</p>
<p>제가 직접 터미널에서 사용할 거라면 상관없지만 박실장은 Discord 요청을
받고 알아서 Calendar를 조회해야 합니다.</p>
<p>따라서 사람이 passphrase를 입력하는 구조면 안 됩니다.</p>
<p><code>.env</code>에 다음 환경변수를 설정했습니다.</p>
<pre><code class="language-text">GOG_KEYRING_BACKEND=file
GOG_KEYRING_PASSWORD=&lt;GOG_KEYRING_PASSPHRASE&gt;
GOG_ACCOUNT=&lt;GOOGLE_ACCOUNT&gt;</code></pre>
<p>실제 비밀번호는 당연히 공개하지 않습니다.</p>
<p>그리고 gog 인증 정보가 Docker container recreate 때 사라지지 않도록 영속
경로도 잡았습니다.</p>
<pre><code class="language-text">GOG_HOME=/home/node/.openclaw/gogcli</code></pre>
<p>최종적으로는:</p>
<pre><code class="language-text">GOG_HOME=/home/node/.openclaw/gogcli
GOG_KEYRING_BACKEND=file
GOG_KEYRING_PASSWORD=...
GOG_ACCOUNT=...</code></pre>
<p>형태입니다.</p>
<p><code>/home/node/.openclaw</code>은 이미 OpenClaw에서 persistent하게 사용하고
있으므로 여기에 gog 데이터를 두면 container를 다시 만들어도 인증 정보를
유지할 수 있습니다.</p>
<p>그 후에는:</p>
<pre><code class="language-bash">docker compose exec openclaw-gateway \
  gog calendar events --today</code></pre>
<p>를 실행해도 passphrase 입력 없이:</p>
<pre><code class="language-text">No events</code></pre>
<p>가 바로 나옵니다.</p>
<p>이제 진짜 자동화가 가능합니다.</p>
<hr>
<h1 id="15-마지막-함정---gog가-설치됐는데-박실장은-모른다">15. 마지막 함정 - gog가 설치됐는데 박실장은 모른다</h1>
<p>이제 Discord에서 박실장에게 말했습니다.</p>
<pre><code class="language-text">@박실장 오늘 일정 확인해줘</code></pre>
<p>그런데 박실장 왈:</p>
<pre><code class="language-text">현재 이 일정관리실에는 캘린더 조회 연결이 아직 없어
오늘 일정을 확인할 수 없습니다.</code></pre>
<p>???</p>
<p>터미널에서는 분명히:</p>
<pre><code class="language-bash">gog calendar events --today</code></pre>
<p>가 잘 되는데 박실장은 Calendar가 없다고 합니다.</p>
<p>Skill 상태를 다시 확인했습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  skills info gog --agent calendar</code></pre>
<p>결과:</p>
<pre><code class="language-text">🎮 gog Disabled

Visible to model: no
Available as command: no

Requirements:
  Binaries: ✓ gog</code></pre>
<p>여기가 핵심이었습니다.</p>
<pre><code class="language-text">Binaries: ✓ gog</code></pre>
<p>는 단순히 <strong>gog 실행 파일이 존재한다</strong>는 뜻입니다.</p>
<p>하지만:</p>
<pre><code class="language-text">Visible to model: no</code></pre>
<p>이면 박실장 모델 입장에서는 gog라는 도구 자체가 없는 것이나 마찬가지입니다.</p>
<p>그래서 박실장에게 gog skill을 허용했습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  config set agents.entries.calendar.skills \
  &#39;[&quot;gog&quot;]&#39; \
  --strict-json</code></pre>
<p>그리고 gog skill도 활성화했습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  config set skills.entries.gog.enabled \
  true \
  --strict-json</code></pre>
<p>Gateway를 재시작했습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  restart openclaw-gateway</code></pre>
<p>다시 확인하면 모델에서 gog를 사용할 수 있는 상태가 됩니다.</p>
<p>그리고 Discord에서 다시:</p>
<pre><code class="language-text">@박실장 오늘 일정 확인해줘</code></pre>
<p>테스트했습니다.</p>
<p>드디어 성공했습니다.</p>
<hr>
<h1 id="16-최종-구조">16. 최종 구조</h1>
<p>여기까지 완성된 구조를 정리하면 이렇습니다.</p>
<pre><code class="language-text">                         👤 나
                           │
                           │ Discord
                           ▼
                    📅 @박실장 Bot
                           │
                           │ accountId: calendar
                           ▼
                 OpenClaw calendar agent
                           │
                    GPT-5.6 Terra
                           │
                           │ gog skill
                           ▼
                      gog CLI
                           │
                           │ OAuth 2.0
                           ▼
                  Google Calendar API
                           │
                           ▼
                    Google Calendar</code></pre>
<p>그리고 김비서는 별도로 존재합니다.</p>
<pre><code class="language-text">Discord
 │
 ├─ 📎 @김비서
 │      ↓
 │   main agent
 │   GPT-5.6 Sol
 │
 └─ 📅 @박실장
        ↓
     calendar agent
     GPT-5.6 Terra
        ↓
       gog
        ↓
  Google Calendar</code></pre>
<p>이게 제가 처음 원했던 <strong>여러 명의 전문 AI 직원이 한 홈 서버에서 돌아가는
구조</strong>의 시작입니다.</p>
<hr>
<h1 id="17-이번-삽질에서-배운-것">17. 이번 삽질에서 배운 것</h1>
<p>이번 작업은 겉으로 보면 그냥:</p>
<pre><code class="language-text">OpenClaw에 Google Calendar 연결</code></pre>
<p>인데 실제로는 꽤 여러 레이어를 건드렸습니다.</p>
<pre><code class="language-text">Discord Bot
    ↓
OpenClaw account routing
    ↓
Agent
    ↓
Skill
    ↓
CLI binary
    ↓
Docker filesystem / permission
    ↓
OAuth
    ↓
Google API</code></pre>
<p>특히 기억할 만한 건 이 정도입니다.</p>
<h3 id="openclaw-source-dockerfile을-함부로-다시-build하지-말자">OpenClaw source Dockerfile을 함부로 다시 build하지 말자</h3>
<p>실행 중인 버전과 로컬 source 버전이 다르면 저처럼:</p>
<pre><code class="language-text">config = 2026.8.2
binary = 2026.8.1</code></pre>
<p>상태가 될 수 있습니다.</p>
<p>외부 binary 하나 추가하는 목적이라면 <strong>공식 image를 base로 한 별도 확장
Dockerfile</strong>이 훨씬 안전했습니다.</p>
<h3 id="docker의-tmp-권한도-중요하다">Docker의 /tmp 권한도 중요하다</h3>
<p>OpenClaw은 <code>/tmp/openclaw-1000</code> 같은 임시 디렉토리를 만듭니다.</p>
<pre><code class="language-text">/tmp = 755</code></pre>
<p>이면 non-root <code>node</code> 사용자가 쓰지 못해서 Gateway가 죽을 수 있습니다.</p>
<p>정상적으로:</p>
<pre><code class="language-text">/tmp = 1777</code></pre>
<p>인지 확인해야 합니다.</p>
<h3 id="oauth-성공과-api-사용-가능은-다른-문제다">OAuth 성공과 API 사용 가능은 다른 문제다</h3>
<pre><code class="language-text">OAuth 인증 성공
≠
Google Calendar API 활성화</code></pre>
<p>Calendar API를 별도로 Enable해야 했습니다.</p>
<h3 id="binary-설치와-skill-노출도-다른-문제다">Binary 설치와 Skill 노출도 다른 문제다</h3>
<p>이게 이번에 제일 재밌었습니다.</p>
<pre><code class="language-text">Binaries: ✓ gog
Visible to model: no</code></pre>
<p>라면 사람은 gog를 실행할 수 있지만 AI Agent는 존재 자체를 모릅니다.</p>
<p>결국 AI agent에게 실제 능력을 붙이려면:</p>
<pre><code class="language-text">Binary
+
Authentication
+
Skill enable
+
Agent allowlist</code></pre>
<p>가 전부 맞아야 합니다.</p>
<hr>
<h1 id="18-이제-다음은">18. 이제 다음은?</h1>
<p>1편에서는 김비서 한 명으로 끝났지만, 이제 2편에서 박실장까지 실제 Google
Calendar를 사용할 수 있게 됐습니다.</p>
<p>앞으로는 이런 식으로 사용하고 싶습니다.</p>
<pre><code class="language-text">나:
내일 오후 3시에 치과 일정 잡아줘

박실장:
내일 15:00에 &#39;치과&#39; 일정 등록했습니다.</code></pre>
<p>또는:</p>
<pre><code class="language-text">나: 이번 주 언제 시간 비어?

박실장:
화요일 14:00~17:00,
목요일 오전이 비교적 여유롭습니다.</code></pre>
<p>그리고 장기적으로는:</p>
<pre><code class="language-text">                      📎 김비서
                          │
          ┌───────────────┼───────────────┐
          ▼               ▼               ▼
       📅 박실장        📈 한팀장        🖥️ 최과장
     Google Calendar     Market          Server</code></pre>
<p>까지 만들어볼 생각입니다.</p>
<p>이제 구조가 진짜 조금씩 <strong>AI 비서 조직</strong>처럼 보이기 시작합니다.</p>
<pre><code class="language-text">1편
└─ 📎 김비서 출근

2편
└─ 📅 박실장 출근 + Google Calendar 연결 완료</code></pre>
<p>박실장은 이제 출근 완료입니다.</p>
<p>다음 편부터는 이 직원들이 대화를 기억할 수 있도록 <strong>Memory 쪽을 손봐볼
생각입니다.</strong></p>
<p>오케이 굿.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[홈 서버 구축기 - OpenClaw 개인 AI 비서 만들기 🦞]]></title>
            <link>https://velog.io/@na_treeeum/%ED%99%88-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%EA%B8%B0-OpenClaw-%EA%B0%9C%EC%9D%B8-AI-%EB%B9%84%EC%84%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@na_treeeum/%ED%99%88-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%EA%B8%B0-OpenClaw-%EA%B0%9C%EC%9D%B8-AI-%EB%B9%84%EC%84%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 03 Sep 2026 15:13:38 GMT</pubDate>
            <description><![CDATA[<p>때는 2026년 1월 9일..</p>
<p>저렴한 가격에 홈 서버를 구매/설치 해놓고 사실 그동안 제대로 활용하지 못하고 있었습니다.</p>
<p>매일 아침 10시에 코인마켓캡 Fear&amp;Greed 지표를 보내주는 봇을 돌리는 서버 정도로만 사용하고 있었죠..</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/95caace8-d5aa-4582-ade5-0e168540c14b/image.png" alt=""></p>
<p>요즘 어딜가도 AI, Agent 등 이야기가 나오고 있고 개인적으로도 관심이 많이 생겨서 </p>
<blockquote>
<p><strong>&quot;그럼 그냥 내 홈 서버에 24시간 돌아가는 개인 AI 비서를 하나
만들어보자.&quot;</strong></p>
</blockquote>
<p>라는 생각이 들었습니다.</p>
<p><code>(사실 예전에 오픈클로를 한번 셋업해보긴 했지만 성능이슈로 버렸습니다)</code></p>
<p>시간도 꽤 지났고, Openclaw 2.0이 출시되었다는 소식!! <code>(실제 2.0은 아니고 자잘한 버그 고치다 보니 여기저기 건든곳이 많아서 이정도면 버전2 아니야? 라는 네이밍이라고는 합니다 )</code> 
그 소식과 AI 성능도 이전에 비하면 훨씬 향상되었기 때문에, OpenClaw를 홈서버에 올려보기로 했습니다.</p>
<p>최종적으로 만들고 싶은 구조는 대충 이런 느낌입니다.</p>
<pre><code class="language-text">Discord
   ↓
OpenClaw Gateway
   ↓
Agent
   ↓
OpenAI GPT
   ↓
Web Search / Tools / Memory ...</code></pre>
<p>일단 이번 글에서는</p>
<p><strong>Docker로 OpenClaw Gateway를 띄우고 → OpenAI를 연결하고 → Discord에서
대화할 수 있는 상태까지</strong></p>
<p>만들어보겠습니다.</p>
<p>그리고 역시나 한 번에 되지는 않았습니다.</p>
<hr>
<h2 id="1-docker부터-설치">1. Docker부터 설치</h2>
<p>OpenClaw를 서버에 직접 설치할 수도 있지만 저는 가능하면 홈 서버에 이것저것 설치해서 환경을 더럽히고 싶지 않았습니다.</p>
<p>또 나중에 OpenClaw를 지우거나 버전을 바꾸기도 편할 것 같아서 Docker로
구성하기로 했습니다.</p>
<p>그런데...</p>
<pre><code class="language-bash">docker --version</code></pre>
<p>Docker부터 없었습니다.</p>
<p>Docker 공식 저장소를 추가해서 설치해줍니다.</p>
<pre><code class="language-bash">sudo apt update
sudo apt install -y ca-certificates curl</code></pre>
<p>Docker 저장소의 GPG key를 등록합니다.</p>
<pre><code class="language-bash">sudo install -m 0755 -d /etc/apt/keyrings

sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  -o /etc/apt/keyrings/docker.asc

sudo chmod a+r /etc/apt/keyrings/docker.asc</code></pre>
<p>Docker apt repository를 추가합니다.</p>
<pre><code class="language-bash">echo \
  &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release &amp;&amp; echo &quot;${UBUNTU_CODENAME:-$VERSION_CODENAME}&quot;) stable&quot; | \
  sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null</code></pre>
<p>패키지 목록을 다시 읽고 Docker를 설치합니다.</p>
<pre><code class="language-bash">sudo apt update

sudo apt install -y \
  docker-ce \
  docker-ce-cli \
  containerd.io \
  docker-buildx-plugin \
  docker-compose-plugin</code></pre>
<p>설치 확인.</p>
<pre><code class="language-bash">docker --version
docker compose version</code></pre>
<p>이렇게 Docker는 설치됐습니다.</p>
<hr>
<h2 id="2-매번-sudo-붙이지-않게-docker-group-추가">2. 매번 sudo 붙이지 않게 docker group 추가</h2>
<p>Docker 명령어를 실행할 때마다 <code>sudo</code>를 붙이는 건 귀찮습니다.</p>
<p>그래서 현재 사용자를 <code>docker</code> 그룹에 추가했습니다.</p>
<pre><code class="language-bash">sudo usermod -aG docker $USER</code></pre>
<p>여기서 옵션은</p>
<pre><code class="language-text">-a : 기존 그룹은 유지하면서 append
-G : supplementary group 지정</code></pre>
<p>이라는 뜻입니다.</p>
<p>즉 현재 사용자를 기존 그룹에서 빼지 않고 <code>docker</code> 그룹에도 추가합니다.</p>
<p>적용하려면 로그아웃 후 다시 로그인해야 합니다.</p>
<p>다시 접속한 다음</p>
<pre><code class="language-bash">docker ps</code></pre>
<p>를 실행해서 <code>sudo</code> 없이 정상적으로 동작하는지 확인했습니다.</p>
<blockquote>
<p>참고로 <code>docker</code> 그룹은 사실상 root에 가까운 권한을 가질 수 있으므로
아무 서버에서나 막 추가하는 건 좋지 않습니다. 제 경우 개인 홈 서버라
편의성을 위해 이렇게 구성했습니다.</p>
</blockquote>
<p>💡  docker 그룹은 무엇인가?</p>
<p>Linux에는 사용자마다 <strong>소속 그룹(group)</strong>이라는 개념이 있습니다.</p>
<p>예를 들어 현재 사용자가 어떤 그룹에 속해 있는지 확인하려면:</p>
<pre><code>groups</code></pre><p>를 사용할 수 있습니다.</p>
<p>Docker를 설치하면 일반적으로 docker라는 그룹이 만들어지고, 이 그룹에 속한 사용자는 Docker socket에 접근할 수 있습니다.</p>
<p>그래서:</p>
<pre><code>sudo usermod -aG docker $USER</code></pre><p>를 실행합니다.</p>
<p>각 부분을 뜯어보면:</p>
<pre><code>sudo
└── 관리자 권한으로 실행

usermod
└── 사용자 계정 정보를 수정

-a
└── append
    기존 supplementary group을 유지

-G docker
└── docker 그룹을 supplementary group으로 추가

$USER
└── 현재 로그인한 사용자</code></pre><p>즉 한마디로:</p>
<p>“현재 사용자를 기존 그룹에서 빼지 말고 docker 그룹에도 추가해줘.”</p>
<p>라는 뜻입니다.</p>
<p>특히 -a가 중요합니다.</p>
<p>실수로:</p>
<pre><code>sudo usermod -G docker $USER</code></pre><p>처럼 -a 없이 사용하면 기존 supplementary group 목록을 의도치 않게 변경할 수 있으므로 주의해야 합니다.</p>
<hr>
<h2 id="3-openclaw-설치">3. OpenClaw 설치</h2>
<p>이제 본론입니다.</p>
<p>OpenClaw 저장소를 clone 합니다.</p>
<pre><code class="language-bash">cd ~

git clone https://github.com/openclaw/openclaw.git

cd openclaw</code></pre>
<p>OpenClaw는 Docker 이미지를 직접 build할 수도 있는데, 홈 서버에서 굳이 소스 전체를 build하지 않고 공식 pre-built image를 사용했습니다.</p>
<pre><code class="language-bash">export OPENCLAW_IMAGE=&quot;ghcr.io/openclaw/openclaw:latest&quot;</code></pre>
<p>그리고 setup script 실행.</p>
<pre><code class="language-bash">./scripts/docker/setup.sh</code></pre>
<p>이 스크립트가 생각보다 많은 일을 해줍니다.</p>
<p>대충</p>
<pre><code class="language-text">Docker image 준비
      ↓
OpenClaw onboarding
      ↓
Gateway 설정
      ↓
Agent workspace 생성
      ↓
인증 설정
      ↓
Docker Compose Gateway 실행</code></pre>
<p>까지 이어집니다.</p>
<hr>
<h2 id="4-openclaw-초기-설정">4. OpenClaw 초기 설정</h2>
<p>setup script를 실행하면 터미널에서 onboarding이 시작됩니다.</p>
<p>처음에는 보안 관련 안내가 나옵니다.</p>
<p>OpenClaw Agent는 단순 챗봇이 아니라 파일이나 Shell 같은 실제 시스템
도구에 접근할 수 있기 때문에 이런 경고가 나오는 것 같습니다.</p>
<p>개인 홈 서버에서 제가 직접 사용하는 환경이므로 내용을 확인하고
진행했습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/366e0eb8-e228-42f4-8f30-255aaadf6d57/image.png" alt=""></p>
<p>Telemetry는 굳이 필요하지 않아서</p>
<pre><code class="language-text">No thanks</code></pre>
<p>를 선택했습니다.</p>
<p>Setup mode는</p>
<pre><code class="language-text">QuickStart</code></pre>
<p>를 선택했습니다.</p>
<hr>
<h2 id="5-gateway-설정">5. Gateway 설정</h2>
<p>QuickStart에서 Gateway 기본값은 다음과 같이 잡았습니다.</p>
<pre><code class="language-text">Gateway port : 18789
Gateway auth : Token
Tailscale    : Off</code></pre>
<p>첫 번째 Agent 이름은 그냥 기본값인</p>
<pre><code class="language-text">main</code></pre>
<p>을 사용했습니다.</p>
<p>나중에는</p>
<pre><code class="language-text">main
dev
research</code></pre>
<p>처럼 Agent를 나눠서 Discord 채널별로 연결해볼 생각입니다.</p>
<p>OpenClaw의 재미있는 점이 단순히 AI 하나를 실행하는 게 아니라 하나의
Gateway 아래에 여러 Agent를 구성할 수 있다는 점입니다.</p>
<hr>
<h2 id="6-openai-연결">6. OpenAI 연결</h2>
<p>모델 Provider는 OpenAI를 선택했습니다.</p>
<p>API Key를 별도로 발급해서 사용하는 방식도 있지만 저는 기존 ChatGPT/Codex
계정을 이용하는 OAuth 방식을 사용했습니다.</p>
<pre><code class="language-text">ChatGPT Login</code></pre>
<p>을 선택했고, 다음과 같은 메세지가 나왔습니다</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/af956ca6-5eb2-482c-b7e0-838a1d7ecf17/image.png" alt=""></p>
<pre><code>https://auth.openai.com/oauth/authorize?response_type=code&amp;client_id=app_~~</code></pre><p>제 홈 서버 환경은 CLI 환경이었기 때문에, OAuth URL을 복사하여 제 PC 에서 따로 접속을 했습니다.</p>
<p>URL을 통해 OpenAI 로그인을 했는데, 
<img src="https://velog.velcdn.com/images/na_treeeum/post/48766c09-5798-4daf-a1cc-11bb6e9b525f/image.png" alt=""></p>
<blockquote>
<p>?</p>
</blockquote>
<p>당황했습니다.</p>
<p>알고보니 저 리다이렉트 된 주소 <code>http://localhost:1455/auth/callback~~~</code> 를 전체 복사하여 CLI 환경에 붙여넣기 하라고 하더군요..
(한국인 특 : 설명서 안읽음)</p>
<p>기본 모델은 설치 과정에서</p>
<pre><code class="language-text">openai/gpt-5.6-sol</code></pre>
<p>로 일단 설정을 했고,</p>
<p>모델 선택 화면에서는 그대로</p>
<pre><code class="language-text">Keep current</code></pre>
<p>를 선택했습니다.</p>
<p>그 다음 실제 completion을 한번 날려서 인증과 모델 접근이 정상인지
테스트하는 화면이 나옵니다.</p>
<pre><code class="language-text">Test AI access now with a live completion?</code></pre>
<pre><code class="language-text">Yes</code></pre>
<p>실제 요청 하나까지 성공하는 걸 확인했습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/84614c01-1020-41ef-bf37-af24f9fa0695/image.png" alt=""></p>
<hr>
<h2 id="7-discord를-openclaw의-메신저로-사용하기">7. Discord를 OpenClaw의 메신저로 사용하기</h2>
<p>홈 서버에 SSH 접속을 해서 OpenClaw에게 말을 거는것은 너무나도 귀찮은 일이기 때문에 메신저를 활용하려고 했고, Discord를 선택했습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/0b582198-4794-428e-89d9-59f7167830d6/image.png" alt=""></p>
<p>(discord 관련 패키지를 설치할까요?에서 계속 no 를 선택했던 뻘짓이 남아있네요)</p>
<pre><code class="language-text">Discord (Bot API)</code></pre>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/f739438c-bcc3-4861-bc76-c1d682e2b2f6/image.png" alt=""></p>
<p>디스코드 봇 토큰을 발급하는 방법도 알려주네요.</p>
<p>제 Bot token을 입력해 주었고 전체 채널에서 동작하게 하는 건 싫어서 접근 정책은</p>
<pre><code class="language-text">Allowlist</code></pre>
<p>로 설정했습니다.</p>
<p>OpenClaw에서는 Discord 서버와 채널을 다음과 같이 지정할 수 있습니다.</p>
<pre><code class="language-text">guildId/channelId</code></pre>
<p>Discord에서 개발자 모드를 켜면 서버 ID와 채널 ID를 복사할 수 있습니다.</p>
<p>저는 개인 Discord 서버의 <code>사장실</code>이라는 채널 하나만 허용했습니다.</p>
<p>구조는 대충 이런 느낌입니다.</p>
<pre><code class="language-text">Discord Server
│
├── 일반 채널
│      └── OpenClaw 접근 X
│
└── #사장실
       └── OpenClaw 접근 O</code></pre>
<p>실제 config에는 다음과 비슷하게 들어갑니다.</p>
<pre><code class="language-json">{
  &quot;channels&quot;: {
    &quot;discord&quot;: {
      &quot;enabled&quot;: true,
      &quot;token&quot;: &quot;&lt;DISCORD_BOT_TOKEN&gt;&quot;,
      &quot;groupPolicy&quot;: &quot;allowlist&quot;,
      &quot;guilds&quot;: {
        &quot;&lt;GUILD_ID&gt;&quot;: {
          &quot;channels&quot;: {
            &quot;&lt;CHANNEL_ID&gt;&quot;: {
              &quot;enabled&quot;: true
            }
          }
        }
      }
    }
  }
}</code></pre>
<p>당연하지만 Discord Bot Token은 비밀번호나 마찬가지이므로 GitHub에
올리거나 블로그에 노출하면 안 됩니다.</p>
<hr>
<h2 id="8-gateway-not-detected">8. &quot;Gateway not detected&quot;...?</h2>
<p>온보딩 마지막쯤 이런 메시지가 나왔습니다.</p>
<pre><code class="language-text">Systemd user services are not available inside this container.

Gateway not detected yet.

Start now:
openclaw gateway run</code></pre>
<p>순간</p>
<blockquote>
<p>&quot;엥? 설치 실패했나?&quot;</p>
</blockquote>
<p>했는데 Docker 환경이라 그런 것이었습니다.</p>
<p>컨테이너 안에서는 systemd user service를 설치하는 방식이 아니라 Docker
Compose가 Gateway 프로세스를 관리합니다.</p>
<p>setup script가 끝까지 실행되고 나니</p>
<pre><code class="language-text">Starting gateway

Container openclaw-openclaw-gateway-1 Started
Container openclaw-openclaw-gateway-1 Running</code></pre>
<p>으로 정상 실행됐습니다.</p>
<p>설정 위치도 출력됩니다.</p>
<pre><code class="language-text">Config: /home/fcsd/.openclaw
Workspace: /home/fcsd/.openclaw/workspace</code></pre>
<hr>
<h2 id="9-gateway-health-check">9. Gateway Health Check</h2>
<p>설치가 끝났으니 Gateway가 실제로 살아있는지 확인했습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  exec openclaw-gateway \
  sh -lc &#39;node dist/index.js gateway health --token &quot;$OPENCLAW_GATEWAY_TOKEN&quot;&#39;</code></pre>
<ul>
<li><code>docker compose exec</code> : 현재 실행 중인 container 안에서 명령 실행</li>
<li><code>openclaw-gateway</code> : 명령을 실행할 서비스</li>
<li><code>gateway health</code> : Gateway health check</li>
<li><code>OPENCLAW_GATEWAY_TOKEN</code> : Gateway 인증에 사용하는 token</li>
</ul>
<p>결과는</p>
<pre><code class="language-text">Gateway Health
OK (5ms)
Discord: blocked</code></pre>
<p>Gateway는 정상인데...</p>
<p>Discord가 blocked?</p>
<p>여기서 첫 번째 삽질이 시작됩니다.</p>
<hr>
<h2 id="10-discord-bot-token-등록">10. Discord Bot Token 등록</h2>
<p>온보딩에서 Discord와 allowlist까지 설정했지만 Bot Token 연결도
확인해주었습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  channels add \
  --channel discord \
  --token &#39;&lt;DISCORD_BOT_TOKEN&gt;&#39;</code></pre>
<p><code>&lt;DISCORD_BOT_TOKEN&gt;</code>에는 Discord Developer Portal에서 발급받은 실제 Bot
Token을 넣으면 됩니다.</p>
<p><code>docker compose run --rm</code>은 OpenClaw CLI용 일회성 container를 실행하고
명령이 끝나면 container를 제거합니다.</p>
<p>등록 결과:</p>
<pre><code class="language-text">Added Discord account &quot;default&quot;.</code></pre>
<p>채널 목록도 확인합니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  channels list</code></pre>
<p>결과:</p>
<pre><code class="language-text">Chat channels:
- Discord default: installed, configured, enabled</code></pre>
<p>오?</p>
<p>그럼 된 건가?</p>
<p>다시 Health Check.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  exec openclaw-gateway \
  sh -lc &#39;node dist/index.js gateway health --token &quot;$OPENCLAW_GATEWAY_TOKEN&quot;&#39;</code></pre>
<p>그런데 여전히</p>
<pre><code class="language-text">Discord: blocked</code></pre>
<p>...</p>
<hr>
<h2 id="11-channels-status---probe로-원인-찾기">11. <code>channels status --probe</code>로 원인 찾기</h2>
<p>단순히 <code>channels list</code>만 봐서는 왜 막혀있는지 알 수 없었습니다.</p>
<p>그래서 실제 Gateway와 Discord 연결 상태까지 확인해봤습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  channels status --probe</code></pre>
<p>결과:</p>
<pre><code class="language-text">Gateway reachable.
- Discord default:
  enabled,
  configured,
  error: Gateway websocket closed: 4014,
  running,
  disconnected,
  bot:@김비서,
  intents:content=disabled,
  health:blocked,
  works,
  audit ok</code></pre>
<p>여기서 범인이 거의 보입니다.</p>
<pre><code class="language-text">Gateway websocket closed: 4014
intents:content=disabled</code></pre>
<p>그래도 정확한 원인을 확인하기 위해 Gateway 로그도 봤습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  logs --tail=200 openclaw-gateway</code></pre>
<p><code>--tail=200</code>은 Gateway 로그의 마지막 200줄만 출력합니다.</p>
<p>그리고 로그에서 아주 친절하게 알려줍니다.</p>
<pre><code class="language-text">Discord Message Content Intent is disabled;
bot may not respond to channel messages.

gateway websocket closed code=4014
reason=Disallowed intent(s).

missing privileged gateway intents.</code></pre>
<p>아하!</p>
<p>OpenClaw 문제가 아니라 Discord Bot 설정 문제였습니다.</p>
<hr>
<h2 id="12-discord-message-content-intent">12. Discord Message Content Intent</h2>
<p>Discord Developer Portal에서 Bot 설정으로 이동했습니다.</p>
<pre><code class="language-text">Discord Developer Portal
        ↓
Application
        ↓
Bot
        ↓
Privileged Gateway Intents
        ↓
MESSAGE CONTENT INTENT</code></pre>
<p><code>MESSAGE CONTENT INTENT</code>를 ON으로 변경했습니다.</p>
<p>저는 추후 사용자/Role 기반 제어도 해볼 수 있어서
<code>SERVER MEMBERS INTENT</code>도 같이 켜두었습니다.</p>
<p>설정을 바꿨으니 Gateway를 재시작합니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  restart openclaw-gateway</code></pre>
<p>그리고 바로 probe를 날렸는데...</p>
<pre><code class="language-text">Gateway not reachable at ws://127.0.0.1:18789
ECONNREFUSED</code></pre>
<p>ㅋㅋ</p>
<p>또 안됩니다.</p>
<hr>
<h2 id="13-이번엔-econnrefused">13. 이번엔 ECONNREFUSED?</h2>
<p>이번 문제는 별거 아니었습니다.</p>
<p>Gateway를 <code>restart</code>한 직후 바로 probe를 실행해서 아직 Gateway가 18789
포트를 열기도 전에 CLI가 접속을 시도한 것이었습니다.</p>
<p>잠깐 기다린 뒤 다시 Health Check.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  exec openclaw-gateway \
  sh -lc &#39;node dist/index.js gateway health --token &quot;$OPENCLAW_GATEWAY_TOKEN&quot;&#39;</code></pre>
<p>이번에는</p>
<pre><code class="language-text">Gateway Health
OK (5ms)
Discord: configured</code></pre>
<p>오케이 굿.</p>
<p>그리고 다시 Discord probe.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  channels status --probe</code></pre>
<p>최종 결과:</p>
<pre><code class="language-text">Gateway reachable.
- Discord default:
  enabled,
  configured,
  running,
  connected,
  bot:@김비서,
  token:***,
  intents:content=limited,
  works,
  audit ok</code></pre>
<p>드디어</p>
<pre><code class="language-text">running
connected
works
audit ok</code></pre>
<p>가 떴습니다.</p>
<p>짜라란~</p>
<hr>
<h2 id="14-현재-구조">14. 현재 구조</h2>
<p>여기까지 구성하고 나면 제 홈 서버는 대충 다음과 같은 구조가 됩니다.</p>
<pre><code class="language-text">Discord
  │
  │  #사장실
  ▼
@김비서
  │
  ▼
OpenClaw Gateway
  │
  ├── main Agent
  │
  ├── OpenAI OAuth
  │      └── openai/gpt-5.6-sol
  │
  ├── Codex Hosted Search
  │
  └── Workspace / Memory / Sessions</code></pre>
<p>기존에는</p>
<pre><code class="language-text">홈 서버
   ↓
매일 아침
   ↓
CoinMarketCap Fear &amp; Greed
   ↓
Telegram 전송</code></pre>
<p>정도로만 사용하고 있었는데 이제는 24시간 켜져 있는 서버에 실제 AI
Agent가 하나 들어왔습니다.</p>
<p>이제 좀 홈 서버 산 보람이 생기는 것 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/87656eb1-5101-445b-be41-9ee980bce4c5/image.png" alt=""></p>
<hr>
<h2 id="15-자주-사용할-것-같은-명령어-정리">15. 자주 사용할 것 같은 명령어 정리</h2>
<p>OpenClaw를 Docker로 운영하면서 자주 사용할 것 같은 명령어도
정리해두겠습니다.</p>
<h3 id="container-상태-확인">Container 상태 확인</h3>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml ps</code></pre>
<p>현재 Gateway container가 정상적으로 실행 중인지 확인합니다.</p>
<h3 id="gateway-로그-확인">Gateway 로그 확인</h3>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  logs -f openclaw-gateway</code></pre>
<p><code>-f</code> 옵션을 사용하면 실시간으로 로그를 따라갑니다.</p>
<p>최근 로그만 보고 싶다면</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  logs --tail=200 openclaw-gateway</code></pre>
<h3 id="gateway-재시작">Gateway 재시작</h3>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  restart openclaw-gateway</code></pre>
<p>설정 변경 후 Gateway를 재시작할 때 사용합니다.</p>
<p>단, <code>.env</code>나 Compose environment 자체를 변경했다면 단순 <code>restart</code>가
아니라 container를 recreate해야 변경된 환경변수가 반영될 수 있습니다.</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  up -d openclaw-gateway</code></pre>
<h3 id="gateway-health-check">Gateway Health Check</h3>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  exec openclaw-gateway \
  sh -lc &#39;node dist/index.js gateway health --token &quot;$OPENCLAW_GATEWAY_TOKEN&quot;&#39;</code></pre>
<p>Gateway와 Channel의 기본 Health 상태를 확인합니다.</p>
<h3 id="discord-상태-확인">Discord 상태 확인</h3>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  channels status --probe</code></pre>
<p>Discord가 실제로 연결되어 있는지 확인할 때 굉장히 유용했습니다.</p>
<h3 id="channel-목록">Channel 목록</h3>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  channels list</code></pre>
<p>현재 등록된 Channel을 확인합니다.</p>
<h3 id="discord-추가">Discord 추가</h3>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  channels add \
  --channel discord \
  --token &#39;&lt;DISCORD_BOT_TOKEN&gt;&#39;</code></pre>
<p>Discord Bot Token을 등록합니다.</p>
<h3 id="openclaw-doctor">OpenClaw Doctor</h3>
<p>설치 도중 다음과 같은 메시지도 한번 확인했습니다.</p>
<pre><code class="language-text">OpenClaw config identity changed while persisting the refreshed plugin registry
Run &quot;openclaw doctor --fix&quot;</code></pre>
<p>이런 config/migration 관련 문제를 정리할 때는</p>
<pre><code class="language-bash">docker compose -f /home/fcsd/openclaw/docker-compose.yml \
  run --rm openclaw-cli \
  doctor --fix</code></pre>
<p>를 사용할 수 있습니다.</p>
<hr>
<h2 id="마치며">마치며</h2>
<p>처음에는</p>
<blockquote>
<p>&quot;Docker 하나 띄우고 OpenAI 연결하면 끝나는 거 아닌가?&quot;</p>
</blockquote>
<p>라고 생각했는데 역시 이런 건 한 번에 되는 법이 없습니다.</p>
<p>특히 이번에 삽질했던 부분은</p>
<pre><code class="language-text">Discord: blocked
      ↓
Bot Token 문제인가?
      ↓
Allowlist 문제인가?
      ↓
channels status --probe
      ↓
4014 Disallowed intent(s)
      ↓
Message Content Intent 발견
      ↓
Gateway restart
      ↓
ECONNREFUSED
      ↓
아 그냥 아직 재시작 중이었구나...
      ↓
connected</code></pre>
<p>였습니다.</p>
<p>그래도 OpenClaw CLI에서 상태와 로그를 꽤 자세하게 보여줘서 원인 찾는 것
자체는 어렵지 않았습니다.</p>
<p>현재는 <code>main</code> Agent 하나만 만들어놓은 상태인데 다음에는 여기서 조금 더
욕심을 내서</p>
<pre><code class="language-text">Discord
│
├── #사장실
│      └── main
│
├── #개발실
│      └── dev
│
└── #자료조사실
       └── research</code></pre>
<p>처럼 <strong>하나의 OpenClaw Gateway에 여러 Agent를 만들고 Discord 채널별로
Agent를 분리</strong>해볼 생각입니다.</p>
<p>그리고 <code>dev</code> Agent에는 개발에 필요한 Tool을 붙이고, <code>research</code> Agent에는
Web Search와 자료조사에 필요한 Tool을 붙여서 각자 역할을 나눠보려고
합니다.</p>
<p>단순히 ChatGPT를 Discord에서 사용하는 것과 달리,</p>
<p><strong>내 서버에서 24시간 살아있고, 각자 Workspace와 Memory를 가지고, 실제
Tool을 사용할 수 있는 Agent를 만드는 것.</strong></p>
<p>이게 OpenClaw를 설치해보고 싶었던 가장 큰 이유였습니다.</p>
<p>일단 오늘은 여기까지.</p>
<p>다음엔 진짜 비서처럼 부려먹어보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[EC2에 내 서버를 올리고 HTTPS로 서비스하기]]></title>
            <link>https://velog.io/@na_treeeum/EC2%EC%97%90-%EB%82%B4-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%98%AC%EB%A6%AC%EA%B3%A0-HTTPS%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@na_treeeum/EC2%EC%97%90-%EB%82%B4-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%98%AC%EB%A6%AC%EA%B3%A0-HTTPS%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 01 Sep 2026 05:18:11 GMT</pubDate>
            <description><![CDATA[<p>부제 : DNS, Nginx, Reverse Proxy, SSL/TLS 이해하기</p>
<blockquote>
<p>바이브코딩 및 AI 를 활용할 때 개인적으로 항상 <code>개념숙지 -&gt; 구현</code> 순으로 진행합니다. 개념숙지가 안된상태로 구현을 하게 되면 찝찝함이 많이 남는 편이라 그렇습니다. 아래 내용은 개념 숙지를 위해 관련된 내용을 AI에게 정리시킨 내용입니다.</p>
</blockquote>
<hr>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/7b5fce27-6a0e-4fb4-9b45-0290815761f2/image.png" alt=""></p>
<p>백엔드 서버를 하나 만들었다고 해보자.</p>
<p>Node.js든 Fastify든 Spring이든 서버를 실행하면 보통 다음과 같은 형태로 동작한다.</p>
<p><code>http://localhost:3000</code></p>
<p>개발할 때는 이것만으로 충분하다.</p>
<p>하지만 실제 인터넷에서</p>
<p><code>https://mock-exchange.example.com</code></p>
<p>같은 주소로 서비스를 제공하려면 몇 가지 요소가 추가로 필요하다.</p>
<p>전체 구조부터 보면 생각보다 단순하다.</p>
<pre><code>사용자
  │
  │ https://mock-exchange.example.com
  ▼
DNS
  │
  │ &quot;이 도메인의 서버 IP는 1.2.3.4입니다&quot;
  ▼
EC2
  │
  │ :443
  ▼
Nginx
  │
  │ http://127.0.0.1:3000
  ▼
내 백엔드 서버</code></pre><p>여기에는 크게 네 가지 개념이 등장한다.</p>
<pre><code>DNS → HTTP/HTTPS → SSL/TLS 인증서 → Nginx Reverse Proxy</code></pre><p>하나씩 이해해보자.</p>
<h2 id="1-가장-먼저-http가-무엇인가">1. 가장 먼저 HTTP가 무엇인가?</h2>
<p>HTTP는 클라이언트와 서버가 데이터를 주고받는 규칙이다.</p>
<p>예를 들어 브라우저나 다른 서버가 다음 요청을 보냈다고 하자.</p>
<pre><code>GET /api/orderbook
Host: mock-exchange.example.com</code></pre><p>서버에서는:</p>
<pre><code>{
  &quot;bids&quot;: [],
  &quot;asks&quot;: []
}</code></pre><p>같은 응답을 반환한다.</p>
<p>이게 기본적인 HTTP 통신이다.</p>
<p>구조를 단순화하면:</p>
<pre><code>Client
   │
   │ HTTP Request
   ▼
Server
   │
   │ HTTP Response
   ▼
Client</code></pre><p>HTTP의 기본 포트는 80이다.</p>
<p>따라서:</p>
<p><code>http://mock-exchange.example.com</code></p>
<p>은 기본적으로:</p>
<p><code>mock-exchange.example.com:80</code></p>
<p>으로 접속한다.</p>
<h2 id="2-http의-문제점">2. HTTP의 문제점</h2>
<p>HTTP 자체에는 암호화가 없다.</p>
<p>예를 들어 클라이언트가 서버에 다음 데이터를 보낸다고 생각해보자.</p>
<pre><code>{
  &quot;email&quot;: &quot;user@example.com&quot;,
  &quot;password&quot;: &quot;12345678&quot;
}</code></pre><p>HTTP만 사용한다면 네트워크를 통과하는 데이터가 암호화되지 않는다.</p>
<p>그래서 중간에서 트래픽을 관찰할 수 있는 공격자가 데이터를 읽거나 변조할 가능성이 생긴다.</p>
<p>이 문제를 해결하기 위해 HTTP에 TLS를 적용한 것이 HTTPS다.</p>
<p>TLS는 통신에 크게 세 가지 보안 속성을 제공한다.</p>
<ul>
<li>Encryption(암호화) — 중간에서 내용을 읽기 어렵게 한다.</li>
<li>Integrity(무결성) — 전송 중 데이터가 몰래 변조되는 것을 탐지한다.</li>
<li>Authentication(인증) — 접속한 서버가 실제 해당 도메인의 서버인지 검증한다. (MDN Web Docs)</li>
</ul>
<h2 id="3-https란">3. HTTPS란?</h2>
<p>HTTPS는 완전히 별개의 웹 프로토콜이라고 생각하기보다,</p>
<pre><code>HTTP
+
TLS</code></pre><p>라고 이해하면 쉽다.</p>
<p>즉:</p>
<pre><code>HTTP
────────────────
평문 HTTP 통신
HTTPS
────────────────
HTTP
  ↓
TLS 암호화
  ↓
TCP</code></pre><p>HTTPS의 기본 포트는 443이다.</p>
<p>그래서:</p>
<p><code>https://mock-exchange.example.com</code></p>
<p>이라고 하면 브라우저는 기본적으로:</p>
<p><code>mock-exchange.example.com:443</code></p>
<p>에 연결한다. (MDN Web Docs)</p>
<h2 id="4-ssl과-tls는-뭐가-다른가">4. SSL과 TLS는 뭐가 다른가?</h2>
<p>서버를 운영하다 보면 이런 표현을 매우 자주 본다.</p>
<pre><code>SSL 인증서
SSL 설정
SSL certificate
HTTPS SSL</code></pre><p>하지만 현대 웹에서 실제로 사용하는 기술은 <code>TLS</code>다.</p>
<p>SSL은 TLS의 전신이다.</p>
<p>대략:</p>
<pre><code>SSL 2.0
   ↓
SSL 3.0
   ↓
TLS 1.0
   ↓
TLS 1.1
   ↓
TLS 1.2
   ↓
TLS 1.3</code></pre><p>으로 발전했다.</p>
<p>현재 웹에서는 TLS 1.2와 TLS 1.3을 사용하며, SSL 및 오래된 TLS 버전은 사용하지 않는 것이 일반적이다. (MDN Web Docs)</p>
<p>그런데 관습적으로 아직도 사람들이:</p>
<p><code>SSL 인증서</code></p>
<p>라고 부른다.</p>
<p>따라서 실무에서는 대충:</p>
<p>SSL 인증서 ≒ TLS 인증서</p>
<p>라고 이해해도 된다.</p>
<h2 id="5-그러면-ssltls-인증서는-왜-필요한가">5. 그러면 SSL/TLS 인증서는 왜 필요한가?</h2>
<p>여기서 중요한 문제가 하나 있다.</p>
<p>사용자가:</p>
<p><code>https://mock-exchange.example.com</code></p>
<p>에 접속했다고 해보자.</p>
<p>암호화를 하기 전에 먼저 확인해야 하는 것이 있다.</p>
<blockquote>
<p>“내가 지금 통신하려는 서버가 정말 mock-exchange.example.com 서버가 맞나?”</p>
</blockquote>
<p>공격자가 중간에서 자신을 진짜 서버인 것처럼 속이면 암호화 자체가 의미가 없어질 수 있기 때문이다.</p>
<p>그래서 서버는 자신의 신원을 증명하는 TLS Certificate를 제공한다.</p>
<p>인증서에는 해당 도메인과 공개키 등 서버의 신원을 검증하는 데 필요한 정보가 포함된다. 브라우저는 신뢰할 수 있는 인증기관(CA)이 발급한 인증서인지, 접속한 도메인과 인증서가 일치하는지 등을 확인한다. (MDN Web Docs)</p>
<ol start="6">
<li>CA(Certificate Authority)는 무엇인가?</li>
</ol>
<p>CA는 인증서를 발급하고 신뢰를 보증하는 기관이다.</p>
<p>예를 들어:</p>
<pre><code>사용자
 │
 │ &quot;이 인증서 믿어도 돼?&quot;
 ▼
브라우저
 │
 │ 신뢰하는 CA가 서명했는지 확인
 ▼
Certificate
 │
 ▼
mock-exchange.example.com</code></pre><p>대표적으로 Let’s Encrypt 같은 CA가 있다.</p>
<p>Let’s Encrypt는 무료 TLS 인증서를 제공하는 비영리 인증기관이다. (letsencrypt.org)</p>
<p>실제로 우리가 EC2를 구성할 때 사용할 수 있는 방법이:</p>
<pre><code>Let&#39;s Encrypt
      │
      │ 인증서 발급
      ▼
Certbot
      │
      │ Nginx 설정
      ▼
Nginx</code></pre><p>이다.</p>
<h2 id="7-certbot은-뭐지">7. Certbot은 뭐지?</h2>
<p>여기서 많이 헷갈리는 부분이다.</p>
<p><code>Let’s Encrypt와 Certbot은 같은 것이 아니다.</code></p>
<p>쉽게 구분하면:</p>
<pre><code>Let&#39;s Encrypt
= 인증서를 발급하는 CA
Certbot
= 인증서 발급/설정/갱신을 자동화해주는 프로그램</code></pre><p>예를 들어 서버에서:</p>
<pre><code>sudo certbot --nginx -d mock-exchange.example.com</code></pre><p>을 실행하면 Certbot이 Let’s Encrypt를 통해 인증서를 발급받고 Nginx에 적용하는 작업을 상당 부분 자동화할 수 있다.</p>
<p>결과적으로 Nginx가 다음과 같은 인증서 파일을 사용하게 된다.</p>
<pre><code>/etc/letsencrypt/live/mock-exchange.example.com/
├── fullchain.pem
└── privkey.pem</code></pre><p>개념적으로:</p>
<pre><code>fullchain.pem
→ 클라이언트에게 보여줄 인증서 + 인증서 체인
privkey.pem
→ 서버의 Private Key</code></pre><p>라고 보면 된다.</p>
<p>특히 privkey.pem은 절대 외부에 노출하면 안 된다.</p>
<h2 id="8-이제-nginx는-왜-필요한가">8. 이제 Nginx는 왜 필요한가?</h2>
<p>사실 Node.js 서버 자체를 인터넷에 직접 노출할 수도 있다.</p>
<p>예를 들어 Node.js 서버가:</p>
<p><code>0.0.0.0:3000</code></p>
<p>으로 실행되고 EC2 Security Group에서 3000을 열어버리면:</p>
<p><code>http://1.2.3.4:3000</code></p>
<p>으로 직접 접근할 수 있다.</p>
<p>하지만 실제 서비스에서는 보통 이렇게 하지 않는다.</p>
<p>대신:</p>
<pre><code>Internet
    │
    ▼
Nginx
    │
    ▼
Application</code></pre><p>구조를 사용한다.</p>
<p>여기서 Nginx가 Reverse Proxy 역할을 한다.</p>
<h2 id="9-proxy란">9. Proxy란?</h2>
<p>Proxy라는 개념부터 보면 쉽다.</p>
<p>일반적인 Proxy는 중간에서 요청을 대신 전달하는 서버다.</p>
<p>예를 들어:</p>
<pre><code>나
 │
 ▼
Proxy
 │
 ▼
Google</code></pre><p>Google 입장에서는 요청을 Proxy가 보낸 것처럼 보일 수 있다.</p>
<p>이런 구조를 흔히 Forward Proxy라고 한다.</p>
<h2 id="10-reverse-proxy란">10. Reverse Proxy란?</h2>
<p>Reverse Proxy는 반대쪽에 있다.</p>
<pre><code>사용자
 │
 ▼
Reverse Proxy
 │
 ▼
실제 서버</code></pre><p>사용자는 실제 Application Server와 직접 통신하지 않는다.</p>
<p><code>Nginx와 통신한다.</code></p>
<p>그리고 Nginx가 요청을 실제 Application Server로 전달한다.</p>
<p>NGINX 공식 문서에서도 reverse proxy를 클라이언트 요청을 proxied server에 전달하고, 그 응답을 다시 클라이언트에게 전달하는 구조로 설명한다. (docs.nginx.com)</p>
<p>우리가 만들 구조가 정확히 이것이다.</p>
<h2 id="11-우리가-만들-ec2-구조">11. 우리가 만들 EC2 구조</h2>
<p>예를 들어 EC2의 Public IP가:</p>
<p><code>3.35.100.100</code></p>
<p>이고 Node.js 서버가:</p>
<p><code>127.0.0.1:3000</code></p>
<p>에서 실행된다고 해보자.</p>
<p>우리가 원하는 것은:</p>
<p><code>https://mock-exchange.example.com</code></p>
<p>으로 요청하면 Node.js의:</p>
<p><code>http://127.0.0.1:3000</code></p>
<p>으로 전달되는 것이다.</p>
<p>전체 흐름은:</p>
<pre><code>                 Internet
User
 │
 │ https://mock-exchange.example.com
 │
 ▼
DNS
 │
 │ mock-exchange.example.com
 │            ↓
 │       3.35.100.100
 │
 ▼
EC2
 │
 │ :443
 ▼
┌─────────────────────┐
│        Nginx        │
│                     │
│ HTTPS / TLS 처리     │
│ Reverse Proxy       │
└──────────┬──────────┘
           │
           │ HTTP
           │ 127.0.0.1:3000
           ▼
┌─────────────────────┐
│    Node.js Server   │
│                     │
│   Fastify / Express │
└─────────────────────┘</code></pre><p>이 구조가 이번 작업에서 가장 중요하다.</p>
<h2 id="12-그런데-dns는-어디에-들어가는가">12. 그런데 DNS는 어디에 들어가는가?</h2>
<p>도메인은 결국 사람이 읽기 편한 이름이다.</p>
<p>컴퓨터가 실제 서버에 연결하려면 IP 주소가 필요하다.</p>
<p>그래서:</p>
<p><code>mock-exchange.example.com</code></p>
<p>을:</p>
<p><code>3.35.100.100</code></p>
<p>으로 변환해야 한다.</p>
<p>그 역할을 DNS가 한다.</p>
<p>도메인 관리 서비스에서 A Record를 다음처럼 설정할 수 있다.</p>
<pre><code>Type     Name             Value
A        mock-exchange    3.35.100.100</code></pre><p>그러면:</p>
<pre><code>mock-exchange.example.com
        │
        │ DNS Lookup
        ▼
    3.35.100.100</code></pre><p>이 된다.</p>
<p>중요한 것은 DNS는 요청을 Node.js로 전달하는 Reverse Proxy가 아니라는 것이다.</p>
<p>DNS가 하는 핵심 역할은:</p>
<pre><code>Domain
   ↓
IP Address</code></pre><p>를 알려주는 것이다.</p>
<h2 id="13-dns-→-nginx-→-nodejs의-관계">13. DNS → Nginx → Node.js의 관계</h2>
<p>따라서 전체 관계를 이렇게 기억하면 된다.</p>
<pre><code>DNS
&quot;어느 서버로 갈까?&quot;
        │
        ▼
EC2 Public IP
Nginx
&quot;이 서버에 들어온 요청을
 어느 Application으로 보낼까?&quot;
        │
        ▼
127.0.0.1:3000
Node.js
&quot;실제 API 로직을 실행&quot;</code></pre><p>각자의 역할이 완전히 다르다.</p>
<h2 id="14-nginx의-server_name">14. Nginx의 server_name</h2>
<p>Nginx에는 이런 설정이 들어간다.</p>
<pre><code>server {
    listen 80;
    server_name mock-exchange.example.com;
}</code></pre><p>여기서:</p>
<p><code>listen 80;</code></p>
<p>은</p>
<p><code>80번 포트로 들어오는 HTTP 연결을 받겠다.</code></p>
<p>는 의미다.</p>
<p>그리고:</p>
<p><code>server_name mock-exchange.example.com;</code></p>
<p>은</p>
<p><code>mock-exchange.example.com을 대상으로 들어온 요청은 이 server block에서 처리하겠다.</code></p>
<p>는 의미다.</p>
<p>이 덕분에 EC2 하나에서 여러 도메인을 운영할 수도 있다.</p>
<p>예를 들어:</p>
<pre><code>EC2
│
│ 3.35.100.100
│
├── api.example.com
│
├── mock-exchange.example.com
│
└── admin.example.com</code></pre><p>이 세 도메인이 전부 같은 IP를 가리켜도 Nginx가 요청의 Host를 보고 서로 다른 서버로 전달할 수 있다.</p>
<pre><code>api.example.com
       │
       ▼
127.0.0.1:3000
mock-exchange.example.com
       │
       ▼
127.0.0.1:4000
admin.example.com
       │
       ▼
127.0.0.1:5000</code></pre><p>이것이 기존 EC2에 이미 Nginx가 있는데도 새로운 서비스를 추가할 수 있는 이유다.</p>
<h2 id="15-proxy_pass가-핵심이다">15. proxy_pass가 핵심이다</h2>
<p>Nginx Reverse Proxy 설정에서 가장 중요한 부분은:</p>
<pre><code>location / {
    proxy_pass http://127.0.0.1:3000;
}</code></pre><p>이다.</p>
<p>의미는 간단하다.</p>
<pre><code>Nginx로 들어온
/
요청을
127.0.0.1:3000
으로 전달한다.</code></pre><p>예를 들어:</p>
<p><code>GET https://mock-exchange.example.com/api/orderbook</code></p>
<p>요청이 들어왔다고 하자.</p>
<p>Nginx가 받아서 내부적으로:</p>
<p><code>GET http://127.0.0.1:3000/api/orderbook</code></p>
<p>으로 전달한다.</p>
<p>그리고 Node.js 응답을 다시 사용자에게 전달한다. proxy_pass는 NGINX에서 이런 upstream 전달을 지정하는 핵심 directive다. (nginx.org)</p>
<p>따라서 사용자는 Node.js가 3000 포트를 사용하는지 알 필요도 없다.</p>
<h2 id="16-왜-nodejs를-127001에-두는가">16. 왜 Node.js를 127.0.0.1에 두는가?</h2>
<p>여기에는 보안상의 장점도 있다.</p>
<p>Node.js가:</p>
<p><code>0.0.0.0:3000</code></p>
<p>에서 실행되고 Security Group도 3000을 열어놓으면 인터넷에서:</p>
<p>EC2:3000</p>
<p>으로 직접 접근할 가능성이 생긴다.</p>
<p>그러면:</p>
<p><code>User ───────────────→ Node.js :3000</code></p>
<p>이라는 Nginx를 우회하는 경로가 존재한다.</p>
<p>반대로 Node.js를:</p>
<p><code>127.0.0.1:3000</code></p>
<p>으로만 listen하게 하면 외부에서 직접 접근할 수 없다.</p>
<pre><code>Internet
    │
    X
127.0.0.1:3000</code></pre><p>같은 EC2 내부의 Nginx만 접근할 수 있다.</p>
<pre><code>Internet
   │
   ▼
Nginx
   │
   ▼
127.0.0.1:3000</code></pre><p>따라서 Security Group도 일반적으로:</p>
<pre><code>80   → Public
443  → Public
3000 → Public에 열지 않음</code></pre><p>으로 구성할 수 있다.</p>
<h2 id="17-https는-어디에서-처리하는가">17. HTTPS는 어디에서 처리하는가?</h2>
<p>여기서 아주 중요한 개념이 하나 나온다.</p>
<p>우리 구조에서는 Node.js가 HTTPS를 직접 처리하지 않는다.</p>
<p>Nginx가 처리한다.</p>
<pre><code>User
 │
 │ HTTPS
 │ encrypted
 ▼
Nginx
 │
 │ HTTP
 │ localhost
 ▼
Node.js</code></pre><p>즉 TLS 연결은:</p>
<pre><code>User ↔ Nginx</code></pre><p>사이에서 끝난다.</p>
<p>이를 흔히 <code>TLS Termination</code>이라고 한다.</p>
<p>그래서 Node.js 코드는 그냥:</p>
<pre><code>app.listen({
  port: 3000,
  host: &quot;127.0.0.1&quot;
});</code></pre><p>처럼 HTTP 서버로 실행해도 된다.</p>
<p>Nginx가:</p>
<pre><code>HTTPS
↓
decrypt
↓
HTTP
↓
Node.js</code></pre><p>를 담당한다.</p>
<h2 id="18-nginx의-https-설정">18. Nginx의 HTTPS 설정</h2>
<p>개념적으로 HTTPS용 Nginx 설정은 다음과 같다.</p>
<pre><code>server {
    listen 443 ssl;
    server_name mock-exchange.example.com;
    ssl_certificate /etc/letsencrypt/live/mock-exchange.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mock-exchange.example.com/privkey.pem;
    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}</code></pre><p>여기서:</p>
<pre><code>listen 443 ssl;</code></pre><p>은 HTTPS 연결을 받겠다는 뜻이다.</p>
<p>그리고:</p>
<p><code>ssl_certificate</code></p>
<p>와:</p>
<p><code>ssl_certificate_key</code></p>
<p>가 TLS 연결에 필요한 인증서와 Private Key를 지정한다.</p>
<h2 id="19-http와-https를-둘-다-열면">19. HTTP와 HTTPS를 둘 다 열면?</h2>
<p>다음처럼 구성할 수도 있다.</p>
<pre><code>HTTP :80
HTTPS :443</code></pre><p>하지만 일반적인 웹 서비스에서는 HTTP 요청을 그대로 서비스하기보다 HTTPS로 보내버린다.</p>
<p>예:</p>
<pre><code>server {
    listen 80;
    server_name mock-exchange.example.com;
    return 301 https://$host$request_uri;
}</code></pre><p>그러면 사용자가:</p>
<p><code>http://mock-exchange.example.com/api/orderbook</code></p>
<p>으로 접속해도 Nginx가:</p>
<p><code>301 Redirect</code></p>
<p>를 반환해서:</p>
<p><code>https://mock-exchange.example.com/api/orderbook</code></p>
<p>으로 이동시킨다.</p>
<p>HTTPS 사이트가 HTTP 요청을 받아 HTTPS로 redirect하는 것은 일반적인 구성이다. (MDN Web Docs)</p>
<p>따라서 최종 구조는:</p>
<pre><code>HTTP :80
   │
   │ 301 Redirect
   ▼
HTTPS :443
   │
   │ TLS
   ▼
Nginx
   │
   │ Reverse Proxy
   ▼
Node.js :3000</code></pre><p>이 된다.</p>
<h2 id="20-proxy_set_header는-왜-필요한가">20. proxy_set_header는 왜 필요한가?</h2>
<p>실제 설정에서는 보통 다음도 넣는다.</p>
<pre><code>location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}</code></pre><p>왜 필요할까?</p>
<p>Reverse Proxy가 있기 때문이다.</p>
<p>원래:</p>
<pre><code>Client
   ↓
Node.js</code></pre><p>였다면 Node.js가 직접 클라이언트 정보를 알 수 있다.</p>
<p>하지만 지금은:</p>
<pre><code>Client
   ↓
Nginx
   ↓
Node.js</code></pre><p>이다.</p>
<p>Node.js 입장에서는 연결한 상대가 Nginx다.</p>
<p>그래서 Nginx가 원래 요청 정보를 Header에 담아 전달한다. NGINX 공식 문서에서도 proxy_set_header를 이용해 proxied server로 전달하는 Host 등의 헤더를 변경하도록 지원한다. (docs.nginx.com)</p>
<p>대표적으로:</p>
<pre><code>Host
→ 사용자가 요청한 도메인
X-Real-IP
→ 실제 Client IP
X-Forwarded-For
→ Proxy를 거친 Client IP 정보
X-Forwarded-Proto
→ 원래 HTTP인지 HTTPS인지</code></pre><p>를 전달한다.</p>
<h2 id="21-aws-security-group은-어디에-있는가">21. AWS Security Group은 어디에 있는가?</h2>
<p>여기까지 이해하면 Security Group도 연결된다.</p>
<p>EC2 앞에는 AWS Security Group이라는 방화벽이 있다.</p>
<pre><code>Internet
   │
   ▼
┌─────────────────────┐
│   Security Group    │
│                     │
│ 80  → Allow         │
│ 443 → Allow         │
│ 3000 → Deny         │
└──────────┬──────────┘
           │
           ▼
         EC2
           │
           ▼
         Nginx
           │
           ▼
        Node.js</code></pre><p>따라서 이번 서버에서는 보통 외부에:</p>
<pre><code>TCP 80
TCP 443</code></pre><p>을 허용하면 된다.</p>
<p>애플리케이션 포트:</p>
<p><code>3000</code></p>
<p>은 외부에 열 필요가 없다.</p>
<h2 id="22-실제-요청-하나를-처음부터-따라가-보자">22. 실제 요청 하나를 처음부터 따라가 보자</h2>
<p>사용자가 브라우저에:</p>
<p><code>https://mock-exchange.example.com/api/orderbook</code></p>
<p>을 입력했다고 하자.</p>
<h3 id="step-1--dns">Step 1 — DNS</h3>
<p>브라우저가 묻는다.</p>
<p><code>mock-exchange.example.com의 IP가 뭐야?</code></p>
<p>DNS가:</p>
<p><code>3.35.100.100</code></p>
<p>이라고 알려준다.</p>
<h3 id="step-2--tcp-연결">Step 2 — TCP 연결</h3>
<p>HTTPS이므로:</p>
<p><code>3.35.100.100:443</code></p>
<p>으로 연결한다.</p>
<h3 id="step-3--tls-handshake">Step 3 — TLS Handshake</h3>
<p>Nginx가 인증서를 제공한다.</p>
<pre><code>Certificate
Domain:
mock-exchange.example.com
Issuer:
Let&#39;s Encrypt</code></pre><p>브라우저가 인증서를 검증한다.</p>
<p>TLS handshake 과정에서는 TLS 버전과 암호화 방식 등을 협상하고, 이후 실제 HTTP 데이터가 암호화되어 전달된다. (MDN Web Docs)</p>
<h3 id="step-4--https-request">Step 4 — HTTPS Request</h3>
<p>암호화된 연결이 만들어지면:</p>
<pre><code>GET /api/orderbook
Host: mock-exchange.example.com</code></pre><p>요청을 보낸다.</p>
<h3 id="step-5--nginx">Step 5 — Nginx</h3>
<p>Nginx는:</p>
<p><code>server_name mock-exchange.example.com;</code></p>
<p>과 일치하는 server block을 찾는다.</p>
<p>그리고:</p>
<p><code>proxy_pass http://127.0.0.1:3000;</code></p>
<p>을 확인한다.</p>
<h3 id="step-6--application">Step 6 — Application</h3>
<p>Nginx가 내부적으로:</p>
<p><code>http://127.0.0.1:3000/api/orderbook</code></p>
<p>으로 요청한다.</p>
<p>Node.js가:</p>
<pre><code>{
  &quot;bids&quot;: [],
  &quot;asks&quot;: []
}</code></pre><p>을 반환한다.</p>
<h3 id="step-7--response">Step 7 — Response</h3>
<p>다시:</p>
<pre><code>Node.js
   │
   ▼
Nginx
   │
   │ TLS encryption
   ▼
Internet
   │
   ▼
User</code></pre><p>순서로 응답이 돌아간다.</p>
<h2 id="23-전체-구조를-한-장으로-정리">23. 전체 구조를 한 장으로 정리</h2>
<p>이번에 우리가 만들 시스템을 전부 합치면 다음과 같다.</p>
<pre><code>                         ┌─────────────────┐
                         │      DNS        │
                         │                 │
                         │ mock-exchange   │
                         │       ↓         │
                         │ EC2 Public IP   │
                         └────────┬────────┘
                                  │
                                  ▼
                              Internet
                                  │
                                  │
                         HTTPS TCP :443
                                  │
                                  ▼
                    ┌─────────────────────────┐
                    │   AWS Security Group    │
                    │                         │
                    │   :80       ALLOW       │
                    │   :443      ALLOW       │
                    │   :3000     BLOCK       │
                    └────────────┬────────────┘
                                 │
                                 ▼
                    ┌─────────────────────────┐
                    │          Nginx          │
                    │                         │
                    │  TLS Certificate        │
                    │  TLS Termination        │
                    │  Reverse Proxy          │
                    │                         │
                    │  mock-exchange...       │
                    │          ↓              │
                    │  127.0.0.1:3000         │
                    └────────────┬────────────┘
                                 │
                                 │ HTTP
                                 │
                                 ▼
                    ┌─────────────────────────┐
                    │      Application        │
                    │                         │
                    │ Node.js / Fastify       │
                    │                         │
                    │ 127.0.0.1:3000          │
                    └─────────────────────────┘</code></pre><h2 id="24-그러면-실제-구축-순서는">24. 그러면 실제 구축 순서는?</h2>
<p>이론을 실제 작업으로 바꾸면 순서는 다음과 같다.</p>
<pre><code>① EC2 준비
      ↓
② Node.js 서버 실행
      ↓
③ localhost에서 서버 정상 동작 확인
      ↓
④ Nginx 설치/기존 설정 확인
      ↓
⑤ mock-exchange용 server block 추가
      ↓
⑥ Nginx → Node.js Reverse Proxy
      ↓
⑦ DNS A Record → EC2 IP
      ↓
⑧ HTTP 접속 확인
      ↓
⑨ Let&#39;s Encrypt 인증서 발급
      ↓
⑩ Nginx HTTPS 설정
      ↓
⑪ HTTP → HTTPS redirect
      ↓
⑫ HTTPS 접속 확인</code></pre><p>이 순서가 중요한 이유는 문제 발생 시 원인을 한 단계씩 분리할 수 있기 때문이다.</p>
<h2 id="25-장애가-났을-때-어디를-봐야-하는가">25. 장애가 났을 때 어디를 봐야 하는가?</h2>
<p>이 구조를 이해하면 장애 원인을 찾기도 쉬워진다.</p>
<p>예를 들어:</p>
<p><code>https://mock-exchange.example.com</code></p>
<p>이 안 열린다고 하자.</p>
<p>무작정 Nginx를 수정할 필요가 없다.</p>
<p>먼저:</p>
<p><code>curl http://127.0.0.1:3000</code></p>
<p>을 실행한다.</p>
<p>이것부터 실패 : Node.js 문제</p>
<p>다음:</p>
<p><code>curl http://localhost</code></p>
<p>또는 Host를 지정해서 Nginx를 테스트한다.</p>
<p>여기서 실패한다면: Nginx 문제</p>
<p>다음:</p>
<p><code>dig mock-exchange.example.com</code></p>
<p>결과가 EC2 IP가 아니면: DNS 문제</p>
<p>443 연결 자체가 안 된다면:</p>
<pre><code>Security Group
Firewall
Nginx listen</code></pre><p>등을 확인한다.</p>
<p>인증서 오류라면:</p>
<pre><code>TLS Certificate
Domain
Certbot
Nginx SSL configuration</code></pre><p>을 확인한다.</p>
<p>즉 구조를 알고 있으면:</p>
<p><code>&quot;사이트가 안 돼요&quot;</code></p>
<p>가 아니라:</p>
<pre><code>DNS?
 ↓
Network?
 ↓
Nginx?
 ↓
TLS?
 ↓
Application?</code></pre><p>순서로 문제를 좁힐 수 있다.</p>
<h2 id="26-자주-혼동하는-것-정리">26. 자주 혼동하는 것 정리</h2>
<h3 id="dns와-nginx는-다르다">DNS와 Nginx는 다르다</h3>
<pre><code>DNS
Domain → IP</code></pre><pre><code>Nginx
Request → Application</code></pre><h3 id="http와-https는-포트가-다르다">HTTP와 HTTPS는 포트가 다르다</h3>
<pre><code>HTTP  → 80
HTTPS → 443</code></pre><h3 id="ssl과-tls">SSL과 TLS</h3>
<p>현재 실제 사용하는 것은 TLS지만 여전히 관습적으로 “SSL 인증서”라는 표현을 많이 사용한다.</p>
<pre><code>SSL Certificate
≈ TLS Certificate</code></pre><p>라고 이해하면 된다.</p>
<h3 id="lets-encrypt와-certbot">Let’s Encrypt와 Certbot</h3>
<pre><code>Let&#39;s Encrypt
→ 인증서를 발급하는 CA</code></pre><pre><code>Certbot
→ 인증서 발급/갱신/설정을 자동화하는 Client</code></pre><h3 id="nginx와-nodejs">Nginx와 Node.js</h3>
<pre><code>Nginx
→ 외부 요청 접수
→ HTTPS 처리
→ Reverse Proxy</code></pre><pre><code>Node.js
→ 실제 Business Logic/API 처리</code></pre><h3 id="public-ip와-domain">Public IP와 Domain</h3>
<pre><code>Public IP
→ 실제 서버 주소</code></pre><pre><code>Domain
→ 사람이 사용하기 쉬운 이름</code></pre><pre><code>DNS
→ Domain과 IP를 연결</code></pre><h2 id="27-이번-작업에서-가장-중요한-개념">27. 이번 작업에서 가장 중요한 개념</h2>
<p>모든 내용을 암기할 필요는 없다.</p>
<p>아래 그림이 머릿속에 있으면 된다.</p>
<pre><code>                 DNS
                  │
mock-exchange.example.com
                  │
                  ▼
             EC2 Public IP
                  │
                  │
        ┌─────────┴─────────┐
        │                   │
      HTTP                HTTPS
      :80                  :443
        │                   │
        │             TLS Certificate
        │                   │
        └───────► Nginx ◄───┘
                    │
                    │ Reverse Proxy
                    │
                    ▼
              127.0.0.1:3000
                    │
                    ▼
               Node.js API</code></pre><p>그리고 각각 한 문장으로 설명할 수 있으면 충분하다.</p>
<p>DNS :</p>
<p><code>도메인을 서버의 IP 주소와 연결한다.</code></p>
<p>HTTP :</p>
<p><code>클라이언트와 서버가 웹 데이터를 주고받는 프로토콜이다.</code></p>
<p>HTTPS :</p>
<p><code>HTTP 통신을 TLS로 보호한 것이다.</code></p>
<p>TLS : </p>
<p><code>통신을 암호화하고 무결성을 보호하며 서버의 신원을 인증한다.</code></p>
<p>TLS Certificate : </p>
<p><code>해당 서버가 특정 도메인의 서버임을 검증하는 데 사용되는 디지털 인증서다.</code></p>
<p>Let’s Encrypt : </p>
<p><code>무료 TLS 인증서를 발급해주는 CA다.</code></p>
<p>Certbot : </p>
<p><code>Let’s Encrypt 등의 인증서 발급과 갱신, 웹 서버 설정을 자동화하는 데 사용하는 도구다.</code></p>
<p>Nginx : </p>
<p><code>웹 서버이면서 Reverse Proxy 등 다양한 역할을 할 수 있는 소프트웨어다.</code></p>
<p>Reverse Proxy : </p>
<p><code>외부 요청을 대신 받아 내부 Application Server로 전달하는 구조다.</code></p>
<p>TLS Termination :</p>
<p><code>HTTPS/TLS 처리를 Nginx에서 끝내고 내부 Application에는 HTTP로 전달하는 구조다.</code></p>
<p>Security Group : </p>
<p><code>EC2 앞에서 어떤 네트워크 트래픽을 허용할지 제어하는 AWS의 방화벽이다.</code></p>
<h2 id="마무리">마무리</h2>
<p>이번에 구축하려는 서버를 한 문장으로 표현하면 다음과 같다.</p>
<p>DNS로 mock-exchange.example.com을 EC2에 연결하고, Nginx가 80/443 포트의 HTTP/HTTPS 요청을 받은 뒤 TLS를 처리하고, Reverse Proxy를 통해 localhost에서 실행 중인 자체 서버로 요청을 전달한다.</p>
<p>그래서 실제 애플리케이션은 인터넷이나 TLS 인증서를 직접 신경 쓰지 않고:</p>
<p><code>127.0.0.1:3000</code></p>
<p>에서 자신의 API만 제공하면 된다.</p>
<p>외부 세계와 Application 사이의 경계를 Nginx가 담당하는 것이다.</p>
<pre><code>               External
─────────────────────────────────
DNS
 ↓
AWS Security Group
 ↓
Nginx
 ├─ HTTP
 ├─ HTTPS
 ├─ TLS Certificate
 └─ Reverse Proxy
─────────────────────────────────
               Internal
 ↓
127.0.0.1:3000
 ↓
Application</code></pre><p>이 그림을 이해했다면, 이제 실제 Nginx 설정에서 listen, server_name, location, proxy_pass, ssl_certificate가 왜 존재하는지도 자연스럽게 연결된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Scafonix (Serverless MPC) 연동기]]></title>
            <link>https://velog.io/@na_treeeum/Scafonix-Serverless-MPC-%EC%97%B0%EB%8F%99%EA%B8%B0</link>
            <guid>https://velog.io/@na_treeeum/Scafonix-Serverless-MPC-%EC%97%B0%EB%8F%99%EA%B8%B0</guid>
            <pubDate>Sun, 26 Jul 2026 08:33:11 GMT</pubDate>
            <description><![CDATA[<h1 id="scafonixserverless-mpc로-지갑-구현하기">Scafonix(Serverless MPC)로 지갑 구현하기</h1>
<p>제가 지금껏 사용해온 Web2 서비스에서의 이탈 지점은 보통 &quot;필요한 서비스가 아니어서&quot;, &quot;재미가 없어서&quot; 같은 이유였습니다. &quot;로그인이 어려워서&quot; 서비스를 그만둔 적은 거의 없었던 것 같습니다.</p>
<p>하지만 Web3에서는 이게 충분히 가능한 이탈 사유가 됩니다. 메타마스크 설치, 네트워크 추가, 시드 문구 백업, 가스비 준비까지 요구하면 사용자는 서비스에 들어오기도 전에 지칩니다. 그렇다고 서버가 개인키를 대신 보관하는 Custodial 방식은 보안적 책임이 너무 큽니다.</p>
<p>이번에는 Scafonix MPC 서비스를 사용해 Google 소셜 로그인만으로 테스트넷 지갑을 만들고, 직접 배포한 <code>SimpleCounter</code> 컨트랙트를 호출해봤습니다. Scafonix는 개인키를 P1, P2, P3로 나누는 2-of-3 MPC 방식입니다. 완성된 개인키를 DB에 저장하거나 서버에서 복구한 뒤 서명하는 구조가 아니라, 브라우저의 보안 샌드박스 안에서 필요한 share를 조합해 서명을 수행합니다. 서비스 제공자 입장에서는 별도의 키 보관 DB나 서명 서버를 운영하지 않아도 된다는 점이 가장 크게 와닿았습니다.</p>
<blockquote>
<p>Scafonix Docs를 참고하여 이번 데모를 제작해 봤습니다. <a href="https://www.scafonix.com/docs">Scafonix Docs</a></p>
</blockquote>
<blockquote>
<p>Scafonix 에서는 AI Agent 가 활용할 수 있는 프롬프트도 지원하기 때문에 어렵지 않게 구현할 수 있습니다. <a href="https://www.scafonix.com/llms.txt">AI Assistant Docs</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/2c10ee5e-0b9c-4312-8bf6-141a36fef2cc/image.png" alt=""></p>
<h2 id="1-simplecounter">1. SimpleCounter</h2>
<p>테스트할 컨트랙트는 일부러 단순하게 만들었습니다. 사용자별 count만 저장하고, <code>getCount()</code>는 <code>msg.sender</code>의 count를 반환합니다. <code>count()</code>는 같은 <code>msg.sender</code>의 count를 1 증가시킵니다.</p>
<h2 id="2-google-로그인으로-지갑-연결">2. Google 로그인으로 지갑 연결</h2>
<p>아래는 Scafonix 연동데모를 위해 만든 간단한 페이지 입니다.
<img src="https://velog.velcdn.com/images/na_treeeum/post/b9926e82-4121-4274-8d4d-91d69a6b1d90/image.png" alt=""></p>
<p>클라이언트는 프레임워크 없이 HTML과 JS만 사용했습니다.
단 두줄(ethers 연동을 위한 코드라인을 빼면 단 한줄)만으로도 손쉽게 라이브러리를 불러와서 사용할 수 있습니다.</p>
<pre><code class="language-html">&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/ethers/6.13.1/ethers.umd.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;https://sdk.scafonix.com/sdk.js?v=2&quot;&gt;&lt;/script&gt;</code></pre>
<p>Google Login 버튼을 누르면 Google OAuth 팝업이 나와서 구글계정으로 손쉽게 로그인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/94b04a8a-105f-499b-a8bc-ac7f36d07fe1/image.png" alt=""></p>
<p>로그인이 완료되면 별다른 과정 없이 나의 지갑이 생성되었고, 생성된 지갑주소를 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/a20f0b58-5385-44d8-a7b0-64409c5e22a5/image.png" alt=""></p>
<p>Google Login만으로 Scafonix Wallet 연결까지 이어지는 간편한 UX가 구현됐습니다.</p>
<h2 id="3-getcount와-count-호출">3. getCount와 count 호출</h2>
<p><code>getCount()</code>는 view 함수라서 단순히 RPC call로 조회했습니다. 다만 컨트랙트가 <code>msg.sender</code> 기준으로 count를 저장하기 때문에, call을 보낼 때도 Scafonix 지갑 주소를 <code>from</code>으로 넣어 현재 로그인한 사용자의 값을 읽었습니다.</p>
<pre><code class="language-js">const value = await contract.getCount({ from: connectedEvmAddress });</code></pre>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/3d4c2be0-f682-4ceb-9d35-799c3d79493b/image.png" alt=""></p>
<p>반면 <code>count()</code>는 실제 상태를 바꾸는 함수라 트랜잭션 서명이 필요합니다. Scafonix를 통해 tx에 서명하고 broadcast되는 과정은 아래 4단계로 이해하면 쉽습니다.</p>
<ol>
<li><p>클라이언트에서 unsigned tx를 만든다<br>먼저 <code>to</code>, <code>data</code>, <code>chainId</code>, <code>nonce</code>, <code>gasLimit</code>, fee 값을 채워 아직 서명되지 않은 transaction object를 만듭니다. <code>SimpleCounter.count()</code>의 경우 <code>contract.count.populateTransaction()</code>으로 호출 data를 만들고, RPC에서 nonce와 gas 정보를 조회합니다.</p>
</li>
<li><p>Scafonix SDK에 서명을 요청한다<br>만든 tx object를 <code>Scafonix.signTransaction(connectedEvmAddress, tx)</code>로 넘깁니다. 여기서 넘기는 것은 개인키가 아니라 서명할 트랜잭션 데이터입니다. device에 저장된 키를 Scafonix 서버로 전송해서 복구하는 구조가 아닙니다.</p>
</li>
<li><p>SDK 보안 샌드박스에서 MPC 서명이 생성된다<br>Scafonix는 P1, P2, P3 중 필요한 share 조합으로 2-of-3 threshold signing을 수행합니다. 문서 기준으로 이 과정에서도 완성된 private key는 메모리 안에서 조립되지 않습니다. EVM의 경우 결과값은 <code>{ r, s, v }</code> 형태의 ECDSA signature입니다.</p>
</li>
<li><p>서명을 붙여 raw transaction을 만들고 RPC에 broadcast한다<br>반환받은 signature를 ethers의 <code>Transaction.from({ ...tx, signature }).serialized</code>로 붙이면 signed raw transaction이 됩니다. 이 raw transaction을 <code>provider.broadcastTransaction(signedTx)</code>로 네트워크에 전송합니다.</p>
</li>
</ol>
<pre><code class="language-js">const signature = await Scafonix.signTransaction(connectedEvmAddress, tx);
const signedTx = ethers.Transaction.from({ ...tx, signature }).serialized;
await provider.broadcastTransaction(signedTx);</code></pre>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/faf6daa5-a09c-4f76-97dd-d6233a75950b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/20bab9f6-02b6-4066-bc3c-f09828c2296c/image.png" alt=""></p>
<p>서비스 제공자가 유저의 개인키를 들고 있지 않은데도, 사용자는 소셜 로그인 이후 자연스럽게 온체인 트랜잭션을 만들 수 있습니다.</p>
<h2 id="4-백업과-복구">4. 백업과 복구</h2>
<p>P3는 사용자가 직접 보관하는 복구 share입니다. Google Drive나 copy key share 같은 옵션을 선택할 수 있습니다.
백업 모달은 <code>showBackupModal(userId)</code>로 열 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/8f5b0ee3-3fdd-4168-a9cf-2b7eecb8545e/image.png" alt=""></p>
<h2 id="privy와-비교하며-느낀-차이">Privy와 비교하며 느낀 차이</h2>
<p>Privy 같은 Embedded Wallet도 Web3 온보딩 문제를 잘 풀어주는 선택지입니다. 다만 Scafonix는 서비스 제공자 서버 및 DB에 키를 저장하지 않으며, 서명 서버를 두지 않는다는 점이 강점입니다. P1은 브라우저, P2는 소셜 인증 기반 share, P3는 유저가 보관하는 백업 share로 나뉘고, 2-of-3 구조에서 서명이 일어납니다.</p>
<p>물론 &quot;프론트엔드가 해킹돼도 무조건 안전하다&quot;는 뜻은 아닙니다. 악성 트랜잭션 승인, 피싱, 스크립트 공급망 공격은 여전히 별도로 방어해야 합니다. 다만 개인키 전체를 앱이나 서버 및 DB에 저장하지 않는다는 점은 확실히 운영 부담을 줄여줄 수 있다고 생각합니다.</p>
<h2 id="마무리">마무리</h2>
<p>이번 데모에서는 Scafonix를 연동하여 Google 로그인 한 번으로 지갑 생성부터 연결까지 하고, 직접 배포한 <code>SimpleCounter</code>의 <code>getCount()</code>와 <code>count()</code>까지 호출했습니다. 메타마스크 설치 없이도 온체인 액션을 만들 수 있었고, 서비스 제공자 입장에서는 키를 따로 보관하는 설계를 하지 않아도 되기 때문에 정책적 리스크를 줄일 수 있으며 개별 키 보관 위험도 줄어든다는 점이 좋았습니다. 또한 분리된 키를 따로따로 보관하기 때문에 단일 share 유출만으로는 서명이 불가능하다는 점도 기존 private key 단일 보관 방식보다 안전하게 느껴졌습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ERC-4337 Account Abstraction]]></title>
            <link>https://velog.io/@na_treeeum/ERC-4337-Account-Abstraction</link>
            <guid>https://velog.io/@na_treeeum/ERC-4337-Account-Abstraction</guid>
            <pubDate>Thu, 28 May 2026 09:39:51 GMT</pubDate>
            <description><![CDATA[<p><a href="https://eips.ethereum.org/EIPS/eip-4337">https://eips.ethereum.org/EIPS/eip-4337</a></p>
<hr>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/289e74ea-03be-489e-9f06-0e75a9626bfe/image.png" alt=""></p>
<h1 id="custodial-wallet-non-custodial-wallet">Custodial Wallet? Non-Custodial Wallet?</h1>
<p>web3 서비스를 이용하다보면 커스터디 월렛, 논-커스터디 월렛이라는 용어를 사용합니다.</p>
<h2 id="custodial-wallet">Custodial Wallet</h2>
<blockquote>
<p>서비스 제공자가 사용자의 private key를 대신 보관하고 관리하는 지갑</p>
</blockquote>
<p><strong>특징</strong></p>
<ul>
<li>사용자는 private key를 직접 관리하지 않음</li>
<li>로그인 기반 (이메일, OAuth 등)으로 접근 가능</li>
<li>UX가 Web2 서비스처럼 간단함</li>
<li>서비스가 계정을 복구하거나 접근을 제한할 수 있음</li>
</ul>
<p><strong>장점</strong></p>
<ul>
<li>사용하기 쉬움 (초보자 친화적)</li>
<li>사용자의 키 분실 리스크 없음</li>
<li>빠른 온보딩</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>서비스가 자산을 통제할 수 있음</li>
<li>해킹 또는 내부 사고 시 리스크 큼(SPOF)</li>
<li>“Not your key, not your coin”</li>
</ul>
<h2 id="non-custodial-wallet">Non-Custodial Wallet</h2>
<blockquote>
<p>Non-Custodial Wallet은 사용자가 자신의 private key를 직접 관리하는 지갑</p>
</blockquote>
<p><strong>특징</strong></p>
<ul>
<li>private key를 사용자 본인이 보관</li>
<li>모든 트랜잭션은 사용자가 직접 서명</li>
<li>!!누구도!! 계정을 임의로 통제할 수 없음</li>
</ul>
<p><strong>장점</strong></p>
<ul>
<li>완전한 자산 소유권</li>
<li>검열 저항성</li>
<li>탈중앙성 유지</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>키 분실 시 복구 불가능</li>
<li>UX가 어렵고 진입장벽 높음</li>
<li>gas fee, 네트워크 이해 필요</li>
</ul>
<h1 id="account-abstraction--계정-추상화">Account Abstraction : 계정 추상화</h1>
<h2 id="aa-는-왜-등장했는가">AA 는 왜 등장했는가</h2>
<p>기존 EOA의 문제점</p>
<ul>
<li>모든 tx는 EOA private key 서명이 필요</li>
<li>gas fee는 반드시 ETH로만 지불 가능</li>
<li>ETH Funding이 꼭 필요함</li>
<li>nonce 관리 강제</li>
<li>하나의 private key가 곧 계정 자체</li>
<li>서명 정책 커스터마이징 불가능</li>
<li>recovery / session key / multi-sig 등을 구현하려면 별도 구조 필요</li>
</ul>
<h2 id="eoa-vs-account-abstraction-비교">EOA vs Account Abstraction 비교</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>EOA</th>
<th>AA</th>
</tr>
</thead>
<tbody><tr>
<td>서명 주체</td>
<td>Private Key만 가능</td>
<td>원하는 검증 로직 적용 가능</td>
</tr>
<tr>
<td>Gas 지불</td>
<td>ETH만 가능</td>
<td>Paymaster를 통해 확장 가능(대납 등)</td>
</tr>
<tr>
<td>복구</td>
<td>불가능</td>
<td>가능</td>
</tr>
<tr>
<td>멀티시그</td>
<td>외부 구현 필요</td>
<td>내장 가능</td>
</tr>
</tbody></table>
<h2 id="definitions--구성-항목">Definitions : 구성 항목</h2>
<ul>
<li><p><strong>UserOperation</strong> : 유저 행동을 정의하는 정보를 갖고 있는 데이터 구조체. 흔히 사용하는 transaction과 비슷한 개념이지만 같은 것은 아니기 때문에 <code>UserOperation</code> 이라는 용어 사용</p>
</li>
<li><p><strong>Sender</strong> : 추상화된 하나의 계정. <code>UserOperation</code>을 보내는 계정이며, 일반적으로 스마트 컨트랙트 계정(Account Contract)을 의미함</p>
</li>
<li><p><strong>Owner</strong> : 추상화된 계정의 주인 계정. Sender에 대한 통제권과 소유권을 갖고있음을 증명할 수 있는 계정이며 일반적으로 Private Key를 사용하는 EOA 계정</p>
</li>
<li><p><strong>EntryPoint</strong> : <code>UserOperation</code>을 검증하고 실행하는 중심 컨트랙트. Bundler는 여러 <code>UserOperation</code>을 모아 <code>EntryPoint.handleOps()</code>를 호출하고, EntryPoint는 각 Account의 검증 로직과 실행 로직을 호출함</p>
</li>
<li><p><strong>Bundler</strong> : UserOperation mempool에서 <code>UserOperation</code>들을 수집하고, 이를 하나의 일반 Ethereum transaction으로 묶어 EntryPoint에 제출하는 주체</p>
</li>
<li><p><strong>Paymaster</strong> : 사용자의 gas 비용을 대신 지불하거나, ETH가 아닌 다른 방식으로 gas 비용을 처리할 수 있게 해주는 선택적 컨트랙트. 예를 들어 서비스가 gas를 sponsor하거나, 사용자가 토큰으로 gas 비용을 지불하는 구조를 만들 수 있음</p>
</li>
<li><p><strong>AccountFactory</strong> : Account Contract를 생성하는 컨트랙트. 사용자의 Account가 아직 배포되지 않은 경우, <code>UserOperation</code>의 <code>initCode</code>를 통해 Factory가 Account를 생성할 수 있음</p>
</li>
<li><p><strong>UserOperation mempool</strong> : ERC-4337 표준 검증 규칙을 따르는 공개 UserOperation mempool. Bundler들이 공통 규칙에 따라 UserOperation을 수집하고 검증할 수 있도록 설계된 기본 mempool 개념</p>
</li>
<li><p><strong>Deposit</strong> : EntryPoint에 예치된 ETH 잔액. Account 또는 Paymaster가 gas 비용 정산을 위해 EntryPoint에 미리 예치해두는 자금</p>
</li>
</ul>
<h2 id="aa-동작방식">AA 동작방식</h2>
<ol>
<li>userOperation을 생성합니다.</li>
</ol>
<pre><code class="language-json">{
  &quot;sender&quot; : &quot;&quot;,
  &quot;nonce&quot; : &quot;&quot;,
  &quot;factory&quot; : &quot;&quot;,
  &quot;factoryData&quot; : &quot;&quot;,
  &quot;callData&quot; : &quot;&quot;,
  &quot;callGasLimit&quot; : &quot;&quot;,
  &quot;verificationGasLimit&quot; : &quot;&quot;,
  &quot;preVerificationGas&quot; : &quot;&quot;,
  &quot;maxFeePerGas&quot; : &quot;&quot;,
  &quot;maxPriorityFeePerGas&quot; : &quot;&quot;,
  &quot;paymaster&quot; : &quot;&quot;,
  &quot;paymasterVerificationGasLimit&quot; : &quot;&quot;,
  &quot;paymasterPostOpGasLimit&quot; : &quot;&quot;,
  &quot;paymasterData&quot; : &quot;&quot;,
  &quot;signature&quot; : &quot;&quot;,

}</code></pre>
<ol start="2">
<li><p>생성된 userOperation의 실행 주체는 sender입니다. sender가 생성한 userOp 이 맞다는 서명을 owner 계정을 통해 생성합니다.</p>
</li>
<li><p>생성된 userOperation 과 signature를 alt_mempool 에 제출합니다.</p>
</li>
<li><p>bundler는 alt_mempool에 등록된 userOperation 정보(signature 포함)를 가지고 entryPoint의 handleOps() 함수를 호출합니다.</p>
</li>
<li><p>handleOps() 함수는 내부적으로 account contract의 validateUserOp을 통해 userOperation을 검증합니다.</p>
</li>
<li><p>paymaster가 지정되었다면 validatePaymasterUserOp을 통해 paymaster도 검증합니다.</p>
</li>
<li><p>bundler가 handleOps()를 네트워크에 전송할 때 가스토큰을 지불합니다.</p>
</li>
<li><p>userOperation이 실행이 끝나면 사용한 gas 만큼 bundler가 user/paymaster로부터 돌려받습니다.</p>
</li>
<li><p>user/paymaster의 deposit에서 돌려받게 되며, atomic하게 정산됩니다.</p>
</li>
</ol>
<h2 id="validateuserop의-의미">validateUserOp의 의미</h2>
<p>기존 Ethereum 에서는 </p>
<p><code>이 트랜잭션이 유효한, 즉 트랜잭션을 전송하는 사람이 계정의 주인이 맞는지</code></p>
<p>를 검증하기 위하여 ECDSA 서명 검증 로직이 protocol 내부에 고정되어있습니다. 
: transaction data 를 생성 -&gt; 계정 소유자가 private key로 서명 -&gt; 프로토콜 레벨에서 ECDSA 서명검증 -&gt; 검증 성공 시 tx 실행</p>
<p>하지만 AA 에서는 이 검증 과정을 원하는 방식으로 직접 구현할 수 있으며 그로 인해 다음 기능들을 구현할 수 있습니다.</p>
<ul>
<li>owner signature 검증</li>
<li>multiSig 검증</li>
<li>특정 시간대만 사용 가능</li>
<li>특정 dApp만 허용</li>
<li>등</li>
</ul>
<blockquote>
<p>기존 Ethereum에서는 “유효한 서명”의 기준이 protocol에 고정되어 있었다면,
ERC-4337에서는 그 검증 기준 자체를 Account Contract가 직접 정의할 수 있게 됩니다.</p>
</blockquote>
<h2 id="paymaster-예시">paymaster 예시</h2>
<h3 id="gas-sponsor">gas sponsor</h3>
<ol>
<li>사용자는 ETH 없음</li>
<li>서비스는 Paymaster 운영</li>
<li>사용자는 단순 서명만 수행</li>
<li>Paymaster가 gas sponsor</li>
<li>사용자는 Web2처럼 서비스 이용</li>
</ol>
<h3 id="erc20-gas-payment">ERC20 gas payment</h3>
<p>사용자는 USDC만 보유</p>
<p>→ Paymaster가 ETH gas를 대신 지불
→ 이후 User의 USDC를 차감</p>
<h2 id="aa로-가능한-것들">AA로 가능한 것들</h2>
<p><code>계정 추상화</code>는 계정들로 하여금 다음을 가능하게 합니다.</p>
<ul>
<li>가스비 대납</li>
<li>일반 ERC-20 토큰으로 가스비 지불</li>
<li>트랜잭션 배치 실행</li>
<li>계정사용정책 적용</li>
<li>계정 signer 로테이션</li>
<li>등등</li>
</ul>
<h1 id="마무리">마무리</h1>
<p>ERC-4337은 단순히 “가스비를 대신 내주는 기술”이 아닙니다.</p>
<p>기존 Ethereum의 EOA는 강력하고 단순했지만, 계정 자체의 동작 방식을 프로그래밍할 수는 없었습니다.</p>
<p>하지만 Account Abstraction에서는 계정(Account)이 하나의 Smart Contract가 되며,
“어떤 요청을 유효하다고 판단할 것인가” 자체를 개발자가 정의할 수 있게 됩니다.</p>
<p>즉, 계정이 단순한 private key의 소유 개념을 넘어:</p>
<ul>
<li>정책을 가질 수 있고</li>
<li>복구 기능을 가질 수 있으며</li>
<li>여러 서명 방식을 사용할 수 있고</li>
<li>서비스 목적에 맞는 UX를 직접 구현할 수 있는</li>
</ul>
<p>하나의 programmable entity가 됩니다.</p>
<p>그리고 ERC-4337은 이러한 구조를 consensus layer 변경 없이 Ethereum 위에서 구현하기 위한 현실적인 접근입니다.</p>
<p>결국 Account Abstraction은 단순한 기능 추가가 아니라,</p>
<p>“Ethereum 계정 모델 자체를 소프트웨어적으로 확장 가능하게 만든 변화”</p>
<p>라고 볼 수 있겠습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Reentrancy Attack]]></title>
            <link>https://velog.io/@na_treeeum/Reentrancy-Attack</link>
            <guid>https://velog.io/@na_treeeum/Reentrancy-Attack</guid>
            <pubDate>Fri, 30 Jan 2026 09:40:16 GMT</pubDate>
            <description><![CDATA[<h1 id="reentrancy-attack재진입-공격이란">Reentrancy Attack(재진입 공격)이란?</h1>
<p>컨트랙트의 상태가 업데이트 되기 전, 함수를 <strong>다시 호출(재진입)</strong>하여 공격하는 기법</p>
<p>다음과 같은 기능을 갖고있는 컨트랙트는 reentrancy attack 에 취약합니다.</p>
<ul>
<li>외부 컨트랙트의 함수를 호출</li>
<li>외부 컨트랙트로 native token 을 전송</li>
<li>자산 전송 이후에 상태업데이트</li>
</ul>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/c5fe361c-55da-48e7-8ed3-7a1e8ade2945/image.png" alt=""></p>
<p>다음은 Reentrancy 에 취약한 컨트랙트와 이를 공격하는 컨트랙트 코드 예시입니다.</p>
<pre><code class="language-js">contract vulnerableContract{
    mapping(address =&gt; uint256) public balances;

    function withdraw() public{
        require(balances[msg.sender] &gt; 0);

        (bool success, ) = msg.sender.call{value: balances[msg.sender]}(&quot;&quot;);
        require(success);

        balances[msg.sender] = 0;
    }
}</code></pre>
<pre><code class="language-js">contract maliciousContract{
    IVulnerableContract public vCa = &quot;0xsomeAddress..&quot;;

    function exploit() external {
        vCa.withdraw();
    }

    fallback() external payable {
        if(address(vCa).balance &gt;= vCa.balances(address(this))){
            vCa.withdraw();
        }
    }
}</code></pre>
<p>위의 두 컨트랙트는 다음과 같은 흐름으로 exploit이 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/67ce38cf-adb7-4702-a628-ff8117e2b6b8/image.png" alt=""></p>
<h1 id="대응법">대응법</h1>
<h2 id="1-자산-전송-전-상태업데이트-먼저-하기">1. 자산 전송 전 상태업데이트 먼저 하기</h2>
<pre><code class="language-js">contract safeContract{
    mapping(address =&gt; uint256) public balances;

    function withdraw() public{
        require(balances[msg.sender] &gt; 0);

        uint256 transferAmount = balnaces[msg.sender];
        balances[msg.sender] = 0;

        (bool success, ) = msg.sender.call{value: transferAmount}(&quot;&quot;);
        require(success);
    }
}</code></pre>
<p><code>상태(msg.sender 의 balance) 업데이트 → 자산 전송</code> 의 순서로 코드가 실행되면 재진입을 하더라도 잔액을 확인하는 부분에서 오류가 발생하기 때문에 재진입 공격에 실패하게 됩니다.</p>
<h2 id="2-mutex-사용">2. Mutex 사용</h2>
<pre><code class="language-js">contract safeContract{
    mapping(address =&gt; uint256) public balances;
    bool mutex = false;

    function withdraw() public{
        require(!mutex, &quot;Mutex is locked&quot;);
        require(balances[msg.sender] &gt; 0, &quot;Insufficient balance&quot;);

        mutex = true;

        (bool success, ) = msg.sender.call{value: balances[msg.sender]}(&quot;&quot;);
        require(success, &quot;tx is failed&quot;);

        balances[msg.sender] = 0;

        mutex = false;
    }
}</code></pre>
<p>재진입을 하더라도 mutex가 잠겨있는 상태이기 때문에 tx가 실패하게 됩니다.</p>
<h2 id="3-openzeppelin-reentrancyguard-사용">3. Openzeppelin ReentrancyGuard 사용</h2>
<pre><code class="language-js">import {ReentrancyGuard} from &quot;@openzeppelin/contracts/utils/ReentrancyGuard.sol&quot;;

contract safeContract {
    mapping(address =&gt; uint256) public balances;

    function withdraw() public nonReentrant{
        require(balances[msg.sender] &gt; 0, &quot;Insufficient balance&quot;);

        (bool success, ) = msg.sender.call{value: balances[msg.sender]}(&quot;&quot;);
        require(success, &quot;tx is failed&quot;);

        balances[msg.sender] = 0;
    }
}</code></pre>
<p>mutex와 동일한 원리입니다. openzeppelin 에서 표준화한 코드를 사용합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[홈 서버 구축기 - 초기세팅]]></title>
            <link>https://velog.io/@na_treeeum/%ED%99%88-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%EA%B8%B0-%EC%B4%88%EA%B8%B0%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@na_treeeum/%ED%99%88-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%EA%B8%B0-%EC%B4%88%EA%B8%B0%EC%84%B8%ED%8C%85</guid>
            <pubDate>Fri, 09 Jan 2026 14:23:32 GMT</pubDate>
            <description><![CDATA[<p>램트코인 이라는 말이 나올 정도로 메모리, ssd 가격이 천정부지로 올라가고 있습니다.</p>
<p>여느때와 같이 당근을 보던 중</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/cd35fe62-445d-4d43-a1be-4850c093cd7a/image.png" alt=""></p>
<p>신품기준 무려 약 29만원짜리 상품을 단돈 13만원에 판매한다는 글을 보았고, 바로 메세지를 보냈습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/ec9fb8c8-beb0-4807-9292-902945c730ee/image.jpg" alt=""></p>
<p>오케이 굿. 바로 구매 ㄱㄱ</p>
<p>연락을 한 다음 날 바~~로 집으로 데려왔습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/9fd16747-858a-425a-86f2-2ea22e8a3637/image.png" alt=""></p>
<p>케이스는 군시절이 떠오르는 국방색이고 사이즈는 수첩? 정도의 아주 작고 귀여운 미니PC 입니다.</p>
<p>이 친구를 처음 데려왔을 때에는 윈도우11 이 설치되어 있었습니다.</p>
<p>저는 개발자기 때문에(?) 바로 우분투를 깔기로 마음먹었습니다.</p>
<p>또한, 그냥 개발자가 아닌 &quot;BackEnd Developer&quot; 이기 때문에, <em>GUI 따위 필요없다</em> 선언하고 바~로 Ubuntu Server 를 설치했습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/c44ee45b-af0b-4564-81b2-9ff2490973d7/image.png" alt=""></p>
<p>화면에 딱 한줄 띄워져 있는 모습, 아주 멋있지 않나요?</p>
<p>그런데, 막상 설치하고 나니 무인도에 알몸으로 던져진 느낌이었습니다.</p>
<p>인터넷 조차 직접 설정해야 되더라구요.. 새삼 GUI의 편리함을 느끼게 되는 계기였습니다.</p>
<p>패기롭게 시작하였지만 아는게 없는 저는 바~로 GPT선생님께 질문하였고 역시나 갓갓갓선생님께서는 설정법을 아주 쉽게 알려주셨습니다.</p>
<p>먼저 <code>ip a</code> 명령어를 입력해서 사용할 수 있는 랜카드를 확인합니다.</p>
<p>확인 결과, 유선랜포트는 enp3s0 라고 라고 돼있었습니다.</p>
<p>인터넷에 연결하기 위해 <code>/etc/netplan/00-wired.yaml</code> 파일을 생성해주고 다음 내용을 작성했습니다.</p>
<pre><code class="language-yaml">network:
  version: 2
  renderer: networkd
  ethernets:
    enp3s0:
      dhcp4: true</code></pre>
<p>그 다음 <code>~$ sudo netplan apply</code> 로 설정파일 적용!</p>
<p><code>~$ ping 8.8.8.8</code> 명령어를 통해 인터넷 연결이 잘 되었는지
(google dns 서버와 통신이 되는지) 확인을 하고</p>
<p>ssh를 설치했습니다</p>
<p><code>apt install -y openssh-server</code>
<code>systemctl enable ssh</code>
<code>systemctl start ssh</code></p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/88c75fc2-2eef-4b65-828b-483c586d79f3/image.png" alt=""></p>
<p>짜라란~ 접속 성공!</p>
<p>자, 그럼 이제 무엇을 해볼지 고민을 해야합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Websocket을 활용한 event indexing]]></title>
            <link>https://velog.io/@na_treeeum/indexing</link>
            <guid>https://velog.io/@na_treeeum/indexing</guid>
            <pubDate>Fri, 09 Jan 2026 05:49:54 GMT</pubDate>
            <description><![CDATA[<h3 id="github">github</h3>
<p><a href="https://github.com/natreeum/HybridIndexer">https://github.com/natreeum/HybridIndexer</a></p>
<h1 id="개요">개요</h1>
<p>기존 인덱싱 방식은 http 방식(Polling)으로 진행. 이로인해 설정된 주기로만 요청하므로 최대 ‘설정된 주기까지의 delay’가 생김. 이로 인해 <strong>낙관적 업데이트</strong>와 같은 추가 작업을 필요로 합니다.</p>
<h1 id="구상">구상</h1>
<ol>
<li>ws provider를 통한 event subscription으로 event 발생 시 트랜잭션(event) 수집</li>
<li>웹소켓으로 수집된 트랜잭션은 DB에 저장
 a. 최초 status 는 unconfirmed상태로 저장</li>
<li>batch indexing을 통해 unconfirmed 상태의 트랜잭션에 대해 confirm
 a. removed : true 인 경우(reorg) 삭제
 b. 누락된 데이터 업데이트</li>
<li>ws connection 이 유실되었을 경우 재연결 시도</li>
</ol>
<h2 id="0-contract">0. Contract</h2>
<p>Event 를 emit 하는 단순한 컨트랙트 배포</p>
<p>code : Test.sol</p>
<pre><code class="language-javascript">// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Test {
    event LogMessage(string message);
    event TestTransfer(address indexed from, address indexed to, uint256 value);

    function emitLogMessage(string memory message) public {
        emit LogMessage(message);
    }

    function transfer(address to, uint256 value) public {
        emit TestTransfer(msg.sender, to, value);
    }
}
</code></pre>
<h2 id="1-websocket으로-emit되는-event-확인">1. Websocket으로 Emit되는 event 확인</h2>
<p>code : indexer.js</p>
<pre><code class="language-js">const { ethers } = require(&quot;ethers&quot;);
module.exports = class HybridIndexer {
  constructor({
    wsUrl,
    httpUrl,
    contractCfg, // {address, abi}
    events, // [&quot;Transfer&quot;, ...]
    confirmations = 10,
    chunkSize = 2000,
    poolIntervalMs = 5000,
  }) {
    this.wsUrl = wsUrl;
    this.httpUrl = httpUrl;
    this.contractCfg = contractCfg;
    this.events = events;
    this.confirmations = confirmations;
    this.chunkSize = chunkSize;
    this.poolIntervalMs = poolIntervalMs;

    this.wsProvider = null;
    this.httpProvider = null;
    this.wsContract = null;
    this.httpContract = null;
    this.running = false;
  }

  async start() {
    this.running = true;

    this.wsProvider = new ethers.WebSocketProvider(this.wsUrl);
    this.httpProvider = new ethers.JsonRpcProvider(this.httpUrl);

    this.wsContract = new ethers.Contract(
      this.contractCfg.address,
      this.contractCfg.abi,
      this.wsProvider
    );
    console.log(`WebSocket Provider connected to ${this.wsUrl}`);

    this.httpContract = new ethers.Contract(
      this.contractCfg.address,
      this.contractCfg.abi,
      this.httpProvider
    );
    console.log(`HTTP Provider connected to ${this.httpUrl}`);

    this._subscribeEvents();
    console.log(&quot;HybridIndexer started&quot;);
  }

  _subscribeEvents() {
    this.events.forEach((eventName) =&gt; {
      this.wsContract.on(eventName, async (...args) =&gt; {
        console.log(args);
      });
    });
  }
};
</code></pre>
<p>code : server.js</p>
<pre><code class="language-js">const express = require(&quot;express&quot;);
const Indexer = require(&quot;./indexer/hybridIndexer&quot;);

const app = express();

// Fuji Network configuration
const wsUrl = &quot;wss://api.avax-test.network/ext/bc/C/ws&quot;;
const httpUrl = &quot;https://api.avax-test.network/ext/bc/C/rpc&quot;;
const abi = require(&quot;./abi/ABI.json&quot;); // 예시 ABI 파일
const contractAddress = &quot;0xa85761417ed8ae28378612a63282b4A576949377&quot;;
const contractCfg = {address : contractAddress, abi}
const events = [&quot;LogMessage&quot;,&quot;TestTransfer&quot;]

app.get(&quot;/&quot;, (req, res) =&gt; {
  return res.send(&quot;Hello, World!&quot;);
});

const indexer = new Indexer({
  wsUrl,
  httpUrl,
  contractCfg, // ethers.Contract Instance
  events, // [&quot;Transfer&quot;, ...]
});


indexer.start()

app.listen(3000, () =&gt; {
  console.log(&quot;Server is running on port 3000&quot;);
});</code></pre>
<p>서버를 실행하면 indexer가 실행되고, events에 대해 subscription 실행</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/175a4963-de86-4a3b-9c36-448d7aedc968/image.png" alt=""></p>
<p>Test.sol 의 LogMessage Event 발생 시 서버측에서 이벤트 확인</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/84e84dde-6f7f-429a-a736-0601c609e8f5/image.png" alt=""></p>
<h2 id="2-db-table">2. DB Table</h2>
<p>확장성을 고려해서 공통 column / eventArgs column을 구분</p>
<h3 id="공통-column">공통 column</h3>
<ul>
<li>transaction_hash</li>
<li>block_number</li>
<li>log_index</li>
<li>transaction_index</li>
<li>confirmed</li>
</ul>
<h3 id="event-args-column">Event Args Column</h3>
<ul>
<li>... </li>
</ul>
<h2 id="3-indexer-constructor">3. Indexer Constructor</h2>
<ul>
<li><code>wsUrl</code> : WebSocket RPC URL</li>
<li><code>httpUrl</code> : http RPC URL</li>
<li><code>contractCfg</code><ul>
<li><code>address</code> : 컨트랙트 주소</li>
<li><code>abi</code> : 컨트랙트 abi</li>
</ul>
</li>
<li><code>eventName</code> : 받아올 event의 이름</li>
<li><code>tableName</code> : 받아온 event data 를 저장하는 table 이름</li>
<li><code>argKeys</code> : args 이름 배열 <code>[”eventArgName1”, “eventArgName2”]</code></li>
<li><code>confirmations</code> : block confirmation 수</li>
<li><code>chunkSize</code> : polling 최대 블록 수</li>
<li><code>pollIntervalMs</code> : polling 인터벌 ms</li>
<li><code>pingIntervalMs</code> : ws connection status 체크를 위한 ping 인터벌 ms</li>
</ul>
<h1 id="구현">구현</h1>
<h2 id="hybrid-indexer">Hybrid Indexer</h2>
<pre><code class="language-js">const { ethers } = require(&quot;ethers&quot;);
const log = require(&quot;../libraries/timestampLog&quot;);
const insertEvent = require(&quot;../database/insert&quot;);
const updateEvent = require(&quot;../database/update&quot;);
const deleteEvent = require(&quot;../database/delete&quot;);
const updateLastQueryBlockNumber = require(&quot;../database/updateLastQueryBlock&quot;);
const getLastBlockNumber = require(&quot;../database/getLastBlockNumber&quot;);
const getEvent = require(&quot;../database/getEvent&quot;);

module.exports = class HybridIndexer {
  constructor({
    wsUrl,
    httpUrl,
    contractCfg, // {address, abi}
    eventName,
    tableName,
    argKeys, // [&quot;eventArgName1&quot;, &quot;eventArgName2&quot;]
    confirmations = 10,
    chunkSize = 1000,
    pollIntervalMs = 30000, // Default 30 seconds
    pingIntervalMs = 300000, // Default 5 minutes
  }) {
    this.wsUrl = wsUrl;
    this.httpUrl = httpUrl;
    this.contractCfg = contractCfg;
    this.eventName = eventName;
    this.confirmations = confirmations;
    this.chunkSize = chunkSize;
    this.pollIntervalMs = pollIntervalMs;
    this.intervalPoll = null;
    this.mutexLock = false;
    this.lastConfirmedBlock = 0;

    this.wsProvider = null;
    this.httpProvider = null;
    this.wsContract = null;
    this.httpContract = null;

    this.tableName = tableName;
    this.argKeys = argKeys;

    this.intervalPing = null;
    this.pingIntervalMs = pingIntervalMs;

    this.running = false;
  }

  async start() {
    this.running = true;

    this.wsProvider = new ethers.WebSocketProvider(this.wsUrl);
    this.httpProvider = new ethers.JsonRpcProvider(this.httpUrl);

    this.wsContract = new ethers.Contract(
      this.contractCfg.address,
      this.contractCfg.abi,
      this.wsProvider
    );
    log(`WebSocket Provider is set to ${this.wsUrl}`);
    this.wsProvider.websocket.on(&quot;open&quot;, () =&gt; {
      log(`WebSocket connected to ${this.wsUrl}`);
    });

    this.httpContract = new ethers.Contract(
      this.contractCfg.address,
      this.contractCfg.abi,
      this.httpProvider
    );
    log(`HTTP Provider is set to ${this.httpUrl}`);

    this._subscribeEvents();
    log(&quot;HybridIndexer started&quot;);

    // reconnect on error
    this.wsProvider.websocket.on(&quot;error&quot;, () =&gt; {
      log(`WebSocket Error Occurred`);
      this.wsProvider?.removeAllListeners?.();
      log(`Removing all listeners from WebSocket Provider`);
      this.wsProvider?.destroy?.();
      log(`WebSocket Provider destroyed`);

      if (this.running) {
        log(&quot;Attempting to reconnect...&quot;);
        this.start();
      }
    });

    // reconnect on close
    this.wsProvider.websocket.on(&quot;close&quot;, () =&gt; {
      log(`WebSocket Connection Closed`);
      this.wsProvider?.removeAllListeners?.();
      log(`Removing all listeners from WebSocket Provider`);
      this.wsProvider?.destroy?.();
      log(`WebSocket Provider destroyed`);

      // Reconnect if still running
      if (this.running) {
        log(&quot;Attempting to reconnect...&quot;);
        this.start();
      }
    });

    // To keep the WebSocket connection alive, send periodic pings
    this.intervalPing = setInterval(() =&gt; {
      if (this.wsProvider.websocket.readyState === 1) {
        this.wsProvider.websocket.ping();
        log(&quot;WebSocket ping sent&quot;);
      } else {
        log(&quot;WebSocket is not open, skipping ping&quot;);
      }
    }, this.pingIntervalMs);

    this._confirmEvents();
  }

  stop() {
    this.running = false;
    try {
      this.wsProvider?.removeAllListeners?.();
      this.wsProvider?.destroy?.();
      this.wsProvider = null;
    } catch (error) {
      error(&quot;Error stopping WebSocket Provider:&quot;, error);
    }

    // Clear the ping interval if it exists
    if (this.intervalPing) {
      clearInterval(this.intervalPing);
      this.intervalPing = null;
      log(&quot;Ping interval cleared&quot;);
    }

    log(&quot;HybridIndexer stopped&quot;);
  }

  _subscribeEvents() {
    this.wsContract.on(this.eventName, async (...args) =&gt; {
      const event = args[args.length - 1]; // ethers v6: 마지막 인자가 Event

      await insertEvent({
        tableName: this.tableName,
        transactionHash: event.log.transactionHash,
        blockNumber: event.log.blockNumber,
        logIndex: event.log.index,
        transactionIndex: event.log.transactionIndex,
        argKeys: this.argKeys,
        argValues: event.log.args,
      });
      log(
        `Event has been inserted into the database : ${event.log.blockNumber}-${event.log.index}-${event.log.transactionIndex}`
      );
    });
    log(`Subscribed to event: ${this.eventName}`);
  }

  _confirmEvents() {
    this.intervalPoll = setInterval(async () =&gt; {
      if (this.mutexLock) {
        console.log(`Mutex is locked`);
        return;
      }
      this.mutexLock = true;
      try {
        const network = await this.httpProvider.getNetwork();
        const lastBlockNumber = await getLastBlockNumber({
          eventName: this.eventName,
          contractAddress: this.contractCfg.address,
          networkId: network.chainId.toString(),
        });
        console.log(&quot;last block number : &quot;, lastBlockNumber);
        const currentBlockNumber = await this.httpProvider.getBlockNumber();
        console.log(&quot;Current block number : &quot;, currentBlockNumber);
        const filter = this.httpContract.filters[this.eventName]();

        const fromBlockNumber = lastBlockNumber + 1;
        const toBlockNumber = Math.min(
          currentBlockNumber - this.confirmations,
          fromBlockNumber + this.chunkSize
        );
        console.log(fromBlockNumber);
        console.log(toBlockNumber);

        // invalid block range
        if (fromBlockNumber &gt;= toBlockNumber) {
          console.log(
            `Block range is invalid: from:${fromBlockNumber} &gt;= to:${toBlockNumber}`
          );
          throw new Error(`Invalid block range`);
        }

        const events = await this.httpContract.queryFilter(
          filter,
          fromBlockNumber,
          toBlockNumber
        );

        for (const e of events) {
          const {
            transactionHash,
            blockNumber,
            removed,
            index,
            transactionIndex,
            args,
          } = e;
          // Check if this event exist on db
          const exists = await getEvent({
            tableName: this.tableName,
            transactionHash,
            blockNumber,
            index,
            transactionIndex,
          });
          if (exists.length &gt; 0) {
            if (!removed) {
              // if the event is not removed, update &quot;confirmed&quot; as 1
              await updateEvent({
                tableName: this.tableName,
                transactionHash,
                blockNumber,
                index,
                transactionIndex,
              });
            } else {
              // if the event is removed, delete the row
              await deleteEvent({
                tableName: this.tableName,
                transactionHash,
                blockNumber,
                index,
                transactionIndex,
              });
            }
          } else {
            // if the event is not exist on db, insert it
            await insertEvent({
              tableName: this.tableName,
              transactionHash,
              blockNumber,
              logIndex: index,
              transactionIndex,
              argKeys: this.argKeys,
              argValues: args,
            });
          }
        }
        console.log(`${events.length} events processed`);
        await updateLastQueryBlockNumber({
          tableName: &quot;event_query_info&quot;,
          eventName: this.eventName,
          contractAddress: this.contractCfg.address,
          networkId: network.chainId.toString(),
          lastQueryBlockNumber: toBlockNumber,
        });
      } catch (e) {
        console.log(e);
      } finally {
        this.mutexLock = false;
      }
    }, this.pollIntervalMs);
  }

  websocketStatus() {
    return this.wsProvider?.websocket?.readyState === 1
      ? &quot;Connected&quot;
      : &quot;Disconnected&quot;;
  }
};</code></pre>
<h2 id="서버-실행">서버 실행</h2>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/a012f1d0-0e78-45aa-b02c-d679132c8138/image.png" alt=""></p>
<h2 id="event-발생-시-즉시-db에-row-생성">Event 발생 시 즉시 DB에 row 생성</h2>
<p>새로운 row 확인
<img src="https://velog.velcdn.com/images/na_treeeum/post/f6b5f02f-d186-4e0e-8b4a-1e52ea770914/image.png" alt=""></p>
<p>새로운 row가 insert된 내용의 log
<img src="https://velog.velcdn.com/images/na_treeeum/post/167e3daa-2abf-4fc6-8fab-20d492a422bd/image.png" alt=""></p>
<h2 id="polling-으로-tx-confirm">Polling 으로 Tx Confirm</h2>
<p>confirmed status 가 0-&gt;1 로 변경됨
<img src="https://velog.velcdn.com/images/na_treeeum/post/5092dc07-1d2d-43b0-86cc-28f7dc0af602/image.png" alt=""></p>
<p>server - confirmed event log
<img src="https://velog.velcdn.com/images/na_treeeum/post/ca40fffe-568d-4f9c-b101-b62aa91919cb/image.png" alt=""></p>
<p>Websocket Ping 으로 health check 
<img src="https://velog.velcdn.com/images/na_treeeum/post/e379e9df-23d9-47fa-b055-805d60904e8d/image.png" alt=""></p>
<p>Polling 할 block range 를 기록하기 위한 테이블
<img src="https://velog.velcdn.com/images/na_treeeum/post/3c96f3a1-9932-4d5d-8af8-d28afde246f9/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Multicall Contract 취약점 사례]]></title>
            <link>https://velog.io/@na_treeeum/Multicall-Contract-%EC%B7%A8%EC%95%BD%EC%A0%90-%EC%82%AC%EB%A1%80</link>
            <guid>https://velog.io/@na_treeeum/Multicall-Contract-%EC%B7%A8%EC%95%BD%EC%A0%90-%EC%82%AC%EB%A1%80</guid>
            <pubDate>Thu, 08 Jan 2026 07:01:44 GMT</pubDate>
            <description><![CDATA[<h1 id="uniswap-multicall">Uniswap Multicall</h1>
<p>Uniswap Multicall 컨트랙트의 코드를 살펴보면 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/6c77b27b-9a67-453c-a1c7-3edb245f5259/image.png" alt=""></p>
<p><em>Call</em> 이라는 data struct를 배열로 받고, target.call() 을 통해 배열로 전달된 callData 를 target contract에 실행시킵니다.</p>
<p>여기서 해당 tx의 실행 주체는 uniswap multicall contract가 됩니다.</p>
<p>이를 악용한 공격 수법을 알아보겠습니다.</p>
<p>공격자는 피해자에게 다음과 같은 Approve 트랜잭션에 서명하도록 합니다.</p>
<ul>
<li>Spender : Uniswap Multicall Contract</li>
<li>Amount :  uint256 최대치</li>
</ul>
<p>피해자의 입장에서는 수많은 트랜잭션이 일어나고, <strong>유명한 Uniswap 컨트랙트</strong>가 spender 이므로 별다른 의심없이 해당 트랜잭션에 서명하게 됩니다.</p>
<p>이 과정에서 Uniswap Contract는 피해자의 지갑이 보유하고 있는 토큰에 대한 제어권을 갖게 됩니다.</p>
<p>Uniswap Contract가 피해자의 토큰에 대한 제어권을 갖게 되면, 공격자는 Uniswap Contract에 대해 transferFrom 메소드를 다음과같이 호출합니다.</p>
<ul>
<li>sender : 피해자 지갑주소</li>
<li>recipient : 공격자 지갑 주소</li>
<li>amount : 수량</li>
</ul>
<blockquote>
<p>여기서 <strong>transferFrom</strong> 은 다음과 같이 작동합니다.
<code>transferFrom(sender, recipient, amount);</code>
tx를 전송하는 주체(msg.sender)는 sender로 부터 allowance 를 취득해야하고,
allowance가 충분할 경우 sender의 지갑에서 recipient에게 amount만큼 전송합니다.</p>
</blockquote>
<p>이 과정을 도식화 하면 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/67dd0257-d551-44eb-aff9-b134ee27c2f8/image.png" alt=""></p>
<h1 id="gnosis-safe">Gnosis Safe</h1>
<p>비슷한 사례로 Gnosis safe wallet (multi sig wallet)을 통한 공격도 있었습니다.</p>
<p>공격의 순서는 다음과 같습니다</p>
<ol>
<li>공격자는 safe wallet(proxy)을 생성합니다</li>
<li>공격자는 피해자에게 safe wallet에 대한 approve를 서명하도록 유도합니다.</li>
<li>피해자가 approve tx에 서명을 하면, 공격자는 proxy 컨트랙트의 execTransaction 메소드를 호출</li>
<li>proxy 컨트랙트는 마스터 컨트랙트의 execTransaction을 delegateCall을 통해 호출</li>
</ol>
<p>이 과정을 도식화 하면 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/4e2a3607-0613-44a7-8141-1f9db59a004c/image.png" alt=""></p>
<h1 id="개발-시-주의할-점">개발 시 주의할 점</h1>
<ul>
<li>공개된 multicall contract 에 approve 를 수행해서는 절대 안됨</li>
<li>multicall 기능이 필요하다면, tx를 대신해서 수행하는 메소드에 권한을 추가하여(whitelisted) 제한적으로 사용</li>
</ul>
<p>참고 : <a href="https://research.checkpoint.com/2024/unveiling-the-scam-how-fraudsters-abuse-legitimate-blockchain-protocols-to-steal-your-cryptocurrency-wallet/">https://research.checkpoint.com/2024/unveiling-the-scam-how-fraudsters-abuse-legitimate-blockchain-protocols-to-steal-your-cryptocurrency-wallet/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Off-chain message Signing 검증하기 (EIP-712)]]></title>
            <link>https://velog.io/@na_treeeum/Off-chain-message-Signing-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0-EIP-712</link>
            <guid>https://velog.io/@na_treeeum/Off-chain-message-Signing-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0-EIP-712</guid>
            <pubDate>Wed, 07 Jan 2026 07:43:22 GMT</pubDate>
            <description><![CDATA[<p><a href="https://eips.ethereum.org/EIPS/eip-712">https://eips.ethereum.org/EIPS/eip-712</a>
<img src="https://velog.velcdn.com/images/na_treeeum/post/fb5cfc76-5632-4382-b256-a1882eb2b00a/image.png" alt=""></p>
<h1 id="eip-712">EIP-712</h1>
<p>EIP-712 는 트랜젝션 데이터가 아닌 모든 데이터를 <strong>어떤 구조로 서명할 지</strong>에 대해 정의한 제안이며 사용자에게 서명할 메세지를 <strong>사람이 읽을 수 있는 구조로 보여주고</strong>, 그 구조에 따라 서명하여 <strong>신뢰성</strong>과 <strong>보안성</strong>을 높이는데 초점을 둡니다.</p>
<h1 id="핵심-요소">핵심 요소</h1>
<h2 id="type_hash">TYPE_HASH</h2>
<p>TYPE_HASH 는 <code>기능이름(parameters)</code> 로 된 문자열을 keccak256 으로 해싱 한 값입니다.</p>
<p>EIP712Domain 에 대한 TYPE_HASH 는 다음과 같습니다.</p>
<pre><code class="language-javascript">keccak256(&quot;EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)&quot;);</code></pre>
<h2 id="domainseperator">domainSeperator</h2>
<blockquote>
<p>어떤 네트워크의 어떤 컨트랙트에서 사용되는(검증되는) 서명인지 구분하기 위한 구분자.</p>
</blockquote>
<p>DomainSeperator 의 구성은 다음과 같습니다.</p>
<ul>
<li><code>string name</code> : 컨트랙트(DApp 혹은 Protocol)의 이름</li>
<li><code>string version</code> : 컨트랙트 버전</li>
<li><code>uint256 chainId</code> : 컨트랙트가 배포된 네트워크 아이디</li>
<li><code>address verifyingContract</code> : 서명을 검증하는 컨트랙트의 주소(배포된 컨트랙트 주소)</li>
</ul>
<p>Openzeppelin 에서 DomainSeperator를 생성하는 코드는 다음과 같습니다. (<a href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/EIP712.sol#L90">EIP712.sol#L90</a>)</p>
<pre><code class="language-javascript">function _buildDomainSeparator() private view returns (bytes32) {
    return keccak256(abi.encode(TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this)));
}</code></pre>
<h2 id="encodetype">encodeType</h2>
<blockquote>
<p>Parameter 로 전달되는 인자의 type을 정의한 정의서. encodeType을 통해 해당 함수에 대한 TYPE_HASH 가 생성됩니다.</p>
</blockquote>
<p>예시</p>
<pre><code class="language-javascript">const type = {
    transfer: [
      {name:&quot;to&quot;, type:&quot;address&quot;},
      {name:&quot;amount&quot;, type:&quot;uint256&quot;},
    ]
};</code></pre>
<h2 id="encodedata">encodeData</h2>
<blockquote>
<p>Parameter 로 전달되는 인자의 값</p>
</blockquote>
<p>예시</p>
<pre><code class="language-javascript">const data = {
    to : wallet.address,
      amount : 1000000000000000000n
};</code></pre>
<h1 id="검증-과정">검증 과정</h1>
<ol>
<li><p>signer는 private key 를 사용하여 <strong>메세지를 서명</strong>합니다.
 서명된 메세지는 다음 데이터들로부터 생성됩니다.</p>
<ul>
<li>domainSeperator</li>
<li>encodeType</li>
<li>encodeData</li>
</ul>
</li>
<li><p>서명된 메세지(signature)는 <strong>v, r, s 로 분리</strong>됩니다.</p>
<ul>
<li>v, r, s 는 ECDSA(타원곡선을 이용한 전자서명알고리즘)을 위한 값</li>
</ul>
</li>
<li><p>함수 호출자는 서명된 <strong>데이터(v,r,s) 와 arguments 를 함께 전달</strong>합니다.</p>
</li>
<li><p>함수에서는 전달받는 arguments 와 v,r,s 값으로 <code>ecrecover</code> 함수를 통해 해당 서명을 검증합니다.</p>
<p> a. 서명에 사용된 값과 arguments 값이 일치하면 <code>ecrecover</code> 는 <strong>서명자의 주소를 반환</strong>합니다.</p>
<p> b. 서명에 사용된 값과 arguments 값이 일치하지 않을경우 <code>ecrecover</code> 는 <strong>엉뚱한 주소를 반환</strong>합니다.</p>
</li>
</ol>
<pre><code>예시
1. owner 가 to:0x1234, amount:100 값에 대해 서명하여 Signature 생성
2. 함수호출자가 to:0x1234, amount:9000000 라는 값을 Owner 의 Signature 와 함께 호출
-&gt; 검증 과정에서 ecrecover 가 엉뚱한 주소를 반환하여 검증 실패

1. owner 가 to:0x1234, amount:100 값에 대해 서명하여 Signature 생성
2. 함수호출자가 to:0x1234, amount:100 라는 값을 Owner 의 Signature 와 함께 호출
-&gt; 검증 과정에서 ecrecover 가 owner의 주소를 반환하여 검증 성공
</code></pre><p>컨트랙트 코드를 함께 보며 이해도를 높이면 좋을것 같습니다.
다음은 설명을 위한 컨트랙트의 함수 코드입니다.</p>
<pre><code class="language-javascript">// mintWithApproval() 은 owner의 private key 로 sign 된 메세지(signature)가 정상일 때 민팅을 할 수 있는 함수입니다.
function mintWithApproval(
    address to,
    uint256 amount,
    uint8 v,
    bytes32 r,
    bytes32 s
) external {
    // 함수의 TYPE_HASH 와 전달받은 arguments 로 structHash 를 생성합니다.
    bytes32 structHash = keccak256(
        abi.encode(
            keccak256(&quot;mintWithApproval(address to,uint256 amount)&quot;), // TYPE_HASH
            to, // arg1
            amount // arg2
        )
    );

    bytes32 hash = _hashTypedDataV4(structHash);

    // 서명된 데이터(v,r,s) 에 사용된 데이터와 arguments(to, amount) 값이 같다면 ecrecover 는 서명한 지갑의 주소를 반환합니다.
    address signer = ecrecover(hash, v, r, s);


    if (signer != owner) {
        revert(&quot;Invalid signature&quot;);
    }

    _mint(to, amount);
}</code></pre>
<h1 id="eip-712-를-따르는-서명-생성하기">EIP-712 를 따르는 서명 생성하기</h1>
<p>EIP-712 를 따르는 서명을 만드는 방법은 다양하겠지만 이 글에서는 ehters.js 라이브러리로 서명을 생성하는 방법만 다뤄보겠습니다.</p>
<h2 id="ethersjs">ethers.js</h2>
<h3 id="도메인-지정">도메인 지정</h3>
<p>domainSeperator 생성을 위한 도메인을 지정해줍니다.</p>
<pre><code class="language-javascript">const domain = {
    name: &quot;Test&quot;,
      version: &quot;1&quot;,
      chainId: 1,
      verifyingContract: &quot;0x1234contractAddress&quot;
};</code></pre>
<h3 id="type-지정">Type 지정</h3>
<p>parameter 로 전달할 인자들의 타입을 지정해줍니다.</p>
<pre><code class="language-javascript">const types = {
    mintWithApproval:[
      {name: &quot;to&quot;, type: &quot;address&quot;},
      {name: &quot;amount&quot;, type: &quot;uint256&quot;},
    ]
};</code></pre>
<h3 id="data-지정">Data 지정</h3>
<p>parameter 로 전달할 인자들의 값을 지정해줍니다.</p>
<pre><code class="language-javascript">const data = {
    to: &quot;0x9876walletAddress&quot;,
      amount: 1000000000000000000n
};</code></pre>
<h3 id="signature-생성--vrs-로-분리">Signature 생성 + vrs 로 분리</h3>
<p>ethers 의 Wallet instance 로 서명을 생성합니다.</p>
<pre><code class="language-javascript">const owner = ethers.Wallet.createRandom();

const signature = owner.signTypedData(domain, types, data);

const {v, r, s} = ethers.Signature.from(signature);</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Easy Rust Korean 051 ~ 060]]></title>
            <link>https://velog.io/@na_treeeum/Easy-Rust-Korean-051-060</link>
            <guid>https://velog.io/@na_treeeum/Easy-Rust-Korean-051-060</guid>
            <pubDate>Sun, 30 Nov 2025 12:36:43 GMT</pubDate>
            <description><![CDATA[<h1 id="051">051</h1>
<p>HashMap : key - value</p>
<p>key와 value 는 어떤 타입도 될 수 있음</p>
<p><code>(key)String -&gt; (value)Vec&lt;String&gt;</code>
의 hashMap 을 만들고 싶다면 다음과 같이 사용</p>
<pre><code class="language-rust">// HashMap&lt;String, Vec&lt;String&gt;&gt;</code></pre>
<p>hashMap example</p>
<pre><code class="language-rust">use std::collections::HashMap;

struct City{
    name: String,
    population: HashMap&lt;u32, u32&gt; // year -&gt; population
}

fn main(){
    let mut tallin = City {
        name : &quot;Tallin&quot;.to_string(),
        population: HashMap::new()
    };

    tallin.population.insert(1372, 3_250);
    tallin.population.insert(1851, 24_000);
    tallin.population.insert(2020, 437_619);

    for(year, population) in tallin.population{
        println!(&quot;In the year {} the population was {}&quot;, year, population)
    }
}</code></pre>
<p>1372년, 1851년, 2020년의 순서로 나오지 않음. (순서는 완전 랜덤)</p>
<p>hashMap 은 순서를 정의할 수 없는 특징이 있음.</p>
<p>BTreeMap : ordering 이 가능한 HashMap</p>
<blockquote>
<p>order가 필요없다면 HashMap, 필요하다면 BTreeMap(상대적으로 느림)</p>
</blockquote>
<p>BTreeMap 예시</p>
<pre><code class="language-rust">use std::collections::BTreeMap;

struct City{
    name: String,
    population: BTreeMap&lt;u32, u32&gt; // year -&gt; population
}

fn main(){
    let mut tallin = City {
        name : &quot;Tallin&quot;.to_string(),
        population: BTreeMap::new()
    };

    tallin.population.insert(1372, 3_250);
    tallin.population.insert(1851, 24_000);
    tallin.population.insert(2020, 437_619);

    for(year, population) in tallin.population{
        println!(&quot;In the year {} the population was {}&quot;, year, population)
    }
}
// 1372년 -&gt; 1851년 -&gt; 2020년 순서가 보장됨</code></pre>
<h1 id="052">052</h1>
<p>HashMap 의 값을 불러오는 방법</p>
<p>(안전x 방법)</p>
<pre><code class="language-rust">use std::collections::HashMap;

fn main(){
    let canadian_cities = vec![&quot;Calgary&quot;, &quot;Vancouver&quot;, &quot;Gimli&quot;];
    let german_cities = vec![&quot;Karlsruhe&quot;, &quot;Bad Doberan&quot;, &quot;Bielefeld&quot;];

    let mut city_hashmap = HashMap::new();

    for city in canadian_cities{
        city_hashmap.insert(city, &quot;Canada&quot;);
    }

    for city in german_cities{
        city_hashmap.insert(city, &quot;Germany&quot;);
    }

    println!(&quot;{:?}&quot;, city_hashmap[&quot;Bielefeld&quot;]); // 정상 출력 (Bielefeld 라는 key가 존재함)

    println!(&quot;{:?}&quot;, city_hashmap[&quot;Bielefeld123&quot;]); // key 가 존재하지 않아서 오류 발생 
}</code></pre>
<p>(안전한 방법, .get 사용)
.get을 사용하면 &quot;None&quot;을 반환</p>
<pre><code class="language-rust">use std::collections::HashMap;

fn main(){
    let canadian_cities = vec![&quot;Calgary&quot;, &quot;Vancouver&quot;, &quot;Gimli&quot;];
    let german_cities = vec![&quot;Karlsruhe&quot;, &quot;Bad Doberan&quot;, &quot;Bielefeld&quot;];

    let mut city_hashmap = HashMap::new();

    for city in canadian_cities{
        city_hashmap.insert(city, &quot;Canada&quot;);
    }

    for city in german_cities{
        city_hashmap.insert(city, &quot;Germany&quot;);
    }

    println!(&quot;{:?}&quot;, city_hashmap.get(&quot;Bielefeld&quot;)); // Some(&quot;Germany&quot;) 출력

    println!(&quot;{:?}&quot;, city_hashmap.get(&quot;Bielefeld123&quot;)); // None 출력
}</code></pre>
<p>이미 값이 존재하는 key 에 대해 insert하게 되면 덮어쓰기가 됨.</p>
<pre><code class="language-rust">use std::collections::HashMap;

fn main(){
    let mut book_hashmap = HashMap::new();

    book_hashmap.insert(1, &quot;Book1&quot;);
    book_hashmap.insert(1, &quot;Book2&quot;);
    book_hashmap.insert(1, &quot;Book3&quot;);
    book_hashmap.insert(1, &quot;Book4&quot;);

    println!(&quot;{:?}&quot;, book_hashmap.get(&amp;1)); // hashmap 에게 소유권을 넘기지 않고 reference 만 제공하기 위해 &amp;1 사용
}</code></pre>
<p>is_none 활용</p>
<pre><code class="language-rust">use std::collections::HashMap;

fn main(){
    let mut book_hashmap = HashMap::new();

    book_hashmap.insert(1, &quot;Book1&quot;);

    if book_hashmap.get(&amp;1).is_none() {
        book_hashmap.insert(1, &quot;Book1&quot;);
    } else {
        println!(&quot;Already got a book&quot;);
    }
}</code></pre>
<ul>
<li>if let 활용</li>
</ul>
<pre><code class="language-rust">use std::collections::HashMap;

fn main(){
    let mut book_hashmap = HashMap::new();

    book_hashmap.insert(1, &quot;Book1&quot;);

    if let Some(book_name) = book_hashmap.get(&amp;1) {
        println!(&quot;Already got a book : {}&quot;, book_name);
    } else {
        book_hashmap.insert(1, &quot;Book1&quot;);
    }
}</code></pre>
<h1 id="053">053</h1>
<p>Entry Method</p>
<pre><code class="language-rust">use std::collections::HashMap;

fn main(){
    let book_collection = vec![
        &quot;book1&quot;,
        &quot;book2&quot;,
        &quot;book3&quot;,
        &quot;book4&quot;,
        &quot;book4&quot;,
    ];

    let mut book_hashmap = HashMap::new();

    for book in book_collection{
        book_hashmap.entry(book).or_insert(true);
    }

    for (book, true_or_false) in book_hashmap{
        println!(&quot;Do we have {}? : {}&quot;, book, true_or_false);
    }
}</code></pre>
<p>.entry() method</p>
<pre><code class="language-rust">pub fn entry(&amp;mut self, key: K) -&gt; Entry&lt;K, V&gt;

pub enum Entry&lt;&#39;a, K: &#39;a, V: &#39;a&gt; {
    Occupied(OccupiedEntry&lt;&#39;a, K, V&gt;),
    Vacant(VacantEntry&lt;&#39;a, K, V&gt;),
}</code></pre>
<p>Entry.or_insert() 는 &amp;&#39;a mut 을 반환하기 때문에 값을 바꿀 수 있음</p>
<pre><code class="language-rust">use std::collections::HashMap;

fn main(){
    let book_collection = vec![
        &quot;book1&quot;,
        &quot;book2&quot;,
        &quot;book3&quot;,
        &quot;book4&quot;,
        &quot;book4&quot;,
    ];

    let mut book_hashmap = HashMap::new();

    for book in book_collection{
        let number_of_books = book_hashmap.entry(book).or_insert(0); // number_of_books -&gt; &amp;ref
        *number_of_books += 1;
    }

    for (book, number) in book_hashmap{
        println!(&quot;{}: {}copies&quot;, book, number);
    }
}</code></pre>
<h1 id="054">054</h1>
<pre><code class="language-rust">use std::collections::HashMap;

fn main(){
    let data = vec![
        (&quot;male&quot;, 9),
        (&quot;female&quot;, 5),
        (&quot;male&quot;, 0),
        (&quot;female&quot;, 6),
        (&quot;female&quot;, 5),
        (&quot;male&quot;, 10),
    ];

    let mut survey_hash = HashMap::new();

    for item in data { // (&amp;str, i32)
        survey_hash.entry(item.0).or_insert(Vec::new()).push(item.1);
    }

    for (male_or_female, numbers) in survey_hash{
        println!(&quot;{:?}, {:?}&quot;, male_or_female, numbers)
    }
}</code></pre>
<p>HashSet</p>
<pre><code class="language-rust">use std::collections::HashSet;

fn main(){
    let many_numbers = vec![1,3,5,7,7,9];

    let mut number_hashset = HashSet::new();

    for number in many_numbers {
        number_hashset.insert(number);
    }

    let hashset_length = number_hashset.len(); // hashset length
    println!(&quot;There are {} unique numbers, so we are missing {}&quot;, hashset_length, 10-hashset_length);

    let mut missing_vec = vec![];
    for number in 0..10{
        if number_hashset.get(&amp;number).is_none() {
            missing_vec.push(number);
        }
    }

    print!(&quot;Missing numbers : &quot;);
    for number in missing_vec{
        print!(&quot;{} &quot;, number);
    }
}</code></pre>
<p>Set 에서도 Map과 동일하게 순서가 중요하다면 BTreeSet 사용!</p>
<h1 id="055">055</h1>
<p>binaryHeap : 아직 실제로 써본적은 없다고 함</p>
<pre><code class="language-rust">use std::collections::BinaryHeap;

fn show_remainder(input: &amp;BinaryHeap&lt;i32&gt;) -&gt; Vec&lt;i32&gt; {
    let mut remainder_vec = vec![];
    for number in input {
        remainder_vec.push(*number)
    }
    remainder_vec
}

fn main(){
    let many_numbers = vec![0, 5, 10, 15, 20, 25, 30];

    let mut my_heap = BinaryHeap::new();

    for number in many_numbers{
        my_heap.push(number);
    }

    while let Some(number) = my_heap.pop(){
        println!(&quot;Popped off {}. Remaining numbers are : {:?}&quot;, number, show_remainder(&amp;my_heap));
    }
}
/* Output
Popped off 30. Remaining numbers are : [25, 15, 20, 0, 10, 5]
Popped off 25. Remaining numbers are : [20, 15, 5, 0, 10]
Popped off 20. Remaining numbers are : [15, 10, 5, 0]
Popped off 15. Remaining numbers are : [10, 0, 5]
Popped off 10. Remaining numbers are : [5, 0]
Popped off 5. Remaining numbers are : [0]
Popped off 0. Remaining numbers are : []
*/</code></pre>
<p>-&gt; 첫 번째 요소는 가장 큰 값, 나머지 요소들의 순서는 일관되지 않음</p>
<p>Most popular Usage : Priority Queue</p>
<p>예시 2</p>
<pre><code class="language-rust">use std::collections::BinaryHeap;

fn main(){
    let mut jobs = BinaryHeap::new();

    jobs.push((100, &quot;Write back to email from the CEO&quot;));
    jobs.push((80, &quot;보고서 마감&quot;));
    jobs.push((5, &quot;Youtube 시청&quot;));
    jobs.push((70, &quot;동료들에게 감사인사 전하기&quot;));
    jobs.push((30, &quot;다음 고용계획 작성&quot;));

    while let Some(job) = jobs.pop(){
        println!(&quot;You need to: {}&quot;, job.1);
    }
}</code></pre>
<h1 id="056">056</h1>
<p>VecDeque</p>
<p>Vec의 경우</p>
<pre><code class="language-rust">fn main(){
    let my_vec = vec![8,9,10];
}</code></pre>
<p>.pop : 맨 마지막값을 뺌
.push : 맨 마지막에 값을 넣음
Vec::with_capacity(10) : reallocation 없이 빠르게 사용</p>
<p>.remove(idx) : idx 의 값을 빼고 나머지 값들의 idx 를 한자리씩 앞으로 이동시킴</p>
<p>다음 상황에서 연산량이 많아서 오래걸리는 처리를 VecDeque 를 사용해서 빠르게 처리할 수 있음</p>
<pre><code class="language-rust">// 오래 걸리는 코드
fn main(){
    let mut my_vec = vec![0; 600_000]; // 0이 60만개있는 vec
    for i in 0..600_000 {
        my_vec.remove(0);
    }
}

// VecDeque 사용
use std::collections::VecDeque;

fn main(){
    let mut my_vec = VecDeque::from(vec![0; 600_000]);
    for i in 0..600_000{
        my_vec.pop_front();
    }
}</code></pre>
<p>Ring buffer 를 이용해서...........빠름</p>
<h1 id="057">057</h1>
<p>?</p>
<p><code>use std::num::ParseIntError</code>
&quot;8&quot; 같은 값을 int로 변환, &quot;8eo&quot;같은 값은 에러 반환</p>
<pre><code class="language-rust">use std::num::ParseIntError;

fn parse_str(input:&amp;str) -&gt; Result&lt;i32, ParseIntError&gt;{
    let parsed_number = input.parse::&lt;i32&gt;()?; // 성공하면 넘어감 실패하면 return error
    Ok(parsed_number)
}

fn main(){
    for item in vec![&quot;seven&quot;, &quot;8&quot;, &quot;9.0&quot;, &quot;nice&quot;,&quot;6060&quot;]{
        let parsed = parse_str(item);
        println!(&quot;{:?}&quot;, parsed);
    }
}

/*
Err(ParseIntError { kind: InvalidDigit })
Ok(8)
Err(ParseIntError { kind: InvalidDigit })
Err(ParseIntError { kind: InvalidDigit })
Ok(6060)
*/</code></pre>
<h1 id="058">058</h1>
<p><a href="https://play.rust-lang.org/">https://play.rust-lang.org/</a>
에서 monaco editor 지원 시작
-&gt; 인텔리센스(자동완성) 가능</p>
<p>format Strings</p>
<p>기존에는 다음과같이 써야만 했다면
<code>println!(&quot;Books\n- {:?}\n- {:?}, book1, book2)</code></p>
<p>업데이트 이후에는 다음과같이 쓸 수 있게 됨
<code>println!(&quot;Books\n- {book1:?}\n- {book2:?})</code></p>
<pre><code class="language-rust">#[derive(Debug)]
struct Book{
    title: String,
    year: u16
}

fn main(){
    let book1 = Book{
        title : &quot;Some title&quot;.to_string(),
        year: 1919
    };
    let book2 = Book{
        title : &quot;Book2&quot;.to_string(),
        year: 2020
    };

    println!(&quot;Books\n{book1:?}\n{book2:?}&quot;);

    /* 아직 struct 안의 property 는 지원안함 */
    // println!(&quot;My book name : {book1.title});
}</code></pre>
<h1 id="059-60">059, 60</h1>
<p>traits (다른 언어의 interface와 비슷)</p>
<pre><code class="language-rust">struct Animal {
    name : String
}

trait Canine {
    fn bark(&amp;self){
        println!(&quot;Woof Woof!&quot;);
    }
    fn run(&amp;self){
        println!(&quot;I am running!&quot;);
    }
}

impl Canine for Animal{
    // 덮어쓰기 가능
    fn bark(&amp;self){
        println!(&quot;멍멍!&quot;);
    }
}

fn main(){
    let my_animal = Animal{
        name : &quot;Mr. Mantle&quot;.to_string()
    };

    my_animal.bark();
    my_animal.run();

}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Easy Rust Korean 031 ~ 040
]]></title>
            <link>https://velog.io/@na_treeeum/Easy-Rust-Korean-031-040</link>
            <guid>https://velog.io/@na_treeeum/Easy-Rust-Korean-031-040</guid>
            <pubDate>Sun, 16 Nov 2025 13:31:19 GMT</pubDate>
            <description><![CDATA[<h1 id="031">031</h1>
<p>match 활용!</p>
<pre><code class="language-rust">fn match_colours(rgb: (u32, u32, u32)){
    match rgb{
        (r, _, _) if r &lt; 10 =&gt; println!(&quot;Not much red!&quot;),
        (_, g, _) if g &lt; 10 =&gt; println!(&quot;Not much green!&quot;),
        (_, _, b) if b &lt; 10 =&gt; println!(&quot;Not much blue!&quot;),
        _ =&gt; println!(&quot;Every colour has at least 10!&quot;)
    }
}

fn main(){
    let first = (200, 0, 0);
    let second = (50, 50, 50);
    let third = (200, 50, 0);

    match_colours(first);
    match_colours(second);
    match_colours(third);
}</code></pre>
<p>다음과 같이 여러 type을 혼용해서 사용할 수 없음</p>
<pre><code class="language-rust">fn main(){
    let my_number = 10;
    let some_var = match my_number {
        10 =&gt; 8,
        _ =&gt; &quot;Not ten&quot;
    };
}</code></pre>
<p>if 를 사용해도 안되는건 마찬가지</p>
<pre><code class="language-rust">fn main(){
    let my_number = 8;
    let some_var = if my_number == 10 {8} else {
        &quot;something else&quot;
    };
}</code></pre>
<p>return 하는 값의 type을 맞추면 가능!</p>
<pre><code class="language-rust">fn main(){
    let my_number = 10;
    let some_var = if my_number == 10 {&quot;ten&quot;} else {
        &quot;else&quot;
    };
    println!(&quot;{}&quot;, some_var);
}</code></pre>
<p>다음과 같이도 사용 가능</p>
<pre><code class="language-rust">fn match_number(input: i32){
    match input{
        number @ 0..=10 =&gt; println!(&quot;It&#39;s between 0 and 10. Number : {}&quot;, number),
        _ =&gt; println!(&quot;It&#39;s greater than ten&quot;)
    }
}

fn main(){
    match_number(10);
    match_number(50);
}</code></pre>
<h1 id="032">032</h1>
<p>Struct!
(나중에 비슷한 enum 도 있음)</p>
<p>rust에는 3가지 struct가 있음</p>
<pre><code class="language-rust">// unit struct ( 아무것도 없는 구조체 )
struct FileDirectory;

fn main(){
    let x = FileDirectory;
    println!(&quot;The size of struct is {}&quot;, std::mem::size_of_val(&amp;x)); // size : 0
}</code></pre>
<pre><code class="language-rust">// tuple struct
struct Colour(u8, u8, u8);

fn main(){
    let my_colour = Colour(20, 50, 100);
    println!(&quot;The second colour is {}&quot;, my_colour.1); 
}</code></pre>
<pre><code class="language-rust">// named struct : 제일 많이 사용됨
struct Country{
    population: u32,
    capital : String,
    leader_name : String
}

fn main(){
    let canada = Country{
        population : 35_000_000,
        capital: &quot;Ottawa&quot;.to_string(),
        leader_name: &quot;Justin Trudeau&quot;.to_string()
    };

    println!(&quot;The population is {}&quot;, canada.population);
}</code></pre>
<h1 id="033">033</h1>
<p>Struct Size</p>
<pre><code class="language-rust">struct Country {
    population: u32,
    capital: String,
    leader_name: String
}

fn main(){
    let population = 35_000_000;
    let capital = &quot;Ottawa&quot;.to_string();
    let leader_name = &quot;Justin Trudeau&quot;.to_string();

    let my_country = Country {
        population,
        capital,
        leader_name
    };
}</code></pre>
<p>-&gt; js와 비슷한 기능!</p>
<p><code>use std::mem::size_of_val</code> 을 사용해서 struct의 사이즈를 볼 수 있음</p>
<pre><code class="language-rust">use std::mem::size_of_val;

struct Country {
    population: u32,
    capital: String,
    leader_name: String
}

fn main(){
    let population = 35_000_000;
    let capital = &quot;Ottawa&quot;.to_string();
    let leader_name = &quot;Justin Trudeau&quot;.to_string();

    let my_country = Country {
        population,
        capital,
        leader_name
    };

    println!(&quot;Country is {} bytes in size&quot;, size_of_val(&amp;my_country));
}</code></pre>
<p>Alignment</p>
<pre><code class="language-rust">use std::mem::size_of_val;

struct Numbers{
    one: u8,
    two: u8,
    three: u8
}

fn main(){
    let my_numbers = Numbers{
        one : 1,
        two : 2,
        three : 3
    };

    println!(&quot;Size of struct : {}&quot;, size_of_val(&amp;my_numbers));
} // -&gt; size : 3 (bytes)</code></pre>
<pre><code class="language-rust">use std::mem::size_of_val;

struct Numbers{
    one: u8,
    two: u8,
    three: u8,
    four: u32
}

fn main(){
    let my_numbers = Numbers{
        one : 1,
        two : 2,
        three : 3,
        four: 4
    };

    println!(&quot;Size of struct : {}&quot;, size_of_val(&amp;my_numbers));
} // -&gt; size : 8 (bytes)</code></pre>
<p>왜 size 가 7 이 아닌 8이냐면 러스트가 바이트수를 맞추기 위해........align하는...부분..</p>
<h1 id="034">034</h1>
<p>ENUM</p>
<p>struct 는 and 의 성격
enum 은 or 의 성격</p>
<pre><code class="language-rust">enum ThingsInTheSky{
    Sun, // compiler 가 보기에 0
    Stars // compiler 가 보기에 1
}

fn create_sky_state(time: i32) -&gt; ThingsInTheSky{
    match time{
        6..=18 =&gt; ThingsInTheSky::Sun,
        _ =&gt; ThingsInTheSky::Stars
    }
}

fn check_sky_state(state: &amp;ThingsInTheSky){
    match state{
        ThingsInTheSky::Sun =&gt; println!(&quot;I can see the sun&quot;),
        ThingsInTheSky::Stars =&gt; println!(&quot;I can see the stars&quot;),
    }
}

fn main(){
    let time = 8;
    let sky_state = create_sky_state(time);

    check_sky_state(&amp;sky_state);
}</code></pre>
<h1 id="035">035</h1>
<p>ENUM2 : 간편하게 사용하기</p>
<pre><code class="language-rust">enum Mood{
    Happy,
    Sleepy,
    NotBad,
    Angry
}

fn match_mood(mood: &amp;Mood) -&gt; i32{
    let happiness_level = match mood {
        Mood::Happy =&gt; 10,
        Mood::Sleepy =&gt; 6,
        Mood::NotBad =&gt; 7,
        Mood::Angry =&gt; 2
    };
    happiness_level
}

fn main(){
    let my_mood = Mood::NotBad;
    let happiness_level = match_mood(&amp;my_mood);
    println!(&quot;Out of 1 to 10, my happiness is {}&quot;, happiness_level);
}</code></pre>
<p>이거를 <code>use</code>를 사용해서 쉽게 쓸 수 있음</p>
<pre><code class="language-rust">enum Mood{
    Happy,
    Sleepy,
    NotBad,
    Angry
}

fn match_mood(mood: &amp;Mood) -&gt; i32{
    use Mood::*;
    let happiness_level = match mood {
        Happy =&gt; 10, // Mood:: 안써도 됨 
        Sleepy =&gt; 6, // Mood:: 안써도 됨 
        NotBad =&gt; 7, // Mood:: 안써도 됨 
        Angry =&gt; 2 // Mood:: 안써도 됨 
    };
    happiness_level
}

fn main(){
    let my_mood = Mood::NotBad;
    let happiness_level = match_mood(&amp;my_mood);
    println!(&quot;Out of 1 to 10, my happiness is {}&quot;, happiness_level);
}</code></pre>
<p>enum의 인덱스(?)를 보려면 </p>
<pre><code class="language-rust">enum Season{
    Spring, // 0
    Summer, // 1
    Autumn, // 2
    Winter //3
}

fn main(){
    use Season::*;

    let four_seasons = vec![Spring, Summer, Autumn, Winter]; // Vec&lt;Season&gt;
    for season in four_seasons {
        println!(&quot;The number is: {}&quot;, season as u32);
    }
}</code></pre>
<h1 id="036">036</h1>
<p>ENUM3</p>
<pre><code class="language-rust">enum Star{
    BrownDwarf = 10,
    RedDwarf = 50,
    YellowStar = 100,
    RedGiant = 1000,
    DeadStar 
}

fn main(){
    use Star::*;

    let starvec = vec![BrownDwarf, RedDwarf, YellowStar, RedGiant, DeadStar];

    for star in starvec{
        match star as u32{
        size if size &lt;= 80 =&gt; println!(&quot;Not the biggest star: {}&quot;, size),
        size if size &gt;= 80 =&gt; println!(&quot;Pretty big star: {}&quot;, size),
        _ =&gt; println!(&quot;Some other star&quot;)
        }
    };

    println!(&quot;What about DeadStar? It is: {}&quot;, DeadStar as u32); // 제일 마지막 숫자 + 1
}</code></pre>
<p>enum 이 값을 가지게 할수있음</p>
<pre><code class="language-rust">enum Number{
    U32(u32),
    I32(i32)
}

fn get_number(input: i32) -&gt; Number {
    let number = match input.is_positive(){
        true =&gt; Number::U32(input as u32),
        false =&gt; Number::I32(input)
    };
    number
}

fn main(){
    let my_vec = vec![get_number(-800), get_number(8)];

    for item in my_vec{
        match item {
            Number::U32(number) =&gt; println!(&quot;It&#39;s a u32 with the value : {}&quot;, number),
            Number::I32(number) =&gt; println!(&quot;It&#39;s a i32 with the value : {}&quot;, number),
        }
    }
}</code></pre>
<h1 id="037">037</h1>
<p>Loops</p>
<p>기본적인 loop</p>
<pre><code class="language-rust">// rust 스럽지 않은 방법!
fn main(){
    let mut counter = 0;
    loop{
        counter += 1;
        println!(&quot;The counter is: {}&quot;, counter);
        if counter == 5 {
            break;
        }
    }
}</code></pre>
<p>loop에 이름을 지을 수 있음! (tick 이라고 함)</p>
<pre><code class="language-rust">fn main(){
    let mut counter = 0;
    let mut counter2 = 0;

    &#39;first_loop: loop {
        counter += 1;
        println!(&quot;The counter is now: {}&quot;, counter);
        if counter &gt; 9 {
            println!(&quot;Now entering the second loop&quot;);

            &#39;second_loop: loop{
                println!(&quot;The second counter is: {}&quot;, counter2);
                counter2 += 1;
                if counter2 == 3 {
                    break &#39;first_loop;
                }
            }
        }
    }
}    </code></pre>
<h1 id="038">038</h1>
<p>counter.. 만들어서 하면 rust 적이지 않은 loop!</p>
<p>그럼 어케?</p>
<pre><code class="language-rust">fn main(){
    let mut counter = 0;
    while counter != 5 {
        counter += 1;
        println!(&quot;The counter is now: {}&quot;, counter);
    }
}</code></pre>
<p>while loop 도 괜찮긴 하지만 제일 많이 사용하는건 for loop</p>
<pre><code class="language-rust">fn main(){
    for number in 0..3 { // 0..3 -&gt; exclusive range!
        println!(&quot;The number is {}&quot;, number);
    }

    for number in 0..=3 { // 0..3 -&gt; inclusive range!
        println!(&quot;The number is {}&quot;, number);
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Easy Rust Korean 021 ~ 030]]></title>
            <link>https://velog.io/@na_treeeum/Easy-Rust-Korean-021-030</link>
            <guid>https://velog.io/@na_treeeum/Easy-Rust-Korean-021-030</guid>
            <pubDate>Sun, 09 Nov 2025 13:35:16 GMT</pubDate>
            <description><![CDATA[<h1 id="021">021</h1>
<p>mutable references and mut in function</p>
<pre><code class="language-rust">fn add_is_great(country_name: &amp;mut String){
    country_name.push_str(&quot; is great!&quot;);
    println!(&quot;Now it says: {}&quot;, country_name);
}

fn main(){
    let mut my_country = &quot;캐나다&quot;.to_string();

    /*
    add_is_great(my_country); // by value
    add_is_great(&amp;my_country); // by reference
    */
    add_is_great(&amp;mut my_country); // by mutable reference
    add_is_great(&amp;mut my_country);
}

/* Output
Now it says: 캐나다 is great!
Now it says: 캐나다 is great! is great!
*/</code></pre>
<p>비슷하지만 완전 다른 개념!</p>
<pre><code class="language-rust">fn add_is_great(mut country_name: String){ // take by value, declare as mutable
    country_name.push_str(&quot; is great!&quot;);
    println!(&quot;Now it says: {}&quot;, country_name);
}

fn main(){
    let my_country = &quot;대한민국&quot;.to_string();
    add_is_great(my_country); // 여기서 소유권이 넘어감!
}</code></pre>
<h1 id="022">022</h1>
<p>Ownership and copy types</p>
<pre><code class="language-rust">fn prints_number(number: i32){
    println!(&quot;{}&quot;, number);
}

fn prints_string(input: String){
    println!(&quot;{}&quot;, input);
}

fn main(){
    let my_number = 8;
    prints_number(my_number); // number 는 copy type(copy 하기 쉬움)이라 소유권 신경쓸 필요 없음!
    prints_number(my_number);

    let my_country = &quot;Austria&quot;.to_string();
    prints_string(my_country); // 여기서 소유권 넘어감!
    prints_string(my_country); // 그래서 여기서 오류남!

    /*
        오류가 안나게 하려면 .clone 메서드를 사용
        prints_string(my_country.clone());

        clone은 빠르게 컴파일을 하고싶을 때 많이 사용함!
    */

}</code></pre>
<h1 id="023">023</h1>
<p>uninitialized variable &amp; loop</p>
<pre><code class="language-rust">fn main(){
    /*
    let my_number; // 는 불가능!
    */

    /*
    let my_number: i8; // 는 사용은 가능!

    println!(&quot;{}&quot;, my_number); // 하지만 여기서 에러남!! value가 initialized 되지 않았기 때문
    */

}</code></pre>
<pre><code class="language-rust">fn loop_then_return(mut counter: i32) -&gt; i32 {
    loop {
        counter += 1;
        if counter % 50 == 0 {
            break;
        }
    }
    counter
}

fn main(){
    let my_number;

    {
        let x = loop_then_return(43);
        my_number = x
    }

    println!(&quot;{}&quot;, my_number); // my_number가 위에서 initialized 됐기 때문에 오류가 발생하지 않음
}</code></pre>
<p>하지만 이 패턴은 잘 안사용!
기존의 코드(C 등등..)을 사용하던 개발자들의 편의를 위해 rust에서 허용하는 기능!</p>
<h1 id="024">024</h1>
<p>Arrays</p>
<pre><code class="language-rust">fn main(){
    let array = [&quot;one&quot;, &quot;two&quot;]; // type -&gt; [&amp;str; 2]
    let array2 = [&quot;one&quot;, &quot;two&quot;, &quot;Five&quot;]; // type -&gt; [&amp;str; 3]]

    // array 와 array2 는 다른 타입이라 비교 자체가 불가능!!
    // array 는 추가할 수 없고, 삭제할 수 없고.. 
}</code></pre>
<pre><code class="language-rust">fn main(){
    let array = [&quot;one&quot;, &quot;two&quot;];

    //type을 보기 위한 꼼수 : 존재하지 않는 method 사용 ㅋㅋ
      array.zxcvzxcvasdfasdfasd(); // &gt;&gt; 여기서 [type]에 대한 method 가 없습니다!  라고 에러를 뱉음
}</code></pre>
<p>indexing 은 다른 언어와 비슷!</p>
<pre><code class="language-rust">fn main(){
    let array = [&quot;one&quot;, &quot;two&quot;];

    println!(&quot;{}&quot;, array[0]); // &quot;one&quot; 출력

    /*
        없는 index 출력하면 오류남 !
    */
}</code></pre>
<h1 id="025">025</h1>
<p>Slices</p>
<pre><code class="language-rust">fn main(){
    let seasons = [&quot;봄&quot;, &quot;여름&quot;, &quot;가을&quot;, &quot;겨울&quot;];
    println!(&quot;{:?}&quot;, seasons[0..2]); // 에러 발생!! dynamically sized type 이라 사이즈를 알 수 없기 때문에 오류가 발생함
}</code></pre>
<pre><code class="language-rust">fn main(){
    let seasons = [&quot;봄&quot;, &quot;여름&quot;, &quot;가을&quot;, &quot;겨울&quot;];
    println!(&quot;{:?}&quot;, &amp;seasons[0..2]); // reference 를 사용해서 넘길 수 있음!

    /*
        &amp;seasons[0..=2] // up to and including
        &amp;seasons[..] // all
        &amp;seasons[3..] // from 4th index
        &amp;seasons[..4] // up to 5th index
    */
}</code></pre>
<h1 id="026">026</h1>
<p>&quot;누구나 좋아하는&quot; Vecs</p>
<p>= vector</p>
<pre><code class="language-rust">fn main(){
    let my_string = String::new(); // string 선언!
    let mut my_vec: Vec&lt;String&gt; = Vec::new(); 
}</code></pre>
<pre><code class="language-rust">fn main(){
    let name1 = String::from(&quot;Windy); 
    let name2 = String::from(&quot;Gomesy&quot;);

    let mut my_vec = Vec::new();
    // Vec::from() 도 사용가능
    /*
        vec![] 로 더 쉽게 사용가능!

        예를 들어
        let my_vec = vec![name1, name2];
    */

    my_vec.push(name1);
    my_vec.push(name2);

    println!(&quot;My cats are {:?}&quot;, my_vec);
}</code></pre>
<h1 id="027">027</h1>
<p>Traits = 초능력</p>
<pre><code class="language-rust">fn main(){
    let my_name = String::from(&quot;Dave Macleod&quot;); // from 사용!
    let my_city: String = &quot;Seoul&quot;.into(); // into 사용!! &quot;Seoul&quot;은 String이 아닌 &amp;str 인데 String 변수에 맞게 변환!?
}</code></pre>
<pre><code class="language-rust">fn main(){
    let my_vec = Vec::from([8,9,10]); // type : [i32; 3]
}</code></pre>
<h1 id="028">028</h1>
<p>tuples : 여러가지 타입을 한 가지 타입으로 표현하고 싶을 때 사용!</p>
<pre><code class="language-rust">fn main(){
    let my_tuple = (8, &quot;Dave MacLeod&quot;, vec![8,9,10]);

    my_tuple.asdfasdfasdf(); // 없는 method 로 컴파일러에게 타입 물어보기 ㅋㅋ 
    // -&gt; no method named `asdfasdfasdf` found for tuple `({integer}, &amp;str, Vec&lt;{integer}&gt;)`
    // type : tuple
}</code></pre>
<p>tuple indexing</p>
<pre><code class="language-rust">/*
    Array 와 Vector 에서 indexing 은 array[0], vec[0] 이런식으로 대괄호를 사용해서 인덱싱
    tuple은 대괄호를 사용하지 않고 .0 .1 이런식으로 사용
*/

fn main(){
    let random_tuple = (&quot;Here is a name&quot;, 8, vec![&#39;a&#39;], &#39;b&#39;, [8,9,10], 7.7);

    println!(&quot;Inside the tuple&quot;);
    println!(&quot;0 : {:?}&quot;, random_tuple.0);
    println!(&quot;1 : {:?}&quot;, random_tuple.1);
    println!(&quot;2 : {:?}&quot;, random_tuple.2);
    println!(&quot;3 : {:?}&quot;, random_tuple.3);
    println!(&quot;4 : {:?}&quot;, random_tuple.4);
    println!(&quot;5 : {:?}&quot;, random_tuple.5);
}</code></pre>
<p>여기서 재밌는 점은, Vector의 경우 같은 type의 요소들만 가질 수 있으나 다음과 같이 여러개의 타입을 묶어서 사용할 수도 있음</p>
<pre><code class="language-rust">fn main(){
    let my_vec = vec![(&quot;hey&quot;,9), (&quot;Hello there&quot;, 123123)];
    println!(&quot;{:?}&quot;, my_vec)
}</code></pre>
<p>Destructuring! -&gt; js의 분해구조할당과 비슷</p>
<pre><code class="language-rust">fn main(){
    let str_tuple = (&quot;one&quot;, &quot;two&quot;, &quot;three&quot;);

    let (a,b,c) = str_tuple;

    println!(&quot;Item &#39;a&#39; is : {}&quot;, a);
}</code></pre>
<p>array 도 똑같이 분해 가능!</p>
<pre><code class="language-rust">fn main(){
    let str_array = [&quot;one&quot;, &quot;two&quot;, &quot;three&quot;];

    let [a,_,_] = str_array; // 관심없는 b, c 대신 _ 사용 가능 -&gt; 워닝 안뜸

    println!(&quot;Item &#39;a&#39; is : {}&quot;, a);
}</code></pre>
<h1 id="029">029</h1>
<p>흐름제어</p>
<p>다른언어에서 if 문을 사용할 때 조건은 () 안에 넣는데 러스트는 괄호를 쓸 필요 없음 (에러는 아님)</p>
<pre><code class="language-rust">fn main(){
    let my_number = 5;
    if my_number == 7 {
        println!(&quot;It&#39;s seven&quot;);
    } else if my_number == 6 {
        println!(&quot;It&#39;s six&quot;);
    } else {
        println!(&quot;It&#39;s a different number&quot;);
    }
}</code></pre>
<p>&amp;&amp; || 도 동일하게 사용 가능</p>
<pre><code class="language-rust">fn main(){
    let my_number = 5;
    if my_number == 5 &amp;&amp; my_number == 11 {
        println!(&quot;Both are matched&quot;);
    } else if my_number == 5 || my_number == 11 {
        println!(&quot;my number is 5 or 11&quot;);
    } else {
        println!(&quot;It&#39;s a different number&quot;);
    }
}</code></pre>
<p>하지만 이것보다 더 자주 쓰이는건 &quot;match&quot;</p>
<p>-&gt; switch 와 비슷</p>
<pre><code class="language-rust">fn main(){
    let my_number: u8 = 5;

    match my_number{
        0 =&gt; println!(&quot;It&#39;s a zero&quot;),
        1 =&gt; println!(&quot;It&#39;s a one&quot;)
    }
} //=&gt;에러남. u8 은 256까지 가능한데 나머지에 대한 정의가 안돼있기 때문 </code></pre>
<pre><code class="language-rust">fn main(){
    let my_number: u8 = 5;

    match my_number{
        0 =&gt; println!(&quot;It&#39;s a zero&quot;),
        1 =&gt; println!(&quot;It&#39;s a one&quot;),
        _ =&gt; println!(&quot;Other number&quot;); // _ 기호를 사용해서 나머지 타입(else)에 대한 정의
    }
} </code></pre>
<p>match 로 변수에 값을 할당할 수도 있음</p>
<pre><code class="language-rust">fn main(){
    let my_number: u8 = 5;

    let my_second_number = match my_number{
        0 =&gt; 23,
        1 =&gt; 34,
        _ =&gt; 0
    };

    println!(&quot;{}&quot;, my_second_number);
} </code></pre>
<h1 id="030">030</h1>
<p>tuple 활용</p>
<p>여러개의 변수를 한번에 비교할 수도 있음!</p>
<pre><code class="language-rust">fn main(){
    let sky = &quot;cloudy&quot;;
    let temperature = &quot;warm&quot;;

    match (sky, temperature){
        (&quot;cloudy&quot;, &quot;cold&quot;) =&gt; println!(&quot;It&#39;s not very nice day&quot;),
        (&quot;cloudy&quot;, &quot;warm&quot;) =&gt; println!(&quot;It&#39;s a nice day&quot;),
        (&quot;cloudy&quot;, _) =&gt; println!(&quot;Cloudy and something else&quot;),
        _ =&gt; println!(&quot;Not sure what the weather is&quot;)
    }
}</code></pre>
<p>조건문도 사용 가능</p>
<pre><code class="language-rust">fn main(){
    let children = 5;
    let married = true;

    match(children, married){
        (children, married) if married == false =&gt; println!(&quot;Not married with {} children&quot;, children),
        (c,m) if c==0 &amp;&amp; m =&gt; println!(&quot;Married but with no children&quot;),
        _ =&gt; println!(&quot;Some other type of marriage and children combination&quot;)
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Easy Rust Korean 011 ~ 020]]></title>
            <link>https://velog.io/@na_treeeum/Easy-Rust-Korean-011-020</link>
            <guid>https://velog.io/@na_treeeum/Easy-Rust-Korean-011-020</guid>
            <pubDate>Sun, 09 Nov 2025 13:35:11 GMT</pubDate>
            <description><![CDATA[<h1 id="011">011</h1>
<h2 id="mutability--shadowing">mutability &amp; shadowing</h2>
<p>mutability : 변경가능여부</p>
<p>기본적으로 immutable (변수 선언 시 기본적으로 변경 불가능)</p>
<pre><code class="language-rust">fn main(){
    let my_number = 10;
    my_number = 9;
}

&gt;&gt;&gt; ERROR</code></pre>
<p>my_number 는 immutable 이기 때문에 에러 발생
이 경우 <code>mut</code> 키워드를 사용해서 변경가능할 수 있게 해줘야함</p>
<pre><code class="language-rust">fn main(){
    let mut my_number = 10;
    my_number = 9;
}</code></pre>
<p><code>mut</code> 키워드를 사용하고나서 변경되지 않았다면, 컴파일러가 이를 알려주고, mut 키워드 삭제 가능</p>
<p>shadowing : 같은 이름을 다시 쓰는 것</p>
<pre><code class="language-rust">fn main(){
    let my_variable = 10;
       let my_variable = &quot;My Variable&quot;;
}

// -&gt; my_variable 은 마지막에 작성된 &quot;My Variable&quot;만 값으로 생각함.</code></pre>
<p>왜 사용?
코딩을 하면서 필요한 값은 최종 결과인데, 이를 위한 여러 변수들이 불필요하게 선언될 필요 없음</p>
<pre><code class="language-rust">fn double(input: i32) -&gt; i32 {
    input * 2
}

fn triple(input: i32) -&gt; i32 {
    input * 3
}

fn main(){
    let x_origin = 9;
       let x_double = double(x_origin);
    let x_final = triple(x_double);

    println!(&quot;{}&quot;, x_final);
}

// -&gt; 결과적으로 필요한 값은 xFinal 인데, xOrigin, xDouble 같은 변수가 선언됨

//-----------Shadowing 으로 다음과같이 사용할 수 있음-----------
fn main(){
    let x = 9;
       let x = double(x);
    let x = triple(x);

    println!(&quot;{}&quot;, x);
}</code></pre>
<p>CodeBlock 을 이용해서 Shadowing을 다양하게 활용할 수 있음</p>
<pre><code class="language-rust">fn main(){
    let my_var = 9;
    {
        let my_var = &quot;Some String&quot;;
        println!(&quot;{}&quot;, my_var); // Some String 출력
    }
    println!(&quot;{}&quot;, my_var); // 9 출력
}</code></pre>
<h1 id="012">012</h1>
<h2 id="stack--heap--pointers">Stack / heap / pointers</h2>
<h3 id="stack-memory">Stack Memory</h3>
<p>먼저 들어온 Task가 다음에 들어온 Task가 종료될때까지 기다리는 메모리 구조.
Task가 종료되면 할당받은 Memory 는 반환</p>
<h3 id="heap-memory">Heap Memory</h3>
<p>창고같은 곳에서 &quot;어디로 가면 어떤 물건이 있어요&quot; 같이 동작하는 메모리 구조. 비교적 속도가 느리지만 많은 용량을 저장할 수 있음.</p>
<h3 id="pointers">Pointers</h3>
<p>아래와 같이 어떤 내용이 어디에 있는지 알려주는 역할</p>
<pre><code class="language-PlainText">Chapter
Chapter 1: My life, Page : 1 
Chapter 2: My life, Page : 15
Chapter 3: My life, Page : 23 </code></pre>
<p>Rust에서는 Pointer를 Reference 라고 부름.
reference 를 사용할 때에는 &quot;&amp;&quot; 기호를 사용함.</p>
<pre><code class="language-rust">fn main(){
    let my_number = 15; // 값. i32
    let single_reference = &amp;my_number; // my_number 에 대한 싱글 레퍼런스. &amp;i32
    let double_reference = &amp;single_reference; // my_number 에 대한 레퍼런스에 대한 레퍼런스. &amp;&amp;i32

    let five_references = &amp;&amp;&amp;&amp;&amp;my_number; // 이것도 사용 가능. 
}</code></pre>
<p>싱글레퍼런스, 더블레퍼런스, 모두 다른 타입이기 때문에 상호 비교가 불가능!!</p>
<h1 id="013">013</h1>
<p>Fancy Printing : print!() 사용</p>
<p>아래 두 줄의 문장은 같은 동작을 함</p>
<p>println!(message);
print!(&quot;{}\n&quot;,message);</p>
<p>예를들면</p>
<pre><code class="language-rust">fn main(){
    //#1
    println!(&quot;This&quot;);
    println!(&quot;is&quot;);
    println!(&quot;a&quot;);
    println!(&quot;message&quot;);

    //#2
    print!(&quot;This\n&quot;);
    print!(&quot;is\n&quot;);
    print!(&quot;a\n&quot;);
    print!(&quot;message\n&quot;);

    //#3
    print!(&quot;This\nis\na\nmessage\n&quot;);
}</code></pre>
<p>위의 1, 2, 3번 코두 동일한 동작을 함</p>
<pre><code class="language-PlainText">Output : 

This
is
a
message</code></pre>
<p><code>\n</code> 이라는 기호를 출력하고 싶다면? -&gt; <code>\\n</code> 사용</p>
<pre><code class="language-rust">fn main(){
    print!(&quot;This \\n is a message&quot;); // Output : This \n is a message
}</code></pre>
<p>그런데 <code>\</code>를 너무 많이 써야하는 상황이라면? 
예를들어 다음 디렉토리를 표시하려면
<code>c:\drive_a\new_drive\path_name</code></p>
<p>코드는 다음과 같이 안예뻐질 것</p>
<pre><code class="language-rust">fn main(){
    print!(&quot;c:\\drive_a\\new_drive\\path_name&quot;);
}</code></pre>
<p>이럴때 사용하는것이 &quot;Raw Text&quot;
다음과 같이 사용할 수 있다. </p>
<pre><code class="language-rust">fn main(){
    print!(r#&quot;c:\drive_a\new_drive\path_name&quot;#);
}</code></pre>
<p>줄바꿈을 다음과같이 사용할 수도 있는데</p>
<pre><code class="language-rust">fn main(){
    println!(&quot;Let me tell you 
    some story
    about
    rust&quot;);
}</code></pre>
<p>출력이 다음과 같다. </p>
<pre><code>Let met tell you
    some story
    about
    rust</code></pre><p>앞에 인덴트가 있기 때문에 그런것! 주의하여 사용할 것</p>
<p>전에 Debug Print 에 대해 잠시 다뤘는데 </p>
<pre><code class="language-rust">println!(&quot;{:?}&quot;, var_name);</code></pre>
<p>으로 사용했다. </p>
<p>왜 물음표로 사용하냐면 어떤 타입이든 출력을 하기 때문.</p>
<p>특정 data type을 출력하기 위해선 다음과 같은 format을 사용한다 </p>
<pre><code class="language-rust">fn main(){
    let my_number = 9000;
    println!(&quot;{:p}&quot;, &amp;my_number); // reference(pointer) 출력
    println!(&quot;{:X}&quot;, my_number); // Hex값 출력
    println!(&quot;{:b}&quot;, my_number); // Bytes값 출력

    println!(&quot;{:p}&quot;, my_number); // 는 오류 : p는 포인터 data type을 출력한다고 하는데, my_number 는 값이기 때문에
}</code></pre>
<p>이 포맷을 조금 더 다양하게 활용이 가능하다.</p>
<pre><code class="language-rust">fn main() {
    let title = &quot;TODAY&#39;S NEWS&quot;;
    println!(&quot;{:-^30}&quot;, title); // no variable name, pad with -, put in centre, 30 characters long
    let bar = &quot;|&quot;;
    println!(&quot;{: &lt;15}{: &gt;15}&quot;, bar, bar); // no variable name, pad with space, 15 characters each, one to the left, one to the right
    let a = &quot;SEOUL&quot;;
    let b = &quot;TOKYO&quot;;
    println!(&quot;{city1:-&lt;15}{city2:-&gt;15}&quot;, city1 = a, city2 = b); // variable names city1 and city2, pad with -, one to the left, one to the right
}</code></pre>
<p>Output: </p>
<pre><code>---------TODAY&#39;S NEWS---------
|                            |
SEOUL--------------------TOKYO</code></pre><p>더 많은 내용은 아래 링크 참고
<a href="https://doc.rust-lang.org/stable/std/fmt/">https://doc.rust-lang.org/stable/std/fmt/</a></p>
<h1 id="014">014</h1>
<p>Strings</p>
<p>String / &amp;str 을 주로 사용</p>
<pre><code class="language-rust">fn main(){
    let my_name = &quot;David&quot;; // &amp;str
    let other_name = String::from(&quot;David2&quot;);

    /*
    let my_other_name = &quot;David3&quot;; // &amp;str
    my_other_name.push(&quot;!&quot;); // 여기서 에러 발생!  &amp;str 에는 .push 가 없기 때문
    */


    // 다음과 같이 하면 오류 발생 안함
    let my_other_name = &quot;David3&quot;.to_string(); // String
    my_other_name.push(&quot;!&quot;);


    println!(&quot;{}&quot;, my_other_name);
}</code></pre>
<p>String type 은 Sized type
str = dynamic type -&gt; 길이를 컴파일러가 알 수 없음 =&gt; &amp;str을 사용</p>
<h1 id="015">015</h1>
<p>&amp;str 보다 String 이 사용하기 더 쉬운 이유!</p>
<p>method 사용이 용이함</p>
<p>.push : 한 글자 추가
.push_str : &amp;str 추가
.capacity : 바이트 크기
.pop : 
with_capacity : 바이트 크기 명시하여 String 선언</p>
<pre><code class="language-rust">let mut my_name = &quot;David&quot;.to_string();
my_name.push(&#39;!&#39;);
println!(&quot;My name is {}&quot;, my_name);</code></pre>
<pre><code class="language-rust">let mut my_name = String::with_capacity(26);
println!(&quot;Length is {} and capacity is {}&quot;, my_name.len(), my_name.capacity());
my_name.push_str(&quot;David!&quot;);
println!(&quot;Length is {} and capacity is {}&quot;, my_name.len(), my_name.capacity());
my_name.push_str(&quot;And I live in Seoul&quot;);
println!(&quot;Length is {} and capacity is {}&quot;, my_name.len(), my_name.capacity());
my_name.push(&#39;a&#39;);
println!(&quot;Length is {} and capacity is {}&quot;, my_name.len(), my_name.capacity());

/* Output : 
Length is 0 and capacity is 26
Length is 6 and capacity is 26
Length is 25 and capacity is 26
Length is 26 and capacity is 26
*/</code></pre>
<h1 id="016">016</h1>
<p>const / static</p>
<pre><code class="language-rust">fn main(){
    let x = 8; // &#39;let&#39; binding 이라고 함
}</code></pre>
<p>const는 Global 변수를 선언함</p>
<pre><code class="language-rust">const NUMBER: i32 = 20; // const 선언 변수명은 대문자로(소문자로 쓰면 warning), 타입 지정은 필수!

fn main(){
    let x = 8; // &#39;let&#39; binding 이라고 함
}</code></pre>
<p>static도 const와 비슷</p>
<pre><code class="language-rust">const NUMBER: i32 = 20; // const 선언 변수명은 대문자로(소문자로 쓰면 warning), 타입 지정은 필수!
static LOW_SCORE: i32 = 0;

/*
static 은 mut 를 통해 unsafe 를 사용할 수는 있으나 거~~의 안씀!!

static mut LOW_SCORE: i32 = 0;
*/

fn main(){
    let x = 8; // &#39;let&#39; binding 이라고 함
}</code></pre>
<h1 id="017">017</h1>
<p>OWNERSHIP</p>
<pre><code class="language-rust">fn main(){
    let country = String::from(&quot;대한민국&quot;);
    let ref_one = &amp;country;
    let ref_two = &amp;country;

    println!(&quot;Country is: {}&quot;, ref_one);
}</code></pre>
<pre><code class="language-rust">fn return_it() -&gt; &amp;String {
    let country = String::from(&quot;대한민국&quot;);
    &amp;country // ref 반환
}

fn main(){
    let my_country = return_it(); // &gt;&gt; 오류!!
}</code></pre>
<h1 id="018">018</h1>
<p>mutable references</p>
<p>&amp; -&gt; immutable reference / shared reference
&amp;mut -&gt; mutable reference / unique reference</p>
<pre><code class="language-rust">fn main(){
    let mut my_number = 9;
    let num_ref = &amp;mut my_number;

    /* 이렇게 코딩하면 오류남!! 
    num_ref = 10;
    */

    *num_ref = 10; // &amp;의 반대개념 *

    // &amp;&amp; 이면 ** 사용!
    /*
        let num_ref = &amp;mut &amp;mut my_number;
        **num_ref = 10;
    */
}</code></pre>
<h1 id="019">019</h1>
<p>references and shadowing</p>
<pre><code class="language-rust">// 오류 반환됨!!
fn main(){
    let mut number = 10;
    let number_ref = &amp;number;
    let number_change = &amp;met number;
    *number_change += 10;
}</code></pre>
<pre><code class="language-rust">// 정상 작동됨
fn main(){
    let mut number = 10;
    let number_change = &amp;met number;
    *number_change += 10;
    let number_ref = &amp;number;
}</code></pre>
<p>shadowing!</p>
<pre><code class="language-rust">// 정상 작동됨
fn main(){
    let country = &quot;대한민국&quot;;
    let country_ref = &amp; country;
    let country = 8; // shadowing

    println!(&quot;{}, {}&quot;, country_ref, country); 
    // -&gt; &quot;대한민국&quot; 이 없어지지 않았음을 확인할 수 있음!&quot;
}</code></pre>
<h1 id="020">020</h1>
<p>OWNERSHIP</p>
<pre><code class="language-rust">fn print_country(country_name: String){
    println!(&quot;My country is {}&quot;, country_name);
}

fn main(){
    let country = &quot;대한민국&quot;.to_string();
    print_country(country); // 여기서 country 의 소유권이 print_country 함수로 넘어감
    print_country(country); // 여기서 에러 발생!!
}</code></pre>
<pre><code class="language-rust">fn print_country(country_name: &amp;String){
    println!(&quot;My country is {}&quot;, country_name);
}

fn main(){
    let country = &quot;대한민국&quot;.to_string();
    print_country(&amp;country); // 여기서 country 의 소유권은 넘어가지 않음!
    print_country(&amp;country); // 에러 발생 X!
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Easy Rust Korean 001 ~ 010]]></title>
            <link>https://velog.io/@na_treeeum/Easy-Rust-001-010</link>
            <guid>https://velog.io/@na_treeeum/Easy-Rust-001-010</guid>
            <pubDate>Sun, 26 Oct 2025 02:26:27 GMT</pubDate>
            <description><![CDATA[<h1 id="001">001</h1>
<h2 id="웹-ide-rust전용">웹 IDE (rust전용)</h2>
<p><a href="https://play.rust-lang.org">https://play.rust-lang.org</a></p>
<ul>
<li>permalink to the playground : 코드를 포함한 ide 링크 복사가능</li>
</ul>
<h2 id="rust-의-장단점">Rust 의 장단점</h2>
<p>장점</p>
<ul>
<li>c, c++ 처럼 빠른속도</li>
</ul>
<p>단점</p>
<ul>
<li>어려움</li>
</ul>
<hr>
<h1 id="002">002</h1>
<h2 id="comments-주석">comments (주석)</h2>
<p>가장 기본적인 주석</p>
<pre><code class="language-rust">// This is normal comment</code></pre>
<p>Documentation 주석</p>
<pre><code class="language-rust">/// This is normal comment</code></pre>
<p>부분적인 주석</p>
<pre><code class="language-rust">fn main() {
    let x/*: i16*/ = 10;
}</code></pre>
<ul>
<li>사용하지 않은 변수에 대해 언더스코어(_) 로 이름을 시작하게 되면 미사용변수에 대한 Warning 이 없음</li>
</ul>
<h1 id="003">003</h1>
<h2 id="primitive-types--기본-데이터-타입">Primitive types : 기본 데이터 타입</h2>
<p>integer (정수, 음수 가능)</p>
<ul>
<li>i8, i16, i32, i64, i128, and isize</li>
</ul>
<p>unsigned integer (부호없는 정수, 음수 불가능)</p>
<ul>
<li>u8, u16, u32, u64, u128, and usize</li>
</ul>
<p>(i -&gt; integer || u -&gt; unsigned) + bit 수 로 표현</p>
<pre><code class="language-rust">let my_number = 100; </code></pre>
<p>변수 선언 시 let 사용</p>
<p>my_number 는 어떤 타입인지 알 수 없음</p>
<p>-&gt; u8타입을 지정하고 싶다면 다음과 같이 지정 가능</p>
<pre><code class="language-rust">let my_number: u8 = 100; </code></pre>
<h2 id="type-inference">type inference</h2>
<p>다음과 같은 코드에서는 컴파일러가 타입을 추론하여 계산함</p>
<pre><code class="language-rust">let my_number: u8 = 100; 
let my_other_number = 50; // type 미지정, 기본 : i32(or i64)
let _third_number = my_number + my_other_number;</code></pre>
<p>u8 + i64 연산은 기본적으로 안되지만, my_other_number 에 대해 타입지정을 안했기 때문에 컴파일러가 타입을 추론하여 연산할 수 있음.</p>
<p>다음과 같이 각 변수에 대해 타입이 지정돼있는 변수사이의 연산은 불가능</p>
<pre><code class="language-rust">let my_number: u8 = 100; 
let my_other_number: u16 = 50; // u16 지정
let _third_number = my_number + my_other_number;

&gt;&gt; ERROR</code></pre>
<h1 id="004">004</h1>
<h2 id="char">Char</h2>
<p>println!(String); 의 형태에서는 무조건 큰 따옴표 (&quot;&quot;)를 사용하고, Char type 변수를 선언할 때에는 작은 따옴표 (&#39;&#39;)를 사용</p>
<p>String : 문자열
Char : 문자</p>
<p>char type은 4bytes 크기</p>
<pre><code class="language-rust">println!(&quot;Hello, world!&quot;);
let first_letter = &#39;A&#39;;
let space = &#39; &#39;;</code></pre>
<h2 id="casting">Casting</h2>
<blockquote>
<p>simple type change using &#39;as&#39;</p>
</blockquote>
<pre><code class="language-rust">let my_number: u16 = 8;
let second_number: u8 = 10;
let third_number = my_number + second_number as u16;</code></pre>
<p>Char -&gt; u8 으로 캐스팅 하면 ASCII Code 가 됨</p>
<pre><code class="language-rust">let my_number = &#39;a&#39; as u8;
println!(&quot;Hello, world! My number is {}&quot;, my_number);</code></pre>
<h1 id="005">005</h1>
<h2 id="char-와-string의-차이">Char 와 String의 차이</h2>
<p>Char type 의 크기는 4bytes</p>
<pre><code class="language-rust">println!(&quot;Size of a char: {}&quot;, std::mem::size_of::&lt;char&gt;()); // 4bytes</code></pre>
<p>Sring (&quot;&quot;)타입의 크기는 <strong>.len()</strong>으로 알 수 있음</p>
<pre><code class="language-rust">println!(&quot;Size of string containing &#39;a&#39;: {}&quot;, &quot;a&quot;.len()); // 1 출력 = 1byte</code></pre>
<p>글자수와 비슷하지만, 사실 글자수가 아닌 bytes 수</p>
<pre><code class="language-rust">println!(&quot;Size of string containing &#39;ß&#39;: {}&quot;, &quot;ß&quot;.len()); // 독일어 : 2bytes
println!(&quot;Size of string containing &#39;国&#39;: {}&quot;, &quot;国&quot;.len()); // 한자 : 3bytes
println!(&quot;Size of string containing &#39;𓅱&#39;: {}&quot;, &quot;𓅱&quot;.len()); // 고대 이집트어 : 4bytes</code></pre>
<p>string의 bytes수가 아닌 글자수를 알려면?</p>
<p><strong>.chars().count()</strong> 사용</p>
<pre><code class="language-rust">let slice = &quot;Hello!&quot;;
println!(&quot;Slice is {} bytes and also {} characters.&quot;, slice.len(), slice.chars().count());

// .chars() : &quot;Hello!&quot; -&gt; &#39;H&#39;, &#39;e&#39;, &#39;l&#39;, ... , &#39;!&#39;;</code></pre>
<h1 id="006">006</h1>
<p>숫자형 (u8, u16, ...) 앞에 숫자를 넣어서 변수선언과 타입지정을 한번에 할 수 있음</p>
<pre><code class="language-rust">// 중간에 underscore(_)는 아무리 많이 써도 상관없음
let my_number = 9_u8; // u8 타입의 9값을 가진 변수
let second_number = 1_000_000_000u64; 
let third_number = 9_____________1u32; </code></pre>
<h2 id="float">Float</h2>
<ol>
<li><code>let float_number = 9.;</code></li>
<li>f32 / f64 로 타입지정(주로 f64사용) <pre><code class="language-rust">let f_number = 9.; // f64
let s_number: f64 = 1.1;
let t_number = 8; // i32
println!(&quot;{}&quot;, f_number + s_number + t_number as f64); // 18.1</code></pre>
</li>
</ol>
<h1 id="007">007</h1>
<h2 id="println">println!()</h2>
<p>-&gt; println meand print line
-&gt; ! means macro</p>
<p>매크로는 super function.</p>
<p>변수 사용({})기호를 사용하고 변수할당을 하지 않으면 에러 발생</p>
<pre><code class="language-rust">let my_name = &quot;David&quot;;
println!(&quot;My name is {}&quot;);

&gt;&gt; ERROR</code></pre>
<h2 id="function">function</h2>
<pre><code class="language-rust">fn func_name() -&gt; return_type {
}</code></pre>
<p>형식으로 함수 선언 가능</p>
<pre><code class="language-rust">fn give_age() -&gt; i32 {
    42 // return 42 라고 하지 않아도 됨!
}

fn main(){
    let my_name = &quot;David&quot;;
    println!(&quot;My name is {} and my age is {}&quot;, my_name, give_age());
}</code></pre>
<h1 id="008">008</h1>
<h2 id="println---2">println!() - 2</h2>
<p>{} 안에 변수명을 넣어서 사용 가능!</p>
<pre><code class="language-rust">fn main(){
    let my_city = &quot;Seoul&quot;;
    let year = 2002;
    let population = 9_987_987;
    println!(&quot;The city of {city} in {year} had a population of {population}&quot;,
    city = my_city,
    year = year,
    population = population);
}</code></pre>
<p>{} 안에 변수 인덱스를 넣어서 사용 가능!</p>
<pre><code class="language-rust">fn main(){
    let my_city = &quot;Seoul&quot;;
    let year = 2002;
    let population = 9_987_987;
    println!(&quot;The city of {0} in {1} had a population of {2}. I love {0}&quot;,
    my_city,
    year,
    population);
}</code></pre>
<p>string interpolation</p>
<pre><code class="language-rust">fn main(){
    let city = &quot;Seoul&quot;;
    let year = 2002;
    let population = 9_987_987;
    println!(&quot;The city of {city} in {year} had a population of {population}. I love {city}&quot;);
}</code></pre>
<p>다음과 같이 expression(표현식)은 사용 불가능</p>
<pre><code class="language-rust">fn main(){
    let city = &quot;Seoul&quot;;
    let year = 2002;
    let population = 9_987_987;
    println!(&quot;The city of {city} in {year+2} had a population of {population}. I love {city}&quot;);
}

&gt;&gt; year+2 에서 Error</code></pre>
<h1 id="009">009</h1>
<h2 id="empty-tuple">empty tuple</h2>
<pre><code class="language-rust">// 8을 반환하는 함수 number()
fn number() -&gt; i32 {
    8
}</code></pre>
<p>하지만 8 뒤에 세미콜론을 붙이게 된다면 에러 발생</p>
<pre><code class="language-rust">// 8을 반환하는 함수 number()
fn number() -&gt; i32 {
    8;
}

fn main() {
    let my_number = number();
}

/*
&gt;&gt; Error : expected i32 but found ()
() 는 Rust 에서 empty tuple, unit type 이라고 부르고 다른 언어에서는 void 라고 부름
*/</code></pre>
<h2 id="display-debug">Display, Debug</h2>
<ul>
<li>Display : {}</li>
<li>Debug Print : {:?}</li>
</ul>
<pre><code class="language-rust">fn empty_tuple() {
}

fn main() {
    let tuple = empty_tuple();
    println!(&quot;{}&quot;, tuple); // error
    println!(&quot;{:?}&quot;, tuple); // 웬만해서는 출력 가능!
}</code></pre>
<h1 id="010">010</h1>
<h2 id="function-1">function</h2>
<p>앞서 알아본 간단한 함수의 모양은 다음과 같음</p>
<pre><code class="language-rust">fn give_number() -&gt; i32 {
    8
}

fn main() {
    let my_number = give_number();
}</code></pre>
<p>Parameter 를 받아서 반환하는 형태의 함수는 다음과 같음</p>
<pre><code class="language-rust">fn give_number(one: i32, two: i32) -&gt; i32 {
    one * two
}

fn main() {
    let my_number = give_number(9,8);
    println!(&quot;{}&quot;, my_number);
}</code></pre>
<h2 id="code-block-활용">Code block 활용</h2>
<pre><code class="language-rust">fn give_number(one: i16, two: i16) -&gt; i16 {
    let multiplied_by_ten = {
        let f_number = 10;
        f_number * one * two
    };
    multiplied_by_ten
}

fn main() {
    let my_number = give_number(9,8);
    println!(&quot;{}&quot;, my_number);
}</code></pre>
<hr>
<p>강의 링크
<a href="https://youtube.com/playlist?list=PLfllocyHVgsSJf1zO6k6o3SX2mbZjAqYE&amp;si=LfVF7DHgzWT7iUrp">https://youtube.com/playlist?list=PLfllocyHVgsSJf1zO6k6o3SX2mbZjAqYE&amp;si=LfVF7DHgzWT7iUrp</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Grafana 대시보드 설정]]></title>
            <link>https://velog.io/@na_treeeum/Grafana-%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@na_treeeum/Grafana-%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Thu, 03 Apr 2025 06:53:36 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/na_treeeum/post/073600c8-8731-4787-a4d7-aa843d0af592/image.png" alt="1">
최종 결과 대시보드!</p>
<h1 id="대시보드-구성">대시보드 구성</h1>
<h2 id="node-exporter">Node Exporter</h2>
<blockquote>
<p>시스템 리소스 정보(CPU, 메모리, 디스크, 네트워크 등)를 HTTP endpoint로 제공</p>
</blockquote>
<ul>
<li><a href="http://localhost:9100/metrics">localhost:9100/metrics</a> 로 시스템 리소스 정보를 제공</li>
</ul>
<h2 id="prometheus">Prometheus</h2>
<blockquote>
<p>다양한 지표를 수집하고 저장하는 시스템</p>
</blockquote>
<ul>
<li>대상(예 : node-exporter) 에 주기적으로 접근하여 메트릭 데이터를 가져옴</li>
</ul>
<h2 id="grafana">Grafana</h2>
<blockquote>
<p>메트릭 데이터를 불러와 대시보드 형태로 시각화해주는 툴</p>
</blockquote>
<h2 id="관계-요약">관계 요약</h2>
<table>
<thead>
<tr>
<th>Element</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>Node Exporter</td>
<td>메트릭 데이터 제공</td>
</tr>
<tr>
<td>⬇️</td>
<td></td>
</tr>
<tr>
<td>Prometheus</td>
<td>데이터 수집 및 저장</td>
</tr>
<tr>
<td>⬇️</td>
<td></td>
</tr>
<tr>
<td>Grafana</td>
<td>데이터 시각화</td>
</tr>
</tbody></table>
<h1 id="설치--실행--설정">설치 &amp; 실행 &amp; 설정</h1>
<blockquote>
<p>OS : Ubuntu, sudo su 로 관리자 계정에서 진행합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/cc5ab4fa-2def-4ed6-af2d-dec1fb34f77d/image.png" alt="2"></p>
<h2 id="node-exporter-1">Node Exporter</h2>
<p><a href="https://prometheus.io/download/#node_exporter">https://prometheus.io/download/#node_exporter</a></p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/8d40da27-92fb-450e-9c5f-2470b92a9e90/image.png" alt="3"></p>
<p>리눅스 버전의 다운로드 링크를 복사합니다.</p>
<p>wget 명령어를 통해서 압축파일을 다운받습니다.</p>
<pre><code>wget https://github.com/prometheus/node_exporter/releases/download/v1.9.0/node_exporter-1.9.0.linux-amd64.tar.gz</code></pre><p>압축파일 다운로드가 완료되었다면 아래 명령어로 파일이 존재하는지 확인합니다.</p>
<pre><code>ls node_exporter*</code></pre><p><img src="https://velog.velcdn.com/images/na_treeeum/post/57f8f554-f86b-45b4-9551-59716d0a070d/image.png" alt="4"></p>
<p>파일이 존재한다면 압축을 풀어준 뒤, 압축해제된 경로로 이동합니다.</p>
<pre><code>tar xzvf node_exporter-1.9.0.linux-amd64.tar.gz
cd node_exporter-1.9.0.linux-amd64</code></pre><p>ls 명령어로 디렉토리의 파일 목록을 확인하면 다음과 같습니다 </p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/8c94a8c5-586f-4827-9311-2f958ec050e8/image.png" alt="5"></p>
<p>이제 node_exporter 파일을 /usr/local/bin 폴더로 옮겨줍니다.</p>
<pre><code>mv node_exporter /usr/local/bin</code></pre><p>파일을 옮겼다면, 시스템 서비스 등록을 합니다.</p>
<pre><code>vim /etc/systemd/system/node-exporter.service</code></pre><p>아래 내용을 복사 붙여넣기 한 뒤 저장합니다.</p>
<pre><code>[Unit]
Description=Node Exporter
After=network.target

[Service]
User=nobody
ExecStart=/usr/local/bin/node_exporter

[Install]
WantedBy=default.target</code></pre><p>서비스를 시작하고, 부팅 시 자동 실행하도록 설정합니다.</p>
<pre><code>systemctl daemon-reexec
systemctl daemon-reload
systemctl start node-exporter
systemctl enable node-exporter</code></pre><p>아래 명령어를 통해 시스템이 정상작동하고 있는지 확인합니다.</p>
<pre><code>systemctl status node-exporter</code></pre><p><img src="https://velog.velcdn.com/images/na_treeeum/post/422e4159-dada-4e52-9002-00ee8ca80393/image.png" alt="6"></p>
<pre><code>curl http://localhost:9100/metrics</code></pre><p><img src="https://velog.velcdn.com/images/na_treeeum/post/bfd28a01-d577-4694-9f91-1699c87c4e39/image.png" alt="7"></p>
<h2 id="prometheus-1">Prometheus</h2>
<p>node-exporter 설정이 끝났으니 prometheus 를 설치해 봅니다.</p>
<p>먼저, Prometheus 시스템 그룹을 생성합니다.</p>
<pre><code>groupadd --system prometheus</code></pre><p>prometheus 사용자를 추가하고, 생성된 그룹에 할당해 줍니다.</p>
<pre><code>useradd -s /sbin/nologin --system -g prometheus prometheus</code></pre><p>그리고 prometheus를 위한 디렉토리를 생성해 줍니다.</p>
<pre><code>mkdir /etc/prometheus</code></pre><p>다음으로 아래 링크에 들어가서 프로메테우스 다운로드 링크를 복사합니다.</p>
<p><a href="https://prometheus.io/download/#prometheus">https://prometheus.io/download/#prometheus</a></p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/58704c5d-3042-42f7-b248-a37ebe2b821f/image.png" alt="8"></p>
<p>LTS 버전을 받도록 하겠습니다.</p>
<p>wget 명령어를 통해서 압축파일을 다운받습니다.</p>
<pre><code>wget https://github.com/prometheus/prometheus/releases/download/v2.53.4/prometheus-2.53.4.linux-amd64.tar.gz</code></pre><p>압축파일을 풀어준 뒤 압축이 해제된 디렉토리로 이동해줍니다.</p>
<pre><code>tar xzvf prometheus*.tar.gz
cd prometheus*/</code></pre><p>해당 폴더에 있는 prometheus 바이너리 파일을 옮겨준 뒤 버전을 확인합니다.</p>
<pre><code>mv prometheus promtool /usr/local/bin/
prometheus --version</code></pre><p><img src="https://velog.velcdn.com/images/na_treeeum/post/139cff0a-bd7c-4de2-8feb-4cad48db9767/image.png" alt="9"></p>
<p>그럼 이제 아까 만들어둔 prometheus 폴더에 구성 파일을 옮겨줍니다.</p>
<pre><code>mv prometheus.yml /etc/prometheus/prometheus.yml
mv consoles/ console_libraries/ /etc/prometheus/</code></pre><p>파일을 옮긴 뒤 구성파일의 내용을 확인해 줍니다.</p>
<pre><code>cat /etc/prometheus/prometheus.yml</code></pre><p><img src="https://velog.velcdn.com/images/na_treeeum/post/a13dc8fa-f808-4b2f-b246-eab653a4d09a/image.png" alt="10"></p>
<p>이제 프로메테우스 시스템을 등록합니다. </p>
<pre><code>vim /etc/systemd/system/prometheus.service</code></pre><p>위 명령어로 에디터를 열어서, 아래 내용을 붙여넣어 줍니다.</p>
<pre><code>[Unit]
Description=Prometheus
Documentation=https://prometheus.io/docs/introduction/overview/
Wants=network-online.target
After=network-online.target
[Service]
User=prometheus
Group=prometheus
Type=simple
ExecStart=/usr/local/bin/prometheus \
--config.file /etc/prometheus/prometheus.yml \
--storage.tsdb.path /var/lib/prometheus/ \
--web.console.templates=/etc/prometheus/consoles \
--web.console.libraries=/etc/prometheus/console_libraries \
--web.listen-address=0.0.0.0:9090 \

[Install]
WantedBy=multi-user.target</code></pre><p>/var/lib/prometheus/  디렉토리를 storage path 로 설정해 놓았기 때문에, 해당 디렉토리를 생성하고, 권한을 부여합니다.</p>
<pre><code>mkdir /var/lib/prometheus
chown -R prometheus:prometheus /var/lib/prometheus</code></pre><p>이제 prometheus 설정은 끝났으니 실행시켜 봅니다.</p>
<pre><code>systemctl daemon-reload
systemctl start prometheus
systemctl enable prometheus</code></pre><p><img src="https://velog.velcdn.com/images/na_treeeum/post/9bce1944-21eb-4ba0-978e-30485a91a308/image.png" alt="11"></p>
<p>잘 작동하고 있음을 확인할 수 있습니다.</p>
<h2 id="node-exporter--prometheus-연동">node-exporter &lt;&gt; prometheus 연동</h2>
<p>Prometheus 의 설정파일을 수정해줍니다.</p>
<pre><code>vim /etc/prometheus/prometheus.yml</code></pre><p>최하단에 있는 scrape_configs: 부분에 아래 내용을 추가해 줍니다.</p>
<pre><code>- job_name: &#39;node-exporter&#39;
    static_configs:
      - targets: [&#39;localhost:9100&#39;]</code></pre><p><img src="https://velog.velcdn.com/images/na_treeeum/post/315e6727-6fb7-4b61-a28e-dd8186854e2f/image.png" alt="12"></p>
<p>prometheus를 재시작 하고, 연동이 잘 되었는지 확인합니다.</p>
<pre><code>systemctl restart prometheus
curl http://localhost:9090/api/v1/targets</code></pre><p><img src="https://velog.velcdn.com/images/na_treeeum/post/9ae7ebaa-ea88-4675-8273-0d35d5a58e78/image.png" alt="13"></p>
<p>activeTargets 에 <a href="http://localhost:9100/metrics">localhost:9100/metrics</a> 가 있는지 확인합니다.</p>
<h2 id="grafana-1">Grafana</h2>
<p>먼저 apt 업데이트를 진행한 뒤, 필요 패키지를 다운받습니다.</p>
<pre><code>apt update -y &amp;&amp; sudo apt upgrade -y
apt install -y apt-transport-https software-properties-common wget</code></pre><p>Grafana GPG 키를 추가해주어야 합니다.</p>
<p>아래 명령어를 통해 GPG 키를 다운받아 줍니다.</p>
<pre><code>sudo mkdir -p /etc/apt/keyrings/
wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/grafana.gpg &gt; /dev/null</code></pre><p>Grafana APT 저장소를 추가해 준 뒤 grafana 를 설치해 줍니다.</p>
<pre><code>echo &quot;deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main&quot; | sudo tee -a /etc/apt/sources.list.d/grafana.list
apt update
apt install grafana</code></pre><p>설치가 완료되었다면 제대로 설치가 되었는지 확인하고, 서비스를 시작합니다.</p>
<pre><code>grafana-server -v

systemctl start grafana-server
systemctl enable grafana-server

systemctl status grafana-server</code></pre><p><img src="https://velog.velcdn.com/images/na_treeeum/post/629eec0b-5a1d-4941-a92a-e754d531c1af/image.png" alt="14"></p>
<p>잘 동작하고 있는것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/b1203271-1902-4b8b-9404-9f7083377a79/image.png" alt="15"></p>
<p>grafana 가 동작하는 3000번 포트를 개방하고 접속해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/0c910a3b-2354-41f4-a4fa-92402df1db6c/image.png" alt="16"></p>
<p>최초 자격증명은 admin/admin 입니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/b55151f0-9fbf-4f96-a2ea-bcbe52212eaf/image.png" alt="17"></p>
<p>비밀번호를 변경하라는 창이 뜹니다만 skip 하겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/104777d5-190d-4c71-8cf1-fc86ccdc4796/image.png" alt="18"></p>
<p>먼저 데이터 소스를 추가해 주겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/beb6055f-766f-4703-a523-b71b119fab80/image.png" alt="19"></p>
<p>프로메테우스 클릭</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/89001ff3-3df7-4a3f-a9ed-4f2d9c74e24e/image.png" alt="20"></p>
<p>connection 에는 <a href="http://localhost:9090">http://localhost:9090</a> 을 입력해줍니다.</p>
<p>제일 밑에 save&amp;test를 누르면 쿼리에 성공했다는 메세지가 나옵니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/4f5d70df-2e69-4931-aeaf-3af5767b6986/image.png" alt="21"></p>
<p>그렇다면 다시 홈으로 돌아와서 dashboard 설정을 해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/a770ee84-4f44-4720-900c-871381fd625f/image.png" alt="22"></p>
<p>우측 상단의 + 버튼을 누르고 import dashboard를 클릭</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/fa157ea5-0ce9-4ac9-bdb9-acf7e4fcee50/image.png" alt="23"></p>
<p>node exporter 기본 템플릿을 불러옵니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/2ad7f633-ee42-497f-8190-94810d73fa92/image.png" alt="24"></p>
<p>데이터 소스를 프로메테우스로 선택해주면</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/5e4b3a0b-dfc8-4afc-aacb-00ead100f275/image.png" alt="25"></p>
<p>대시보드 설정 완료입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Multicall Contract]]></title>
            <link>https://velog.io/@na_treeeum/Multicall-Contract</link>
            <guid>https://velog.io/@na_treeeum/Multicall-Contract</guid>
            <pubDate>Sun, 17 Nov 2024 11:52:08 GMT</pubDate>
            <description><![CDATA[<h1 id="multicall-이란">MultiCall 이란?</h1>
<p>여러개의 트랜잭션을 한번에 전송할 수 있게 도와주는 컨트랙트입니다.</p>
<h2 id="장점">장점</h2>
<ul>
<li>여러개의 트랜잭션을 한번에 묶어서 보내기 때문에 각각의 트랜잭션을 전송하는것 보다 가스비가 저렴할 수 있습니다.</li>
<li>트랜잭션에 대해 서명을 한 번만 하면 되기 때문에, UX향상을 기대할 수 있습니다.</li>
</ul>
<h1 id="multicall-컨트랙트-주소">Multicall 컨트랙트 주소</h1>
<blockquote>
<p>웬만한 네트워크에는 배포되어있는듯 합니다.
신기하게도 모든 네트워크에 배포된 컨트랙트 주소가 동일합니다</p>
</blockquote>
<p><a href="https://www.multicall3.com/deployments">https://www.multicall3.com/deployments</a></p>
<h1 id="multicall-abi">Multicall Abi</h1>
<p>아래 링크에서 확인할 수 있습니다.</p>
<p><a href="https://www.multicall3.com/abi#json">https://www.multicall3.com/abi#json</a></p>
<h1 id="사용법">사용법</h1>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/973512a8-2a12-445d-84ca-d1531af9e7fb/image.png" alt=""></p>
<ul>
<li>여러개의 contract 에서 read 함수를 호출하여 결과를 종합하는 경우</li>
<li>여러개의 상태가 변경되는 함수를 호출하는 경우</li>
</ul>
<h3 id="🍏여러개의-contract-에서-read-함수를-호출하여-결과를-종합하는-경우">🍏여러개의 contract 에서 read 함수를 호출하여 결과를 종합하는 경우</h3>
<ol>
<li><p>callData를 생성</p>
<pre><code class="language-jsx"> // callData 형식
 struct {
     target : contractAddress,
     callData : contract.interface.encodeFunctionData(&quot;methodName&quot;, [param1, param2, ...]
 }

 // contract instance 에서 functionData encoding
 const mulltiCallData = [
     {
         target : contractAddress1,
         callData : contract1.interface.encodeFunctionData(&quot;methodName1&quot;, [param1, param2, ...]
     },
     {
         target : contractAddress2,
         callData : contract2.interface.encodeFunctionData(&quot;methodName2&quot;, [param1, param2, ...]
     },
 ]

 // contractInterface instance 에서 functionData encoding
 const contractInterface1 = new ethers.Interface(ABI1)
 const contractInterface2 = new ethers.Interface(ABI2)

 const mulltiCallData = [
     {
         target : contractAddress1,
         callData : contractInterface1.encodeFunctionData(&quot;methodName1&quot;, [param1, param2, ...]
     },
     {
         target : contractAddress2,
         callData : contractInterface2.encodeFunctionData(&quot;methodName2&quot;, [param1, param2, ...]
     },
 ]</code></pre>
</li>
<li><p>aggregate 함수 호출</p>
<pre><code class="language-jsx"> // ethers.js v6 기준!

 const multicallResultRaw = await multicallContract.aggregate.staticCall(multiCallData)</code></pre>
</li>
<li><p>호출결과를 decoding</p>
<pre><code class="language-jsx"> /*
 aggregate 반환값 형식 = [blockNumber , [ Result1, Result2, Result3, ... ]
 */

 // contract instance 에서 functionDataResult decoding
 const [ blockNum, results ] = multicallResultRaw
 const [ result1, result2 ] = [
     contract1.interface.decodeFunctionResult(&quot;methodName1&quot;,results[0]),
     contract2.interface.decodeFunctionResult(&quot;methodName2&quot;,results[1]),
 ]

 // contractInterface instance 에서 functionDataResult decoding
 const [ result1, result2 ] = [
     contractInterface1.decodeFunctionResult(&quot;methodName1&quot;,results[0]),
     contractInterface2.decodeFunctionResult(&quot;methodName2&quot;,results[1]),
 ]</code></pre>
</li>
</ol>
<ul>
<li><p>코드예시</p>
<pre><code class="language-jsx">  async function main() {
    const rpcUrl = &quot;https://api.avax-test.network/ext/bc/C/rpc&quot;;
    const provider = new ethers.JsonRpcProvider(rpcUrl);
    const multicallContractAddress = &quot;0xcA11bde05977b3631167028862bE2a173976CA11&quot;;

    const multicallContract = new ethers.Contract(
      multicallContractAddress,
      multicallAbi,
      provider
    );

    const token0Address = &quot;0x2B4931ab37d33AE909d605476168F90E10C1A9e4&quot;;
    const token1Address = &quot;0xb13BebA6d96244C38dcbcbd448303351BaF7723c&quot;;

    const tokenInterface = new ethers.Interface(tokenAbi);

    const multiCallData = [
      {
        target: token0Address,
        callData: tokenInterface.encodeFunctionData(&quot;name&quot;),
      },
      {
        target: token0Address,
        callData: tokenInterface.encodeFunctionData(&quot;symbol&quot;),
      },
      {
        target: token1Address,
        callData: tokenInterface.encodeFunctionData(&quot;name&quot;),
      },
      {
        target: token1Address,
        callData: tokenInterface.encodeFunctionData(&quot;symbol&quot;),
      },
    ];

      // only call once
    const [, [token0NameRaw, token0SymbolRaw, token1NameRaw, token1SymbolRaw]] =
      await multicallContract.aggregate.staticCall(multiCallData);

    const [token0Name] = tokenInterface.decodeFunctionResult(
      &quot;name&quot;,
      token0NameRaw
    );
    const [token0Symbol] = tokenInterface.decodeFunctionResult(
      &quot;symbol&quot;,
      token0SymbolRaw
    );
    const [token1Name] = tokenInterface.decodeFunctionResult(
      &quot;name&quot;,
      token1NameRaw
    );
    const [token1Symbol] = tokenInterface.decodeFunctionResult(
      &quot;symbol&quot;,
      token1SymbolRaw
    );

    console.log(token0Name);
    console.log(token0Symbol);
    console.log(token1Name);
    console.log(token1Symbol);
  }</code></pre>
</li>
</ul>
<h3 id="🍏여러개의-contract-에서-state-changing-함수를-호출하여-결과를-종합하는-경우">🍏여러개의 contract 에서 state-changing 함수를 호출하여 결과를 종합하는 경우</h3>
<ol>
<li><p>callData를 생성</p>
<pre><code class="language-jsx"> // callData 형식
 struct {
     address target;
     bool allowFailure; // false 일 경우 해당 callData 호출 실패시, 전체 트랜잭션이 revert됨
     bytes callData;
 }

 // contract instance 에서 functionData encoding
 const mulltiCallData = [
     {
         target : contractAddress1,
         allowFailure : true,
         callData : contract1.interface.encodeFunctionData(&quot;methodName1&quot;, [param1, param2, ...]
     },
     {
         target : contractAddress2,
         allowFailure : false,
         callData : contract2.interface.encodeFunctionData(&quot;methodName2&quot;, [param1, param2, ...]
     },
 ]

 // contractInterface instance 에서 functionData encoding
 const contractInterface1 = new ethers.Interface(ABI1)
 const contractInterface2 = new ethers.Interface(ABI2)

 const mulltiCallData = [
     {
         target : contractAddress1,
         allowFailure : true,
         callData : contractInterface1.encodeFunctionData(&quot;methodName1&quot;, [param1, param2, ...]
     },
     {
         target : contractAddress2,
         allowFailure : false,
         callData : contractInterface2.encodeFunctionData(&quot;methodName2&quot;, [param1, param2, ...]
     },
 ]</code></pre>
</li>
<li><p>aggregate3 함수 호출</p>
<pre><code class="language-jsx"> // ethers.js v6 기준!

 const tx = await multicallContract.connect(wallet).aggregate3(multiCallData)
 await tx.wait()</code></pre>
</li>
</ol>
<ul>
<li><p>코드예시</p>
<pre><code class="language-jsx">  const multicallData2 = [
      {
        target: token0Address,
        allowFailure: true,
        callData: tokenInterface.encodeFunctionData(&quot;faucet&quot;, [tokenAmount]),
      },
      {
        target: token0Address,
        allowFailure: true,
        callData: tokenInterface.encodeFunctionData(&quot;transfer&quot;, [
          wallet.address,
          tokenAmount,
        ]),
      },
      {
        target: token1Address,
        allowFailure: true,
        callData: tokenInterface.encodeFunctionData(&quot;faucet&quot;, [tokenAmount]),
      },
      {
        target: token1Address,
        allowFailure: true,
        callData: tokenInterface.encodeFunctionData(&quot;transfer&quot;, [
          wallet.address,
          tokenAmount,
        ]),
      },
    ];

    const tx = await multicallContract.connect(wallet).aggregate3(multicallData2);

    await tx.wait();

    const token0BalanceAfter = await token0Contract.balanceOf(wallet.address);
    const token1BalanceAfter = await token1Contract.balanceOf(wallet.address);

    console.log(
      `Token0 Balance : ${ethers.formatEther(
        token0BalanceBefore
      )} -&gt; ${ethers.formatEther(token0BalanceAfter)}`
    );

    console.log(
      `Token1 Balance : ${ethers.formatEther(
        token1BalanceBefore
      )} -&gt; ${ethers.formatEther(token1BalanceAfter)}`
    );</code></pre>
</li>
</ul>
<blockquote>
<p>💡multicall 을 통해 함수를 실행할 때 주의해야할 점</p>
</blockquote>
<ul>
<li>msg.sender 가 multicall contract Address 가 되기 때문에 transfer, approve같은 메서드를 호출할 때 메서드 실행주체에 대해 잘 생각해야함.<blockquote>
</blockquote>
</li>
</ul>
<p>실행결과 tx 정보 확인 → <a href="https://testnet.snowtrace.io/tx/0x3395ec1e1888acc7e91f78f4b4969f6cde93e43d881fc37086f80bc8c2d8a94d/internal?chainid=43113">https://testnet.snowtrace.io/tx/0x3395ec1e1888acc7e91f78f4b4969f6cde93e43d881fc37086f80bc8c2d8a94d/internal?chainid=43113</a></p>
<hr>
<h1 id="multicall-contract-와-import-된-multicall-차이점">Multicall Contract 와 import 된 Multicall 차이점</h1>
<h2 id="multicall-contract">Multicall Contract</h2>
<p>Multicall Contract 를 통해 tx 를 전송하게 될 경우 구조는 다음과 같습니다.</p>
<p>[ Wallet ] → [ Multicall Contract ] → [ Target Contract ]</p>
<p>따라서 Target Contract 입장에서 tx의 주체, 즉 msg.sender 는 Multicall Contract가 됩니다.</p>
<p><strong>상황 예시</strong> </p>
<ul>
<li>내 지갑(W0)에 90 TokenA 가 있다.</li>
<li>(W1, W2, W3) 에 30 TokenA 씩 전송하려고 한다.</li>
<li>( token.transfer ) 트랜잭션 3개를 multicall로 전송한다.</li>
<li>multicall은 3개의 트랜잭션을 TokenA 컨트랙트로 전송한다.</li>
<li>이때, transfer의 호출 주체(msg.sender) 는 multicall contract 이고, multicall contract 는 tokenA 잔액이 0 이기 때문에 모든 트랜잭션이 revert 된다.</li>
</ul>
<p>코드를 살펴보면, multicall Contract 가 target Contract로 callData 를 그대로 전송하고 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/29ff5cd5-64a8-4c7e-83fb-ffeaa4783590/image.png" alt=""></p>
<h2 id="import-된-multicall">import 된 Multicall</h2>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/ed46ca9c-adb6-42ed-85dc-6b38b405d337/image.png" alt=""></p>
<p>위 사진과 같이 Multicall 을 상속받는 경우는 조금 다른 부분이 있습니다.</p>
<p>위 코드에서 상속받은 Multicall 코드를 살펴보면 다음과 같이 작성돼있습니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/cc8516ef-90ee-467c-a51c-62e3a7c85415/image.png" alt=""></p>
<p>여기서는 delegatecall 을 사용하여 tx를 처리하고 있음을 확인할 수 있습니다.</p>
<p>delegateCall 은 특정 함수를 호출할 때 msg.sender 와 msg.value를 그대로 유지하여 트랜잭션을 전송하게 됩니다.</p>
<h2 id="상황-비유">상황 비유</h2>
<p>예시 상황으로 두 방법에 대해 정리하자면,</p>
<p>상황 : 사람A 가 사람B 에게 중간 매개자(사람C)를 통해 물건을 전송하는 상황.</p>
<ul>
<li>Multicall Contract를 사용한다면<ol>
<li>사람A 가 사람C에게 물건을 전달한다. 물건에는 [보낸 사람 : 사람A] 라고 적혀있다.</li>
<li>사람C 가 사람B에게 물건을 전달한다. 물건에는 [보낸 사람 : 사람C] 라고 적혀있다.</li>
</ol>
</li>
<li>import된 Multicall 을 사용한다면<ol>
<li>사람A 가 사람C에게 물건을 전달한다. 물건에는 [보낸 사람 : 사람A] 라고 적혀있다.</li>
<li>사람C 가 사람B에게 물건을 전달한다. 물건에는 [보낸 사람 : 사람A] 라고 적혀있다.</li>
</ol>
</li>
</ul>
<h2 id="정리">정리</h2>
<ul>
<li>Multicall Contract<ul>
<li>&quot;한번에&quot; 여러개의 target Contract로, 여러개의 트랜잭션을 보낼 수 있는것</li>
<li>트랜잭션의 주체가 multicall Contract</li>
</ul>
</li>
<li>import된 Multicall<ul>
<li>한번에 여러개의 트랜잭션을 보낼 수 있지만, target Contract는 해당 컨트랙트로만 한정</li>
<li>트랜잭션의 주체가 wallet(caller)</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[UniswapV2 동작 이해하기 (1)]]></title>
            <link>https://velog.io/@na_treeeum/UniswapV2-%EB%8F%99%EC%9E%91-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-1</link>
            <guid>https://velog.io/@na_treeeum/UniswapV2-%EB%8F%99%EC%9E%91-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-1</guid>
            <pubDate>Tue, 04 Jun 2024 15:17:36 GMT</pubDate>
            <description><![CDATA[<h1 id="uniswap-v2-란">Uniswap V2 란?</h1>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/b284edfb-2a06-4fc2-8ea0-345305ca8855/image.png" alt=""></p>
<h1 id="cpmm">CPMM</h1>
<p>uniswapV2 에서는 CPMM (Constant Product Market Maker)을 사용합니다.
다른건 몰라도 이것만 알면 됩니다.</p>
<p><code>x * y = k</code></p>
<p>뭔가 복잡해보이는 이름에 걸맞지 않는 간단한 수식입니다.
매우 간단해 보여도 uniswapV2의 핵심이 되는 수식이니 잘 이해해야합니다.
위의 수식이 어떻게 사용되는지 예시를 통해 알아봅시다.</p>
<h1 id="유동성liquidity">유동성(Liquidity)</h1>
<p>유동성이란 거래를 위해 사용될 수 있는 자산을 의미합니다.</p>
<h1 id="swap">Swap</h1>
<p>swap은 &quot;교환&quot;과 같은 의미입니다.</p>
<p><img src="https://velog.velcdn.com/images/na_treeeum/post/4e9eed40-025f-4a7f-8e22-3ac78e021359/image.png" alt=""></p>
<p>사과를 구매하려는 사람의 이야기로 예시를 들어보겠습니다.</p>
<pre><code>금화를 1개 가지고 있는 사람A가 사과를 구매하려고 한다.

길을 가다 한 무인과일가게에 들어가서 보니
상자에 금화 100 개와 사과 101개가 들어있었다.

옆에 종이에는 이렇게 적혀있었다.

&quot;상자에 남아있는 금화와 사과의 개수의 곱은 항상 10100 이어야합니다.&quot;

사람A 는 상자에 금화를 한개 넣고 사과를 하나 뺐다.

상자에 남은건 금화 101개와 사과 100개로 두 수의 곱은 10100으로 변함이 없다.</code></pre><p>여기서 가장 핵심적이 되는 부분은 <strong>금화의 개수</strong>와 <strong>사과의 개수</strong>의 곱이 10100로 일정했다는 것입니다.
그렇다면 금화(x)와 사과(y)의 현재 개수를 알면 <strong>지불하는 금화(혹은 사과)의 양에 대해 수령하게 되는 사과(혹은 금화)의 양</strong>을 계산할 수 있다는 것입니다.</p>
<p>간단하게 수식으로 정리하면</p>
<ul>
<li>reserve0(금화 개수) = r0</li>
<li>reserve1(사과 개수) = r1</li>
<li>k = r0 * r1</li>
<li>amount0In(지불하는 금액) = amt0In</li>
<li>amount1Out(수령하는 금액) = amt1Out</li>
</ul>
<p><code>(r0 + amt0In)(r1 - amt1Out) = k</code></p>
<p>위 식을 다시 위의 사과예시에 대입해보면 다음과 같습니다.</p>
<p><code>k = r0(금화의 유동성=100) * r1(사과의 유동성=101) = 10100</code>
<code>(r0(금화의 유동성=100) + amt0In(A가 지불한 금화=1)) * (r1(사과의 유동성=101) - amt1Out(A가 받게 될 사과의 개수)) = k</code></p>
<h1 id="price-impact">Price Impact</h1>
<p>CPMM 특성 상 같은 금액을 지불해도 유동성 풀 규모에 따라 받게되는 금액이 차이가 납니다.</p>
<p><code>50개의 token0 을 token1로 swap</code> 하는 상황에서 예를 들어보겠습니다.</p>
<ul>
<li>금화가 100개, 사과가 1000개 라고 했을 때<ul>
<li>금화 : 사과 = 1 : 10</li>
<li>금화 50개에 대한 사과의 기대수량 = 500개</li>
<li>실제 스왑량<ul>
<li>(100 + 50) * (1000 - 사과) = 100000</li>
<li>사과 개수 ≈ 333.333</li>
</ul>
</li>
<li>PriceImpact = (1 - 실제수량 / 기대수량) * 100 ≈ 33.33%</li>
</ul>
</li>
<li>금화가 10,000,000개, 사과가 100,000,000개 라고 했을 때<ul>
<li>금화:사과 = 1: 10<ul>
<li>금화 50개에 대한 사과의 기대수량 : 500개</li>
</ul>
</li>
<li>실제 스왑량<ul>
<li>(10,000,000+50) * (100,000,000 - 사과) = 1,000,000,000,000,000</li>
<li>사과 개수 ≈ 499.998</li>
</ul>
</li>
<li>PriceImpact = (1 - 실제수량 / 기대수량) * 100 ≈ 0.0004%</li>
</ul>
</li>
</ul>
<p>유동성이 클 수록 기대수량에 대한 오차가 작아집니다.</p>
<hr>
<p>유동성 공급/제거와 수수료에 대한 내용은 다음편에서..</p>
]]></description>
        </item>
    </channel>
</rss>