<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://hsnks100.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://hsnks100.github.io/" rel="alternate" type="text/html" /><updated>2025-10-26T15:30:01+09:00</updated><id>https://hsnks100.github.io/feed.xml</id><title type="html">Kangssu’s programming world</title><subtitle>제가 접하는 많은 것들을 공유하고 싶습니다. 프로그래밍, 알고리즘, 시스템 개발 등  다양한 기술 이야기를 다룹니다.</subtitle><author><name>경수</name></author><entry><title type="html">Raspberry Pi Pico로 커스텀 게임 컨트롤러 만들기</title><link href="https://hsnks100.github.io/custom-game-controller-raspberry-pi-pico/" rel="alternate" type="text/html" title="Raspberry Pi Pico로 커스텀 게임 컨트롤러 만들기" /><published>2025-10-04T00:00:00+09:00</published><updated>2025-10-04T00:00:00+09:00</updated><id>https://hsnks100.github.io/custom-game-controller-raspberry-pi-pico</id><content type="html" xml:base="https://hsnks100.github.io/custom-game-controller-raspberry-pi-pico/"><![CDATA[<p>게임을 좋아하는 사람이라면 한 번쯤은 나만의 게임 컨트롤러를 만들어보고 싶었을 것입니다. <del>사실 그냥 사는 게 훨씬 싸고 편하긴 한데</del> 이번에는 Raspberry Pi Pico를 이용해서 완전히 커스텀한 게임 컨트롤러를 제작해보겠습니다. 돈도 더 들고 시간도 더 걸리고 결과물도 더 못하지만, 나만의 컨트롤러를 만드는 재미는 돈으로 살 수 없죠!</p>

<h2 id="준비물">준비물</h2>

<h3 id="하드웨어">하드웨어</h3>
<ul>
  <li><strong>[Raspberry Pi] 라즈베리파이 피코 (Raspberry Pi Pico)</strong> - 메인 컨트롤러</li>
  <li><strong>[럭킷] UL1007 AWG22 검정 10M</strong> - 전선</li>
  <li><strong>[ATTEN] 실습용 무연납 TS-99.38050 (0.8mm / 50g)</strong> - 납땜용</li>
  <li><strong>[SMG-A] 30mm Arcade Game Machine Switch (White) [SZH-ZR003]</strong> - 아케이드 버튼</li>
  <li><strong>[거상인] [RA3]보급형 원형만능기판(100*100_양면)</strong> - 회로 기판</li>
  <li><strong>[(주)엔티렉스] M3 황동 서포트 키트 [NT-KIT-M002]</strong> - 지지대</li>
  <li><strong>[CONNFLY (중국)] 핀헤더소켓 Single 1x40 Straight(2.54mm)</strong> - 피코 결합용</li>
  <li><strong>[SMG] 둥근머리 렌치볼트 + 너트 샘플키트(340pcs) [NT-KIT-LS027]</strong> - 결합용 나사</li>
</ul>

<h3 id="도구">도구</h3>
<ul>
  <li>납땜 인두</li>
  <li>3D 프린터 (또는 3D 프린팅 서비스)</li>
  <li>드릴, 파일 등 가공 도구</li>
</ul>

<h2 id="1단계-3d-모델링-및-프린팅">1단계: 3D 모델링 및 프린팅</h2>

<p><img src="/assets/img/game-controller/3dmodel.jpg" alt="3D 모델" /></p>

<p>먼저 TinkerCAD를 사용해서 컨트롤러 케이스를 모델링했습니다. 주요 설계 포인트는:</p>

<ul>
  <li><strong>M3 규격 나사</strong>로 결합할 수 있도록 설계</li>
  <li><strong>체리스위치 구멍</strong>: 40mm × 40mm 사각형으로 뚫음</li>
  <li><strong>아케이드 버튼 구멍</strong>: 버튼 스펙에 맞춰 원형으로 뚫음</li>
</ul>

<p>3D 프린터가 없어서 숨고에서 견적을 받아 진행했습니다. 비용은 약 5만원 정도 들었네요. <del>이미 여기서부터 사는 게 더 싸다는 걸 알고 있었지만</del></p>

<blockquote>
  <p>💡 <strong>팁</strong>: 사실 사는 게 훨씬 싸긴 합니다. 5만원이면 좋은 컨트롤러 2개는 살 수 있는데, 하지만 나만의 디자인으로 만드는 재미는 돈으로 살 수 없죠!</p>
</blockquote>

<h2 id="2단계-회로-구성">2단계: 회로 구성</h2>

<p><img src="/assets/img/game-controller/workspace.jpg" alt="작업 공간" /></p>

<h3 id="핀헤더-설치">핀헤더 설치</h3>
<p>만능기판에 핀헤더를 꽂고 피코를 연결했습니다.</p>

<p><img src="/assets/img/game-controller/pico.jpg" alt="피코 연결" /></p>

<h3 id="gnd-허브-구성">GND 허브 구성</h3>
<p>GND가 많이 필요해서 핀헤더를 이용해 GND 허브를 만들었습니다.</p>

<p><img src="/assets/img/game-controller/groundhub.jpg" alt="GND 허브" /></p>

<h3 id="간단한-테스트">간단한 테스트</h3>
<p>먼저 간단하게 버튼 확인용으로 게임 컨트롤러를 테스트해봤습니다. <a href="https://hardwaretester.com/gamepad">Hardware Tester</a>에서 테스트한 결과 잘 작동하는 것을 확인했습니다.</p>

<p><img src="/assets/img/game-controller/gamepadtester.jpg" alt="게임패드 테스터" /></p>

<h2 id="3단계-스위치-설치-및-납땜">3단계: 스위치 설치 및 납땜</h2>

<p><img src="/assets/img/game-controller/installbutton.jpg" alt="버튼 설치" /></p>

<p>3D 프린팅한 상판에 모든 스위치를 결합했습니다.</p>

<h3 id="납땜-작업">납땜 작업</h3>
<p>먼저 GND 연결부터 시작했습니다.</p>

<p><img src="/assets/img/game-controller/groundsolder.jpg" alt="GND 납땜" /></p>

<p>그리고 나머지 핀들도 각각의 핀에 맞춰서 납땜했습니다. 손가락도 몇 번 데고 납땜도 몇 번 다시 하고…</p>

<p><img src="/assets/img/game-controller/solderready.jpg" alt="납땜 준비" /></p>

<p><img src="/assets/img/game-controller/wiring.jpg" alt="배선" /></p>

<h2 id="4단계-코드-작성">4단계: 코드 작성</h2>

<p>Raspberry Pi Pico용 Arduino 코드입니다:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"Adafruit_TinyUSB.h"</span><span class="cp">
#include</span> <span class="cpf">"hardware/regs/sio.h"</span><span class="c1"> // sio_hw-&gt;gpio_in 사용을 위해 추가</span><span class="cp">
</span>
<span class="c1">// --- 핀 설정 (최대 16버튼 지원) ---</span>
<span class="c1">// 방향키 핀 (위: GP18, 아래: GP19, 왼쪽: GP20, 오른쪽: GP21)</span>
<span class="k">const</span> <span class="kt">int</span> <span class="n">directionPins</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span><span class="mi">18</span><span class="p">,</span> <span class="mi">19</span><span class="p">,</span> <span class="mi">20</span><span class="p">,</span> <span class="mi">21</span><span class="p">};</span> 

<span class="c1">// 액션 버튼 핀 (총 12개, B0 ~ B11 버튼에 순서대로 할당됨)</span>
<span class="k">const</span> <span class="kt">int</span> <span class="n">actionPins</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span>
    <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">11</span>
<span class="p">};</span> 

<span class="c1">// --- USB HID 장치 설정 ---</span>
<span class="kt">uint8_t</span> <span class="k">const</span> <span class="n">desc_hid_report</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span> <span class="n">TUD_HID_REPORT_DESC_GAMEPAD</span><span class="p">()</span> <span class="p">};</span>

<span class="n">Adafruit_USBD_HID</span> <span class="n">usb_hid</span><span class="p">;</span>
<span class="n">hid_gamepad_report_t</span> <span class="n">report</span><span class="p">;</span>
<span class="n">hid_gamepad_report_t</span> <span class="n">prevReport</span><span class="p">;</span>
<span class="kt">uint32_t</span> <span class="n">pin_mask</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

<span class="kt">void</span> <span class="n">report_init</span><span class="p">()</span> <span class="p">{</span>
  <span class="c1">// report와 prevReport를 모두 0으로 초기화</span>
  <span class="n">memset</span><span class="p">(</span><span class="o">&amp;</span><span class="n">report</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">report</span><span class="p">));</span>
  <span class="n">memcpy</span><span class="p">(</span><span class="o">&amp;</span><span class="n">prevReport</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">report</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">report</span><span class="p">));</span>
<span class="p">}</span>

<span class="c1">// hid_task: 모든 입력 처리 및 리포트 생성을 담당</span>
<span class="kt">void</span> <span class="n">hid_task</span><span class="p">()</span> <span class="p">{</span>
  <span class="kt">uint32_t</span> <span class="n">pins</span> <span class="o">=</span> <span class="o">~</span><span class="n">sio_hw</span><span class="o">-&gt;</span><span class="n">gpio_in</span> <span class="o">&amp;</span> <span class="n">pin_mask</span><span class="p">;</span>

  <span class="c1">// 1. 방향키 입력 상태 읽기</span>
  <span class="kt">bool</span> <span class="n">up_pressed</span>    <span class="o">=</span> <span class="p">(</span><span class="n">pins</span> <span class="o">&amp;</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">directionPins</span><span class="p">[</span><span class="mi">0</span><span class="p">]));</span>
  <span class="kt">bool</span> <span class="n">down_pressed</span>  <span class="o">=</span> <span class="p">(</span><span class="n">pins</span> <span class="o">&amp;</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">directionPins</span><span class="p">[</span><span class="mi">1</span><span class="p">]));</span>
  <span class="kt">bool</span> <span class="n">left_pressed</span>  <span class="o">=</span> <span class="p">(</span><span class="n">pins</span> <span class="o">&amp;</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">directionPins</span><span class="p">[</span><span class="mi">2</span><span class="p">]));</span>
  <span class="kt">bool</span> <span class="n">right_pressed</span> <span class="o">=</span> <span class="p">(</span><span class="n">pins</span> <span class="o">&amp;</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">directionPins</span><span class="p">[</span><span class="mi">3</span><span class="p">]));</span>

  <span class="c1">// 2. SOCD 처리 로직</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">left_pressed</span> <span class="o">&amp;&amp;</span> <span class="n">right_pressed</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">left_pressed</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
    <span class="n">right_pressed</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">up_pressed</span> <span class="o">&amp;&amp;</span> <span class="n">down_pressed</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">up_pressed</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
    <span class="n">down_pressed</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="c1">// 3. report.buttons 값만 계산 (hat, x, y는 사용 안 함)</span>
  <span class="kt">uint16_t</span> <span class="n">new_buttons</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
  
  <span class="c1">// 액션 버튼 매핑 (B0 ~ B11)</span>
  <span class="k">const</span> <span class="kt">int</span> <span class="n">numActionPins</span> <span class="o">=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">actionPins</span><span class="p">)</span> <span class="o">/</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">actionPins</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">numActionPins</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">pins</span> <span class="o">&amp;</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">actionPins</span><span class="p">[</span><span class="n">i</span><span class="p">]))</span> <span class="p">{</span>
      <span class="n">new_buttons</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">i</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="c1">// 방향키 버튼 매핑 (B12 ~ B15)</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">up_pressed</span><span class="p">)</span>    <span class="n">new_buttons</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="p">(</span><span class="n">numActionPins</span> <span class="o">+</span> <span class="mi">0</span><span class="p">));</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">down_pressed</span><span class="p">)</span>  <span class="n">new_buttons</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="p">(</span><span class="n">numActionPins</span> <span class="o">+</span> <span class="mi">1</span><span class="p">));</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">left_pressed</span><span class="p">)</span>  <span class="n">new_buttons</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="p">(</span><span class="n">numActionPins</span> <span class="o">+</span> <span class="mi">2</span><span class="p">));</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">right_pressed</span><span class="p">)</span> <span class="n">new_buttons</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="p">(</span><span class="n">numActionPins</span> <span class="o">+</span> <span class="mi">3</span><span class="p">));</span>
  
  <span class="n">report</span><span class="p">.</span><span class="n">buttons</span> <span class="o">=</span> <span class="n">new_buttons</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// send_report: 상태가 변경되었을 때만 리포트를 전송</span>
<span class="kt">void</span> <span class="n">send_report</span><span class="p">()</span> <span class="p">{</span>
  <span class="c1">// x, y, hat 등은 항상 0이므로 버튼 값만 비교</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">prevReport</span><span class="p">.</span><span class="n">buttons</span> <span class="o">!=</span> <span class="n">report</span><span class="p">.</span><span class="n">buttons</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span> <span class="n">TinyUSBDevice</span><span class="p">.</span><span class="n">mounted</span><span class="p">()</span> <span class="o">&amp;&amp;</span> <span class="n">usb_hid</span><span class="p">.</span><span class="n">ready</span><span class="p">()</span> <span class="p">)</span> <span class="p">{</span>
      <span class="n">usb_hid</span><span class="p">.</span><span class="n">sendReport</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">report</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">report</span><span class="p">));</span>
      <span class="n">memcpy</span><span class="p">(</span><span class="o">&amp;</span><span class="n">prevReport</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">report</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">report</span><span class="p">));</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">setup</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">pin</span> <span class="o">:</span> <span class="n">directionPins</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">pinMode</span><span class="p">(</span><span class="n">pin</span><span class="p">,</span> <span class="n">INPUT_PULLUP</span><span class="p">);</span>
    <span class="n">pin_mask</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">pin</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">pin</span> <span class="o">:</span> <span class="n">actionPins</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">pinMode</span><span class="p">(</span><span class="n">pin</span><span class="p">,</span> <span class="n">INPUT_PULLUP</span><span class="p">);</span>
    <span class="n">pin_mask</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">pin</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="n">report_init</span><span class="p">();</span>
  
  <span class="n">usb_hid</span><span class="p">.</span><span class="n">setReportDescriptor</span><span class="p">(</span><span class="n">desc_hid_report</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">desc_hid_report</span><span class="p">));</span>
  <span class="n">usb_hid</span><span class="p">.</span><span class="n">begin</span><span class="p">();</span>
  
  <span class="k">while</span> <span class="p">(</span> <span class="o">!</span><span class="n">TinyUSBDevice</span><span class="p">.</span><span class="n">mounted</span><span class="p">()</span> <span class="p">)</span> <span class="n">delay</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">loop</span><span class="p">()</span> <span class="p">{</span>
  <span class="n">hid_task</span><span class="p">();</span>
  <span class="n">send_report</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="코드-설명">코드 설명</h3>

<ul>
  <li><strong>16개 버튼 지원</strong>: 방향키 4개 + 액션 버튼 12개</li>
  <li><strong>SOCD 처리</strong>: 동시에 반대 방향을 누르면 무효화 (게임에서 일반적인 규칙)</li>
  <li><strong>입력 처리</strong>: 하드웨어 레지스터를 직접 사용</li>
  <li><strong>USB HID 호환</strong>: 표준 게임패드로 인식되어 대부분의 게임에서 사용 가능</li>
</ul>

<h2 id="5단계-최종-테스트">5단계: 최종 테스트</h2>

<p><img src="/assets/img/game-controller/checkpin.jpg" alt="핀 체크" /></p>

<p>모든 납땜이 완료된 후 다시 <a href="https://hardwaretester.com/gamepad">Hardware Tester</a>에서 테스트했습니다. 모든 버튼이 정상적으로 작동하는 것을 확인했습니다.</p>

<h2 id="6단계-실제-게임플레이-테스트">6단계: 실제 게임플레이 테스트</h2>

<p>이제 진짜 게임에서 테스트해볼 시간입니다! 드디어 이 고생의 결실을 맛볼 시간이 왔네요. 완성된 컨트롤러로 실제 게임을 플레이해보겠습니다.</p>

<p><img src="/assets/img/game-controller/gameplay1.jpg" alt="게임플레이 장면" /></p>

<h3 id="게임플레이-영상">게임플레이 영상</h3>

<video width="100%" controls="" preload="metadata">
  <source src="/assets/img/game-controller/gameplay.mp4" type="video/mp4" />
  <p>브라우저가 비디오 태그를 지원하지 않습니다. 
  <a href="/assets/img/game-controller/gameplay.mp4">여기서 영상을 다운로드</a>하세요.</p>
</video>

<blockquote>
  <p><strong>참고</strong>: 비디오가 재생되지 않는다면 위의 다운로드 링크를 클릭해보세요.</p>
</blockquote>

<h2 id="완성">완성!</h2>

<p><img src="/assets/img/game-controller/gamepadtester.jpg" alt="게임패드 테스터 결과" /></p>

<p>드디어 나만의 커스텀 게임 컨트롤러가 완성되었습니다!</p>

<h3 id="총평">총평</h3>
<p>직접 만들면 비싸니까 시제품 사세요 ㅠ <del>하지만 만드는 재미는 있으니까 한 번쯤은 해볼 만합니다</del></p>]]></content><author><name>경수</name></author><category term="DIY" /><category term="Hardware" /><category term="Raspberry Pi Pico" /><category term="3D Printing" /><category term="Game Controller" /><category term="Arduino" /><category term="DIY" /><summary type="html"><![CDATA[3D 프린팅과 아두이노 코딩으로 나만의 게임 컨트롤러를 제작해보자]]></summary></entry><entry><title type="html">구연산으로 공구 녹 제거하기 - 안전하고 효과적인 방법</title><link href="https://hsnks100.github.io/rust-removal-using-citric-acid/" rel="alternate" type="text/html" title="구연산으로 공구 녹 제거하기 - 안전하고 효과적인 방법" /><published>2025-09-29T00:00:00+09:00</published><updated>2025-09-29T00:00:00+09:00</updated><id>https://hsnks100.github.io/rust-removal-using-citric-acid</id><content type="html" xml:base="https://hsnks100.github.io/rust-removal-using-citric-acid/"><![CDATA[<h1 id="구연산으로-공구-녹-제거하기">구연산으로 공구 녹 제거하기</h1>

<p>오래된 공구들을 정리하다 보니 녹이 슨 것들이 꽤 있더라고요. 염산 같은 강산 쓰기엔 무섭고, 그냥 버리기엔 아까워서 구연산으로 녹 제거를 시도해봤습니다. 결과가 생각보다 괜찮아서 과정을 정리해봅니다.</p>

<h2 id="왜-구연산인가">왜 구연산인가?</h2>

<p>일단 구연산은 식품첨가물로도 쓰이는 천연 산이라 안전하고, 다이소에서도 쉽게 구할 수 있습니다. pH가 2 정도로 녹 제거에 충분한 산성도를 가지고 있어요.</p>

<p>다른 방법들과 비교해보면:</p>
<ul>
  <li><strong>염산</strong>: 효과는 좋지만 냄새도 심하고 위험함</li>
  <li><strong>인산</strong>: 비싸고 구하기 어려움</li>
  <li><strong>식초</strong>: 너무 약해서 시간이 오래 걸림</li>
  <li><strong>콜라</strong>: 인터넷에서 많이 나오는데 사실상 효과 없음</li>
</ul>

<p>구연산이 적당히 효과적이면서도 안전한 선택지더라고요.</p>

<h2 id="준비물">준비물</h2>

<p><img src="/assets/img/removerust/citric_acid.jpg" alt="구연산과 준비물들" /></p>

<h3 id="필요한-재료">필요한 재료</h3>
<ul>
  <li><strong>구연산</strong>: 다이소에서 500g짜리 팔아요. 2000원 정도면 충분</li>
  <li><strong>플라스틱 용기</strong>: PET병이나 플라스틱 통 아무거나</li>
  <li><strong>따뜻한 물</strong>: 미지근한 정도면 OK</li>
  <li><strong>칫솔</strong>: 마무리 청소용</li>
</ul>

<p><img src="/assets/img/removerust/pet.jpg" alt="플라스틱 용기 준비" /></p>

<h3 id="용액-만들기">용액 만들기</h3>
<p>물 1리터에 구연산 한 줌(50-100g) 정도 넣으면 됩니다. 정확히 재지 않아도 되고, 진해도 별 문제 없어요. 따뜻한 물에 넣으면 더 빨리 녹이 빠지더라고요.</p>

<h2 id="녹-제거-과정">녹 제거 과정</h2>

<h3 id="1단계-사전-준비">1단계: 사전 준비</h3>

<p>녹슨 공구들을 먼저 살펴봤습니다. 생각보다 심하게 녹이 슬어있네요.</p>

<p><img src="/assets/img/removerust/before.jpg" alt="녹슨 공구 - 처리 전" /></p>

<p>일단 표면의 기름기나 먼지는 미리 제거해주는 게 좋습니다. 중성세제로 가볍게 닦고 마른 천으로 물기 제거하고, 심한 녹은 사포로 살짝 긁어내는 정도면 충분해요.</p>

<h3 id="2단계-구연산-용액에-담그기">2단계: 구연산 용액에 담그기</h3>

<p>용액 만들어서 공구 넣고 기다리기만 하면 됩니다. 간단하죠?</p>

<p>알루미늄 공구는 좀 조심해야 하는데, 표면이 거칠어질 수 있어요. 그 외엔 별 문제 없더라고요.</p>

<h3 id="3단계-대기-시간">3단계: 대기 시간</h3>

<p>녹의 정도에 따라 시간이 달라집니다:</p>
<ul>
  <li><strong>가벼운 녹</strong>: 2-4시간</li>
  <li><strong>보통 녹</strong>: 6-12시간</li>
  <li><strong>심한 녹</strong>: 24시간 이상</li>
</ul>

<p>중간중간 확인해서 녹이 떨어지는 정도를 봅니다. 거품이 올라오고 물 색깔이 변하면 잘 되고 있는 거더라고요.</p>

<h3 id="4단계-마무리-작업">4단계: 마무리 작업</h3>

<p>시간이 지나고 건져보니 녹이 많이 제거됐습니다!</p>

<p><img src="/assets/img/removerust/after.jpg" alt="녹 제거 완료" /></p>

<p>남은 찌꺼기는 칫솔로 살살 문지르면 쉽게 제거되더라고요. 찬물로 깨끗하게 헹구고, 마른 천으로 물기 완전히 제거하고, 마지막에 기계유나 방청제 발라주면 끝!</p>

<h2 id="결과-및-후기">결과 및 후기</h2>

<p>결과적으로 80-90% 정도는 제거되더라고요. 표면 손상도 거의 없고 원래 금속 광택도 유지됩니다. 실제로 손이 가는 시간은 30분 정도고 나머지는 그냥 기다리기만 하면 되니까 편해요.</p>

<p>물론 한계점도 있습니다:</p>
<ul>
  <li>아주 깊은 녹은 완전히 제거하기 어려움</li>
  <li>염산보다는 시간이 오래 걸림</li>
  <li>알루미늄 공구는 좀 조심해야 함</li>
</ul>

<p>비용 면에서는 다이소 구연산 정말 저렴합니다.</p>

<h2 id="팁">팁</h2>

<p>몇 가지 팁을 정리해보면:</p>
<ul>
  <li>따뜻한 물 쓰면 더 빨라요</li>
  <li>심한 녹은 하룻밤 담가두기</li>
  <li>중간중간 칫솔로 문질러주면 더 좋음</li>
  <li>용액은 2-3번 재사용 가능</li>
</ul>

<h2 id="마무리">마무리</h2>

<p>구연산으로 녹 제거하는 방법, 생각보다 간단하죠? 완벽하게 새것처럼 되지는 않지만 80-90% 정도는 깨끗해지니까 충분히 만족스럽더라고요.</p>

<p>오래된 공구들이 다시 살아나니까 뭔가 뿌듯해요. 다이소에서 구연산 하나 사다가 한번 시도해보세요!</p>

<h2 id="추가로-쓸-수-있는-곳">추가로 쓸 수 있는 곳</h2>

<p>구연산 사놓으면 다른 곳에도 쓸 수 있어요:</p>
<ul>
  <li>주전자 물때 제거</li>
  <li>변기 청소</li>
  <li>세탁기 청소</li>
  <li>커피머신 청소</li>
</ul>

<p>한 번 사면 정말 오래 쓸 수 있습니다!</p>]]></content><author><name>경수</name></author><category term="diy" /><category term="rust-removal" /><category term="citric-acid" /><category term="tools" /><category term="maintenance" /><category term="restoration" /><summary type="html"><![CDATA[구연산으로 공구 녹 제거하기]]></summary></entry><entry><title type="html">DHT22와 Grafana, Prometheus를 이용한 집안 온습도 모니터링 시스템</title><link href="https://hsnks100.github.io/dht22-grafana-prometheus-temperature-humidity-monitoring/" rel="alternate" type="text/html" title="DHT22와 Grafana, Prometheus를 이용한 집안 온습도 모니터링 시스템" /><published>2025-09-28T00:00:00+09:00</published><updated>2025-09-28T00:00:00+09:00</updated><id>https://hsnks100.github.io/dht22-grafana-prometheus-temperature-humidity-monitoring</id><content type="html" xml:base="https://hsnks100.github.io/dht22-grafana-prometheus-temperature-humidity-monitoring/"><![CDATA[<h1 id="dht22와-grafana-prometheus를-이용한-집안-온습도-모니터링-시스템">DHT22와 Grafana, Prometheus를 이용한 집안 온습도 모니터링 시스템</h1>

<p>집안 온습도를 실시간으로 모니터링하고 싶어서 만든 IoT 시스템입니다. ESP32와 DHT22 센서로 데이터를 수집하고, Prometheus와 Grafana로 시각화하는 간단한 프로젝트인데 생각보다 잘 동작하네요.</p>

<h2 id="시스템-구성">시스템 구성</h2>

<p>전체적인 흐름은 이렇습니다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ESP32 + DHT22 → API 서버 → Prometheus → Grafana
</code></pre></div></div>

<ul>
  <li><strong>ESP32</strong>: 5분마다 깨어나서 DHT22에서 온습도 읽고 WiFi로 서버에 전송</li>
  <li><strong>API 서버</strong>: ESP32 데이터 받아서 Prometheus 메트릭으로 변환</li>
  <li><strong>Prometheus</strong>: 데이터 저장 (15일간 보관)</li>
  <li><strong>Grafana</strong>: 예쁜 그래프로 시각화</li>
</ul>

<h2 id="하드웨어-구성">하드웨어 구성</h2>

<h3 id="필요한-부품들">필요한 부품들</h3>
<ul>
  <li>ESP32 개발보드 (아무거나)</li>
  <li>DHT22 온습도 센서 (3000원 정도)</li>
  <li>점퍼 와이어</li>
  <li>브레드보드 (테스트용)</li>
  <li>만능기판 (영구 제작용)</li>
  <li>케이스 (플라스틱 박스)</li>
  <li>USB 연장 케이블 (Micro 5Pin, 50cm)</li>
  <li>납땜 도구들</li>
  <li>보조배터리 (집에 굴러다니는 거 사용)</li>
</ul>

<h3 id="1단계-브레드보드로-테스트">1단계: 브레드보드로 테스트</h3>

<p>일단 브레드보드에 회로 꽂아서 제대로 동작하는지 확인해봤습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>DHT22 센서 연결:
- VCC → ESP32 3.3V
- GND → ESP32 GND  
- DATA → ESP32 GPIO 4
</code></pre></div></div>

<p><img src="/assets/img/grafana/circuit.png" alt="브레드보드 테스트 회로도" />
<img src="/assets/img/grafana/bread.jpg" alt="브레드보드실제" /></p>

<p>브레드보드에서 확인한 것들:</p>
<ul>
  <li>DHT22 센서가 제대로 값 읽는지</li>
  <li>WiFi 연결 잘 되는지</li>
  <li>Deep Sleep 모드로 전력 절약되는지</li>
  <li>데이터 전송 잘 되는지</li>
</ul>

<h3 id="2단계-만능기판에-납땜">2단계: 만능기판에 납땜</h3>

<p>테스트가 잘 되니까 이제 만능기판에 영구적으로 납땜했습니다.</p>

<p><img src="/assets/img/grafana/solder.jpg" alt="만능기판 회로도" /></p>

<p><strong>만능기판 제작 과정:</strong></p>
<ol>
  <li>ESP32와 DHT22 센서 위치 정하기</li>
  <li>점퍼 와이어로 연결할 핀들 확인</li>
  <li>납땜으로 안정적으로 연결</li>
  <li>다시 한번 테스트해보기</li>
</ol>

<h3 id="3단계-케이스-만들기">3단계: 케이스 만들기</h3>

<p>만능기판이 완성되니까 이제 케이스에 넣어야겠더라고요.</p>

<p><strong>케이스 제작 과정:</strong></p>

<ol>
  <li><strong>케이스 고르기</strong>: 적당한 크기의 플라스틱 박스 구했음</li>
  <li><strong>구멍 뚫기</strong>:
    <ul>
      <li>USB 포트용 구멍: 드라이버 달궈서 플라스틱 녹여서 구멍 만들기</li>
      <li>부족한 부분은 핀바이스로 넓혀서 맞춤</li>
      <li>가운데 구멍은 못 쓰는 인두기로 뚫어서 정리</li>
    </ul>
  </li>
  <li><strong>USB 연장 케이블 설치</strong>:
    <ul>
      <li>[Coms] USB 연장 포트 케이블 - Micro 5Pin (M)/(F)브라켓연결용 판넬형, 50cm, Black[NE776] 사용</li>
      <li>케이스 안에서 ESP32 USB 포트와 연결</li>
      <li>케이스 밖으로 USB 포트 노출</li>
    </ul>
  </li>
</ol>

<p><img src="/assets/img/grafana/drill.jpg" alt="뚫기" />
<img src="/assets/img/grafana/drill-in.jpg" alt="뚫기-안" /></p>

<h2 id="esp32-펌웨어-코드">ESP32 펌웨어 코드</h2>

<p>ESP32는 Deep Sleep 모드로 전력 절약하면서 5분마다 깨어나서 센서 데이터 읽고 전송합니다.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;WiFi.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;WiFiClientSecure.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;HTTPClient.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;DHT.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;ArduinoJson.h&gt;</span><span class="cp">
</span>
<span class="c1">// WiFi 설정</span>
<span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">ssid</span> <span class="o">=</span> <span class="s">"YOUR_WIFI_SSID"</span><span class="p">;</span>
<span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">password</span> <span class="o">=</span> <span class="s">"YOUR_WIFI_PASSWORD"</span><span class="p">;</span>

<span class="c1">// 센서 설정</span>
<span class="cp">#define DHTPIN 4
#define DHTTYPE DHT22
</span>
<span class="c1">// API 설정</span>
<span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">serverUrl</span> <span class="o">=</span> <span class="s">"https://your-domain.com/api/env"</span><span class="p">;</span>
<span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">location</span> <span class="o">=</span> <span class="s">"living_room"</span><span class="p">;</span>

<span class="c1">// Deep Sleep 설정</span>
<span class="cp">#define uS_TO_S_FACTOR 1000000ULL  // 마이크로초 -&gt; 초 변환
#define TIME_TO_SLEEP  300         // 5분 (300초)
</span>
<span class="c1">// RTC 메모리에 저장 (Deep Sleep 중에도 유지)</span>
<span class="n">RTC_DATA_ATTR</span> <span class="kt">int</span> <span class="n">bootCount</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

<span class="n">DHT</span> <span class="nf">dht</span><span class="p">(</span><span class="n">DHTPIN</span><span class="p">,</span> <span class="n">DHTTYPE</span><span class="p">);</span>

<span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="mi">115200</span><span class="p">);</span>
  <span class="n">bootCount</span><span class="o">++</span><span class="p">;</span>
  
  <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"부팅 횟수: "</span><span class="p">);</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="n">bootCount</span><span class="p">);</span>
  
  <span class="c1">// 센서 초기화</span>
  <span class="n">dht</span><span class="p">.</span><span class="n">begin</span><span class="p">();</span>
  <span class="n">delay</span><span class="p">(</span><span class="mi">2000</span><span class="p">);</span>  <span class="c1">// DHT22 안정화</span>
  
  <span class="c1">// 센서 읽기</span>
  <span class="kt">float</span> <span class="n">humidity</span> <span class="o">=</span> <span class="n">dht</span><span class="p">.</span><span class="n">readHumidity</span><span class="p">();</span>
  <span class="kt">float</span> <span class="n">temperature</span> <span class="o">=</span> <span class="n">dht</span><span class="p">.</span><span class="n">readTemperature</span><span class="p">();</span>
  
  <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"온도: "</span><span class="p">);</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="n">temperature</span><span class="p">);</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"°C, 습도: "</span><span class="p">);</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="n">humidity</span><span class="p">);</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"%"</span><span class="p">);</span>
  
  <span class="c1">// 센서 값 확인</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isnan</span><span class="p">(</span><span class="n">humidity</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">isnan</span><span class="p">(</span><span class="n">temperature</span><span class="p">))</span> <span class="p">{</span>
    <span class="c1">// WiFi 연결 (전송할 때만)</span>
    <span class="n">connectWiFi</span><span class="p">();</span>
    
    <span class="c1">// 데이터 전송</span>
    <span class="n">sendData</span><span class="p">(</span><span class="n">temperature</span><span class="p">,</span> <span class="n">humidity</span><span class="p">);</span>
    
    <span class="c1">// WiFi 연결 해제 (전력 절약)</span>
    <span class="n">WiFi</span><span class="p">.</span><span class="n">disconnect</span><span class="p">(</span><span class="nb">true</span><span class="p">);</span>
    <span class="n">WiFi</span><span class="p">.</span><span class="n">mode</span><span class="p">(</span><span class="n">WIFI_OFF</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"센서 읽기 실패!"</span><span class="p">);</span>
  <span class="p">}</span>
  
  <span class="c1">// Deep Sleep 설정</span>
  <span class="n">esp_sleep_enable_timer_wakeup</span><span class="p">(</span><span class="n">TIME_TO_SLEEP</span> <span class="o">*</span> <span class="n">uS_TO_S_FACTOR</span><span class="p">);</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"5분 후 깨어납니다..."</span><span class="p">);</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">flush</span><span class="p">();</span>
  
  <span class="c1">// Deep Sleep 진입 (전력 소비 ~10μA)</span>
  <span class="n">esp_deep_sleep_start</span><span class="p">();</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">loop</span><span class="p">()</span> <span class="p">{</span>
  <span class="c1">// Deep Sleep 사용시 loop는 실행되지 않음</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">connectWiFi</span><span class="p">()</span> <span class="p">{</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"WiFi 연결 중..."</span><span class="p">);</span>
  <span class="n">WiFi</span><span class="p">.</span><span class="n">mode</span><span class="p">(</span><span class="n">WIFI_STA</span><span class="p">);</span>
  <span class="n">WiFi</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="n">ssid</span><span class="p">,</span> <span class="n">password</span><span class="p">);</span>
  
  <span class="kt">int</span> <span class="n">attempts</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
  <span class="k">while</span> <span class="p">(</span><span class="n">WiFi</span><span class="p">.</span><span class="n">status</span><span class="p">()</span> <span class="o">!=</span> <span class="n">WL_CONNECTED</span> <span class="o">&amp;&amp;</span> <span class="n">attempts</span> <span class="o">&lt;</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">delay</span><span class="p">(</span><span class="mi">500</span><span class="p">);</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"."</span><span class="p">);</span>
    <span class="n">attempts</span><span class="o">++</span><span class="p">;</span>
  <span class="p">}</span>
  
  <span class="k">if</span> <span class="p">(</span><span class="n">WiFi</span><span class="p">.</span><span class="n">status</span><span class="p">()</span> <span class="o">==</span> <span class="n">WL_CONNECTED</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">" 연결됨!"</span><span class="p">);</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"IP: "</span><span class="p">);</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="n">WiFi</span><span class="p">.</span><span class="n">localIP</span><span class="p">());</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">" 연결 실패!"</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">sendData</span><span class="p">(</span><span class="kt">float</span> <span class="n">temp</span><span class="p">,</span> <span class="kt">float</span> <span class="n">hum</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">WiFi</span><span class="p">.</span><span class="n">status</span><span class="p">()</span> <span class="o">!=</span> <span class="n">WL_CONNECTED</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"WiFi 연결 없음"</span><span class="p">);</span>
    <span class="k">return</span><span class="p">;</span>
  <span class="p">}</span>
  
  <span class="n">WiFiClientSecure</span> <span class="n">client</span><span class="p">;</span>
  <span class="n">HTTPClient</span> <span class="n">https</span><span class="p">;</span>
  
  <span class="c1">// HTTPS 설정</span>
  <span class="n">client</span><span class="p">.</span><span class="n">setInsecure</span><span class="p">();</span>
  
  <span class="c1">// JSON 생성</span>
  <span class="n">StaticJsonDocument</span><span class="o">&lt;</span><span class="mi">200</span><span class="o">&gt;</span> <span class="n">doc</span><span class="p">;</span>
  <span class="n">doc</span><span class="p">[</span><span class="s">"temperature"</span><span class="p">]</span> <span class="o">=</span> <span class="n">temp</span><span class="p">;</span>
  <span class="n">doc</span><span class="p">[</span><span class="s">"humidity"</span><span class="p">]</span> <span class="o">=</span> <span class="n">hum</span><span class="p">;</span>
  <span class="n">doc</span><span class="p">[</span><span class="s">"location"</span><span class="p">]</span> <span class="o">=</span> <span class="n">location</span><span class="p">;</span>
  
  <span class="n">String</span> <span class="n">payload</span><span class="p">;</span>
  <span class="n">serializeJson</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="n">payload</span><span class="p">);</span>
  
  <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"전송: "</span><span class="p">);</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="n">payload</span><span class="p">);</span>
  
  <span class="c1">// POST 전송</span>
  <span class="n">https</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="n">client</span><span class="p">,</span> <span class="n">serverUrl</span><span class="p">);</span>
  <span class="n">https</span><span class="p">.</span><span class="n">addHeader</span><span class="p">(</span><span class="s">"Content-Type"</span><span class="p">,</span> <span class="s">"application/json"</span><span class="p">);</span>
  
  <span class="kt">int</span> <span class="n">httpCode</span> <span class="o">=</span> <span class="n">https</span><span class="p">.</span><span class="n">POST</span><span class="p">(</span><span class="n">payload</span><span class="p">);</span>
  
  <span class="k">if</span> <span class="p">(</span><span class="n">httpCode</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"응답: "</span><span class="p">);</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="n">httpCode</span><span class="p">);</span>
    
    <span class="k">if</span> <span class="p">(</span><span class="n">httpCode</span> <span class="o">==</span> <span class="n">HTTP_CODE_OK</span> <span class="o">||</span> <span class="n">httpCode</span> <span class="o">==</span> <span class="n">HTTP_CODE_CREATED</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"전송 성공!"</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"에러: "</span><span class="p">);</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="n">https</span><span class="p">.</span><span class="n">errorToString</span><span class="p">(</span><span class="n">httpCode</span><span class="p">));</span>
  <span class="p">}</span>
  
  <span class="n">https</span><span class="p">.</span><span class="n">end</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="docker-compose-설정">Docker Compose 설정</h2>

<p>Grafana와 Prometheus를 Docker Compose로 구성합니다:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3'</span>
<span class="na">services</span><span class="pi">:</span>
  <span class="na">prometheus</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">prom/prometheus:v2.53.0</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">prometheus</span>
    <span class="na">user</span><span class="pi">:</span> <span class="s">root</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./prometheus/config/prometheus.yml:/etc/prometheus/prometheus.yml:ro</span>
      <span class="pi">-</span> <span class="s">./data/prometheus:/prometheus</span>
    <span class="na">command</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">--config.file=/etc/prometheus/prometheus.yml'</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">--storage.tsdb.path=/prometheus'</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">--storage.tsdb.retention.time=15d'</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">--web.enable-lifecycle'</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">monitoring</span>

  <span class="na">grafana</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">grafana/grafana:11.0.0</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">grafana</span>
    <span class="na">user</span><span class="pi">:</span> <span class="s">root</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">GF_SECURITY_ADMIN_USER=admin</span>
      <span class="pi">-</span> <span class="s">GF_SECURITY_ADMIN_PASSWORD=your_password</span>
      <span class="pi">-</span> <span class="s">GF_USERS_ALLOW_SIGN_UP=false</span>
      <span class="pi">-</span> <span class="s">GF_INSTALL_PLUGINS=grafana-piechart-panel</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./data/grafana:/var/lib/grafana</span>
      <span class="pi">-</span> <span class="s">./grafana/provisioning:/etc/grafana/provisioning</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">3000:3000"</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">prometheus</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">monitoring</span>

<span class="na">networks</span><span class="pi">:</span>
  <span class="na">monitoring</span><span class="pi">:</span>
    <span class="na">driver</span><span class="pi">:</span> <span class="s">bridge</span>
</code></pre></div></div>

<h2 id="prometheus-설정">Prometheus 설정</h2>

<p><code class="language-plaintext highlighter-rouge">prometheus/config/prometheus.yml</code> 파일을 생성합니다:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">global</span><span class="pi">:</span>
  <span class="na">scrape_interval</span><span class="pi">:</span> <span class="s">15s</span>
  <span class="na">evaluation_interval</span><span class="pi">:</span> <span class="s">15s</span>

<span class="na">scrape_configs</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">job_name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">temperature-humidity-api'</span>
    <span class="na">scheme</span><span class="pi">:</span> <span class="s">https</span>
    <span class="na">metrics_path</span><span class="pi">:</span> <span class="s">/prometheus</span>
    <span class="na">static_configs</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">targets</span><span class="pi">:</span> <span class="pi">[</span><span class="s1">'</span><span class="s">your-api-server.com'</span><span class="pi">]</span>
</code></pre></div></div>

<p>이 설정은 API 서버의 <code class="language-plaintext highlighter-rouge">/prometheus</code> 엔드포인트에서 HTTPS로 메트릭을 수집합니다.</p>

<h2 id="api-서버-구현">API 서버 구현</h2>

<p>ESP32에서 전송된 데이터를 받아 Prometheus 메트릭으로 변환하는 API 서버가 필요합니다.</p>

<p><strong>API 서버의 주요 기능:</strong></p>
<ol>
  <li>ESP32에서 전송된 JSON 데이터 수신 (<code class="language-plaintext highlighter-rouge">/api/env</code> 엔드포인트)</li>
  <li>온도/습도 데이터를 Prometheus 메트릭으로 변환</li>
  <li>Prometheus가 수집할 수 있는 <code class="language-plaintext highlighter-rouge">/prometheus</code> 엔드포인트 제공</li>
</ol>

<p><strong>구현 방법:</strong></p>
<ul>
  <li>Node.js + Express + prom-client 라이브러리 사용</li>
  <li>또는 Python + Flask + prometheus_client 라이브러리 사용</li>
  <li>또는 다른 언어/프레임워크로 구현 가능</li>
</ul>

<p>API 서버는 ESP32에서 전송된 데이터를 받아서 Prometheus 형식의 메트릭으로 변환하여 저장하는 역할을 합니다.</p>

<h2 id="grafana-대시보드-설정">Grafana 대시보드 설정</h2>

<h3 id="데이터-소스-설정">데이터 소스 설정</h3>

<p>Grafana 데이터 소스는 <code class="language-plaintext highlighter-rouge">grafana/provisioning/datasources/datasource.yml</code> 파일로 자동 설정됩니다:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="m">1</span>

<span class="na">datasources</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Prometheus</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">prometheus</span>
    <span class="na">access</span><span class="pi">:</span> <span class="s">proxy</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s">http://prometheus:9090</span>
    <span class="na">isDefault</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p>이 설정으로 Grafana가 자동으로 Prometheus를 기본 데이터 소스로 사용합니다.</p>

<h3 id="대시보드-설정">대시보드 설정</h3>

<p><strong>Grafana 접속</strong>: http://localhost:3000 (각자 설정한 관리자 계정 사용)
<strong>대시보드 생성</strong>: Dashboard → New Dashboard
<strong>패널 추가</strong>: 온도/습도 그래프 생성</p>
<ul>
  <li>Query: <code class="language-plaintext highlighter-rouge">temperature_celsius{location="living_room"}</code></li>
  <li>Query: <code class="language-plaintext highlighter-rouge">humidity_percent{location="living_room"}</code></li>
</ul>

<p><img src="/assets/img/grafana/photo_2025-09-27_22-45-41.jpg" alt="Grafana 대시보드" /></p>

<p>실제로 동작하는 Grafana 대시보드입니다. 온도와 습도가 실시간으로 그래프에 표시되고 있어요.</p>

<h2 id="전력-소비-최적화">전력 소비 최적화</h2>

<ul>
  <li><strong>Deep Sleep 모드</strong>: ESP32가 5분마다 깨어나서 데이터 전송 후 다시 잠들어서 전력 절약</li>
  <li><strong>WiFi 연결 최적화</strong>: 데이터 전송할 때만 WiFi 연결하고 바로 해제</li>
  <li><strong>배터리 수명</strong>: 집에 굴러다니는 보조배터리로도 충분히 동작 (약 1-2개월)</li>
</ul>

<h2 id="제작-과정-요약">제작 과정 요약</h2>

<h3 id="브레드보드--만능기판--케이스-순서로-진행">브레드보드 → 만능기판 → 케이스 순서로 진행</h3>

<p><strong>브레드보드 테스트</strong>: 회로 동작 확인하고 펌웨어 테스트
<strong>만능기판 제작</strong>: 안정적으로 납땜해서 영구적인 회로 구성
<strong>케이스 제작</strong>:</p>
<ul>
  <li>플라스틱 케이스에 구멍 뚫기 (드라이버 가열 + 핀바이스 + 인두기)</li>
  <li>USB 연장 케이블 설치해서 외부에서 접근 가능하게</li>
  <li>DHT22 센서 노출을 위한 구멍 뚫기</li>
</ul>

<h3 id="제작할-때-주의할-점">제작할 때 주의할 점</h3>

<ul>
  <li><strong>브레드보드 테스트</strong>: 모든 기능이 제대로 동작하는지 확인하고 다음 단계로</li>
  <li><strong>만능기판 납땜</strong>: 연결이 안정적이도록 솔더 충분히 사용</li>
  <li><strong>케이스 구멍</strong>: USB 포트와 센서 노출을 위한 구멍을 정확한 위치에 뚫기</li>
  <li><strong>USB 케이블</strong>: 50cm 길이로 충분한 여유 두고 설치</li>
</ul>

<h2 id="마무리">마무리</h2>

<p>DHT22 센서와 Grafana, Prometheus로 집안 온습도를 모니터링하는 시스템을 만들어봤습니다. Deep Sleep으로 전력 절약하면서도 장기간 안정적으로 동작하네요.. ㅎㅎ</p>]]></content><author><name>경수</name></author><category term="iot" /><category term="dht22" /><category term="grafana" /><category term="prometheus" /><category term="temperature" /><category term="humidity" /><category term="monitoring" /><category term="esp32" /><category term="arduino" /><summary type="html"><![CDATA[DHT22와 Grafana, Prometheus를 이용한 집안 온습도 모니터링 시스템]]></summary></entry><entry><title type="html">Kaillera 의 게임입력 처리방법</title><link href="https://hsnks100.github.io/kaillera-%EC%9D%98-%EA%B2%8C%EC%9E%84%EC%9E%85%EB%A0%A5-%EC%B2%98%EB%A6%AC%EB%B0%A9%EB%B2%95/" rel="alternate" type="text/html" title="Kaillera 의 게임입력 처리방법" /><published>2022-12-05T00:00:00+09:00</published><updated>2022-12-05T00:00:00+09:00</updated><id>https://hsnks100.github.io/kaillera-%EC%9D%98-%EA%B2%8C%EC%9E%84%EC%9E%85%EB%A0%A5-%EC%B2%98%EB%A6%AC%EB%B0%A9%EB%B2%95</id><content type="html" xml:base="https://hsnks100.github.io/kaillera-%EC%9D%98-%EA%B2%8C%EC%9E%84%EC%9E%85%EB%A0%A5-%EC%B2%98%EB%A6%AC%EB%B0%A9%EB%B2%95/"><![CDATA[<h1 id="카일레라란">카일레라란?</h1>

<blockquote>
  <p>Kaillera enables emulators to play on the Internet.
With Kaillera you can enjoy playing video games with others from all over the world.
it consists of a client and a server. The client is usually embedded into your favorite emulator and the server is a stand-alone application that needs to be run on a machine directly wired to the Internet.</p>
</blockquote>

<p>Kaillera는 에뮬레이터가 인터넷을 통해 넷플레이를 할 수 있게 해주는 서버/클라이언트 프로토콜이다.</p>

<p>기본적으로 UDP 프로토콜을 사용하며, UDP 특성상 패킷 누락을 보완하기 위해 서버/클라이언트는 최근 패킷들을 함께 전송한다.</p>

<p>예를 들어 패킷 하나를 캡처하면:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0000   03 07 00 04 00 0b 00 ff ff 06 00 09 00 08 00 2e
0010   2e 2e 2e 2e 2e 00 05 00 0e 00 0c 00 00 00 00 00
0020   00 00 00 00 00 ff ff 02
Kaillera Protocol
    Messages: 3 -- 1 byte
    Message Sequence: 7 -- 2 bytes
    len(message): 4 -- 2 bytes
    Message Type: 0x0b -- 1 byte
    Message Sequence: 6
    len(message): 9
    Message Type: 0x08
    Message Sequence: 5
    len(message): 14
    Message Type: 0x0c
</code></pre></div></div>

<p>위와 같이 몇 개의 Messages를 전송하는지 명시하고, 그 개수만큼 최근 보낸 패킷들을 함께 전송한다.</p>

<p>서버/클라이언트는 항상 누락된 패킷에 대한 복구 로직을 가져야 하며, 누락이 발생하면 <code class="language-plaintext highlighter-rouge">게임 갈림</code> 현상이 발생한다.</p>

<blockquote>
  <p><strong>참고:</strong> 이 문서는 공식 Kaillera 프로토콜 문서에서 설명하지 않는 <strong>실제 동작 메커니즘</strong>을 Wireshark 패킷 분석과 역공학을 통해 발견한 내용을 담고 있다. 특히 플레이어별 캐싱, 프레임 인터리빙, 다중 지연 동기화 등의 핵심 메커니즘은 공식 문서에 없는 내용이다.</p>
</blockquote>

<h1 id="게임-입력-동기화-시스템">게임 입력 동기화 시스템</h1>

<p>Kaillera는 모든 참여자가 입력 데이터를 전달하면, 서버가 입력을 취합하여 모든 참여자에게 브로드캐스팅한다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>P1 -&gt; Server: A 키
P2 -&gt; Server: 아무키도 안 누름
Server -&gt; P1: [P1의 입력][P2의 입력]
Server -&gt; P2: [P1의 입력][P2의 입력]
</code></pre></div></div>

<p>Kaillera는 대역폭을 절약하기 위해 <code class="language-plaintext highlighter-rouge">GameData</code>와 <code class="language-plaintext highlighter-rouge">GameCache</code> 개념을 사용한다.</p>

<h2 id="패킷-포맷">패킷 포맷</h2>

<h3 id="game-data-0x12">Game Data (0x12)</h3>

<p><strong>클라이언트 → 서버:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+------+------+-------------------+
| 0x12 | 0x00 | Input Data        |
+------+------+-------------------+
  1B     1B     N bytes (N = delay × 2)
</code></pre></div></div>

<p><strong>서버 → 클라이언트:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+------+------+----------------------------+
| 0x12 | 0x00 | Combined Data              |
+------+------+----------------------------+
  1B     1B     player_count × delay × 2 bytes
</code></pre></div></div>

<h3 id="game-cache-0x13">Game Cache (0x13)</h3>

<p><strong>클라이언트 → 서버:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+------+------+----------+
| 0x13 | 0x00 | Position |
+------+------+----------+
  1B     1B       1B
</code></pre></div></div>

<p><strong>서버 → 클라이언트:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+------+------+----------+
| 0x13 | 0x00 | Position |
+------+------+----------+
  1B     1B       1B
</code></pre></div></div>

<h2 id="gamedata와-gamecache">GameData와 GameCache</h2>

<p><code class="language-plaintext highlighter-rouge">GameData</code>는 실제 입력 데이터이고, <code class="language-plaintext highlighter-rouge">GameCache</code>는 캐싱된 데이터의 슬롯 위치를 참조하는 방식이다.</p>

<p>Kaillera는 매 프레임마다 입력값을 서버로 전송하여 클라이언트들과 동기화한다. 기본적으로 각 프레임의 입력은 2바이트이다.</p>

<h3 id="기본-예제">기본 예제</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># GameData 전송
P1 -&gt; Server: [0x12][0x00][0x03][0x00]
P2 -&gt; Server: [0x12][0x00][0x00][0x00]
Server -&gt; P1: [0x12][0x00][0x03][0x00][0x00][0x00]
Server -&gt; P2: [0x12][0x00][0x03][0x00][0x00][0x00]
</code></pre></div></div>

<p>플레이어가 2명이므로 각 2바이트씩 받아서 4바이트를 응답한다.</p>

<p>다음 프레임에서 P1이 동일한 입력 <code class="language-plaintext highlighter-rouge">0x03 0x00</code>을 보낼 때, 실제 데이터를 보내지 않고 <code class="language-plaintext highlighter-rouge">GameCache</code>를 사용한다.</p>

<p>클라이언트는 보낸 입력 <code class="language-plaintext highlighter-rouge">0x03 0x00</code>에 대한 슬롯을 할당한다. 예를 들어 슬롯 1번에 할당했다면:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># GameCache 사용
P1 -&gt; Server: [0x13][0x00][0x01]  # 클라이언트 캐시 슬롯 번호
P2 -&gt; Server: [0x12][0x00][0x04][0x00]  # 새 입력
Server -&gt; P1: [0x12][0x00][0x03][0x00][0x04][0x00]
Server -&gt; P2: [0x12][0x00][0x03][0x00][0x04][0x00]

# 다음 프레임
P1 -&gt; Server: [0x13][0x00][0x01]  # P1 캐시 슬롯
P2 -&gt; Server: [0x13][0x00][0x01]  # P2 캐시 슬롯
Server -&gt; P1: [0x13][0x00][0x02]  # 서버 캐시 슬롯 (0x03 0x00 0x04 0x00)
Server -&gt; P2: [0x13][0x00][0x02]  # 서버 캐시 슬롯
</code></pre></div></div>

<p>반복되는 입력에 대해 서버/클라이언트가 각각 슬롯을 관리하므로, 슬롯 번호만 전송해도 어떤 입력인지 알 수 있다.</p>

<h2 id="캐시-시스템-상세">캐시 시스템 상세</h2>

<h3 id="아키텍처">아키텍처</h3>

<ul>
  <li><strong>256개의 FIFO 캐시 슬롯</strong> (위치 0-255, circular buffer로 순환)</li>
  <li><strong>클라이언트 입력 캐시</strong>: 클라이언트가 보낸 입력을 저장</li>
  <li><strong>서버 출력 캐시 (플레이어별)</strong>: <strong>각 플레이어마다 별도로 관리되는</strong> 결합 데이터 캐시</li>
</ul>

<h3 id="캐시-동작">캐시 동작</h3>

<p><strong>전송 시:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>IF 현재 데이터가 캐시의 위치 P에 존재:
    Game Cache(P) 전송
ELSE:
    Game Data(data) 전송
    cache[next_position] = data
    next_position = (next_position + 1) % 256
</code></pre></div></div>

<p><strong>수신 시:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>data = cache[received_position]
데이터 처리
</code></pre></div></div>

<h3 id="플레이어별-서버-캐시-중요">플레이어별 서버 캐시 (중요!)</h3>

<p><strong>서버는 각 플레이어마다 별도의 출력 캐시를 유지한다.</strong></p>

<p>예제:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>프레임 1: P0이 [A1 A2 B1 B2]를 수신 → P0의 캐시 위치 5번

프레임 5: 서버가 [A1 A2 B1 B2]를 다시 전송할 때
  → P0: Game Cache(5)  (P0은 이미 본 데이터)
  → P1: Game Data([A1 A2 B1 B2])  (P1은 처음 보는 데이터)
</code></pre></div></div>

<h3 id="캐시-슬롯-관리">캐시 슬롯 관리</h3>

<p>Game Cache의 슬롯 인덱스는 0-255 범위를 가진다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>초기 상태: [A, B, C, D, E, ..., Y, -]
next_position: 25

새 입력 Z:
상태: [A, B, C, D, E, ..., Y, Z]
next_position: 26

슬롯이 꽉 찼을 때 새 입력 AA:
상태: [AA, B, C, D, E, ..., Y, Z]  (0번 위치에 덮어쓰기)
next_position: 1
</code></pre></div></div>

<p>circular buffer처럼 <code class="language-plaintext highlighter-rouge">next_position</code>만 증가시키며 가장 오래된 슬롯에 덮어쓴다. 이는 O(1) 작업이므로 효율적이다.</p>

<h2 id="플레이어-지연delay">플레이어 지연(Delay)</h2>

<h3 id="정의">정의</h3>

<p>플레이어는 접속 타입에 따라 프레임 지연을 설정할 수 있다:</p>

<table>
  <thead>
    <tr>
      <th>Delay</th>
      <th>전송 주기 (60fps)</th>
      <th>입력 크기</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>매 프레임 (~16.7ms)</td>
      <td>2바이트</td>
    </tr>
    <tr>
      <td>2</td>
      <td>2 프레임마다 (~33.3ms)</td>
      <td>4바이트</td>
    </tr>
    <tr>
      <td>3</td>
      <td>3 프레임마다 (~50ms)</td>
      <td>6바이트</td>
    </tr>
    <tr>
      <td>N</td>
      <td>N 프레임마다</td>
      <td>N × 2바이트</td>
    </tr>
  </tbody>
</table>

<h3 id="다중-프레임-입력">다중 프레임 입력</h3>

<p>Delay가 2인 플레이어는 2개의 프레임 입력을 한 번에 전송한다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Delay 2 플레이어 전송: [0x12][0x00][0xAA][0xBB][0xCC][0xDD]
                                     ├─프레임 N─┤ ├─프레임 N+1┤
</code></pre></div></div>

<p>서버는 이를 개별 2바이트 프레임으로 분리하여 처리한다.</p>

<h2 id="프레임-동기화">프레임 동기화</h2>

<h3 id="핵심-규칙">핵심 규칙</h3>

<p><strong>서버는 모든 플레이어가 프레임 N의 입력을 제공할 때까지 프레임 N을 배포할 수 없다.</strong></p>

<h3 id="예제-2명의-플레이어-다른-delay">예제: 2명의 플레이어, 다른 Delay</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>설정:
  P0: delay=1 (매 프레임 전송)
  P1: delay=2 (2 프레임마다 전송)

타임라인:

시간 0ms:
  P0이 프레임 1 전송
  P1이 프레임 1-2 전송
  → 서버가 프레임 1 배포: [P0_F1][P1_F1]

시간 16ms:
  P0이 프레임 2 전송
  P1은 대기 (이미 프레임 1-2 전송함)
  → 서버가 프레임 2 배포: [P0_F2][P1_F2]

시간 33ms:
  P0이 프레임 3 전송
  P1이 프레임 3-4 전송
  → 서버가 프레임 3 배포: [P0_F3][P1_F3]
</code></pre></div></div>

<h3 id="블로킹">블로킹</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>P0: 프레임 5 ✓
P1: 프레임 5 ✓
P2: 프레임 5 ✗ (누락)

→ 서버는 대기
→ P2의 입력이 도착할 때까지 배포하지 않음
</code></pre></div></div>

<p>이것이 지연이 큰 플레이어 한 명 때문에 모든 플레이어가 느려지는 이유다.</p>

<h2 id="preemptive-padding-선행-패딩">Preemptive Padding (선행 패딩)</h2>

<h3 id="공식">공식</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>padding_frames = player_delay - minimum_delay_in_game
</code></pre></div></div>

<h3 id="초기화">초기화</h3>

<p>게임 시작 시, 느린 플레이어의 입력 큐는 <code class="language-plaintext highlighter-rouge">[0x00, 0x00]</code> 프레임으로 미리 채워진다.</p>

<p><strong>예제: P0 (delay=1), P1 (delay=2), P2 (delay=3)</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>초기 상태:
  P0 큐: []                      (가장 빠름, 패딩 없음)
  P1 큐: [[00 00]]               (1 프레임 패딩)
  P2 큐: [[00 00][00 00]]        (2 프레임 패딩)

첫 입력 후:
  P0이 [AA BB] 전송
  P1이 [CC DD][EE FF] 전송
  P2이 [11 22][33 44][55 66] 전송

큐 상태:
  P0: [[AA BB]]
  P1: [[00 00][CC DD][EE FF]]
  P2: [[00 00][00 00][11 22][33 44][55 66]]

프레임 1 배포: [AA BB][00 00][00 00]
프레임 2 배포: [다음P0][CC DD][00 00]
프레임 3 배포: [다음P0][EE FF][11 22]
</code></pre></div></div>

<p>이 메커니즘은 서로 다른 지연을 가진 플레이어들이 동시에 게임을 시작할 수 있게 한다.</p>

<h2 id="프레임-배포-스케줄">프레임 배포 스케줄</h2>

<p>각 플레이어는 자신의 delay 비율에 따라 결합 데이터를 수신한다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>시간 0ms:
  프레임 1 준비
  → P0 (delay=1): [프레임_1]
  → P1 (delay=2): 대기

시간 16ms:
  프레임 2 준비
  → P0: [프레임_2]
  → P1: [프레임_1][프레임_2]  (한 번에 2 프레임)

시간 33ms:
  프레임 3 준비
  → P0: [프레임_3]
  → P1: 대기

시간 50ms:
  프레임 4 준비
  → P0: [프레임_4]
  → P1: [프레임_3][프레임_4]
</code></pre></div></div>

<h2 id="프레임-인터리빙-매우-중요">프레임 인터리빙 (매우 중요!)</h2>

<p>입력은 <strong>프레임 단위로 인터리빙</strong>되어야 하며, 플레이어별로 연결되어서는 안 된다.</p>

<p><strong>잘못된 방식:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>P0: [01 00][02 00]
P1: [AA 00][BB 00]

결합: [01 00][02 00][AA 00][BB 00]  ✗
      └──모든 P0──┘ └──모든 P1──┘
</code></pre></div></div>

<p><strong>올바른 방식:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>P0: [01 00][02 00]
P1: [AA 00][BB 00]

결합: [01 00][AA 00][02 00][BB 00]  ✓
      └─프레임 1──┘ └─프레임 2──┘
</code></pre></div></div>

<p><strong>알고리즘:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FOR 각 프레임 F in (0..frame_count):
    FOR 각 플레이어 P in (0..player_count):
        결합_데이터에 player[P].frame[F] 추가
</code></pre></div></div>

<p><strong>예제 (3 플레이어, 2 프레임):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>입력:
  P0: [A1 A2][A3 A4]
  P1: [B1 B2][B3 B4]
  P2: [C1 C2][C3 C4]

출력:
  [A1 A2][B1 B2][C1 C2][A3 A4][B3 B4][C3 C4]
   └────프레임 0──────┘ └────프레임 1──────┘
</code></pre></div></div>

<p>이 방식은 각 프레임의 모든 플레이어 입력이 함께 있어야 하는 게임 로직의 요구사항을 충족시킨다.</p>

<h2 id="시퀀스-다이어그램">시퀀스 다이어그램</h2>

<h3 id="정상-동작">정상 동작</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>클라이언트 0 (delay=1)      서버               클라이언트 1 (delay=1)
      |                       |                         |
      | GD [01 00]           |                         |
      |---------------------&gt;|                         |
      |                       | GD [02 00]             |
      |                       |&lt;------------------------|
      |                       |                         |
      |                   [결합]                        |
      |                       |                         |
      | GD [01 00 02 00]     |                         |
      |&lt;---------------------|                         |
      |                       | GD [01 00 02 00]       |
      |                       |------------------------&gt;|
</code></pre></div></div>

<h3 id="캐시-히트">캐시 히트</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>클라이언트 0                서버               클라이언트 1
      |                       |                         |
      | GD [AA BB]           |                         |
      |---------------------&gt;|                         |
      |                       | GD [CC DD]             |
      |                       |&lt;------------------------|
      | GD [AA BB CC DD] (캐시 위치 0)                 |
      |&lt;------------------------------------------------|
      |                       |                         |
      | GC(0) [AA BB]        |                         |
      |---------------------&gt;|                         |
      |                       | GC(0) [CC DD]          |
      |                       |&lt;------------------------|
      | GC(0) [AA BB CC DD]  |                         |
      |&lt;------------------------------------------------|
</code></pre></div></div>

<h3 id="다른-delay">다른 Delay</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>클라이언트 0 (delay=1)      서버               클라이언트 1 (delay=2)
      |                       |                         |
      | GD [01 00]           |                         |
      |---------------------&gt;|                         |
      |                       | GD [AA BB CC DD]       |
      |                       |&lt;------------------------|
      |                       |                         |
      | GD [01 00 AA BB]     |                         |
      |&lt;---------------------|                         |
      | GD [02 00]           |                         |
      |---------------------&gt;|                         |
      | GD [02 00 CC DD]     |                         |
      |&lt;---------------------|                         |
      |                       | GD [01 00 AA BB]       |
      |                       |      [02 00 CC DD]     |
      |                       |------------------------&gt;|
</code></pre></div></div>

<h3 id="game-cache가-새로운-조합-생성">Game Cache가 새로운 조합 생성</h3>

<p>Game Cache를 사용해도 각 플레이어의 입력 조합이 달라지면 새로운 데이터가 생성된다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>프레임 1: P0=[AA BB], P1=[CC DD] → [AA BB CC DD] (캐시 위치 0)
프레임 2: P0이 GC(0) [AA BB] 전송, P1=[EE FF] → [AA BB EE FF] (새 데이터!)

클라이언트 0                서버               클라이언트 1
      |                       |                         |
      | GD [AA BB]           |                         |
      |---------------------&gt;|                         |
      |                       | GD [CC DD]             |
      |                       |&lt;------------------------|
      | GD [AA BB CC DD]     |                         |
      |&lt;---------------------|                         |
      |                       | GD [AA BB CC DD]       |
      |                       |------------------------&gt;|
      |                       |                         |
      | GC(0) [AA BB]        |                         |
      |---------------------&gt;|                         |
      |                       | GD [EE FF]             |
      |                       |&lt;------------------------|
      | GD [AA BB EE FF]     |                         |
      |&lt;---------------------|                         |
      |                       | GD [AA BB EE FF]       |
      |                       |------------------------&gt;|
</code></pre></div></div>

<p>클라이언트가 캐시를 재사용해도, 다른 플레이어의 입력이 다르면 서버는 새로운 조합을 생성해야 한다.</p>

<h1 id="서버-구현체">서버 구현체</h1>

<p>https://github.com/hsnks100/direlera-rs</p>]]></content><author><name>경수</name></author><category term="kaillera" /><category term="networking" /><category term="game" /><category term="emulator" /><category term="protocol" /><summary type="html"><![CDATA[카일레라란?]]></summary></entry><entry><title type="html">간단하게 펄과 함께 하는 숏코딩 Short Coding</title><link href="https://hsnks100.github.io/%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%ED%8E%84%EA%B3%BC-%ED%95%A8%EA%BB%98-%ED%95%98%EB%8A%94-%EC%88%8F%EC%BD%94%EB%94%A9-short-coding/" rel="alternate" type="text/html" title="간단하게 펄과 함께 하는 숏코딩 Short Coding" /><published>2020-10-09T00:00:00+09:00</published><updated>2020-10-09T00:00:00+09:00</updated><id>https://hsnks100.github.io/%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%ED%8E%84%EA%B3%BC-%ED%95%A8%EA%BB%98-%ED%95%98%EB%8A%94-%EC%88%8F%EC%BD%94%EB%94%A9-short-coding</id><content type="html" xml:base="https://hsnks100.github.io/%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%ED%8E%84%EA%B3%BC-%ED%95%A8%EA%BB%98-%ED%95%98%EB%8A%94-%EC%88%8F%EC%BD%94%EB%94%A9-short-coding/"><![CDATA[<h1 id="숏코딩">숏코딩</h1>

<p>숏코딩은 코드 길이를 극한까지 줄이는 코딩을 의미한다. 
골프에서 낮은 타수로 홀인하는 것처럼 숏코딩은 최소한의 바이트수로 해당 문제를 풀어내는 것을 말한다.</p>

<p>예를 들면 int value = 0; 를 공백과 이름을 줄여서 int v=0; 으로 하는 것도 일종의 숏코딩 기법이다.</p>

<p>가독성 따윈 개나줘라의 코딩이 무슨 의미가 있냐고 물어볼 수 있겠으나, 해당 언어의 테크닉을 극한까지 알고 있어야 최대한으로 바이트 절약이 가능하다.</p>

<p>숏코딩을 시도하면 언어의 상세 스펙문서를 얼마나 꿰차는지가 중요한지 알게된다.</p>

<h1 id="문제">문제</h1>

<p>https://www.acmicpc.net/problem/14425</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>5 11
baekjoononlinejudge
startlink
codeplus
sundaycoding
codingsh
--------------------------------
baekjoon
codeplus
codeminus
startlink
starlink
sundaycoding
codingsh
codinghs
sondaycoding
startrink
icerink
</code></pre></div></div>
<p>위와 같이 s 집합의 문자열이 나머지 문자열에서 얼마나 나타나는지 체크하는 프로그램을 작성하라고 한다. 위 그림에서 ———- 로 나눠진 부분.</p>

<p>세어보면 4개 임을 알 수 있다. 문제 풀이는 간단해보인다. 해시형 자료구조에 해당 문자열을 넣고, 다음 검사문자열에서 해당 문자열이 있다면 카운팅을 하고 최종적으로 프린트 하면 될것 같다.</p>

<p>떠오르는 대로 코딩해보자.</p>

<div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">$n</span><span class="p">,</span> <span class="nv">$m</span><span class="p">)</span> <span class="o">=</span> <span class="nb">split</span><span class="p">(</span><span class="sr">/ /</span><span class="p">,</span> <span class="o">&lt;</span><span class="bp">STDIN</span><span class="o">&gt;</span><span class="p">);</span>
<span class="nv">%s</span> <span class="o">=</span> <span class="p">{};</span>
<span class="k">while</span><span class="p">(</span><span class="o">--</span><span class="nv">$n</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="vg">$_</span> <span class="o">=</span> <span class="o">&lt;</span><span class="bp">STDIN</span><span class="o">&gt;</span><span class="p">;</span>
    <span class="nb">chomp</span><span class="p">;</span>
    <span class="nv">$s</span><span class="o">-&gt;</span><span class="p">{</span><span class="vg">$_</span><span class="p">}</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> 
<span class="p">}</span>
<span class="nv">$sol</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">while</span><span class="p">(</span><span class="nv">$m</span><span class="o">--</span><span class="p">)</span> <span class="p">{</span>
    <span class="vg">$_</span> <span class="o">=</span> <span class="o">&lt;</span><span class="bp">STDIN</span><span class="o">&gt;</span><span class="p">;</span>
    <span class="nb">chomp</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="nv">$s</span><span class="o">-&gt;</span><span class="p">{</span><span class="vg">$_</span><span class="p">}</span> <span class="o">==</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
        <span class="nv">$sol</span><span class="o">++</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">print</span> <span class="nv">$sol</span><span class="p">;</span>
</code></pre></div></div>
<p>두개의 루프로 문제풀이를 했다. 먼저 %s 에 해당 문자열에 대한 값을 1로 셋하고 다음 루프에서 검사하면서 1 이면 카운팅 하는 방식.</p>

<p>점점 줄여보도록 하자. 먼저 <STDIN> 는 표준입력으로부터 문자열을 리턴하는 펄 명령어인데, &lt;&gt; 처럼 STDIN 을 생략해도 된다.</STDIN></p>

<p>또한 함수 호출의 괄호또한 생략가능하므로 split/ /,&lt;&gt;; 으로 고쳐 쓸 수 있다.</p>

<div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="vg">$_</span> <span class="o">=</span> <span class="o">&lt;</span><span class="bp">STDIN</span><span class="o">&gt;</span><span class="p">;</span>
<span class="nb">chomp</span><span class="p">;</span>
<span class="nv">$s</span><span class="o">-&gt;</span><span class="p">{</span><span class="vg">$_</span><span class="p">}</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</code></pre></div></div>
<p>또한 다음과 같이 고칠 수 있다.</p>

<div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$s</span><span class="o">-&gt;</span><span class="nv">$</span><span class="p">{</span><span class="o">&lt;&gt;</span><span class="p">}</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span>
</code></pre></div></div>

<p>위 과정을 적용 시키면</p>

<div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">$n</span><span class="p">,</span> <span class="nv">$m</span><span class="p">)</span> <span class="o">=</span> <span class="nb">split</span><span class="sr">/ /</span><span class="p">,</span> <span class="o">&lt;&gt;</span><span class="p">;</span>
<span class="k">while</span><span class="p">(</span><span class="nv">$n</span><span class="o">--</span><span class="p">)</span> <span class="p">{</span>
    <span class="nv">$s</span><span class="o">-&gt;</span><span class="p">{</span><span class="o">&lt;&gt;</span><span class="p">}</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> 
<span class="p">}</span> 
<span class="nv">$v</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">while</span><span class="p">(</span><span class="nv">$m</span><span class="o">--</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nv">$s</span><span class="o">-&gt;</span><span class="p">{</span><span class="o">&lt;&gt;</span><span class="p">}</span> <span class="o">==</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
        <span class="nv">$v</span><span class="o">++</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
<span class="k">print</span> <span class="nv">$v</span><span class="p">;</span>
</code></pre></div></div>

<p>perl 에서 한줄 짜리 명령어일 때 if, while, for 등의 명령어는 뒤에 위치하면서 괄호를 생략할 수 있다.</p>

<p>예를 들면</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if(condition) { s; }
는 
s if(condition);
으로 고쳐쓸 수 있다.
</code></pre></div></div>

<p>적용시켜보자.</p>

<div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">$n</span><span class="p">,</span> <span class="nv">$m</span><span class="p">)</span> <span class="o">=</span> <span class="nb">split</span><span class="sr">/ /</span><span class="p">,</span> <span class="o">&lt;&gt;</span><span class="p">;</span>
<span class="nv">$s</span><span class="o">-&gt;</span><span class="p">{</span><span class="o">&lt;&gt;</span><span class="p">}</span> <span class="o">=</span> <span class="mi">1</span> <span class="k">while</span><span class="nv">$n</span><span class="o">--</span><span class="p">;</span>
<span class="nv">$v</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">while</span><span class="p">(</span><span class="nv">$m</span><span class="o">--</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nv">$s</span><span class="o">-&gt;</span><span class="p">{</span><span class="o">&lt;&gt;</span><span class="p">}</span> <span class="o">==</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
        <span class="nv">$v</span><span class="o">++</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
<span class="k">print</span> <span class="nv">$v</span><span class="p">;</span>
</code></pre></div></div>

<p>밑의 while 은 하나의 명령이 아니다. 하지만 삼항연산자로 하나의 명령으로 바꾸고 한줄 짜리 while 로 바꾸면</p>

<div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$s</span><span class="o">-&gt;</span><span class="p">{</span><span class="o">&lt;&gt;</span><span class="p">}</span><span class="o">==</span><span class="mi">1</span><span class="p">?</span><span class="nv">$v</span><span class="o">++</span><span class="p">:</span><span class="mi">0</span> <span class="k">while</span><span class="nv">$m</span><span class="o">--</span><span class="p">;</span>
<span class="k">print</span><span class="nv">$v</span><span class="p">;</span>
</code></pre></div></div>

<p>조금 더 줄여보자.perl 에서는 ==1 으로 검사를 하지 않더라도 undefined 나 0 이 아니면 참으로 반환한다.</p>
<div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$s</span><span class="o">-&gt;</span><span class="p">{</span><span class="o">&lt;&gt;</span><span class="p">}?</span><span class="nv">$v</span><span class="o">++</span><span class="p">:</span><span class="mi">0</span> <span class="k">while</span><span class="nv">$m</span><span class="o">--</span><span class="p">;</span>
<span class="k">print</span><span class="nv">$v</span><span class="p">;</span>
</code></pre></div></div>
<p>print 함수는 인자가 생략되면 특별한 변수 $_ 를 출력하게 되어있다. 고쳐보자.</p>
<div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="vg">$_</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span><span class="nv">$s</span><span class="o">-&gt;</span><span class="p">{</span><span class="o">&lt;&gt;</span><span class="p">}?</span><span class="vg">$_</span><span class="o">++</span><span class="p">:</span><span class="mi">0</span> <span class="k">while</span><span class="nv">$m</span><span class="o">--</span><span class="p">;</span><span class="k">print</span><span class="p">;</span>
</code></pre></div></div>

<p>위 테크닉을 다 적용하고 나면~</p>

<div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">$n</span><span class="p">,</span><span class="nv">$m</span><span class="p">)</span><span class="o">=</span><span class="nb">split</span><span class="sr">/ /</span><span class="p">,</span><span class="o">&lt;&gt;</span><span class="p">;</span><span class="nv">$s</span><span class="o">-&gt;</span><span class="p">{</span><span class="o">&lt;&gt;</span><span class="p">}</span><span class="o">=</span><span class="mi">1</span> <span class="k">while</span><span class="nv">$n</span><span class="o">--</span><span class="p">;</span><span class="vg">$_</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span><span class="nv">$s</span><span class="o">-&gt;</span><span class="p">{</span><span class="o">&lt;&gt;</span><span class="p">}?</span><span class="vg">$_</span><span class="o">++</span><span class="p">:</span><span class="mi">0</span> <span class="k">while</span><span class="nv">$m</span><span class="o">--</span><span class="p">;</span><span class="k">print</span><span class="p">;</span>
</code></pre></div></div>

<p>78 bytes!</p>

<p>현재 숏코딩 1위 파이썬 코드 84 bytes</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>I=input;n,m=map(int,I().split());*s,=map(I,['']*n);print(sum(I()in s for _ in[0]*m))
</code></pre></div></div>

<p>보다 6 bytes 줄인 78 bytes 로 1등이다~^_^</p>

<p>더 줄일 방법이 있을까요?</p>]]></content><author><name>경수</name></author><category term="perl" /><category term="short-coding" /><category term="algorithm" /><category term="programming" /><summary type="html"><![CDATA[숏코딩]]></summary></entry><entry><title type="html">Shell과 Perl의 찰떡궁합</title><link href="https://hsnks100.github.io/shell%EA%B3%BC-perl%EC%9D%98-%EC%B0%B0%EB%96%A1%EA%B6%81%ED%95%A9/" rel="alternate" type="text/html" title="Shell과 Perl의 찰떡궁합" /><published>2020-10-02T00:00:00+09:00</published><updated>2020-10-02T00:00:00+09:00</updated><id>https://hsnks100.github.io/shell%EA%B3%BC-perl%EC%9D%98-%EC%B0%B0%EB%96%A1%EA%B6%81%ED%95%A9</id><content type="html" xml:base="https://hsnks100.github.io/shell%EA%B3%BC-perl%EC%9D%98-%EC%B0%B0%EB%96%A1%EA%B6%81%ED%95%A9/"><![CDATA[<h1 id="쉘과-perl의-환상-조합">쉘과 Perl의 환상 조합</h1>

<p>대부분의 시스템에서는 Perl 컴파일러가 기본적으로 제공되고 있고, 쉘과의 조합이 매우 막강하여 Perl을 깊게 배우지 않더라도 간단히 교양 수준으로 배워놓으면 활용도가 매우 크다.</p>

<table>
  <tbody>
    <tr>
      <td>기본적으로 쉘 프로그래밍은 stdin, stdout을 다루는 작업이라고 볼 수 있는데, linux/unix에서는 파이프(</td>
      <td>) 문자를 이용해서 이 목적을 달성하곤 한다.</td>
    </tr>
  </tbody>
</table>

<p>예를 들어 process name이 ksoo인 프로세스를 모두 종료시키는 스크립트를 짜본다고 가정한다. (pkill 있는거 안다 쫌!!)</p>

<p>먼저 <code class="language-plaintext highlighter-rouge">ps aux</code>로 프로세스 목록을 뽑고 <code class="language-plaintext highlighter-rouge">grep</code>을 통해서 필터링하고 <code class="language-plaintext highlighter-rouge">kill -9</code>으로 종료, 빵!</p>

<p><code class="language-plaintext highlighter-rouge">ps aux | grep ksoo</code>를 실행해보면</p>

<p>USER PID %CPU %MEM VSZ RSS TTY STAT …</p>

<p>으로 이뤄진 테이블이 출력된다. 우리가 관심있는 필드는 두 번째 PID 다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ps aux | grep ksoo | awk '{print $2}'
</code></pre></div></div>

<p>이러면 ksoo인 pid가 싸그리 나오게 된다. 그러면 <code class="language-plaintext highlighter-rouge">kill -9 pid1 pid2 pid3 ...</code> 형태로 넘겨줘야 하는데 어떻게 하나? 우리의 친구 xargs를 이용한다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ps aux | grep ksoo | awk '{print $2}'  | xargs kill -9
</code></pre></div></div>

<p>이처럼 쉘 프로그래밍은 stdin/stdout을 이용하여 작업을 하게 되는데, 여기서 정보 처리의 복잡도를 더 늘려보자. ksoo 프로세스 이름을 가지는 애들의 TIME을 분 단위로 나타내보자. TIME은 hh:mm 단위로 나타나고 hh * 60 + mm 으로 표시하면 된다.</p>

<p>자, 어떻게 하나?</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ps aux | grep ksoo | awk '{print $10}' | awk -F ':' '{print $1,"___",$2, $1 * 60 + $2}'
</code></pre></div></div>

<p>위처럼 하면 되지만, awk의 문서를 찾아봐야 하고, 어떤 경우에는 한 줄로 쓰기가 곤란할 정도의 처리를 할 때도 있고, 쉘 프로그래밍을 하기에도 곤란한 경우가 있다. 정규식으로 파싱하고 캡쳐하여 후처리가 필요하다거나…</p>

<p>Perl로 해볼까?</p>

<p>some.pl</p>
<div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">while</span><span class="p">(</span><span class="o">&lt;&gt;</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">@s</span><span class="o">=</span><span class="nb">split</span><span class="p">("</span><span class="s2"> </span><span class="p">");</span>
<span class="nv">@a</span><span class="o">=</span><span class="nb">split</span><span class="p">("</span><span class="s2">:</span><span class="p">",</span> <span class="nv">$s</span><span class="p">[</span><span class="mi">9</span><span class="p">]);</span>
<span class="k">print</span> <span class="nv">$a</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">*</span><span class="mi">60</span> <span class="o">+</span> <span class="nv">$a</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="p">"</span><span class="se">\n</span><span class="p">";</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ps aux | grep ksoo | perl some.pl
</code></pre></div></div>

<p>동작을 확인한 후 이제 oneline으로 바꿔보자.</p>

<p>여기서 <code class="language-plaintext highlighter-rouge">-ne</code> 옵션, <code class="language-plaintext highlighter-rouge">perl -ne 'CODE'</code> 다음 프로그램과 똑같다.</p>

<div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">while</span> <span class="p">(</span><span class="o">&lt;&gt;</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">CODE</span>
<span class="p">}</span>
</code></pre></div></div>

<p>그러면 oneline으로 고쳐보면,</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ps aux | grep usr | perl -ne '@s=split(" "); @a=split(":", $s[9]); print $a[0]*60 + $a[1], "\n"'
</code></pre></div></div>

<p>정규식을 이용한 방법</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ps aux | grep usr | perl -ne '@s=split(" "); $s[9]=~/(.+):(.+)/; print $1*60+$2,"\n";'
</code></pre></div></div>

<h1 id="마무리">마무리</h1>

<p>Perl은 쉘 프로그래밍과 찰떡 궁합의 퍼포먼스를 보이며, 익혀 놓으면 여러분의 리눅스 생활을 윤택하게 해줄 수 있는 도구가 될 것이다.</p>]]></content><author><name>경수</name></author><category term="shell" /><category term="perl" /><category term="unix" /><category term="programming" /><summary type="html"><![CDATA[쉘과 Perl의 환상 조합]]></summary></entry><entry><title type="html">Aws Serverless를 이용한 Telegram Bot 만들기</title><link href="https://hsnks100.github.io/aws-serverless%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-telegram-bot-%EB%A7%8C%EB%93%A4%EA%B8%B0/" rel="alternate" type="text/html" title="Aws Serverless를 이용한 Telegram Bot 만들기" /><published>2020-09-10T00:00:00+09:00</published><updated>2020-09-10T00:00:00+09:00</updated><id>https://hsnks100.github.io/aws-serverless%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-telegram-bot-%EB%A7%8C%EB%93%A4%EA%B8%B0</id><content type="html" xml:base="https://hsnks100.github.io/aws-serverless%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-telegram-bot-%EB%A7%8C%EB%93%A4%EA%B8%B0/"><![CDATA[<h1 id="serverless-telegram-bot-만들기">Serverless Telegram Bot 만들기</h1>

<p>최근 서버 설계의 한 축을 담당하고 있는 핫한 serverless 기반으로 텔레그램 봇을 만들어보자.</p>

<h1 id="serverless-란">Serverless 란?</h1>

<p>서버리스하면 <strong>서버가 없다</strong> 는 것인가? 생각을 할 수 있는데, 서버는 있다. 전통적인 서버-클라이언트 구조에서는 서버는 <strong>하나의 물리적 장비</strong> 를 쓰거나 대여하여 24시간 돌아가는 데몬을 기반으로 서버-클라이언트 구조를 구현했다. 서버리스는 이러한 전통적인 구조를 완전 깼다. 하나의 서비스를 제공하기 위해 굳이 하나의 서버를 잡고 데몬이 돌아가는 것이 아니고, <strong>함수 단위</strong> 로 잘게 나눠서 임의의 서버에서 이 작업을 수행하는 것을 뜻한다. 주로 유명한 서버리스 프레임워크는 firebase, aws.lambda 가 있다. 여러가지 많지만 저는 꼬꼬마라 다 알지는 못함.</p>

<h1 id="써야-하는-aws-서비스들">써야 하는 aws 서비스들</h1>

<p>aws.lambda, aws.cloudwatch, aws.dynamodb, aws.apigateway</p>

<h1 id="비용은">비용은?</h1>

<p>비용이 또 ㅁㅊㄸ.</p>

<p><img src="https://user-images.githubusercontent.com/3623889/92875003-85067300-f443-11ea-9e29-079e2f468fc0.png" alt="image" /></p>

<p>월 200만건에 각 실행시간 100ms 에 대해서 월 0.2$ 라고 나온다.</p>

<p>개인이나 스타트업입장의 개발에서는 공짜나 다름없다.</p>

<h1 id="개발-환경-꾸리기">개발 환경 꾸리기</h1>
<h2 id="nodejs--npm-설치">nodejs &amp; npm 설치</h2>
<p>https://nodejs.org/ko/download/package-manager/</p>
<h2 id="aws-cli-설치">AWS Cli 설치</h2>
<p>https://aws.amazon.com/ko/cli/</p>

<h2 id="serverless-framework-설치하기">Serverless framework 설치하기</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo npm install -g serverless
</code></pre></div></div>

<h2 id="aws-iam-사용자-추가">aws IAM 사용자 추가</h2>

<ol>
  <li>AWS 메뉴 중, Identity and Access Management(IAM) 들어가서 <strong>사용자 추가</strong> 를 한다.</li>
  <li>AWS 액세스 유형 선택
액세스 유형: 프로그래밍 방식 액세스</li>
  <li>엑세스 그룹
admin: AdministratorAccess</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws configure
AWS Access Key ID[]: _______
AWS Secret Access Key ID[]: _________
</code></pre></div></div>

<h1 id="프로젝트-생성">프로젝트 생성</h1>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ mkdir telegram-echo
$ cd telegram-echo
$ serverless create --template hello-world
Serverless: Generating boilerplate...
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.79.0
 -------'

Serverless: Successfully generated boilerplate for template: "hello-world"
Serverless: NOTE: Please update the "service" property in serverless.yml with your service name
╭─dire@dire-81w4 ~/workspace/telegram-echo
╰─$ ls
handler.js  serverless.yml
</code></pre></div></div>

<p>handler.js, serverless.yml 이 생성된 모습이 보인다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>╭─dire@dire-81w4 ~/workspace/telegram-echo
╰─$ cat handler.js
'use strict';

module.exports.helloWorld = (event, context, callback) =&gt; {
  const response = {
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': '*', // Required for CORS support to work
    },
    body: JSON.stringify({
      message: 'Go Serverless v1.0! Your function executed successfully!',
      input: event,
    }),
  };

  callback(null, response);
};

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>╭─dire@dire-81w4 ~/workspace/telegram-echo
╰─$ cat serverless.yml
# Welcome to serverless. Read the docs
# https://serverless.com/framework/docs/

# Serverless.yml is the configuration the CLI
# uses to deploy your code to your provider of choice

# The `service` block is the name of the service
service: telegram-echo

# The `provider` block defines where your service will be deployed
provider:
  name: aws
  runtime: nodejs12.x

# The `functions` block defines what code to deploy
functions:
  helloWorld:
    handler: handler.helloWorld
    # The `events` block defines how to trigger the handler.helloWorld code
    events:
      - http:
          path: hello-world
          method: get
          cors: true

</code></pre></div></div>

<p>별거 없고, helloWorld 를 핸들러로 하는 프로젝트 하나가 세팅된 모습이다.</p>

<p>provider: 항목에 region 을 넣을 수 있는데, 기본 region 은 east 어딘가로 설정이 되는데, 우리는 한국인이니 다음 줄을 추가하자. <strong>region: ap-northeast-2</strong></p>

<p>그리고 게이트웨이는 직접 세팅할 것이므로</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>provider:
  name: aws
  runtime: nodejs12.x
  region: ap-northeast-2
</code></pre></div></div>
<p>이런 모양이 될거다.</p>

<p>이제 드디어</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>╭─dire@dire-81w4 ~/workspace/telegram-echo
╰─$ serverless deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service telegram-echo.zip file to S3 (740 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
...............
Serverless: Stack update finished...
Service Information
service: telegram-echo
stage: dev
region: ap-northeast-2
stack: telegram-echo-dev
resources: 6
api keys:
  None
endpoints:
  None
functions:
  helloWorld: telegram-echo-dev-helloWorld
layers:
  None

***********************************************************************
Serverless: Announcing an enhanced experience for running Express.js apps: https://github.com/serverless-components/express.
***********************************************************************

</code></pre></div></div>

<p>우리의 aws console 들어가서 뭐가 어떻게 됐나 확인해볼까?</p>

<p><img src="https://user-images.githubusercontent.com/3623889/92711123-4c0ec580-f393-11ea-8f24-1b4b79877811.png" alt="image" /></p>

<p>외부 url 를 호출하면 우리 람다를 호출할 http api 가 필요하다. 트리거 추가를 누른다.</p>

<ol>
  <li>API Gateway</li>
  <li>Create an API</li>
  <li>HTTP API</li>
  <li>보안: 열기</li>
</ol>

<p>아래에 보면 API endpoint 가 있다. 이 url 에 접근을 하게 되면 telegram-echo-dev-helloWorld 함수를 실행한다는 의미다.</p>

<p>함수를 실행하는 방법은 크게 두 가지. api gateway 를 통해 호출하거나 테스트 버튼을 이용해 테스트를 하거나.</p>

<p>handler.js 에 한줄 추가해서 다시 deploy 해보자.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>console.log("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@KSOO@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
</code></pre></div></div>

<p>추가 후, <code class="language-plaintext highlighter-rouge">serverless deploy</code></p>

<p>그러고 url 호출해본다. 로그는 어디서 확인하나?</p>

<p><strong>cloudwatch.loggroup./aws/lambda/telegram-echo-dev-helloWorld</strong></p>

<p><img src="https://user-images.githubusercontent.com/3623889/92702848-a35d6780-f38c-11ea-9fb3-73a46ee92c47.png" alt="image" /></p>

<p>GET 변수를 넘길 수도 있다.</p>

<p>https://xxxxxxxxx.execute-api.ap-northeast-2.amazonaws.com/dev/hello-world?var=KSOOKSOOKSOO</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>..."queryStringParameters":{"var":"KSOOKSOOKSOO"},...
</code></pre></div></div>

<h1 id="텔레그램과-연동">텔레그램과 연동</h1>

<p>먼저 봇을 하나 만들어야 한다.</p>

<p>텔레그램앱에서 BotFather 를 친구 추가한다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/newbot

BotFather, [10.09.20 17:41]
Alright, a new bot. How are we going to call it? Please choose a name for your bot.

input: telegless_bot

BotFather, [10.09.20 17:42]
Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot.

input: teleless_bot

BotFather, [10.09.20 17:42]
Done! Congratulations on your new bot. You will find it at t.me/teleless_bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this.

Use this token to access the HTTP API:
1349292513:AAEyIjGis0AHzrEuP4uFkRl4AvgLM*********
Keep your token secure and store it safely, it can be used by anyone to control your bot.

For a description of the Bot API, see this page: https://core.telegram.org/bots/api
</code></pre></div></div>

<p>여기 나온 API 는 잘 기억하거나 기록 해둬야 한다. 만든 봇에 접근할 수 있는 키의 역할을 한다.</p>

<p><strong>Telegram Webhook</strong></p>

<p>우리의 목표는 지금 하나다. 텔레그램 봇에 타자를 치면 이 정보를 우리의 aws.lambda 로 전달하는 것.</p>

<p>텔레그램에서는 webhook 를 지원한다. webhook 을 걸게 되면 텔레그램 봇으로 들어오는 정보를 어떤 url 로 포워딩 시켜줄 수가 있다.</p>

<p>https://core.telegram.org/bots/api#setwebhook</p>

<p>문서를 보면</p>

<p>https://api.telegram.org/bot${telegram_token}/setWebhook?url=${callback_url}</p>

<p>이에 맞춰서 웹브라우저에서 호출해보자.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://api.telegram.org/*********/setWebhook?url=https://**************.com/default/telegram-echo-dev-helloWorld
</code></pre></div></div>
<p>이렇게 호출을 하면 웹훅이 등록된다고 되어있다. 등록해보자. Webhook was set. 과 같은 문구가 뜨면 성공. 우리 telegless_bot 을 친구 추가하고 말 걸어보자.</p>

<p>CloudWatch 에 우리가 추가한 로그가 찍혀있는지 확인해보자. @@@@@@@@@@ 가 많아서 확인하기 쉬울 거다.</p>

<h1 id="echo-bot">Echo Bot</h1>

<p><img src="https://user-images.githubusercontent.com/3623889/92715531-8a5ab380-f398-11ea-871c-17607fa286ce.png" alt="image" /></p>

<p>간단하게 에코봇을 만들어보자.</p>

<p>handler.js</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">'</span><span class="s1">use strict</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">AWS</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">aws-sdk</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">TOKEN</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">{telegram_api_token}</span><span class="dl">'</span><span class="p">;</span> <span class="c1">// YOUR TOKEN</span>
<span class="kd">const</span> <span class="nx">https</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">https</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">util</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">util</span><span class="dl">'</span><span class="p">);</span>

<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">update</span><span class="p">({</span><span class="na">region</span><span class="p">:</span> <span class="dl">'</span><span class="s1">ap-northeast-2</span><span class="dl">'</span><span class="p">})</span>
<span class="kd">const</span> <span class="nx">docClient</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">DynamoDB</span><span class="p">.</span><span class="nx">DocumentClient</span><span class="p">();</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span><span class="p">.</span><span class="nx">helloWorld</span> <span class="o">=</span> <span class="k">async</span> <span class="p">(</span><span class="nx">event</span><span class="p">,</span> <span class="nx">context</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="p">{</span>
	<span class="na">statusCode</span><span class="p">:</span> <span class="mi">200</span><span class="p">,</span>
	<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
	    <span class="dl">'</span><span class="s1">Access-Control-Allow-Origin</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">*</span><span class="dl">'</span><span class="p">,</span> <span class="c1">// Required for CORS support to work</span>
	<span class="p">},</span>
	<span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span>
	    <span class="na">message</span><span class="p">:</span> <span class="dl">'</span><span class="s1">test message</span><span class="dl">'</span><span class="p">,</span>
	    <span class="na">input</span><span class="p">:</span> <span class="nx">event</span><span class="p">,</span>
	<span class="p">}),</span>
    <span class="p">};</span>
    <span class="nx">callback</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">response</span><span class="p">);</span>
    <span class="kd">let</span> <span class="nx">telegramCall</span> <span class="o">=</span> <span class="k">typeof</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">body</span><span class="p">)</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">undefined</span><span class="dl">'</span><span class="p">;</span>
    <span class="k">if</span><span class="p">(</span><span class="nx">telegramCall</span><span class="p">)</span> <span class="p">{</span>
	<span class="kd">const</span> <span class="nx">obj</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">body</span><span class="p">);</span>
	<span class="nx">console</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="dl">"</span><span class="s2">telegram obj: </span><span class="dl">"</span><span class="p">,</span> <span class="nx">obj</span><span class="p">);</span>
	<span class="kd">let</span> <span class="nx">chatId</span> <span class="o">=</span> <span class="p">(</span><span class="nx">obj</span><span class="p">.</span><span class="nx">message</span><span class="p">.</span><span class="nx">chat</span><span class="p">.</span><span class="nx">id</span><span class="p">).</span><span class="nx">toString</span><span class="p">();</span>
	<span class="kd">const</span> <span class="nx">requestText</span> <span class="o">=</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">message</span><span class="p">.</span><span class="nx">text</span><span class="p">;</span>
	<span class="nx">console</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="dl">"</span><span class="s2">chatId: </span><span class="dl">"</span><span class="p">,</span> <span class="nx">chatId</span><span class="p">,</span> <span class="dl">"</span><span class="s2">, requestText: </span><span class="dl">"</span><span class="p">,</span> <span class="nx">requestText</span><span class="p">);</span>
	<span class="kd">var</span> <span class="nx">splits</span> <span class="o">=</span> <span class="nx">requestText</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">"</span><span class="s2"> </span><span class="dl">"</span><span class="p">);</span>

	<span class="k">if</span> <span class="p">(</span><span class="nx">splits</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="dl">"</span><span class="s2">/start</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
	    <span class="kd">const</span> <span class="nx">postData</span> <span class="o">=</span> <span class="p">{</span>
		<span class="dl">"</span><span class="s2">chat_id</span><span class="dl">"</span><span class="p">:</span> <span class="nx">chatId</span><span class="p">,</span>
		<span class="dl">"</span><span class="s2">text</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">따라쟁이 봇 시작합니다.</span><span class="dl">"</span>
	    <span class="p">};</span>
	    <span class="nx">sendMessage</span><span class="p">(</span><span class="nx">context</span><span class="p">,</span> <span class="nx">postData</span><span class="p">);</span>
	<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
	    <span class="kd">const</span> <span class="nx">postData</span> <span class="o">=</span> <span class="p">{</span>
		<span class="dl">"</span><span class="s2">chat_id</span><span class="dl">"</span><span class="p">:</span> <span class="nx">chatId</span><span class="p">,</span>
		<span class="dl">"</span><span class="s2">text</span><span class="dl">"</span><span class="p">:</span> <span class="nx">requestText</span>
	    <span class="p">};</span>
	    <span class="nx">sendMessage</span><span class="p">(</span><span class="nx">context</span><span class="p">,</span> <span class="nx">postData</span><span class="p">);</span>
	<span class="p">}</span>
    <span class="p">}</span>
<span class="p">};</span>

<span class="kd">function</span> <span class="nx">sendMessage</span><span class="p">(</span><span class="nx">context</span><span class="p">,</span> <span class="nx">content</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
	<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
	<span class="na">hostname</span><span class="p">:</span> <span class="dl">'</span><span class="s1">api.telegram.org</span><span class="dl">'</span><span class="p">,</span>
	<span class="na">port</span><span class="p">:</span> <span class="mi">443</span><span class="p">,</span>
	<span class="na">headers</span><span class="p">:</span> <span class="p">{</span><span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">},</span>
	<span class="na">path</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/bot</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">TOKEN</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">/sendMessage</span><span class="dl">"</span>
    <span class="p">};</span>

    <span class="kd">const</span> <span class="nx">req</span> <span class="o">=</span> <span class="nx">https</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">options</span><span class="p">,</span> <span class="p">(</span><span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
	<span class="nx">res</span><span class="p">.</span><span class="nx">setEncoding</span><span class="p">(</span><span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">);</span>
	<span class="nx">res</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">data</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">chunk</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
	    <span class="nx">context</span><span class="p">.</span><span class="nx">done</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>
	<span class="p">});</span>
    <span class="p">});</span>

    <span class="nx">req</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">error</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
	<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">problem with request: </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">e</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span>
    <span class="p">});</span>

    <span class="nx">req</span><span class="p">.</span><span class="nx">write</span><span class="p">(</span><span class="nx">util</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="dl">"</span><span class="s2">%j</span><span class="dl">"</span><span class="p">,</span> <span class="nx">content</span><span class="p">));</span>
    <span class="nx">req</span><span class="p">.</span><span class="nx">end</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="dynamodb">dynamodb</h1>

<p>따라하는건 된다. 그리고 하나의 의문이 들거다. 만약 문맥(Context)이 있는 봇을 만들고 싶다면? 즉, 입력되는 데이터를 저장하고 싶을 때는?</p>

<p>이 때 사용하는 것이 dynamodb 다. 다이나모디비 이름 참 이쁘다.</p>]]></content><author><name>경수</name></author><category term="aws" /><category term="serverless" /><category term="telegram" /><category term="bot" /><category term="lambda" /><category term="web-development" /><summary type="html"><![CDATA[Serverless Telegram Bot 만들기]]></summary></entry><entry><title type="html">Mame Rom Hacking Reversing 롬 해킹 및 리버싱</title><link href="https://hsnks100.github.io/MAME-rom-hacking-reversing-%EB%A1%AC-%ED%95%B4%ED%82%B9-%EB%B0%8F-%EB%A6%AC%EB%B2%84%EC%8B%B1/" rel="alternate" type="text/html" title="Mame Rom Hacking Reversing 롬 해킹 및 리버싱" /><published>2020-04-09T00:00:00+09:00</published><updated>2020-04-09T00:00:00+09:00</updated><id>https://hsnks100.github.io/MAME-rom-hacking-reversing-%EB%A1%AC-%ED%95%B4%ED%82%B9-%EB%B0%8F-%EB%A6%AC%EB%B2%84%EC%8B%B1</id><content type="html" xml:base="https://hsnks100.github.io/MAME-rom-hacking-reversing-%EB%A1%AC-%ED%95%B4%ED%82%B9-%EB%B0%8F-%EB%A6%AC%EB%B2%84%EC%8B%B1/"><![CDATA[<h1 id="mame-기반의-게임-해킹">MAME 기반의 게임 해킹</h1>

<p>최근에 중국 해킹롬 “The King of Fighters 98”을 플레이하다가 문득, 고전게임의 롬파일 수정은 어떻게 하는 걸까? 라는 생각이 들었다. 고전게임도 결국 프로그래머가 작성한 프로그램일 텐데, 아무리 복잡해봐야 폰노이만 모델 아닌가? 라는 생각에 자료를 찾아보기 시작했다.</p>

<p>가장 기본적인 해킹 방식은 역시 메모리 치팅이다. 메모리 치팅의 진행 방식은 처음에 모든 값을 검색하고, 값의 범위를 점진적으로 좁혀나가는 작업을 반복하는 것이다.</p>

<p>예를 들어 생명의 개수를 조작하고 싶다면, 처음에 메모리를 초기화하고 생명의 현재 개수를 검색한다. 생명이 5개라면 5를 검색하고, 한 번 죽고 나서 4를 검색하는 과정을 반복한다.</p>

<p>이런 과정을 반복하다 보면 필연적으로 하나의 메모리 주소가 나오게 된다. 그 메모리 값을 99와 같은 큰 값으로 고정시키면 목숨이 무한개가 된다. 어릴 때 다들 T-search나 GameHack 같은 툴로 한 번씩은 해봤을 법한 작업이다.</p>

<p>이 작업은 MAME의 디버거 툴에서도 똑같이 진행할 수 있다. 간단한 것부터 해볼까?</p>

<p>우리의 타겟이 되는 게임은 <strong>Dungeons &amp; Dragons: Shadow over Mystara</strong> 일명 던전 앤 드래곤이다. 줄여서 던드라고 부르겠다.</p>

<p>이 게임은 내 기억으로는 죽어가던 아케이드 시장에서 마지막으로 꽃을 피운 작품이다. 타임어택 방식을 제외한 일반 게임 진행 시 1시간을 넘어가는 말도 안 되는 볼륨을 보여준다. 고작 20MB 용량으로 말이다. 여러 명이 함께 즐길 수 있는 게임으로 아마 아케이드 시장에서 가장 성공한 게임이 아닐까 싶다. 이전의 타워 오브 둠에서의 단점이었던 빈약한 액션감을 보완하면서 최고의 게임으로 평가받고 있으며, 현재에도 매니아들은 이 게임을 여전히 매일매일 즐기고 있다. (액션감을 너무 강조한 나머지 난이도가 말이 안 되게 쉬워졌지만)</p>

<h1 id="에뮬레이터--롬-설정">에뮬레이터 &amp; 롬 설정</h1>

<p>여러 자식 롬들이 있지만 가장 수정하기 쉬운 <code class="language-plaintext highlighter-rouge">ddsomud</code>로 진행한다.</p>

<p>MAME에 디버거를 띄우는 방법부터 알아야 한다. MAC과 WINDOWS 기준으로 설명한다.</p>

<h2 id="mac">MAC</h2>

<p><strong>버전 정보</strong>: sdlmame 0.174</p>

<ul>
  <li><a href="https://nagarry.tistory.com/182">설치법</a></li>
  <li><a href="https://sdlmame.lngn.net/stable/">다운로드</a></li>
</ul>

<p>MAC에서는 sdlmame 0.174를 다운로드하여 압축을 풀고 <code class="language-plaintext highlighter-rouge">./mame64 -debug</code> 명령어로 실행한다.</p>

<h2 id="windows">WINDOWS</h2>

<p><strong>버전 정보</strong>: MAMEUI 0.145</p>

<p>UI 상에서 디버거 옵션을 체크하여 활성화한다.</p>

<p><img src="https://github.com/user-attachments/assets/5a638477-5694-4888-862c-3756ecd9d3e3" alt="윈도우즈 MAME 디버거 설정" /></p>

<h1 id="가장-간단한-게임-조작---무적-만들기">가장 간단한 게임 조작 - 무적 만들기</h1>

<p>지금부터 가장 기본적인 게임 조작을 해보겠다. 캐릭터가 맞아도 데미지를 받지 않게 만들어보자.</p>

<p>위에서 설명한 대로 설정하고 롬파일을 구동시키면 디버거 창이 나타난다.</p>

<p><img src="https://user-images.githubusercontent.com/3623889/81496755-ed5a1200-92f4-11ea-9f7e-297eed8d4e98.png" alt="image" /> 
<img src="https://user-images.githubusercontent.com/3623889/81496762-f814a700-92f4-11ea-9f12-fd22331a5f22.png" alt="image" /></p>

<p>기본적인 MAME 디버거의 사용법은 <a href="https://docs.mamedev.org/debugger/cheats.html">공식 문서</a>를 참고하고, 여기서는 따라하기 식으로 설명한다.</p>

<p><img src="https://user-images.githubusercontent.com/3623889/81497681-e6360280-92fa-11ea-8da0-0abf50767209.png" alt="image" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. 처음 마을에 도착한 후 디버거 창에서 "ci" 입력 - 메모리 검색 초기화
2. 한 대 맞고 "cn de" 입력 - 감소한 값 찾기
3. 한 대 맞고 "cn de" 입력 - 감소한 값 찾기
4. 한 대 맞고 "cn de" 입력 - 감소한 값 찾기
</code></pre></div></div>

<p>피통과 관련된 메모리 주소는 두 개의 후보가 나온다.</p>

<p><code class="language-plaintext highlighter-rouge">FF831D</code>는 값을 바꿔도 피통이 변하지 않았고, <code class="language-plaintext highlighter-rouge">FF8641</code>을 <code class="language-plaintext highlighter-rouge">AA</code>로 고쳤을 때 피통이 차는 모습을 확인할 수 있었다.</p>

<p>우리가 찾던 변수는 <code class="language-plaintext highlighter-rouge">FF8641</code>이라는 것을 알 수 있다. <code class="language-plaintext highlighter-rouge">FF8641</code> 변수를 항상 특정값으로 고정시키는 치트를 써도 원하는 목적을 달성할 수 있지만, 핵롬을 만들기 위해서는 변수를 고치는 것이 아니라 이 변수를 설정하는 코드를 수정해야 한다.</p>

<p><img src="https://user-images.githubusercontent.com/3623889/81499458-c573aa00-9306-11ea-9b73-16b0c1e70e13.png" alt="image" /></p>

<p>이제 어떤 코드에서 해당 변수를 바꾸는지 찾아야 한다. 이때 사용하는 디버거 명령어는 <code class="language-plaintext highlighter-rouge">wp</code>이다.</p>

<p>명령어 입력창에 <code class="language-plaintext highlighter-rouge">wp ff8641,1,w</code>를 입력하고 몬스터에게 맞아본다. <code class="language-plaintext highlighter-rouge">ff8641</code> 영역의 1바이트에 쓰기 작업이 일어나면 브레이크포인트가 걸리게 하는 명령이다.</p>

<p><img src="https://user-images.githubusercontent.com/3623889/81499836-3b791080-9309-11ea-9177-ab24d02580d4.png" alt="image" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>27566 | sub.w D0, ($62, A0) | 9168 0062
2756A | bpl $27570
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">2756A</code>에서 게임이 멈추게 된다. 아마 <code class="language-plaintext highlighter-rouge">($62, A0)</code> 위치에서 <code class="language-plaintext highlighter-rouge">D0</code>만큼 값을 빼면서 브레이크포인트가 걸린 것 같다.</p>

<p><code class="language-plaintext highlighter-rouge">27566</code> 위치의 명령어를 아무것도 하지 않게 만들면 맞아도 데미지를 받지 않을 것이다.</p>

<p>한번 바꿔보자. 디버거에서 <code class="language-plaintext highlighter-rouge">Ctrl+M</code>을 누르고 <code class="language-plaintext highlighter-rouge">Region ':maincpu'</code> 영역으로 이동한 뒤 <code class="language-plaintext highlighter-rouge">27566</code> 위치로 이동해서</p>

<p><code class="language-plaintext highlighter-rouge">4E71 4E71</code>로 값을 입력해보자. 참고로 <code class="language-plaintext highlighter-rouge">4E71 4E71</code>은 아무것도 하지 않는 명령어인 <code class="language-plaintext highlighter-rouge">nop</code>이 두 개 들어간 형태다.</p>

<p><img src="https://user-images.githubusercontent.com/3623889/81505921-c53ad500-932d-11ea-9fc5-470ebf78bfe8.png" alt="image" /></p>

<p>이제 맞아도 데미지를 받지 않는다.</p>

<h1 id="code-cave로-향하는-여정">Code Cave로 향하는 여정</h1>

<h2 id="던전-앤-드래곤의-cpu-모델">던전 앤 드래곤의 CPU 모델</h2>

<p>던드의 롬파일 식별자는 <code class="language-plaintext highlighter-rouge">ddsom</code>이며, 자식 롬 구조는 <code class="language-plaintext highlighter-rouge">ddsomxx</code>로 정해져 있다.</p>

<p><img src="https://user-images.githubusercontent.com/3623889/81494487-453c4d00-92e4-11ea-980a-8cf261dae81f.png" alt="image" /></p>

<p>MAME 소스의 <code class="language-plaintext highlighter-rouge">cps2.cpp</code>를 보면 던드의 CPU 모델은 68000(68k)이다.</p>

<h1 id="m86k-assembler">M86K Assembler</h1>

<h2 id="특정-아이템-개수-제한하기---code-cave-실습">특정 아이템 개수 제한하기 - Code Cave 실습</h2>

<p>이번에는 더 복잡한 작업을 해보자. 던전 앤 드래곤에서 특정 아이템(LB oil)만 개수 제한을 걸어보는 코드 케이브 작업을 진행해보겠다.</p>

<h3 id="실행-방법">실행 방법</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mame64.exe -debug
</code></pre></div></div>

<p>타겟 롬: <code class="language-plaintext highlighter-rouge">ddsomud</code></p>

<h3 id="아이템-구매-관련-코드-찾기">아이템 구매 관련 코드 찾기</h3>

<p>먼저 아이템을 구매할 때 어떤 메모리 주소가 변경되는지 찾아보자.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ci
</code></pre></div></div>

<p>메모리 검색을 초기화한 후 아이템을 하나 사보자.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cn +
</code></pre></div></div>

<p>아이템을 하나 더 사보자.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cn +
</code></pre></div></div>

<p>아이템을 하나 더 사보자.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cn +
</code></pre></div></div>

<p>이 과정을 통해 <code class="language-plaintext highlighter-rouge">FF8728</code>이 어떤 아이템의 개수를 담고 있는 주소임을 확인할 수 있다.</p>

<h3 id="write-point-설정">Write Point 설정</h3>

<p>이제 해당 주소에 쓰기 작업이 일어날 때 브레이크포인트를 걸어보자.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wp ff8728,1,w
</code></pre></div></div>

<p>브레이크포인트를 설정한 후 다시 아이템을 사보면 <code class="language-plaintext highlighter-rouge">AFBAC</code>에서 걸린다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>000afbaa 52 14           addq.b     #0x1,(A4)
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">addq.b #0x1,(A4)</code> 명령어는 A4가 가리키는 주소의 값을 1 증가시키는 명령이다. 아이템을 구매하는 동작이 확실해 보인다.</p>

<h3 id="최초-호출-지점-찾기">최초 호출 지점 찾기</h3>

<p>이제 아이템을 구매할 때 호출되는 최초의 위치를 찾아야 한다. <code class="language-plaintext highlighter-rouge">AFBAA</code> 위의 주소들에 브레이크포인트를 여러 개 설정해보자.</p>

<p><img src="https://github.com/user-attachments/assets/65755791-96fa-465d-84a6-840beccc97c7" alt="마구잡이 브레이크포인트 설정" /></p>

<p><code class="language-plaintext highlighter-rouge">AFB2A</code>에서 처음 걸리는 것 같다. 여기서부터 코드를 따라가다 보면 <code class="language-plaintext highlighter-rouge">AFB9E</code>가 보인다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmpi.b #$9, (A4)
</code></pre></div></div>

<p>이 부분에서 9개로 제한을 두는 것 같다. 이 부분을 적절히 수정해서 원하는 아이템만 3개로 제한하면 될 것 같다.</p>

<h3 id="code-cave-작업을-위한-도구-준비">Code Cave 작업을 위한 도구 준비</h3>

<p>이제 본격적인 코드 케이브 작업을 시작해보자. 이 작업에는 여러 도구가 필요하다:</p>

<ol>
  <li><strong>MAME 디버거</strong>: 실시간으로 메모리를 수정하고 브레이크포인트를 설정</li>
  <li><strong>Ghidra</strong>: 어셈블리 코드 분석 및 옵코드 생성</li>
  <li><strong>68000 어셈블리어</strong>: 실제 코드 작성</li>
</ol>

<h3 id="메모리-덤프-및-분석">메모리 덤프 및 분석</h3>

<p>먼저 디버거에서 현재 실행 중인 메모리의 덤프를 뜬다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>save v.bin,0,3fffff
</code></pre></div></div>

<p>이 덤프 파일을 Ghidra에서 열어서 전체 메모리 구조를 파악하고 빈 공간을 찾아보자. Ghidra에서는 인라인 어셈에 대한 옵코드를 쉽게 얻을 수 있다.</p>

<h3 id="코드-케이브-위치-선택">코드 케이브 위치 선택</h3>

<p>Ghidra에서 메모리를 둘러보다 보면 <code class="language-plaintext highlighter-rouge">63000</code> 주소 근처가 비어있는 공간으로 보인다. 이런 빈 공간을 “코드 케이브”라고 부르며, 여기에 우리가 원하는 새로운 로직을 삽입할 수 있다.</p>

<p><code class="language-plaintext highlighter-rouge">AFB9E</code>에서 이 <code class="language-plaintext highlighter-rouge">63000</code> 주소로 점프하는 명령어를 사용하여 코드 케이브 작업을 진행하자.</p>

<h3 id="기존-코드-분석">기존 코드 분석</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0AFB9E cmpi.b $#9, (A4)    0c14 0009
0AFBA2 bge $afbec          6c00 0048
</code></pre></div></div>

<p>이 코드는 A4가 가리키는 값(아이템 개수)이 9개 이상인지 비교하고, 9개 이상이면 <code class="language-plaintext highlighter-rouge">afbec</code>로 점프하는 코드다.</p>

<h3 id="새로운-로직-설계">새로운 로직 설계</h3>

<p><code class="language-plaintext highlighter-rouge">$63000</code> 번지에 작성할 새로운 규칙을 의사코드로 설계해보자:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 코드 케이브 시작 ($63000)</span>

<span class="k">if</span> <span class="p">(</span><span class="err">현재</span> <span class="err">아이템의</span> <span class="n">ID</span><span class="p">(</span><span class="n">D7</span> <span class="err">레지스터</span><span class="p">)</span> <span class="o">==</span> <span class="err">특정</span> <span class="err">아이템</span><span class="p">(</span><span class="err">$</span><span class="mi">23</span><span class="p">))</span> <span class="p">{</span>
    <span class="c1">// 특정 아이템의 개수 제한 로직</span>
    <span class="k">if</span> <span class="p">(</span><span class="err">현재</span> <span class="err">아이템의</span> <span class="err">개수</span><span class="p">(</span><span class="n">A4</span> <span class="err">포인터가</span> <span class="err">가리키는</span> <span class="err">값</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="mi">3</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// 3개 이상이면 실패</span>
        <span class="k">goto</span> <span class="err">실패주소</span><span class="p">(</span><span class="err">$</span><span class="n">afbec</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="c1">// 일반 아이템의 개수 제한 로직</span>
    <span class="k">if</span> <span class="p">(</span><span class="err">현재</span> <span class="err">아이템의</span> <span class="err">개수</span> <span class="o">&gt;=</span> <span class="mi">9</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// 9개 이상이면 실패</span>
        <span class="k">goto</span> <span class="err">실패주소</span><span class="p">(</span><span class="err">$</span><span class="n">afbec</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// 성공시 아이템 획득</span>
<span class="k">goto</span> <span class="err">성공주소</span><span class="p">(</span><span class="err">$</span><span class="n">afba6</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="실제-어셈블리-코드-구현">실제 어셈블리 코드 구현</h3>

<p>위 의사코드를 실제 68000 어셈블리어로 변환하면:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                             LAB_000afb9e                                    XREF[1]:     000afb5e(j)  
        000afb9e 4e f9 00        jmp        LAB_00062ffe+2.l
                 06 30 00
        000afba4 4e 71           nop
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                             LAB_00063000                                    XREF[1]:     000afb9e(j)  
        00063000 4e 71           nop
        00063002 be 3c 00 23     cmp.b      #0x23,D7b
        00063006 67 04           beq.b      LAB_0006300c
        00063008 60 00 00 0a     bra.w      LAB_00063014
                             LAB_0006300c                                    XREF[1]:     00063006(j)  
        0006300c 0c 14 00 03     cmpi.b     #0x3,(A4)
        00063010 60 00 00 06     bra.w      LAB_00063018
                             LAB_00063014                                    XREF[1]:     00063008(j)  
        00063014 0c 14 00 09     cmpi.b     #0x9,(A4)
                             LAB_00063018                                    XREF[1]:     00063010(j)  
        00063018 6c 00 00 08     bge.w      LAB_00063022
        0006301c 4e f9 00        jmp        LAB_000afba6.l
                 0a fb a6
                             LAB_00063022                                    XREF[1]:     00063018(j)  
        00063022 4e f9 00        jmp        LAB_000afbec.l
                 0a fb ec
</code></pre></div></div>

<h3 id="코드-적용">코드 적용</h3>

<p>위 코드를 MAME 디버거의 <code class="language-plaintext highlighter-rouge">region ':maincpu'</code>에 hex 값으로 입력하면 실제 동작이 적용된다.</p>

<p><img src="https://github.com/user-attachments/assets/3b5e8ceb-825f-432f-8390-28744e079d5f" alt="성공 장면" /></p>

<p>이렇게 하면 특정 아이템(LB oil, ID: $23)은 3개까지만 구매할 수 있고, 다른 아이템들은 기존처럼 9개까지 구매할 수 있게 된다.</p>

<h3 id="코드-설명">코드 설명</h3>

<ol>
  <li><code class="language-plaintext highlighter-rouge">cmp.b #0x23,D7b</code>: D7 레지스터의 하위 바이트가 $23(특정 아이템 ID)인지 비교</li>
  <li><code class="language-plaintext highlighter-rouge">beq.b LAB_0006300c</code>: 같다면 특정 아이템 처리 로직으로 점프</li>
  <li><code class="language-plaintext highlighter-rouge">cmpi.b #0x3,(A4)</code>: 특정 아이템의 경우 3개 제한</li>
  <li><code class="language-plaintext highlighter-rouge">cmpi.b #0x9,(A4)</code>: 일반 아이템의 경우 9개 제한</li>
  <li><code class="language-plaintext highlighter-rouge">bge.w LAB_00063022</code>: 제한을 초과하면 실패 주소로 점프</li>
  <li><code class="language-plaintext highlighter-rouge">jmp LAB_000afba6.l</code>: 성공시 원래 성공 주소로 점프</li>
  <li><code class="language-plaintext highlighter-rouge">jmp LAB_000afbec.l</code>: 실패시 원래 실패 주소로 점프</li>
</ol>

<p>이런 식으로 코드 케이브를 이용하면 게임의 특정 로직만을 선택적으로 수정할 수 있다.</p>

<h2 id="마무리">마무리</h2>

<p>이번 실습을 통해 다음과 같은 과정을 거쳤다:</p>

<ol>
  <li><strong>메모리 검색</strong>: <code class="language-plaintext highlighter-rouge">ci</code>, <code class="language-plaintext highlighter-rouge">cn +</code> 명령어로 아이템 구매 관련 메모리 주소 찾기</li>
  <li><strong>브레이크포인트 설정</strong>: <code class="language-plaintext highlighter-rouge">wp</code> 명령어로 쓰기 포인트 설정</li>
  <li><strong>코드 추적</strong>: 브레이크포인트를 통해 아이템 구매 로직의 최초 호출 지점 찾기</li>
  <li><strong>도구 활용</strong>: MAME 디버거, Ghidra, 68000 어셈블리어를 조합한 분석</li>
  <li><strong>코드 케이브 구현</strong>: 빈 메모리 공간에 새로운 로직 삽입</li>
</ol>

<p>이 과정은 단순한 메모리 치팅을 넘어서 게임의 내부 로직을 이해하고 수정하는 진정한 리버스 엔지니어링의 시작점이다.</p>

<p>고전 게임 해킹의 매력은 바로 이런 과정을 통해 게임 개발자들이 어떻게 생각하고 코드를 작성했는지 엿볼 수 있다는 점에 있다. 68000 어셈블리어라는 낯선 언어를 통해 30년 전 개발자들과 대화하는 듯한 느낌을 받을 수 있을 것이다.</p>]]></content><author><name>경수</name></author><category term="mame" /><category term="rom-hacking" /><category term="reverse-engineering" /><category term="game" /><category term="emulator" /><summary type="html"><![CDATA[MAME 기반의 게임 해킹]]></summary></entry><entry><title type="html">이론없이 컴파일러 만들기 꿈파일러 1</title><link href="https://hsnks100.github.io/%EC%9D%B4%EB%A1%A0%EC%97%86%EC%9D%B4-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%BF%88%ED%8C%8C%EC%9D%BC%EB%9F%AC-1/" rel="alternate" type="text/html" title="이론없이 컴파일러 만들기 꿈파일러 1" /><published>2020-01-19T00:00:00+09:00</published><updated>2020-01-19T00:00:00+09:00</updated><id>https://hsnks100.github.io/%EC%9D%B4%EB%A1%A0%EC%97%86%EC%9D%B4-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%BF%88%ED%8C%8C%EC%9D%BC%EB%9F%AC-1</id><content type="html" xml:base="https://hsnks100.github.io/%EC%9D%B4%EB%A1%A0%EC%97%86%EC%9D%B4-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%BF%88%ED%8C%8C%EC%9D%BC%EB%9F%AC-1/"><![CDATA[<h1 id="어릴-적-이야기-및-제작동기">어릴 적 이야기 및 제작동기</h1>
<p>어릴 적 이야기. 나는 학교에서 한 반에 있을 법한 컴퓨터를 매우 좋아하는 아이였다. 뭐 누구나 그렇겠지만 좋아하면 더 하고 싶고, 더 공부하고 싶고 그런다. 흘러 흘러 프로그래밍을 배우게 되었고 중3 겨울방학 Win32 API 에 빠져있던 무렵, 나에게 프로그래밍을 알려주는 멘토와 같은 형에게 물어봤다.</p>

<p>Q. 컴파일러 만드는거 어려워?</p>

<p>A. <strong>어렵지 않을까?</strong></p>

<p>그 때 당시는 프로그래밍에 대해서는 걸음마를 하고 있던터라 감히 컴파일러를 만들고 싶은 생각은 무참히 꺾였다. 실제로 알아 보니까 컴파일러를 만드는 과정은 정규 컴퓨터공학과정에서도 4학년에 다룰 정도로 <strong>난이도가 있었다.</strong> 사실은 수 년전에 컴파일러 비슷한 것을 만들긴 했다. 하지만 그 퀄리티가 너무 떨어지고 너무 급하게 진행했고 툴(lex, yacc)의 도움을 너무 심하게 받았기 때문에 아는 것이 <strong>제대로 없는 상태로</strong> 지나갔다.</p>

<p>직장생활을 계속 하던 중 머릿속에는 계속 <strong>컴파일러 만드는 과정을 한바퀴는 돌려봐야 하지 않을까?</strong> 라는 생각이 자꾸 들었다. 그 와중 회사동료가 1인 집짓기를 3년에 걸쳐서 하는 과정을 담은 링크를 보내줬다. 그 과정을 보면서 3년에 걸쳐서 집도 짓는데 키보드만 두들기면 되는 컴파일러 만들기를 못할 이유가 있을까? 생각이 들었고, 곧장 그 작업을 하기 시작했다.</p>

<p>이 때 참고한 두 서적은 컴파일러 책과 가상머신에 대한 책이다.</p>

<p><img src="https://user-images.githubusercontent.com/3623889/72685915-05cdd300-3b32-11ea-9aaf-85a813dff641.png" alt="image" /></p>

<p><img src="https://user-images.githubusercontent.com/3623889/72685923-2007b100-3b32-11ea-98c4-e5cae341df11.png" alt="image" /></p>

<p>하지만 내 글에서 __이론적인 이야기는 최대한 배제__하고 진행할 것이다. 어릴 적 간직하고 있는 꿈을 실현한다는 의미로 컴파일러의 이름을 꿈파일러로~~</p>

<h1 id="방법에-대한-개요">방법에 대한 개요</h1>

<p><img src="https://user-images.githubusercontent.com/3623889/72682192-dc02b500-3b0d-11ea-973b-d82562f943b8.png" alt="image" /></p>

<p>일반적인 컴파일러는 <strong>토큰화(Lexical Analysis 라고도 부른다) - 구문분석 - 의미분석 - 중간코드생성 - 최적화 - 코드생성</strong> 과정을 거친다. 내 글에서는 의미분석과 최적화 코드생성은 <strong>개나 줘라</strong> 마인드로 과감하게 버린다. 컴파일러에 밀접한 것들만 다루기에도 벅차며, 코드생성에서 아키텍쳐에 대해 종속적인 요소가 있게 되면 컴파일러를 만드는 과정보다 아키텍쳐공부가 더 크다.</p>

<p>구현 방식은 가장 구현하기 편한 방식으로 진행한다. 토큰화만 flex 를 쓰고 나머지 구문분석, 중간코드 생성 및 가상머신 설계까지 다 손수 코딩을 하며 진행한다. 구문분석은 Top-Down 재귀방식으로 구현하겠다. 토큰화 과정을 flex 를 쓴 이유에 대해서 말하자면, 토큰화 과정은 순수하게 노가다과정이며 툴의 도움을 받아도 전체 과정을 이해하기 부족하지 않다. 그리고 타입은 없는 것으로 한다. 정적 타입도 좋지만 해보면 알겠지만 동적 타입이 더 만들기 쉽다.</p>

<h1 id="전체-흐름">전체 흐름</h1>

<p><strong>토큰화 - 구분분석 - 가상머신이 실행할 수 있는 형태로 코드 생성 - 가상머신 동작</strong></p>

<p>가상머신은 스택기반의 가상머신을 이용한다. 여기서 지역변수를 어떻게 다루는지 이해를 하면서 봐야한다.</p>

<h1 id="토큰화">토큰화</h1>

<p><strong>토큰화는 입력받은 문자열을 기본적인 카테고리로 분류하는 작업을 말한다.</strong> 보통 <strong>예약 키워드, 자료형, 식별자, 숫자, 사용되는 기호(+, -, *, /, %, !=, == 등등)로 분류한다.</strong> 이 과정에는 식별자가 함수이름인지 변수이름인지 모르는 단계다. 예를 들면</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>abc = 33;
abcd(11, 22, 33);
if a == 33 {
    
}
</code></pre></div></div>
<p>라는 코드가 있고 토큰화과정을 거친다면 순서대로 배열에 다음과 같이 담길 수 있다.</p>

<p>[id, ‘=’, number, ‘;’, id, ‘(‘, number, ‘,’, number, ‘,’, number, ‘)’, ‘;’, if, id, ‘==’, number, ‘{‘, ‘}’]</p>

<p>말 그대로 1차적인 분류다. 이 분류를 이용해 구문분석이 이뤄진다.</p>

<p>지면이 길어져 이만하고 다음장으로 넘긴다.</p>]]></content><author><name>경수</name></author><summary type="html"><![CDATA[어릴 적 이야기 및 제작동기 어릴 적 이야기. 나는 학교에서 한 반에 있을 법한 컴퓨터를 매우 좋아하는 아이였다. 뭐 누구나 그렇겠지만 좋아하면 더 하고 싶고, 더 공부하고 싶고 그런다. 흘러 흘러 프로그래밍을 배우게 되었고 중3 겨울방학 Win32 API 에 빠져있던 무렵, 나에게 프로그래밍을 알려주는 멘토와 같은 형에게 물어봤다.]]></summary></entry><entry><title type="html">이론없이 컴파일러 만들기 꿈파일러 2</title><link href="https://hsnks100.github.io/%EC%9D%B4%EB%A1%A0%EC%97%86%EC%9D%B4-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%BF%88%ED%8C%8C%EC%9D%BC%EB%9F%AC-2/" rel="alternate" type="text/html" title="이론없이 컴파일러 만들기 꿈파일러 2" /><published>2020-01-19T00:00:00+09:00</published><updated>2020-01-19T00:00:00+09:00</updated><id>https://hsnks100.github.io/%EC%9D%B4%EB%A1%A0%EC%97%86%EC%9D%B4-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%BF%88%ED%8C%8C%EC%9D%BC%EB%9F%AC-2</id><content type="html" xml:base="https://hsnks100.github.io/%EC%9D%B4%EB%A1%A0%EC%97%86%EC%9D%B4-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%BF%88%ED%8C%8C%EC%9D%BC%EB%9F%AC-2/"><![CDATA[<h1 id="토큰화하는-방법">토큰화하는 방법</h1>

<p>이전 장에서 토큰화가 무엇인지 알아봤다.</p>

<p>우리는 산업역군의 프로그래머이기 때문에 구현가능한 형태를 원한다. 예제: flex 를 이용해서 간단히 토큰화를 어떻게 하는지 살펴보겠다.</p>

<p>flex 가 무엇인지 살펴보면</p>

<blockquote>
  <p>flex는 《fast lexical analyzer generator》의 줄임말로 lex의 기능을 개선한 자유 소프트웨어이다. 주로 bison과 쌍을 이루어 구문 분석기를 만드는 데 사용된다. flex를 이용하면 C로 구문 문석 코드를 만들 수 있다. 한편 C++ 코드를 만들어 주는 비슷한 기능을 하는 프로그램으로 flex++가 있으며 flex와 함께 배포된다. 작성자는 “Vern Paxson”씨로 1987년도에 처음 만들어졌다.</p>
</blockquote>

<p>앞 장에서 봤듯 입력된 문법이 적법한지 상관없이 어떤 종류의 문자열인가를 판단하는 1차 분류로 생각하면 된다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>%{
  Prologue
%} 
Bison declarations 
%%
Grammar rules
%% 
Epilogue
</code></pre></div></div>

<p>구조는 위와 같은데, 우리는 마지막 Epilogue 는 쓰지 않을 것이다. 처음 %{ … %} 사이에는 include 문들이 오고, Bison declarations 에는 flex 가 해당 문법파일을 어떻게 컴파일할것인지 옵션이 담긴다. 다음으로 Grammer Rules 에 우리가 분류해야 할 토큰 규칙들이 위치한다. 자세한 것은 인터넷 검색을 통해 해결하도록 하자. flex 에 대한 것들을 자세히 설명하기보다 바로 예제를 통해 이해하도록 한다.</p>

<p>예를 들어 보자.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">%</span><span class="p">{</span>
<span class="cp">#include</span> <span class="cpf">&lt;iostream&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;cstdlib&gt;</span><span class="cp">
#include</span> <span class="cpf">"scanner.h"</span><span class="cp">
</span><span class="k">using</span> <span class="k">namespace</span> <span class="n">std</span><span class="p">;</span>
<span class="cp">#define yyterminate() 0;
</span><span class="o">%</span><span class="p">}</span>
<span class="o">%</span><span class="n">option</span> <span class="n">nodefault</span>
<span class="o">%</span><span class="n">option</span> <span class="n">noyywrap</span>
<span class="o">%</span><span class="n">option</span> <span class="n">c</span><span class="o">++</span>
<span class="o">%</span><span class="n">option</span> <span class="n">yyclass</span><span class="o">=</span><span class="s">"Scanner"</span>
<span class="o">%</span><span class="n">option</span> <span class="n">yylineno</span>
<span class="o">%%</span>
<span class="p">[</span><span class="n">a</span><span class="o">-</span><span class="n">z</span><span class="p">][</span><span class="n">a</span><span class="o">-</span><span class="n">z0</span><span class="o">-</span><span class="mi">9</span><span class="p">]</span><span class="o">*</span> <span class="p">{</span> 
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="n">yytext</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
    <span class="k">return</span> <span class="mi">11</span><span class="p">;</span>
<span class="p">}</span> 
<span class="p">[</span><span class="mi">0</span><span class="o">-</span><span class="mi">9</span><span class="p">][</span><span class="mi">0</span><span class="o">-</span><span class="mi">9</span><span class="p">]</span><span class="o">*</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="n">yytext</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
    <span class="k">return</span> <span class="mi">12</span><span class="p">;</span>
<span class="p">}</span> 
<span class="o">=</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="n">yytext</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
    <span class="k">return</span> <span class="mi">13</span><span class="p">;</span>
<span class="p">}</span> 
<span class="p">[</span><span class="err">\</span><span class="n">n</span><span class="err">\</span><span class="n">t</span> <span class="p">]</span> <span class="p">{</span>
<span class="p">}</span> 
<span class="p">.</span> <span class="p">{</span> 
<span class="p">}</span> 
<span class="o">&lt;&lt;</span><span class="n">EOF</span><span class="o">&gt;&gt;</span> <span class="p">{</span> 
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"EOF"</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
    <span class="k">return</span> <span class="n">yyterminate</span><span class="p">();</span> 
<span class="p">}</span> 
<span class="o">%%</span> 
</code></pre></div></div>

<p>%% 사이의 문법만 주목해서 보자. 정규식 { … } 형태로 기술되는데, 블록{} 사이에는 C/C++ 코드가 들어갈 수 있다.</p>

<p>이것만 기억하면 된다. <code class="language-plaintext highlighter-rouge">좌측에 정규식이 평가될 때 {} 사이의 코드가 실행된다.</code></p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// scanner.h</span>
<span class="k">class</span> <span class="nc">Scanner</span> <span class="o">:</span> <span class="k">public</span> <span class="n">yyFlexLexer</span> <span class="p">{</span>
    <span class="nl">public:</span>
        <span class="n">Scanner</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">istream</span> <span class="o">*</span><span class="n">in</span><span class="p">)</span> <span class="o">:</span> <span class="n">yyFlexLexer</span><span class="p">(</span><span class="n">in</span><span class="p">)</span> <span class="p">{}</span> 
        <span class="k">virtual</span> <span class="kt">int</span> <span class="n">yylex</span><span class="p">();</span> 
<span class="p">};</span>

<span class="c1">// lex.yy.cc</span>
<span class="cp">#define YY_DECL int Scanner::yylex() 
</span><span class="p">...</span>
<span class="cp">#include</span> <span class="cpf">"scanner.h"</span><span class="cp">
</span><span class="p">...</span>
<span class="n">YY_DECL</span>
<span class="p">{</span>
	<span class="k">register</span> <span class="n">yy_state_type</span> <span class="n">yy_current_state</span><span class="p">;</span>
	<span class="k">register</span> <span class="kt">char</span> <span class="o">*</span><span class="n">yy_cp</span><span class="p">,</span> <span class="o">*</span><span class="n">yy_bp</span><span class="p">;</span>
	<span class="k">register</span> <span class="kt">int</span> <span class="n">yy_act</span><span class="p">;</span>
    <span class="p">...</span>
<span class="p">}</span>

<span class="k">case</span> <span class="mi">1</span><span class="p">:</span>
<span class="n">YY_RULE_SETUP</span>
<span class="cp">#line 14 "scanner.l"
</span><span class="p">{</span> 
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="n">yytext</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
    <span class="k">return</span> <span class="mi">11</span><span class="p">;</span>
<span class="p">}</span> 
	<span class="n">YY_BREAK</span>
<span class="k">case</span> <span class="mi">2</span><span class="p">:</span>
<span class="n">YY_RULE_SETUP</span>
<span class="cp">#line 18 "scanner.l"
</span><span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="n">yytext</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
    <span class="k">return</span> <span class="mi">12</span><span class="p">;</span>
<span class="p">}</span> 
	<span class="n">YY_BREAK</span>
<span class="k">case</span> <span class="mi">3</span><span class="p">:</span>
<span class="n">YY_RULE_SETUP</span>
<span class="cp">#line 22 "scanner.l"
</span><span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="n">yytext</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
    <span class="k">return</span> <span class="mi">13</span><span class="p">;</span>
<span class="p">}</span> 
	<span class="n">YY_BREAK</span>
<span class="k">case</span> <span class="mi">4</span><span class="p">:</span>
<span class="cm">/* rule 4 can match eol */</span>
<span class="n">YY_RULE_SETUP</span>
<span class="cp">#line 26 "scanner.l"
</span><span class="p">{</span>
<span class="p">}</span> 
	<span class="n">YY_BREAK</span>
<span class="k">case</span> <span class="mi">5</span><span class="p">:</span>
<span class="n">YY_RULE_SETUP</span>
<span class="cp">#line 28 "scanner.l"
</span><span class="p">{</span> 
<span class="p">}</span> 
	<span class="n">YY_BREAK</span>
<span class="k">case</span> <span class="n">YY_STATE_EOF</span><span class="p">(</span><span class="n">INITIAL</span><span class="p">):</span>
<span class="cp">#line 30 "scanner.l"
</span><span class="p">{</span> 
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"EOF"</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
    <span class="k">return</span> <span class="n">yyterminate</span><span class="p">();</span> 
<span class="p">}</span> 
	<span class="n">YY_BREAK</span>

</code></pre></div></div>

<p>위 코드를 보면 scanner.l 이 flex를 이용하여 생성된 lex.yy.cc 파일에 블록 {} 사이의 코드가 그대로 들어간 모습을 볼 수 있다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>▾ tutorial1/
    main.cpp
    scanner.h
    scanner.l
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cd tutorial1 
$ flex scanner.l &amp;&amp; g++ lex.yy.cc main.cpp -std=c++11 &amp;&amp; ./a.out 
</code></pre></div></div>

<p>일부러 흐름을 잘 보여주기 위해 당분간은 CMake 를 쓰지 않겠다. 컴파일 되는 흐름을 잘 이해해야 한다. 먼저 flex 명령어를 통해 lex.yy.cc 가 생긴다. lex.yy.cc 안에는 <code class="language-plaintext highlighter-rouge">%option yyclass="Scanner"</code> 에 의해 <code class="language-plaintext highlighter-rouge">YY_DECL</code> 매크로가 정의된다.</p>

<p>또한, Scanner::yylex 가 overriding 으로 구현된다.</p>

<p>렉서파일에 scanner.h 를 쓰고 또 Scanner 클래스가 flex 를 쓰고, 생성된 파일(lex.yy.cc) 에서 scanner.h 를 쓰는 이상한 기분이지만 이 상호작용을 잘 이해하고 넘어가기 바란다.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// main.cpp</span>
    <span class="n">std</span><span class="o">::</span><span class="n">istringstream</span> <span class="nf">is</span><span class="p">(</span><span class="s">R"(
    apple 5543 hello world =
    )"</span><span class="p">);</span> 
    <span class="n">Scanner</span> <span class="nf">scanner</span><span class="p">(</span><span class="o">&amp;</span><span class="n">is</span><span class="p">);</span>
    <span class="k">while</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">int</span> <span class="n">t</span> <span class="o">=</span> <span class="n">scanner</span><span class="p">.</span><span class="n">yylex</span><span class="p">();</span>
        <span class="k">if</span><span class="p">(</span><span class="n">t</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="n">t</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
    <span class="p">}</span>

</code></pre></div></div>

<p>위 프로그램에 의해 flex 를 실행할 수 있다.</p>

<p><code class="language-plaintext highlighter-rouge">apple 5543 hello world =</code> 입력된 문자열이 이와 같을 때 토큰 분류기는 <code class="language-plaintext highlighter-rouge">11 12 11 11 13</code> 과 같이 반환된다. 이렇게 토큰화를 통해 숫자형 배열에 담은 후 구문분석(Parsing)을 하게 된다.</p>

<p>동작하는 예제는 git clone https://github.com/hsnks100/dreampiler.git 의 tutorial1</p>]]></content><author><name>경수</name></author><summary type="html"><![CDATA[토큰화하는 방법]]></summary></entry></feed>