<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>상구너의 개발노트</title>
    <link>https://smokingbird.tistory.com/</link>
    <description>개발, AI, 자동화, 일상 기록을 담는 상구너의 작업노트. 직접 해본 것, 써본 것, 정리한 것을 가볍고 실용적으로 남깁니다</description>
    <language>ko</language>
    <pubDate>Sat, 25 Jul 2026 10:23:50 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>상구너</managingEditor>
    <image>
      <title>상구너의 개발노트</title>
      <url>https://tistory1.daumcdn.net/tistory/110138/attach/bbbab70f3da9484d97fc28b76bf1b129</url>
      <link>https://smokingbird.tistory.com</link>
    </image>
    <item>
      <title>손바닥 위의 작업실 &amp;mdash; AI라는 사진작가에게 말 거는 법</title>
      <link>https://smokingbird.tistory.com/87</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평범한 개발자가 AI로 1,000장을 뽑으며 알게 된 것들&lt;br /&gt;&amp;mdash; 2026년 7월 10일, 에르메스단 웨비나 후기&lt;/p&gt;
&lt;/blockquote&gt;
&lt;!-- [대표 이미지] 발표용표지.webp — 색연필 톤 일러스트, 손바닥 위 카메라 든 흰 생쥐를 바라보는 소녀 --&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;발표용표지.webp&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMCo1W/dJMcadiCswt/GMe6jEukcTpRgnUUb1eOWK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMCo1W/dJMcadiCswt/GMe6jEukcTpRgnUUb1eOWK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMCo1W/dJMcadiCswt/GMe6jEukcTpRgnUUb1eOWK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMCo1W%2FdJMcadiCswt%2FGMe6jEukcTpRgnUUb1eOWK%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1672&quot; height=&quot;941&quot; data-filename=&quot;발표용표지.webp&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;안녕하세요, 스레드에서 &lt;b&gt;크로아상생쥐&lt;/b&gt;로 활동하는 사람입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 7월 10일, 제가 활동 중인 AI 커뮤니티에서 &lt;b&gt;'AI 이미지 생성하며 얻은 인사이트'&lt;/b&gt;를 주제로 웨비나를 하나 진행했어요. 그날 나눴던 이야기를 못 오신 분들을 위해 짧게 갈무리로 남겨둡니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미리 말하자면 이건 강의가 아니에요. 그림도 못 그리고 포토샵도 모르는 &lt;b&gt;평범한 개발자&lt;/b&gt;가 어쩌다 이미지를 1,000장이나 뽑게 됐는지, 그 과정을 솔직하게 풀어본 이야기예요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작은 우연이었어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 날 밤, 습관처럼 타임라인을 넘기다 누가 올린 이미지 생성 프롬프트를 봤어요. *&quot;아이폰으로 우연히 찍힌 일상처럼 보여줘&quot;* 정도의 몇 줄이 전부였죠. 아무 기대 없이 그대로 따라 해봤는데, 결과를 본 순간 저도 모르게 멈칫했어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;어? 이게 생각보다 괜찮은데?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 별거 아닌 '어?' 한 마디가 시작이었어요. 그날 이후로 멈출 수가 없었거든요. 남들 넷플릭스 볼 시간에 저는 이미지를 뽑고 있었고, 마음에 드는 프롬프트는 슬쩍슬쩍 메모해뒀어요. 그렇게 놀이처럼 뽑다 보니 어느새 1,000장이 넘어 있더라고요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그 과정에서 알게 된 세 가지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수백 장을 뽑는 동안, 기술 같은 건 하나도 안 늘었어요. 대신 이런 걸 알게 됐어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫째, 양이 안목을 만들더라고요.&lt;/b&gt; 많이 보다 보니 잘 나온 그림과 그냥 그런 그림이 눈으로 구분되기 시작했어요. 재능이 아니라 그냥 많이 봤을 뿐이에요. 양이 먼저 오고, 안목은 그 뒤를 따라왔어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;둘째, 프롬프트는 결국 '촬영 기획'이었어요.&lt;/b&gt; 어렵게 생각할 필요가 없었어요. 제가 감독이고, AI는 말귀 잘 알아듣는 베테랑 사진작가라고 생각하니까 확 편해지더라고요. 전문용어 없이 *&quot;청명한 하늘 느낌으로, 8월 오전 9시 색감으로, 눈빛은 몽환적으로&quot;* 라고 말해도 AI는 찰떡같이 알아들어요.&lt;/p&gt;
&lt;!-- [이미지] S10-plain.webp (왼쪽 · 막연한 &quot;여자 사진&quot;) / S10-detail.webp (오른쪽 · 디렉션을 준 결과) 나란히 비교 --&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;S10-plain.webp&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4LhId/dJMcaidbpai/N2lCe9VCGNBYwGIt1kLyy1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4LhId/dJMcaidbpai/N2lCe9VCGNBYwGIt1kLyy1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4LhId/dJMcaidbpai/N2lCe9VCGNBYwGIt1kLyy1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4LhId%2FdJMcaidbpai%2FN2lCe9VCGNBYwGIt1kLyy1%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;1200&quot; data-filename=&quot;S10-plain.webp&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;1200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;S10-detail.webp&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s9gkE/dJMcaiKSr0M/VKXSDu1F9obog2ucQDIpR1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s9gkE/dJMcaiKSr0M/VKXSDu1F9obog2ucQDIpR1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s9gkE/dJMcaiKSr0M/VKXSDu1F9obog2ucQDIpR1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs9gkE%2FdJMcaiKSr0M%2FVKXSDu1F9obog2ucQDIpR1%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;1200&quot; data-filename=&quot;S10-detail.webp&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;1200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽은 그냥 &quot;여자 사진&quot; 정도로 막연하게, 오른쪽은 색감&amp;middot;시간대&amp;middot;무드까지 디렉션을 준 결과예요. 핵심은 전문지식이 아니라 &lt;b&gt;내가 원하는 걸 얼마나 구체적으로 말로 옮기느냐&lt;/b&gt;였어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;셋째, 그래서 진짜 실력은 취향을 언어로 번역하는 거였어요.&lt;/b&gt; '청명한', '몽환적인' 같은 표현이 처음엔 잘 안 나왔는데, 자꾸 하다 보니 제 취향이 어떤 단어인지 알게 되더라고요. 결국 그 단어들이 전부 제 취향이었어요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;나머지 이야기는 발표 자료에&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 관점을 계속 밀고 나가다 보니 어느 순간 저만의 &lt;b&gt;'공식'&lt;/b&gt;이 생겼어요. 누가 &amp;rarr; 뭘 입고 &amp;rarr; 어디서 &amp;rarr; 어떤 빛으로 &amp;rarr; 어떤 무드로. 이 순서만 지키면 화풍을 바꿔도 같은 인물이 유지되더라고요.&lt;/p&gt;
&lt;!-- [이미지] 발표용표지.webp (색연필 톤) / 홍보용표지.webp (애니 톤) 나란히 — 같은 캐릭터, 다른 화풍 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 '공식'을 실물로 뜯어보고, 제가 실제로 쓰는 템플릿이나 라이브로 한 장 뽑는 과정, 그리고 저작권 같은 자주 나오는 질문들까지 &amp;mdash; 자세한 이야기는 발표 자료에 그대로 담아뒀어요. 슬라이드를 넘기면서 직접 눌러보실 수 있게 만들어뒀으니, 관심 있으신 분들은 편하게 구경해 주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;웨비나 발표 자료 보러 가기: &lt;a href=&quot;https://sanguneo.github.io/webinar260710&quot;&gt;https://sanguneo.github.io/webinar260710&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌아보면 제가 특별한 재능이 생긴 게 아니에요. 그냥 첫 한 장을 뽑아봤고, 재밌어서 계속했을 뿐이에요. 사실 오늘 이야기한 공식이니 템플릿이니 하는 것도 다 잊으셔도 돼요. 머릿속에 어렴풋이 떠 있는 그 막연한 그림을, 겁먹지 말고 솔직하게 말로 꺼내보는 것 그게 전부거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니 오늘, 딱 한 장만 뽑아보세요. 제가 느꼈던 그 '어?' 하는 순간을, 이제 여러분이 느껴보실 차례예요.  &lt;/p&gt;</description>
      <category>Study/AI</category>
      <category>AI그림</category>
      <category>AI이미지생성</category>
      <category>chatGPT</category>
      <category>나노바나나</category>
      <category>생성형AI</category>
      <category>에르메스단</category>
      <category>웨비나후기</category>
      <category>이미지프롬프트</category>
      <category>캐릭터시트</category>
      <category>프롬프트</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/87</guid>
      <comments>https://smokingbird.tistory.com/87#entry87comment</comments>
      <pubDate>Fri, 24 Jul 2026 15:57:48 +0900</pubDate>
    </item>
    <item>
      <title>AI 이미지 생성기 4종 비교 &amp;mdash; GPT vs Nano Banana vs MAI vs Grok (실사 인물 i2i 테스트)</title>
      <link>https://smokingbird.tistory.com/86</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트는 &lt;a href=&quot;https://www.threads.com/@reactor_art&quot;&gt;@reactor_art&lt;/a&gt; 님의 프롬프트를 변형해, i2i 인물 유지용으로 다듬어 사용했습니다. &lt;br /&gt;좋은 출발점을 주신 데 감사드려요.  &lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  테스트 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건은 최대한 공정하게 맞췄습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동일한 참조 사진&lt;/b&gt; 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동일한 프롬프트&lt;/b&gt;(글 맨 아래 전문 첨부)를 4개 도구에 그대로 입력&lt;/li&gt;
&lt;li&gt;핵심 평가 기준은 두 가지 &amp;mdash; ① &lt;b&gt;인물 동일성 유지&lt;/b&gt;, ② &lt;b&gt;분위기&amp;middot;무드 재현&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고사진은 이렇게 두개입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1000079265.png&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;1448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/us4TJ/dJMcaiXSaHJ/w13vH4uf3TWyik8jk6CFR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/us4TJ/dJMcaiXSaHJ/w13vH4uf3TWyik8jk6CFR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/us4TJ/dJMcaiXSaHJ/w13vH4uf3TWyik8jk6CFR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fus4TJ%2FdJMcaiXSaHJ%2Fw13vH4uf3TWyik8jk6CFR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;560&quot; data-filename=&quot;1000079265.png&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;1448&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1000079264.png&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;1402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GMCrU/dJMcaiXSaHI/PUBHdIPDsy10aOFcjhPRy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GMCrU/dJMcaiXSaHI/PUBHdIPDsy10aOFcjhPRy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GMCrU/dJMcaiXSaHI/PUBHdIPDsy10aOFcjhPRy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGMCrU%2FdJMcaiXSaHI%2FPUBHdIPDsy10aOFcjhPRy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;525&quot; data-filename=&quot;1000079264.png&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;1402&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트한 도구는 아래 4종입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Google &amp;mdash; Nano Banana Pro&lt;/b&gt; (Imagen 계열)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Microsoft &amp;mdash; MAI Image 2.5&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OpenAI &amp;mdash; GPT Image Gen2&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;xAI &amp;mdash; Grok&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자, 그럼 하나씩 결과를 볼까요?&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1️⃣ Google &amp;mdash; Nano Banana Pro&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;718906834_18091103075167340_2503962199770782553_n.webp&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1365&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WtCJd/dJMcabqZDic/x420YGOELGba3IvGv29whk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WtCJd/dJMcabqZDic/x420YGOELGba3IvGv29whk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WtCJd/dJMcabqZDic/x420YGOELGba3IvGv29whk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWtCJd%2FdJMcabqZDic%2Fx420YGOELGba3IvGv29whk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;746&quot; data-filename=&quot;718906834_18091103075167340_2503962199770782553_n.webp&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1365&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 구글의 &lt;b&gt;나노바나나 프로&lt;/b&gt;입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;✅ &lt;b&gt;사실적인 화질&lt;/b&gt;은 확실히 좋습니다. 디테일이 살아있어요.&lt;/li&gt;
&lt;li&gt;⚠️ 다만 &lt;b&gt;인물 유지가 애매합니다.&lt;/b&gt; 첨부 사진을 여러 장 넣어보기도 하고 한 장만 넣어보기도 했는데, 원본 인물과 다른 얼굴이 나오는 경우가 꽤 잦았어요.&lt;/li&gt;
&lt;li&gt;  화질 하나는 좋지만, &quot;이 사람 그대로&quot;가 중요한 i2i 작업에는 아쉬움이 남습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 줄 평:&lt;/b&gt; 화질은 깡패, 근데 사람이 자꾸 바뀐다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2️⃣ Microsoft &amp;mdash; MAI Image 2.5&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;719188784_18091103129167340_9036909336590678620_n.webp&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;1184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0xTI0/dJMcab5zT2g/Eaeo8RhEgNu0Y0CvQ8tDwk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0xTI0/dJMcab5zT2g/Eaeo8RhEgNu0Y0CvQ8tDwk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0xTI0/dJMcab5zT2g/Eaeo8RhEgNu0Y0CvQ8tDwk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0xTI0%2FdJMcab5zT2g%2FEaeo8RhEgNu0Y0CvQ8tDwk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;565&quot; data-filename=&quot;719188784_18091103129167340_9036909336590678620_n.webp&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;1184&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;720199873_18091103126167340_3814381500489376307_n.webp&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;1184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCgN4p/dJMcab5zT2h/DkzOjgPnNlPZDyavAjEaTk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCgN4p/dJMcab5zT2h/DkzOjgPnNlPZDyavAjEaTk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCgN4p/dJMcab5zT2h/DkzOjgPnNlPZDyavAjEaTk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCgN4p%2FdJMcab5zT2h%2FDkzOjgPnNlPZDyavAjEaTk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;565&quot; data-filename=&quot;720199873_18091103126167340_3814381500489376307_n.webp&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;1184&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;719251699_18091103105167340_4421914760124032910_n.webp&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;1184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceTZxY/dJMcacKeC6e/IEs8EZtJKBbfkXsl86uKK1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceTZxY/dJMcacKeC6e/IEs8EZtJKBbfkXsl86uKK1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceTZxY/dJMcacKeC6e/IEs8EZtJKBbfkXsl86uKK1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceTZxY%2FdJMcacKeC6e%2FIEs8EZtJKBbfkXsl86uKK1%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;565&quot; data-filename=&quot;719251699_18091103105167340_4421914760124032910_n.webp&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;1184&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이크로소프트가 최근 선보인 &lt;b&gt;MAI Image 2.5&lt;/b&gt;입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;  결과물이 &lt;b&gt;사진보다는 일러스트에 가까운 느낌&lt;/b&gt;이에요.&lt;/li&gt;
&lt;li&gt;  그런데 &lt;b&gt;색감과 빛 표현만큼은 이번에 테스트한 어떤 도구보다도 뛰어났습니다.&lt;/b&gt; 이 부분은 정말 인상적이었어요.&lt;/li&gt;
&lt;li&gt;  사실적인 인물 사진보다는, 분위기 있고 무드가 살아있는 작화에 강점이 있는 도구입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 줄 평:&lt;/b&gt; 색감&amp;middot;조명 깡패. 빛을 다룰 줄 안다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3️⃣ OpenAI &amp;mdash; GPT Image Gen2&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;720019262_18091103138167340_1229870755499510919_n.webp&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;1402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cACDTZ/dJMcaayRxsj/zEKFv3ROIgjiLztmxgKU2K/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cACDTZ/dJMcaayRxsj/zEKFv3ROIgjiLztmxgKU2K/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cACDTZ/dJMcaayRxsj/zEKFv3ROIgjiLztmxgKU2K/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcACDTZ%2FdJMcaayRxsj%2FzEKFv3ROIgjiLztmxgKU2K%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;525&quot; data-filename=&quot;720019262_18091103138167340_1229870755499510919_n.webp&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;1402&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;719171937_18091103108167340_5340893851428290548_n.webp&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;1402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNllw2/dJMcaayRxsk/dQwYRYz3kvkVJaa1Lu6I4K/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNllw2/dJMcaayRxsk/dQwYRYz3kvkVJaa1Lu6I4K/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNllw2/dJMcaayRxsk/dQwYRYz3kvkVJaa1Lu6I4K/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNllw2%2FdJMcaayRxsk%2FdQwYRYz3kvkVJaa1Lu6I4K%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;525&quot; data-filename=&quot;719171937_18091103108167340_5340893851428290548_n.webp&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;1402&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 OpenAI의 &lt;b&gt;GPT Image Gen2&lt;/b&gt;.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;  &lt;b&gt;실사 느낌 하나는 끝내줍니다.&lt;/b&gt; 진짜 사진 같은 표현력이 압권이에요.&lt;/li&gt;
&lt;li&gt;  색감도 과하지 않게 적당히 잘 뽑아내고요.&lt;/li&gt;
&lt;li&gt;⚠️ 다만 제가 잘 못 쓰는 건지, 이미지가 가끔 &lt;b&gt;살짝 뭉개져 보이는&lt;/b&gt; 현상이 있었어요. 나중에 업스케일링하고 정리 좀 하면 확실히 나아지긴 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 줄 평:&lt;/b&gt; 실사력 1등. 가끔 뭉개지는 건 업스케일로 커버.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4️⃣ xAI &amp;mdash; Grok&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;718189006_18091103096167340_4024835690702105161_n.webp&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;1136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf7slf/dJMcahktOm7/reCuarUBsKFD6nBOWMj8F0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf7slf/dJMcahktOm7/reCuarUBsKFD6nBOWMj8F0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf7slf/dJMcahktOm7/reCuarUBsKFD6nBOWMj8F0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf7slf%2FdJMcahktOm7%2FreCuarUBsKFD6nBOWMj8F0%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;523&quot; data-filename=&quot;718189006_18091103096167340_4024835690702105161_n.webp&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;1136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막은 일론 머스크의 xAI, &lt;b&gt;Grok&lt;/b&gt;입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;  유료 계정이 아니라 솔직히 기대는 안 했어요.&lt;/li&gt;
&lt;li&gt;  그런데 &lt;b&gt;생각보다 준수하게 뽑아줍니다.&lt;/b&gt; 무료 티어 기준으로는 충분히 만족스러운 수준이었어요.&lt;/li&gt;
&lt;li&gt;  가볍게 써보기엔 나쁘지 않은 선택지.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 줄 평:&lt;/b&gt; 무료 치고 잘한다. 기대 안 했는데 의외.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  종합 비교표&lt;/h2&gt;
&lt;table style=&quot;width: 100%;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 191px;&quot; align=&quot;left&quot;&gt;도구&lt;/th&gt;
&lt;th style=&quot;width: 114px;&quot; align=&quot;center&quot;&gt;실사 표현&lt;/th&gt;
&lt;th style=&quot;width: 97px;&quot; align=&quot;center&quot;&gt;인물 유지&lt;/th&gt;
&lt;th style=&quot;width: 116px;&quot; align=&quot;center&quot;&gt;색감&amp;middot;빛&lt;/th&gt;
&lt;th style=&quot;width: 339px;&quot; align=&quot;left&quot;&gt;특이사항&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 191px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;Nano Banana Pro&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 114px;&quot; align=&quot;center&quot;&gt;★★★★☆&lt;/td&gt;
&lt;td style=&quot;width: 97px;&quot; align=&quot;center&quot;&gt;★★☆☆☆&lt;/td&gt;
&lt;td style=&quot;width: 116px;&quot; align=&quot;center&quot;&gt;★★★☆☆&lt;/td&gt;
&lt;td style=&quot;width: 339px;&quot; align=&quot;left&quot;&gt;인물 유지 불안정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 191px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;MAI Image 2.5&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 114px;&quot; align=&quot;center&quot;&gt;★★★☆☆&lt;/td&gt;
&lt;td style=&quot;width: 97px;&quot; align=&quot;center&quot;&gt;★★★☆☆&lt;/td&gt;
&lt;td style=&quot;width: 116px;&quot; align=&quot;center&quot;&gt;★★★★★&lt;/td&gt;
&lt;td style=&quot;width: 339px;&quot; align=&quot;left&quot;&gt;일러스트 느낌 강함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 191px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;GPT Image Gen2&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 114px;&quot; align=&quot;center&quot;&gt;★★★★★&lt;/td&gt;
&lt;td style=&quot;width: 97px;&quot; align=&quot;center&quot;&gt;★★★★☆&lt;/td&gt;
&lt;td style=&quot;width: 116px;&quot; align=&quot;center&quot;&gt;★★★★☆&lt;/td&gt;
&lt;td style=&quot;width: 339px;&quot; align=&quot;left&quot;&gt;간혹 블러 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 191px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;Grok&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 114px;&quot; align=&quot;center&quot;&gt;★★★☆☆&lt;/td&gt;
&lt;td style=&quot;width: 97px;&quot; align=&quot;center&quot;&gt;★★★☆☆&lt;/td&gt;
&lt;td style=&quot;width: 116px;&quot; align=&quot;center&quot;&gt;★★★☆☆&lt;/td&gt;
&lt;td style=&quot;width: 339px;&quot; align=&quot;left&quot;&gt;무료 치고 준수&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  그래서 결론은?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 도구를 다 써보고 나니 한 가지 생각이 들었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MAI의 색감&amp;middot;조명 표현력&lt;/b&gt;과 &lt;b&gt;GPT의 실사 묘사력&lt;/b&gt;을 잘 버무리면 정말 좋은 결과가 나오겠다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 실제로 두 도구의 장점을 조합해서 최종 결과물을 만들어봤는데요&amp;hellip;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과는? 너무 마음에 들었습니다.&lt;/b&gt;  &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;719050585_18091103111167340_5821457961986540622_n.webp&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;1402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cg9Eva/dJMb99012au/yk48MBT6hFCOv9jRlykkPK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cg9Eva/dJMb99012au/yk48MBT6hFCOv9jRlykkPK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cg9Eva/dJMb99012au/yk48MBT6hFCOv9jRlykkPK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcg9Eva%2FdJMb99012au%2Fyk48MBT6hFCOv9jRlykkPK%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;525&quot; data-filename=&quot;719050585_18091103111167340_5821457961986540622_n.webp&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;1402&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;색감은 MAI처럼 분위기 있게, 디테일과 실사감은 GPT처럼. 하나의 도구만 고집하기보다는, &lt;b&gt;각 도구의 강점을 이해하고 조합하는 것&lt;/b&gt;이 지금 단계에선 가장 현실적인 정답인 것 같아요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  사용한 프롬프트 (인물 유지 i2i)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 따라 해보고 싶은 분들을 위해 프롬프트 전문 공유합니다. 참조 사진을 업로드하고 아래 프롬프트를 넣으시면 됩니다. (인물의 얼굴 동일성 유지에 최우선 가중치를 둔 구성이에요.)&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Create a whimsical portrait based on the uploaded reference photo,
preserving the subject's recognizable facial identity as the highest priority.
The generated woman must clearly look like the same person as in the reference image.

Keep her core facial features consistent, including her face shape, head shape,
eye shape and spacing, nose structure, lip shape, jawline, skin tone,
overall age impression, and natural facial asymmetry.
Do not redesign her face, overly beautify her, or make her look like a different person.

Show the same young East Asian woman standing in a lush rose garden,
gently holding a white rabbit against her chest.
Her skin should have a dewy chok-chok glow with natural texture.
Give her a bright blue bob haircut with soft bangs,
and round pale-blue sunglasses that add a playful, surreal charm
while still keeping her identity recognizable.
Her expression should feel calm, gentle, and slightly dreamy.

She wears an elegant white high-neck dress decorated with delicate lace,
creating a refined vintage feeling.
Around her, pink roses bloom densely, and several white rabbits with red eyes
rest among the flowers.
Tiny blue butterflies flutter throughout the scene, adding a dreamy storybook atmosphere.
The blurred greenery in the background should keep the focus on her face,
vivid hair, soft styling, and the tender fantasy mood of the garden.

Use a high-detail, photorealistic fashion portrait style
with a soft pastel atmosphere and a whimsical fairytale mood.
Maintain realistic anatomy, believable hair strands, natural skin texture
with slight imperfections, and a clean, polished composition.
Avoid plastic-looking skin, excessive retouching, warped facial features,
awkward hands, meaningless text, or watermarks.&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✍️ 마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 비교를 정리하면 이렇습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;실사가 중요하다면 &amp;rarr; GPT Image Gen2&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분위기&amp;middot;색감이 중요하다면 &amp;rarr; MAI Image 2.5&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;화질만 본다면 &amp;rarr; Nano Banana Pro (단, 인물 유지는 포기)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부담 없이 무료로 &amp;rarr; Grok&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 이미지 도구는 지금도 빠르게 발전하고 있어서, 몇 달 뒤면 또 순위가 바뀌어 있을지도 모르겠네요. 그래도 &lt;b&gt;각 모델의 성격을 파악해두면&lt;/b&gt; 어떤 작업에 어떤 도구를 꺼내 들지 감이 잡힙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분은 어떤 도구를 가장 즐겨 쓰시나요? 댓글로 알려주세요!  &lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이 글이 도움이 되셨다면 공감과 댓글 부탁드려요!&lt;/i&gt;&lt;/p&gt;</description>
      <category>Study/AI</category>
      <category>Ai</category>
      <category>GPTImage</category>
      <category>grok</category>
      <category>i2i</category>
      <category>MAIImage</category>
      <category>나노바나나</category>
      <category>이미지생성</category>
      <category>이미지생성AI비교</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/86</guid>
      <comments>https://smokingbird.tistory.com/86#entry86comment</comments>
      <pubDate>Wed, 10 Jun 2026 17:38:08 +0900</pubDate>
    </item>
    <item>
      <title>한글(.hwp) 파일을 브라우저에서 바로 PDF 로 &amp;mdash; rhwptopdf 만든 이야기</title>
      <link>https://smokingbird.tistory.com/85</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국에서 일하다 보면 &lt;code&gt;.hwp&lt;/code&gt; / &lt;code&gt;.hwpx&lt;/code&gt; 를 피할 수가 없습니다. 관공서, 학교, 사내 양식까지 &amp;mdash; &quot;PDF 로 좀 바꿔 주세요&quot; 한 줄이 의외로 무겁습니다. 이 글은 그 한 줄을 &lt;b&gt;브라우저 안에서, 업로드 없이, 오프라인으로&lt;/b&gt; 해결하려고 만든 오픈소스 &lt;code&gt;rhwptopdf&lt;/code&gt; 의 제작기입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;온라인 데모&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://sanguneo.github.io/rhwptopdf/&quot;&gt;https://sanguneo.github.io/rhwptopdf/&lt;/a&gt;&lt;br /&gt;  &lt;b&gt;GitHub&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://github.com/sanguneo/rhwptopdf&quot;&gt;https://github.com/sanguneo/rhwptopdf&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;demo-screenshot.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6YT0C/dJMcaf021Ho/rgLJZTYTt2kgQ9Di05F5BK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6YT0C/dJMcaf021Ho/rgLJZTYTt2kgQ9Di05F5BK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6YT0C/dJMcaf021Ho/rgLJZTYTt2kgQ9Di05F5BK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6YT0C%2FdJMcaf021Ho%2FrgLJZTYTt2kgQ9Di05F5BK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;1000&quot; data-filename=&quot;demo-screenshot.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 만들었나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HWP 파일을 PDF 로 바꿔야 할 때, 보통 선택지는 두 가지입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;한컴 오피스를 설치한다.&lt;/b&gt; 1인 라이선스도 비싸고, 서버 자동화에는 더 부담스럽습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;온라인 변환 사이트에 올린다.&lt;/b&gt; 무료 SaaS 는 대체로 어딘가의 서버에 파일을 업로드합니다. 회사 문서나 개인 정보가 든 양식을 그렇게 던지긴 찝찝합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 세 번째 선택지를 만들고 싶었습니다. &lt;b&gt;브라우저 안에서, 네트워크 없이, 사용자 PC 안에서만&lt;/b&gt; 끝나는 변환기.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;출발점 &amp;mdash; &lt;code&gt;rhwp&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HWP 파서를 제가 처음부터 짠 건 아닙니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HWP 5.0 binary spec 은 공개돼 있지만, 실제로 파싱하려면 OLE compound document, record-based 인코딩, 본문 흐름 / 표 / 도형 / 수식까지 &amp;mdash; 학습 곡선이 만만치 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 &lt;a href=&quot;https://github.com/edwardkim/rhwp&quot;&gt;&lt;code&gt;edwardkim/rhwp&lt;/code&gt;&lt;/a&gt; 라는 훌륭한 Rust 구현체가 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HWP 5.0 / HWPX 파서&lt;/li&gt;
&lt;li&gt;페이지네이션과 SVG / Canvas 렌더링&lt;/li&gt;
&lt;li&gt;WebAssembly 빌드 가능&lt;/li&gt;
&lt;li&gt;Apache License 2.0&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 &quot;브라우저에서 HWP 를 보는&quot; 부분은 이쪽에서 해결돼 있던 거죠. 다만 &lt;code&gt;rhwp&lt;/code&gt; 는 &lt;b&gt;뷰어 / 에디터&lt;/b&gt; 입니다. PDF 출력 파이프라인이 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 결정한 컨셉은 다음과 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;rhwp&lt;/code&gt; 의 &lt;b&gt;파서 + 레이아웃 렌더러&lt;/b&gt; 를 cherry-pick 해서, 그 위에 &lt;b&gt;PDF 출력 한 가지&lt;/b&gt; 만 얹은 별개 트랙 라이브러리를 만들자.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;rhwptopdf&lt;/code&gt; 라는 이름이 그대로 의도입니다. &quot;rhwp to pdf&quot;, &lt;code&gt;rhwp&lt;/code&gt; 가 만든 SVG 출력을 PDF 로 마무리하는 얇은 한 겹.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떻게 동작하는가 &amp;mdash; 5 단계 파이프라인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 한 장당 다음 5 단계를 거칩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1860ea6a-21f1-418a-94c0-d3aaf6ce923f.png&quot; data-origin-width=&quot;2172&quot; data-origin-height=&quot;724&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R78Fk/dJMcaiKdMe0/sBR7RqKe3tnX8sIsGUDKX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R78Fk/dJMcaiKdMe0/sBR7RqKe3tnX8sIsGUDKX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R78Fk/dJMcaiKdMe0/sBR7RqKe3tnX8sIsGUDKX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR78Fk%2FdJMcaiKdMe0%2FsBR7RqKe3tnX8sIsGUDKX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2172&quot; height=&quot;724&quot; data-filename=&quot;1860ea6a-21f1-418a-94c0-d3aaf6ce923f.png&quot; data-origin-width=&quot;2172&quot; data-origin-height=&quot;724&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) SvgRenderer &amp;mdash; HWP &amp;rarr; SVG&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;rhwp&lt;/code&gt; 가 이미 잘 하던 일입니다. HWP 페이지 (본문, 표, 도형, 수식) 를 96 dpi 픽셀 단위의 SVG 로 그립니다. WASM 안에서 메모리 상으로만 일어나고, 디스크로 떨어지지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) font-family 정규화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 첫 번째 함정. HWP 의 doc_info 안에 들어 있는 폰트명이 가끔 깨져 있습니다. 어떤 문서에는 &lt;code&gt;&quot;신명조■얒a&quot;&lt;/code&gt; 같은 식으로 &amp;mdash; 한글 + 끔찍한 컨트롤 문자 조합. 이걸 그대로 PDF 폰트 매칭에 쓰면 매칭이 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 sanitizer 를 강하게 짰지만, 결국 한 단계 더 단순화했습니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;바탕 / 명조 / 궁서 / Batang / Myeongjo &amp;rarr; serif
돋움 / 고딕 / 굴림 / Dotum  / Gothic   &amp;rarr; sans-serif&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SVG 안의 모든 &lt;code&gt;font-family&lt;/code&gt; 를 두 가지 generic 으로 줄여 버리고, 실제 렌더는 fontdb 에 등록된 폰트가 담당합니다. &quot;정확히 그 폰트로 그린다&quot; 가 아니라 &quot;비슷한 계열의 안 깨지는 폰트로 그린다&quot; 쪽으로 트레이드오프 한 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) usvg text-to-path&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PDF 의 텍스트는 &quot;어떤 글자를 어디에&quot; 라는 추상 정보일 뿐이고, 글자가 어떻게 생겼는지는 PDF 뷰어가 가진 폰트에 달려 있습니다. 한글 폰트는 PDF 뷰어마다 다르고 &amp;mdash; 특히 모바일 뷰어, 사내 뷰어에서 깨질 가능성이 큽니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;usvg&lt;/code&gt; 를 써서 &lt;b&gt;글자를 vector path 로 변환&lt;/b&gt; 해 버립니다. 모든 글자가 SVG 의 &lt;code&gt;&amp;lt;path&amp;gt;&lt;/code&gt; 가 되고, PDF 안에서도 path 로 박힙니다. 뷰어가 어떤 폰트를 가지고 있든 상관없이 똑같이 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대가는 fontdb 의존성입니다. 변환 시점에 폰트 데이터가 메모리에 있어야 글리프 outline 을 뽑을 수 있어요. 이게 다음 챕터로 이어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) svg2pdf &amp;rarr; 5) pdf-writer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;svg2pdf::to_chunk()&lt;/code&gt; 가 SVG 한 페이지를 PDF XObject 청크로 변환하고, &lt;code&gt;pdf-writer&lt;/code&gt; 가 여러 페이지의 청크를 묶어서 page tree 를 짭니다. &lt;code&gt;media_box&lt;/code&gt; 는 96 dpi 픽셀을 &lt;code&gt;&amp;times; 72 / 96&lt;/code&gt; 으로 PT 로 환산해서 표준 A4 (&lt;code&gt;595 &amp;times; 842 pt&lt;/code&gt;) 가 되도록 맞췄습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 함정 하나 더. 초반에 PT 환산을 빼먹어서 PDF 가 1.33&amp;times; 부풀어 나왔습니다. SvgRenderer 출력이 픽셀이라는 걸 까먹은 거죠. 한 줄로 끝.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const PX_TO_PT: f32 = 72.0 / 96.0;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가장 오래 걸린 챕터 &amp;mdash; 폰트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 파이프라인의 3) 단계가 잘 동작하려면, fontdb 에 &lt;b&gt;그 문서가 쓰는 글리프를 다 가진 폰트&lt;/b&gt; 가 등록돼 있어야 합니다. 한글이라는 게 11,172 자 (KS X 1001 완성형) 라, 부분 폰트만 가지고는 절반은 &lt;code&gt;.notdef&lt;/code&gt; 로 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 단계를 거쳤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시도 1 &amp;mdash; 한컴 함초롬 Ext 폰트&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HAN*Ext 시리즈가 가장 먼저 떠올랐는데, 이름이 &quot;Ext&quot; 인 데에는 이유가 있더군요. &lt;b&gt;한글 음절 0 / 11172 자&lt;/b&gt; &amp;mdash; 확장 한자 / 특수 기호만 든 폰트였습니다. PDF 출력에서 한글이 완전히 사라지는 사태.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시도 2 &amp;mdash; 함초롬바탕 / 함초롬돋움 (full)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;HANBatang.ttf&lt;/code&gt; / &lt;code&gt;HANDotum.ttf&lt;/code&gt; 로 교체. 11172 / 11172 자 모두 커버. 이제 글자가 보입니다. 다만 다음 문제 &amp;mdash; &lt;b&gt;WASM 번들에 6 MB 짜리 폰트 두 개를 박으면 라이브러리 크기가 폭발합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시도 3 &amp;mdash; 시스템 폰트를 먼저 써 보자&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Font Access API&lt;/b&gt; 라는 게 있습니다. Chrome 105+ 부터 &lt;code&gt;window.queryLocalFonts()&lt;/code&gt; 로 OS 에 설치된 폰트 목록을 (사용자 허가 하에) 읽을 수 있어요.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;const fonts = await window.queryLocalFonts();
// [
//   { family: &quot;맑은 고딕&quot;,
//     fullName: &quot;Malgun Gothic&quot;,
//     postscriptName: &quot;MalgunGothic&quot;,
//     blob() },
//   ...
// ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름이 데모 페이지의 기본값이 됐습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;페이지 로드 시 &lt;code&gt;window.queryLocalFonts()&lt;/code&gt; 시도&lt;/li&gt;
&lt;li&gt;사용자가 &lt;code&gt;local-fonts&lt;/code&gt; 권한을 허락하면, 시스템에 설치된 한글 폰트 (맑은 고딕, 나눔고딕, 본고딕 등) 를 그대로 &lt;code&gt;registerPdfFont()&lt;/code&gt; 에 넣음&lt;/li&gt;
&lt;li&gt;권한 거부 or 미지원 브라우저 (Firefox / Safari) 면 &lt;b&gt;정적 fallback 폰트&lt;/b&gt; 로 떨어짐 &amp;mdash; 데모에서는 &lt;code&gt;HANBatang.ttf&lt;/code&gt; / &lt;code&gt;HANDotum.ttf&lt;/code&gt; 를 별도 다운로드&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;라이브러리 자체&lt;/b&gt; 는 폰트를 번들하지 않습니다. &lt;code&gt;registerPdfFont(bytes)&lt;/code&gt; API 만 노출해 두고, &quot;그 바이트를 어디서 가져와서 넣는지&quot; 는 호출자에게 맡깁니다. 데모 페이지만 정적 fallback 을 갖고 있을 뿐.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 라이브러리 사이즈를 &lt;b&gt;6 MB (.wasm) + 16 KB (.umd.js)&lt;/b&gt; 로 유지할 수 있게 한 결정적인 트레이드오프였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시행착오 모음&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PDF 출력이 처음엔 멀쩡해 보였는데, 실제 문서로 테스트해 보니 자잘한 버그가 줄줄이 나왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;root2&quot; 가 글자 그대로 박혀 나옴&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수능 기출 한 문제로 테스트하다가 발견. HWP 수식이 PDF 에 &lt;code&gt;root2&lt;/code&gt; 라는 raw 텍스트로 박혀 있었습니다. 수학 기호 &lt;code&gt;&amp;radic;2&lt;/code&gt; 가 되어야 할 자리에. 원인은 &lt;code&gt;rhwp&lt;/code&gt; 의 수식 토크나이저였습니다. &lt;code&gt;root&lt;/code&gt; / &lt;code&gt;sqrt&lt;/code&gt; 같은 키워드 뒤에 숫자가 붙으면 (&lt;code&gt;root2&lt;/code&gt;, &lt;code&gt;sqrt9&lt;/code&gt;) prefix-split 을 안 하고 통째로 식별자로 잡고 있었어요. 토크나이저에서 prefix 매칭 후 나머지를 별도 토큰으로 잘라내는 한 줄을 끼워 넣어 해결.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데모 페이지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sanguneo.github.io/rhwptopdf/&quot;&gt;데모 페이지&lt;/a&gt; 구현은 아래처럼 진행했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;왼쪽: 드롭존 + &quot;PDF 로 변환&quot; 버튼 + 4-step stepper (분석 &amp;rarr; 폰트 &amp;rarr; 변환 &amp;rarr; 완료)&lt;/li&gt;
&lt;li&gt;오른쪽: PDF 미리보기 iframe&lt;/li&gt;
&lt;li&gt;아래: 접을 수 있는 실행 로그&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로는 &lt;code&gt;HwpToPdfJob extends EventTarget&lt;/code&gt; 이라는 작은 wrapper 클래스가 변환 라이프사이클을 캡슐화합니다. &lt;code&gt;progress&lt;/code&gt; / &lt;code&gt;complete&lt;/code&gt; / &lt;code&gt;error&lt;/code&gt; 이벤트를 발행하고, 데모 UI 는 이걸 구독해서 stepper 상태를 진행합니다. WASM 쪽에는 progress streaming 을 안 넣었습니다 &amp;mdash; JS 만으로도 stepper 효과는 충분히 나오고, WASM 표면을 단순하게 유지하고 싶었어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용법 &amp;mdash; CDN 한 줄&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포된 UMD 번들을 그대로 끌어다 쓰면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;&amp;lt;script src=&quot;https://github.com/sanguneo/rhwptopdf/releases/latest/download/rhwptopdf.umd.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script type=&quot;module&quot;&amp;gt;
  // 1) WASM 초기화
  await RhwpToPdf({ module_or_path: &quot;.../rhwptopdf.umd_bg.wasm&quot; });

  // 2) 변환용 폰트 등록 (한 번씩)
  const ttf = new Uint8Array(await (await fetch(&quot;/fonts/HANBatang.ttf&quot;)).arrayBuffer());
  RhwpToPdf.registerPdfFont(ttf);

  // 3) HWP &amp;rarr; PDF
  const hwpBytes = new Uint8Array(await file.arrayBuffer());
  const pdfBytes = RhwpToPdf.hwpToPdf(hwpBytes);  // Uint8Array
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 표면은 작습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;analyzeHwp(bytes): { pageCount, fontsRequired }   // 메타데이터만 빠르게
hwpToPdf(bytes): Uint8Array                       // 본 변환
registerPdfFont(bytes): string                    // family name 자동 감지
clearPdfFonts(): void
extractThumbnail(bytes): unknown                  // PrvImage 추출 (옵션)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한계 / 솔직한 단점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자랑만 하면 곤란하니까 몇 가지 명확히 짚어 둡니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;fontdb 가 필수입니다.&lt;/b&gt; 글리프를 path 로 박는 트레이드오프의 대가입니다. 사용자가 폰트를 한 번은 넣어 줘야 합니다. (데모에서는 시스템 폰트로 우회했습니다)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;font-family 가 generic 으로 단순화됩니다.&lt;/b&gt; &quot;이 문서는 굴림체로 통일&quot; 같은 디테일은 보존되지 않습니다. 대신 어떤 환경에서도 깨지지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수식 / 도형 100% 호환 아닙니다.&lt;/b&gt; &lt;code&gt;rhwp&lt;/code&gt; 의 렌더러가 커버하는 범위가 곧 &lt;code&gt;rhwptopdf&lt;/code&gt; 의 한계입니다. 학회지 같은 복잡한 수식은 일부 어긋날 수 있어요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;6 MB WASM.&lt;/b&gt; 모바일에서 초기 로딩이 가볍지는 않습니다. 첫 변환 후엔 캐싱되니 두 번째부터는 빠릅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;라이선스 &amp;mdash; &lt;code&gt;rhwp&lt;/code&gt; 에 대한 감사&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자체 코드는 &lt;b&gt;MIT&lt;/b&gt; 입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rhwp&lt;/code&gt; 에서 cherry-pick 한 parser / renderer 모듈은 &lt;b&gt;Apache-2.0&lt;/b&gt; 그대로 유지됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apache-2.0 &amp;sect; 4 는 재배포 시 attribution 과 변경 기록 보존을 요구합니다. &lt;code&gt;NOTICE&lt;/code&gt; 파일에 정확히 어떤 모듈이 어디서 왔는지, 어떤 변경을 가했는지 적어 뒀습니다. 같은 영역 (한국어 문서 처리) 에서 이런 토대를 만들어 두신 Edward Kim 님과 &lt;code&gt;rhwp&lt;/code&gt; 컨트리뷰터분들께 감사드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;rhwptopdf&lt;/code&gt; 는 &quot;있으면 좋겠다&quot; 가 &quot;있다&quot; 가 된 결과물입니다. 지금은 v0.1.0 이고, 사용 중 발견된 버그 / 한계는 &lt;a href=&quot;https://github.com/sanguneo/rhwptopdf/issues&quot;&gt;GitHub Issues&lt;/a&gt; 로 받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오프라인 PC 에서, 사내망 안에서, 모바일에서 &amp;mdash; 어디서든 &lt;code&gt;.hwp&lt;/code&gt; 한 장을 PDF 로 바꿔야 할 일이 생기면, 한 번 써 보고 의견 주세요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;  데모 &amp;mdash; &lt;a href=&quot;https://sanguneo.github.io/rhwptopdf/&quot;&gt;https://sanguneo.github.io/rhwptopdf/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;  GitHub &amp;mdash; &lt;a href=&quot;https://github.com/sanguneo/rhwptopdf&quot;&gt;https://github.com/sanguneo/rhwptopdf&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽어 주셔서 감사합니다.&lt;/p&gt;</description>
      <category>Develop/rhwptopdf</category>
      <category>HWP</category>
      <category>hwpx</category>
      <category>rhwptopdf</category>
      <category>Rust</category>
      <category>WASM</category>
      <category>WebAssembly</category>
      <category>오픈소스</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/85</guid>
      <comments>https://smokingbird.tistory.com/85#entry85comment</comments>
      <pubDate>Fri, 29 May 2026 16:46:46 +0900</pubDate>
    </item>
    <item>
      <title>Supertonic TTS 커스텀 프록시에서 공식 serve로 갈아탄 썰</title>
      <link>https://smokingbird.tistory.com/84</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;official_supertonic.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bymtpb/dJMcadvgjW9/gJKaj5NdKyycTP9X2FKiGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bymtpb/dJMcadvgjW9/gJKaj5NdKyycTP9X2FKiGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bymtpb/dJMcadvgjW9/gJKaj5NdKyycTP9X2FKiGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbymtpb%2FdJMcadvgjW9%2FgJKaj5NdKyycTP9X2FKiGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;official_supertonic.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 TTS 서버를 운영한 지 꽤 됐다. Supertonic이라는 한국 회사(Supertone)가 만든 TTS 엔진을 FastAPI로 직접 감싸서 OpenAI 호환 &lt;code&gt;/v1/audio/speech&lt;/code&gt; 엔드포인트로 만들어 쓰고 있었는데 &amp;mdash; 예전에 썼던 &lt;a title=&quot;Supertonic 3 업그레이드 후기 &amp;mdash; 온디바이스 TTS 프록시를 v2에서 v3로 올리며&quot; href=&quot;https://blog.sanguneo.com/83&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Supertonic 3 업그레이드 후기 &amp;mdash; 온디바이스 TTS 프록시를 v2에서 v3로 올리며&lt;/a&gt; 에서 다뤘던 그 프록시다 &amp;mdash; v1.3.1부터 공식 &lt;code&gt;serve&lt;/code&gt; CLI가 나오면서 모든 게 달라졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원래 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 이렇게 생겼었다:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;supertonic_openai_tts_proxy/
├── main.py          # FastAPI 앱, 엔드포인트 정의
├── schemas.py       # Pydantic 요청 모델
├── synth.py         # supertonic.TTS 직접 호출
├── audio.py         # ffmpeg 변환
└── frontend/        # 테스트용 플레이그라운드&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;synth.py&lt;/code&gt;에서 &lt;code&gt;supertonic.TTS&lt;/code&gt; 객체를 직접 생성하고, 모델을 로딩하고, 음성을 합성해서 WAV로 내보내고, 다시 &lt;code&gt;audio.py&lt;/code&gt;에서 ffmpeg로 mp3/opus로 변환했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 재미있었다. TTS 엔진을 직접 다루니까 내부 동작도 이해되고 제어할 수 있다는 만족감도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 시간이 지나면서 몇 가지 아쉬운 점이 눈에 띄기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한계&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Supertonic 버전 올릴 때마다 내 코드도 고쳐야 함&lt;/b&gt; &amp;mdash; API 변경이 생기면 synth.py부터 다시 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내가 만든 &lt;code&gt;/v1/audio/speech&lt;/code&gt; 밖에 없음&lt;/b&gt; &amp;mdash; health 체크, 음성 목록 조회, 배치 합성 같은 건 직접 만들기는 귀찮아서 안 만듦&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프론트엔드 플레이그라운드도 내가 직접 만듦&lt;/b&gt; &amp;mdash; TTS 서비스를 만드는 게 본업은 아니라서&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유지보수 포인트가 계속 쌓임&lt;/b&gt; &amp;mdash; 시간이 갈수록 신경 써야 할 부분이 늘어남&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 serve 등장&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Supertonic v1.3.1부터 &lt;code&gt;supertonic serve&lt;/code&gt;라는 CLI가 생겼다. &lt;code&gt;pip install 'supertonic[serve]'&lt;/code&gt; 한 방으로 실행되는, 완전한 OpenAI 호환 &lt;code&gt;/v1/audio/speech&lt;/code&gt; 서버였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게다가 내가 안 만든 것들도 다 있었다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/v1/health&lt;/code&gt; &amp;mdash; 상태 체크&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/v1/styles&lt;/code&gt; &amp;mdash; 음성 목록&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/v1/tts&lt;/code&gt; &amp;mdash; 네이티브 합성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/v1/tts/batch&lt;/code&gt; &amp;mdash; 배치 합성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/docs&lt;/code&gt; &amp;mdash; Swagger 문서&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딱 하나 빠진 게 있었는데, 바로 &lt;b&gt;mp3/opus 출력&lt;/b&gt;이었다. 공식 serve가 지원하는 포맷은 wav, flac, ogg(Vorbis) 뿐이었다. 문제는 텔레그램이 opus를 쓰고, 일반적인 TTS 클라이언트들이 mp3를 자주 요청한다는 점.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 바꾼 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;serve와 커스텀 프록시를 완전히 분리했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;supertonic serve (:8788)        &amp;larr; 공식 serve (Supertone이 관리)
    ↕
supertonic-proxy (:8789)        &amp;larr; 130줄 ffmpeg 변환 프록시 (내가 관리)
    ↕
Hermes / OpenAI 클라이언트&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;supertonic serve&lt;/code&gt;는 8788번 포트에서 내부 백엔드로만 띄움&lt;/li&gt;
&lt;li&gt;8789번 프록시는 들어오는 요청 중 &lt;b&gt;&lt;code&gt;/v1/audio/speech&lt;/code&gt;만 가로채서&lt;/b&gt; WAV로 받은 뒤 ffmpeg로 변환&lt;/li&gt;
&lt;li&gt;그 외 모든 요청(/v1/health, /v1/styles, /v1/tts 등)은 그냥 통과(패스스루)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 코드는 직접 짰지만 단 130줄. httpx + FastAPI + ffmpeg 서브프로세스 호출이 전부다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 로직만 보면:&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;if fmt in (&quot;wav&quot;, &quot;flac&quot;, &quot;ogg&quot;):
    # 공식 serve가 네이티브로 지원 &amp;mdash; 그냥 통과
    return await forward(body)

# 나머지(mp3, opus, aac): WAV로 받아서 ffmpeg 변환
wav = await request_wav(body)
return await ffmpeg_convert(wav, fmt)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 끝이다. 변환이 필요 없는 포맷은 serve가 직접 처리하고, 필요한 포맷만 WAV를 받아서 바꿔준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;뭐가 좋아졌나&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;전&lt;/th&gt;
&lt;th&gt;후&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TTS 엔진 코드&lt;/td&gt;
&lt;td&gt;내가 직접 호출&lt;/td&gt;
&lt;td&gt;serve가 다 해줌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;유지보수&lt;/td&gt;
&lt;td&gt;synth.py 등 4개 모듈&lt;/td&gt;
&lt;td&gt;serve는 Supertone한테 맡김&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;엔드포인트&lt;/td&gt;
&lt;td&gt;/v1/audio/speech 하나&lt;/td&gt;
&lt;td&gt;health, styles, batch, docs 전부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프록시 코드&lt;/td&gt;
&lt;td&gt;복잡&lt;/td&gt;
&lt;td&gt;130줄 (ffmpeg 변환만)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;포맷 지원&lt;/td&gt;
&lt;td&gt;mp3/opus/wav&lt;/td&gt;
&lt;td&gt;serve 네이티브(wav/flac/ogg) + 프록시(mp3/opus/aac)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 큰 변화는 &lt;b&gt;&quot;써드파티 TTS 서버 운영&quot;에서 &quot;공식 serve 앞단에 변환기 하나 붙인&quot;&lt;/b&gt; 걸로 지위가 바뀐 점이다. 예전엔 Supertonic이 업데이트되면 내 코드도 같이 고쳐야 했는데, 지금은 그냥 &lt;code&gt;pip install --upgrade supertonic[serve]&lt;/code&gt; 한 방이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;근데 왜 굳이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;그냥 serve만 써도 되지 않냐?&quot; &amp;mdash; 맞다. wav/flac/ogg만 써도 되는 환경이면 serve 하나로 끝이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 Hermes Agent 와 텔레그램을 연결해서 쓰고있는데, 이 음성메시지가 opus 포멧으로 해야 올라간다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변환 단계가 하나 더 끼면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;포트 하나 더 씀 (8788, 8789)&lt;/li&gt;
&lt;li&gt;ffmpeg 서브프로세스 오버헤드 (작지만 무시할 순 없음)&lt;/li&gt;
&lt;li&gt;고장 포인트가 하나 더 생김&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 mp3/opus/aac를 쓸 수 있고, serve가 커버 못 하는 포맷을 유연하게 추가할 수 있다. 트레이드오프는 명확하다.&lt;/p&gt;</description>
      <category>Study/AI</category>
      <category>ffmpeg</category>
      <category>hermes-agent</category>
      <category>on-device-tts</category>
      <category>openai-compatible</category>
      <category>Proxy</category>
      <category>Serve</category>
      <category>Supertonic</category>
      <category>tts</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/84</guid>
      <comments>https://smokingbird.tistory.com/84#entry84comment</comments>
      <pubDate>Thu, 21 May 2026 17:16:17 +0900</pubDate>
    </item>
    <item>
      <title>Supertonic 3 업그레이드 후기 &amp;mdash; 온디바이스 TTS 프록시를 v2에서 v3로 올리며</title>
      <link>https://smokingbird.tistory.com/83</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1000066139.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnfBz9/dJMcai4mhpC/qmlITbpNRgkJPQM8tscGe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnfBz9/dJMcai4mhpC/qmlITbpNRgkJPQM8tscGe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnfBz9/dJMcai4mhpC/qmlITbpNRgkJPQM8tscGe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnfBz9%2FdJMcai4mhpC%2FqmlITbpNRgkJPQM8tscGe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;1000066139.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;며칠 전 Supertonic 3가 릴리즈되었다는 소식을 들었다. Supertonic은 한국 스타트업 Supertone이 만든 온디바이스 TTS 엔진으로, ONNX Runtime 기반으로 로컬에서 구동되어 클라우드 의존성이 전혀 없다. 나는 이걸 OpenAI &lt;code&gt;/v1/audio/speech&lt;/code&gt; 규격으로 감싸는 프록시(&lt;code&gt;supertonic-openai-tts-proxy&lt;/code&gt;)를 만들어 Hermes Agent의 TTS 백엔드로 쓰고 있었는데, 이번에 v3로 업그레이드하면서 경험한 내용을 정리해본다.참고로 이 작업에는 Hermes Agent를 쓰고 있고, 백엔드 모델로 이번에는`deepseek/deepseek-v4-flash`를 사용했다. AI 에이전트한테 &quot;Supertonic 3 나왔대, 비교해봐&quot;부터 시작해서 모델&amp;nbsp;다운로드,&amp;nbsp;코드&amp;nbsp;수정,&amp;nbsp;서비스&amp;nbsp;재시작까지&amp;nbsp;전반적인&amp;nbsp;도움을&amp;nbsp;받았다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Supertone?&lt;/b&gt; 한국의 AI 음성 스타트업. 음성 합성 기술로 유명하며, &lt;br /&gt;Supertonic은 그들의 오픈웨이트 온디바이스 TTS 프로젝트다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;v2 &amp;rarr; v3, 뭐가 달라졌나?&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;Supertonic 2&lt;/th&gt;
&lt;th&gt;Supertonic 3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;출시일&lt;/td&gt;
&lt;td&gt;2026.01.06&lt;/td&gt;
&lt;td&gt;2026.04.29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;지원 언어&lt;/td&gt;
&lt;td&gt;&lt;b&gt;5개&lt;/b&gt; (en, ko, ja 등)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;31개&lt;/b&gt;로 대폭 확장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;읽기 정확도&lt;/td&gt;
&lt;td&gt;기본 양호&lt;/td&gt;
&lt;td&gt;반복/건너뛰기(skip/repeat) 실패 &lt;b&gt;대폭 감소&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;화자 유사도&lt;/td&gt;
&lt;td&gt;기준&lt;/td&gt;
&lt;td&gt;동일 언어군에서 &lt;b&gt;향상&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모델 크기&lt;/td&gt;
&lt;td&gt;~252MB&lt;/td&gt;
&lt;td&gt;~381MB (vector_estimator가 127M&amp;rarr;245M)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;샘플레이트&lt;/td&gt;
&lt;td&gt;24000 Hz (추정)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;44100 Hz&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONNX 호환성&lt;/td&gt;
&lt;td&gt;v2 전용&lt;/td&gt;
&lt;td&gt;&lt;b&gt;v2 인터페이스 호환 유지&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;표현 태그&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;laugh&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;breath&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;sigh&amp;gt;&lt;/code&gt; 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가장 체감되는 변화&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;언어가 5개에서 31개로&lt;/b&gt; &amp;mdash; 한국어/영어/일본어만 되던 게 갑자기 독일어, 프랑스어, 스페인어, 러시아어, 베트남어, 힌디어, 아랍어 등등 유럽어 + 아시아어까지 커버&lt;/li&gt;
&lt;li&gt;&lt;b&gt;읽기 안정성&lt;/b&gt; &amp;mdash; v2에서 가끔 단어를 건너뛰거나 반복해서 읽던 문제가 거의 사라짐&lt;/li&gt;
&lt;li&gt;&lt;b&gt;샘플레이트 44100 Hz&lt;/b&gt; &amp;mdash; CD 음질로 업그레이드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;v2와 호환되는 ONNX 인터페이스&lt;/b&gt; &amp;mdash; 기존 연동 코드를 거의 안 건드려도 됨&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;업그레이드 과정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 모델 다운로드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;v3는 아직 &lt;code&gt;pip install supertonic&lt;/code&gt;에서 기본 모델로 제공되지 않는다. (&lt;code&gt;AVAILABLE_MODELS&lt;/code&gt;에 &lt;code&gt;supertonic-3&lt;/code&gt;가 아직 없음) 그래서 Hugging Face에서 직접 받았다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;# git-lfs 설치
sudo apt install git-lfs
git lfs install

# v3 모델을 ~/.cache/supertonic3에 클론
cd ~/.cache &amp;amp;&amp;amp; git clone https://huggingface.co/Supertone/supertonic-3 supertonic3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;v2는 252MB였는데 v3는 &lt;b&gt;381MB&lt;/b&gt;로 약 1.5배 커졌다. 특히 &lt;code&gt;vector_estimator.onnx&lt;/code&gt;가 127MB &amp;rarr; 245MB로 거의 두 배.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 프록시 코드 수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 supertonic 2 tts 를 디바이스에 설치하고 헤르메스에 tts 엔진으로 구축했는데 이번에 3로 판올림을 진행했다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# Before: auto_download로 v2 모델 사용
def get_tts():
    return TTS(auto_download=True)

# After: v3 모델 디렉토리를 직접 지정
V3_MODEL_DIR = os.path.expanduser(&quot;~/.cache/supertonic3&quot;)

def get_tts():
    return TTS(auto_download=False, model_dir=V3_MODEL_DIR, model=&quot;supertonic-2&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트는 &lt;code&gt;auto_download=False&lt;/code&gt;로 하고 &lt;code&gt;model_dir&lt;/code&gt;에 v3 경로를 직접 박는 것. &lt;code&gt;model&lt;/code&gt; 파라미터는 어차피 하위호환성을 위해 &lt;code&gt;supertonic-2&lt;/code&gt;로 남겨도 정상 동작한다. (내부적으로 multilingual 플래그만 확인)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 엔드포인트 및 기본값 업데이트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/v1/models&lt;/code&gt; &amp;rarr; &lt;code&gt;supertonic-2&lt;/code&gt;에서 &lt;code&gt;supertonic-3&lt;/code&gt;로&lt;/li&gt;
&lt;li&gt;&lt;code&gt;total_steps&lt;/code&gt; 기본값 3 &amp;rarr; 6 (음질 향상, 속도와 트레이드오프)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Hermes Config 업데이트&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;tts:
  provider: openai
  openai:
    model: supertonic-3    # &amp;larr; 이거 한 줄
    voice: F1
    speed: 1.3
    base_url: http://127.0.0.1:8789/v1&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 서비스 재시작&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;systemctl --user restart supertonic-openai-tts-proxy&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트&lt;/h2&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;# 한국어 테스트
curl -X POST http://127.0.0.1:8789/v1/audio/speech \
  -H &quot;Content-Type: application/json&quot; \
  -d '{&quot;model&quot;:&quot;supertonic-3&quot;,&quot;input&quot;:&quot;안녕하세요, Supertonic 3 테스트입니다.&quot;,&quot;voice&quot;:&quot;F1&quot;,&quot;lang&quot;:&quot;ko&quot;}' \
  -o test.wav

# 영어 테스트
curl -X POST http://127.0.0.1:8789/v1/audio/speech \
  -H &quot;Content-Type: application/json&quot; \
  -d '{&quot;model&quot;:&quot;supertonic-3&quot;,&quot;input&quot;:&quot;Hello, this is Supertonic 3.&quot;,&quot;voice&quot;:&quot;M1&quot;,&quot;lang&quot;:&quot;en&quot;,&quot;response_format&quot;:&quot;mp3&quot;}' \
  -o test.mp3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 깔끔하게 200 OK. 44100 Hz WAV, 128kbps MP3 모두 정상.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;알게 된 점 / 팁&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;pip 패키지가 아직 v3를 공식 지원하지 않는다&lt;/b&gt; &amp;mdash; &lt;code&gt;AVAILABLE_MODELS&lt;/code&gt;가 &lt;code&gt;['supertonic', 'supertonic-2']&lt;/code&gt; 뿐. &lt;code&gt;model_dir&lt;/code&gt;로 우회해야 함. pip 패키지 업데이트를 기다리거나 직접 PR을 넣을 수도 있을 듯.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;v2 ONNX 인터페이스와 완전 호환&lt;/b&gt; &amp;mdash; &lt;code&gt;text_encoder.onnx&lt;/code&gt;, &lt;code&gt;duration_predictor.onnx&lt;/code&gt;, &lt;code&gt;vector_estimator.onnx&lt;/code&gt;, &lt;code&gt;vocoder.onnx&lt;/code&gt; 구조가 v2와 동일해서 Drop-in replacement 가능.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모델 크기 증가&lt;/b&gt; &amp;mdash; 252MB &amp;rarr; 381MB (약 50% 증가). 하지만 그래도 0.7B~2B급 대형 TTS 모델들에 비하면 엄청 가벼운 편. 라즈베리파이에서도 돌아간다는 게 증명됨.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;total_steps로 품질/속도 조절&lt;/b&gt; &amp;mdash; 기본값 3에서 6 정도면 음질 눈에 띄게 좋아지고, 속도도 실사용에 무리 없는 수준.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Hermes TTS와의 연동&lt;/b&gt; &amp;mdash; OpenAI 호환 API 덕분에 &lt;code&gt;base_url&lt;/code&gt;만 프록시 주소로 바꾸면 끝. 프록시가 중간에서 Supertonic을 OpenAI 규격으로 감싸주는 구조.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Supertonic 3는 단순한 마이너 업데이트가 아니라 언어 지원 대폭 확장과 읽기 안정성 개선이 확실히 체감되는 메이저 업데이트다. v2에서 v3로의 마이그레이션은 ONNX 인터페이스 호환성 덕분에 아주 순조로웠고, 거의 Drop-in 수준이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온디바이스 TTS의 퀄리티가 이 정도면, 클라우드 TTS를 대체할 만한 실용적인 선택지가 된 것 같다. 특히 Supertonic은 &lt;b&gt;무료 + 프라이버시 완전 보장 + 오프라인&lt;/b&gt;이라는 세 가지를 다 잡은 게 가장 큰 강점.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;참고: 프록시 코드 : &lt;a href=&quot;https://github.com/sanguneo/supertonic-openai-tts-proxy&quot;&gt;https://github.com/sanguneo/supertonic-openai-tts-proxy&lt;/a&gt;&lt;br /&gt;*Supertone 공식: &lt;a href=&quot;https://github.com/supertone-inc/supertonic&quot;&gt;https://github.com/supertone-inc/supertonic&lt;/a&gt;&lt;/i&gt;&lt;br /&gt;&lt;i&gt;Hugging Face 모델: &lt;a href=&quot;https://huggingface.co/Supertone/supertonic-3&quot;&gt;https://huggingface.co/Supertone/supertonic-3&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;</description>
      <category>Study/AI</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/83</guid>
      <comments>https://smokingbird.tistory.com/83#entry83comment</comments>
      <pubDate>Sat, 16 May 2026 16:40:31 +0900</pubDate>
    </item>
    <item>
      <title>Smart Drop Point 개발기 - 하차 지점을 추천하는 모바일 웹앱 만들기</title>
      <link>https://smokingbird.tistory.com/82</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;665bc495-037c-4e0f-908c-bcd57d4f5dd7.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doNac9/dJMcaipOwmL/FagYTo0F3daVSn1qakDelK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doNac9/dJMcaipOwmL/FagYTo0F3daVSn1qakDelK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doNac9/dJMcaipOwmL/FagYTo0F3daVSn1qakDelK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdoNac9%2FdJMcaipOwmL%2FFagYTo0F3daVSn1qakDelK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;665bc495-037c-4e0f-908c-bcd57d4f5dd7.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Smart Drop Point는 같이 차를 탔을 때 &amp;ldquo;어디서 내리면 좋을지&amp;rdquo;를 추천하는 모바일 웹앱입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 아이디어가 꽤 단순했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운전자는 자기 목적지로 가야 하고, 승객은 다른 곳으로 가야 할 때.&lt;br /&gt;둘 사이에서 적당한 하차 지점을 찾아주면 되지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 막상 만들기 시작하니 단순한 길찾기 앱과는 조금 다른 문제였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글은 Smart Drop Point를 만들면서 어떤 기준으로 구조를 잡았는지 정리한 개발기입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용한 기술 스택&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 구조는 Next.js App Router 기반으로 잡았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체적으로는 모바일 웹/PWA를 먼저 생각했고, 추천 계산과 UI 상태가 섞이지 않도록 도메인 로직을 분리하는 방향으로 구성했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Next.js App Router&lt;/li&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;TypeScript strict mode&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;Zustand persist&lt;/li&gt;
&lt;li&gt;TanStack Query&lt;/li&gt;
&lt;li&gt;Yup&lt;/li&gt;
&lt;li&gt;Kakao Map / Kakao Local&lt;/li&gt;
&lt;li&gt;Tmap / ODsay 연동을 고려한 어댑터 구조&lt;/li&gt;
&lt;li&gt;Vitest 기반 테스트 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드 앱처럼 보이지만, 내부적으로는 입력 검증, 좌표 정규화, 후보 필터링, 추천 점수화, 외부 API fallback 같은 서버/도메인 로직이 꽤 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 화면 컴포넌트 안에 계산 로직을 넣지 않고, &lt;code&gt;features&lt;/code&gt;, &lt;code&gt;adapters&lt;/code&gt;, &lt;code&gt;types&lt;/code&gt;, &lt;code&gt;constants&lt;/code&gt; 영역으로 나눠서 관리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략 이런 식입니다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;src/
├── adapters/          지도, 경로, 대중교통, 장소 검색 어댑터
├── app/               Next.js App Router 페이지와 API 라우트
├── components/        홈/결과 화면 UI 컴포넌트
├── constants/         앱 상수와 역 데이터
├── features/          추천 계산, 입력 흐름, URL 직렬화 같은 도메인 로직
├── stores/            사용자 설정과 최근 검색 상태
└── types/             도메인 타입&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 나눠두면 UI를 바꾸는 작업과 추천 품질을 바꾸는 작업이 서로 덜 엉킵니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제를 좁게 잡기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 모든 카풀 문제를 풀 생각은 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낯선 사람을 매칭하거나, 결제를 붙이거나, 실시간 위치를 추적하는 쪽으로 가면 제품의 범위가 너무 커집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 잡은 문제는 훨씬 좁았습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미 같은 차를 탄 상태&lt;/li&gt;
&lt;li&gt;출발지는 같음&lt;/li&gt;
&lt;li&gt;운전자 목적지와 승객 목적지가 다름&lt;/li&gt;
&lt;li&gt;중간에 어디서 내리면 좋을지 정해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네 가지 조건에만 집중했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 좁히고 나니 앱의 목적도 분명해졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;새로운 이동을 만들어주는 앱&amp;rdquo;이 아니라,&lt;br /&gt;&amp;ldquo;이미 같이 이동 중인 사람들이 어디서 자연스럽게 갈라질지 정하게 도와주는 앱&amp;rdquo;입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 지도 앱과 다른 계산&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 지도 앱은 보통 A에서 B까지의 최적 경로를 계산합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 앱은 조금 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A에서 출발해서 운전자는 B로 가고, 승객은 C로 가야 합니다.&lt;br /&gt;여기서 궁금한 건 A &amp;rarr; B 또는 A &amp;rarr; C가 아니라, A &amp;rarr; ? &amp;rarr; B에서 ?가 어디면 좋을지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 후보 지점은 운전자만 기준으로 봐도 안 되고, 승객만 기준으로 봐도 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운전자 입장에서는 우회 시간이 짧아야 하고,&lt;br /&gt;승객 입장에서는 하차 후 대중교통이나 도보 이동이 괜찮아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 추천 엔진은 크게 이런 흐름으로 잡았습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;운전자 출발지, 운전자 목적지, 승객 목적지의 좌표를 확정한다&lt;/li&gt;
&lt;li&gt;후보가 될 수 있는 전철역이나 환승 거점을 추린다&lt;/li&gt;
&lt;li&gt;운전자가 해당 지점을 경유했을 때 얼마나 우회하는지 본다&lt;/li&gt;
&lt;li&gt;승객이 그 지점에서 목적지까지 얼마나 걸리는지 본다&lt;/li&gt;
&lt;li&gt;우회 시간, 도보 시간, 환승 횟수, 총 이동 시간을 기준으로 점수를 낸다&lt;/li&gt;
&lt;li&gt;상위 후보를 카드로 보여준다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말로 쓰면 간단하지만, 실제로는 &amp;ldquo;후보를 얼마나 많이 계산할 것인가&amp;rdquo;가 중요했습니다.&lt;br /&gt;전국 역을 모두 대상으로 매번 외부 API를 호출하면 비용도 크고 느려집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 먼저 공간적으로 후보를 줄이고, 그다음 필요한 후보에만 경로 계산을 붙이는 방향으로 설계했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추천 엔진을 파이프라인으로 나누기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추천 로직은 한 번에 크게 계산하는 함수로 만들기보다, 단계별 파이프라인으로 나누었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략 흐름은 이렇습니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;입력 정규화
&amp;rarr; 좌표 확정
&amp;rarr; 후보 하차지 prefilter
&amp;rarr; 운전자 우회 시간 계산
&amp;rarr; 승객 대중교통/도보 시간 계산
&amp;rarr; 정책별 점수화
&amp;rarr; 상위 후보 정렬
&amp;rarr; 결과 카드 모델로 변환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 나눈 이유는 각 단계의 책임이 다르기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌표를 확정하는 단계는 장소 검색과 관련이 있고,&lt;br /&gt;후보를 줄이는 단계는 공간 계산과 관련이 있고,&lt;br /&gt;우회 시간과 대중교통 시간은 외부 API나 mock 어댑터와 관련이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 한 함수 안에 다 넣으면 처음엔 빠르게 만들 수 있지만, 나중에 추천 품질을 조정할 때 어디를 고쳐야 하는지 헷갈립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 도메인 로직은 최대한 pure TypeScript 모듈에 두고, 외부 API 호출은 어댑터 뒤로 밀었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점수 계산도 단순히 총 시간이 짧은 순서만 보지 않았습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;운전자 우회 시간&lt;/li&gt;
&lt;li&gt;승객 총 이동 시간&lt;/li&gt;
&lt;li&gt;도보 시간&lt;/li&gt;
&lt;li&gt;환승 횟수&lt;/li&gt;
&lt;li&gt;사용자가 고른 우선순위&lt;/li&gt;
&lt;li&gt;필터 조건&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값을 조합해서 후보를 정렬합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &amp;ldquo;운전자 배려&amp;rdquo;를 고르면 운전자 우회 시간이 더 강하게 반영되고, &amp;ldquo;환승 최소&amp;rdquo;를 고르면 승객 이동 시간 중에서도 환승 횟수 쪽 가중치가 커지는 식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 추천 결과는 단순 최단 시간이 아니라, 사용자가 그 상황에서 어떤 기준을 더 중요하게 보는지 반영한 결과가 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;좌표를 먼저 믿기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트에서 계속 중요하게 본 건 좌표입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장소 이름만 가지고 계산하면 애매한 경우가 많습니다.&lt;br /&gt;동명이인처럼 같은 이름의 장소가 여러 개 있을 수도 있고, 역명이나 동 이름만으로는 실제 위치가 흔들릴 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 입력 단계에서 가능한 한 좌표를 확정하고, 이후 추천 계산은 좌표를 기준으로 흐르게 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장소 자동완성도 이 목적에 맞춰 붙였습니다.&lt;br /&gt;사용자가 동 이름이나 역 이름을 입력하면 실제 좌표를 얻고, 그 좌표를 URL에도 실어 결과 페이지가 같은 입력을 재현할 수 있게 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유 링크를 열었을 때도 다시 검색 결과가 달라지면 안 되기 때문입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;URL을 상태 저장소처럼 쓰기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 페이지는 URL 쿼리만으로도 다시 열릴 수 있게 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 상태를 전역 store에만 넣어도 될 것 같았는데, 이 앱은 공유가 중요했습니다.&lt;br /&gt;상대에게 링크를 보냈을 때 같은 결과가 열려야 하고, 새로고침해도 같은 후보가 유지되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 입력값은 이런 형태로 URL에 직렬화했습니다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;driverOrigin
driverOriginLat
driverOriginLng
driverDestination
driverDestinationLat
driverDestinationLng
passengerDestination
passengerDestinationLat
passengerDestinationLng
maxDetourMinutes
maxWalkMinutes
priority
picked&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장소명뿐 아니라 위도/경도를 같이 싣는 이유는 재현성 때문입니다.&lt;br /&gt;장소명을 다시 검색하면 같은 이름의 다른 장소가 나올 수도 있고, 자동완성 결과가 시간이 지나며 바뀔 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 좌표가 URL에 있으면 결과 페이지는 다시 장소 검색을 하지 않아도 같은 추천 계산을 시작할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폼 입력과 URL 경계에서는 Yup 스키마로 값을 검증하고, 내부 도메인에서는 TypeScript 타입을 기준으로 다루도록 나눴습니다.&lt;br /&gt;폼 검증 스키마가 도메인 타입을 대체하지 않게 한 점도 의도한 부분입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어댑터로 외부 의존성 분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지도나 경로 계산, 대중교통 정보는 외부 API에 의존합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구조에서는 각각을 바로 코드에 박아 넣지 않고 어댑터 뒤에 두었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;운전 경로&lt;/li&gt;
&lt;li&gt;대중교통 경로&lt;/li&gt;
&lt;li&gt;지도 SDK&lt;/li&gt;
&lt;li&gt;장소 검색&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네 영역을 분리해두면, 처음에는 mock으로 시작하고 나중에 Tmap, ODsay, Kakao 같은 실제 API로 바꾸기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식이 마음에 들었던 이유는 개발 단계에서도 앱의 흐름을 먼저 만들 수 있다는 점이었습니다.&lt;br /&gt;외부 키가 없거나 API가 잠깐 실패해도, 최소한 핵심 화면은 확인할 수 있어야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 fallback 구조도 같이 넣었습니다.&lt;br /&gt;키가 없으면 mock으로 돌아가고, 지도나 자동완성처럼 실제 키가 필요한 기능은 가능한 범위에서만 켜지게 했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 API와 mock을 같은 인터페이스로 맞추기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API는 붙이는 것보다 바꾸기 쉽게 만드는 쪽이 더 중요하다고 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운전 경로는 Tmap, 대중교통은 ODsay, 지도는 Kakao Map, 장소 검색은 Kakao Local을 염두에 두었지만, 코드 곳곳에서 직접 호출하지는 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 각각을 이런 식의 역할로 분리했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;IRoutingAdapter&lt;/code&gt;: 운전 경로와 우회 시간 계산&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ITransitAdapter&lt;/code&gt;: 하차 후 대중교통/도보 이동 계산&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IMapAdapter&lt;/code&gt;: 지도 렌더링과 마커/폴리라인 갱신&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IPlaceSearchAdapter&lt;/code&gt;: 장소 자동완성과 좌표 확정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 구현체와 mock 구현체는 같은 인터페이스를 따릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 개발 중에는 API 키 없이도 화면과 추천 흐름을 계속 볼 수 있고, 실제 키가 들어오면 같은 도메인 로직을 유지한 채 어댑터만 바꿔서 정밀도를 올릴 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나 좋았던 점은 장애 대응입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API가 실패해도 앱 전체가 멈추는 대신, 가능한 범위에서 fallback 결과를 보여줄 수 있습니다.&lt;br /&gt;지도나 자동완성은 비활성화될 수 있어도, 핵심 추천 화면은 mock 데이터로 확인할 수 있게 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 사이드 프로젝트라도 이런 분리선을 미리 잡아두면, 나중에 실제 서비스를 붙일 때 훨씬 덜 흔들립니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모바일이 먼저인 UI&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 앱은 PC에서 오래 보는 서비스가 아닙니다.&lt;br /&gt;대부분 차 안에서 잠깐 열어볼 가능성이 높습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음부터 모바일 웹을 기준으로 잡았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홈 화면에서는 운전자 출발지, 운전자 도착지, 승객 목적지를 하나의 여정 클러스터처럼 묶었습니다.&lt;br /&gt;각 입력이 따로 흩어져 있으면 관계가 잘 안 보이기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 화면에서는 1순위 후보를 크게 보여주고, 나머지 후보는 압축해서 비교할 수 있게 했습니다.&lt;br /&gt;후보가 너무 많으면 오히려 결정이 늦어지니, 빠르게 고를 수 있는 카드 구성이 더 맞다고 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 비교도 숫자만 보여주기보다 가로 막대로 같이 표현했습니다.&lt;br /&gt;차 안에서 작은 글씨를 오래 읽기보다, 대략 어느 후보가 더 유리한지 바로 보이게 하고 싶었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과 UI는 &amp;ldquo;설명&amp;rdquo;보다 &amp;ldquo;비교&amp;rdquo;에 가깝게&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 화면을 만들 때 가장 신경 쓴 건 후보 간 비교였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 이 앱을 오래 읽으려고 여는 게 아닙니다.&lt;br /&gt;지금 차 안에서 빨리 정해야 하니까 여는 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 결과 카드에는 많은 설명을 넣기보다, 의사결정에 필요한 숫자를 최대한 같은 위치에 반복해서 배치했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;운전자 추가 시간&lt;/li&gt;
&lt;li&gt;승객 총 이동 시간&lt;/li&gt;
&lt;li&gt;도보 시간&lt;/li&gt;
&lt;li&gt;환승 횟수&lt;/li&gt;
&lt;li&gt;추천 사유&lt;/li&gt;
&lt;li&gt;여정 타임라인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 스택바도 이 맥락에서 넣었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;승객 이동 시간을 대기, 대중교통, 도보처럼 나눠서 보여주면 &amp;ldquo;총 30분&amp;rdquo;이라는 숫자만 볼 때보다 더 판단하기 쉽습니다.&lt;br /&gt;예를 들어 같은 30분이라도 도보가 긴 30분과 지하철 한 번 타는 30분은 체감이 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1순위 후보는 크게 보여주고, 2&amp;middot;3순위는 압축 카드로 두었습니다.&lt;br /&gt;그리고 다른 후보가 더 마음에 들면 바로 promote해서 지도와 카드 상태가 같이 바뀌도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 단순 UI라기보다, 추천 결과를 사용자가 납득하는 과정이라고 생각했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과 페이지는 공유 가능해야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 앱은 혼자만 보는 도구가 아닐 가능성이 큽니다.&lt;br /&gt;운전자에게 보여주거나, 같이 탄 사람에게 링크를 보내야 할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 결과 상태가 URL에 담기도록 했습니다.&lt;br /&gt;선택한 후보도 &lt;code&gt;picked&lt;/code&gt; 값으로 직렬화해서, 링크를 다시 열었을 때 같은 후보를 볼 수 있게 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에는 카카오톡이나 SNS에 공유했을 때 선택한 후보의 이름과 시간이 들어간 미리보기 이미지도 만들 수 있도록 구조를 잡았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 이 앱의 결과는 &amp;ldquo;내가 보는 화면&amp;rdquo;이면서 동시에 &amp;ldquo;상대에게 보여줄 근거&amp;rdquo;이기도 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능과 호출 수 줄이기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추천 계산은 여러 외부 정보를 섞기 때문에 호출 수가 쉽게 늘어날 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 개발 환경에서는 React StrictMode나 새로고침 때문에 같은 요청이 반복될 수 있습니다.&lt;br /&gt;이 부분은 &lt;code&gt;runRecommendation&lt;/code&gt; 쪽에 캐시를 두는 방식으로 줄였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 입력에 대해서는 일정 시간 동안 결과를 재사용하고,&lt;br /&gt;이미 진행 중인 요청이 있으면 같은 Promise를 공유하도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 앱이라도 이런 부분을 초기에 잡아두면, 나중에 실제 API를 붙였을 때 비용과 속도 면에서 훨씬 안정적입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;후보를 줄이는 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전국 전철역과 환승 거점을 모두 후보로 두면 보기에는 좋아 보이지만, 실제 계산에서는 부담이 큽니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음부터 전체 후보에 대해 외부 API를 호출하지 않고, 먼저 가벼운 공간 계산으로 후보를 줄였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 아이디어는 이렇습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;운전자 출발지와 도착지 주변 영역을 잡는다&lt;/li&gt;
&lt;li&gt;승객 목적지와 너무 동떨어진 후보를 제외한다&lt;/li&gt;
&lt;li&gt;운전자 출발지와 너무 가까운 지점은 제외한다&lt;/li&gt;
&lt;li&gt;남은 후보를 직선거리와 대략적인 위치 관계로 정렬한다&lt;/li&gt;
&lt;li&gt;그중 상위 후보에 대해서만 routing/transit 계산을 수행한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 haversine 거리 계산과 bounding box를 사용했습니다.&lt;br /&gt;정밀 경로 계산은 비싸지만, 후보를 줄이는 단계에서는 직선거리 기반 계산만으로도 충분히 의미가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 prefilter를 먼저 거치면 실제 API 호출 수를 줄일 수 있고, 결과 응답도 빨라집니다.&lt;br /&gt;사용자 입장에서는 그냥 빠르게 결과가 뜨는 것처럼 보이지만, 내부적으로는 &amp;ldquo;비싼 계산을 하기 전에 싼 계산으로 줄이는&amp;rdquo; 흐름입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 구조는 나중에 후보 데이터가 1천 개, 1만 개로 늘어나도 그대로 확장할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개인정보는 최소로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 앱은 위치를 다루기 때문에 개인정보에 대한 기준도 단순하게 잡았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 서버에 입력한 출발지나 목적지를 저장하지 않습니다.&lt;br /&gt;최근 검색이나 즐겨찾기는 사용자 브라우저의 localStorage에만 둡니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 키가 필요한 API는 Next API 라우트 뒤로 숨기고, 브라우저에는 공개 가능한 키만 노출되도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 없이 쓰는 도구이기 때문에, 데이터 저장을 최소화하는 쪽이 제품 성격에도 맞았습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지금 상태와 다음 단계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 모바일 웹앱 형태로 흐름을 볼 수 있는 상태입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 화면, 추천 결과 카드, 지도 미리보기, 후보 비교, 공유를 고려한 URL 구조까지 잡아두었습니다.&lt;br /&gt;전국 전철&amp;middot;도시철도역 데이터를 기반으로 후보를 만들고, 외부 API가 있으면 실제 경로 계산을 쓰고, 없으면 mock fallback으로 화면을 확인할 수 있게 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에 더 다듬고 싶은 부분은 이런 것들입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시간대별 가중치 자동 조정&lt;/li&gt;
&lt;li&gt;막차나 심야 안전성을 반영한 추천&lt;/li&gt;
&lt;li&gt;결과 공유용 이미지 카드&lt;/li&gt;
&lt;li&gt;여러 명의 승객을 동시에 고려하는 모드&lt;/li&gt;
&lt;li&gt;추천이 좋았는지 피드백을 받는 UX&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 다 넣기보다는, 지금은 &amp;ldquo;한 명의 운전자와 한 명의 승객&amp;rdquo;이라는 기본 문제를 제대로 푸는 데 집중하고 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Smart Drop Point는 거대한 이동 플랫폼을 만들려는 프로젝트는 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 아주 작고 구체적인 순간을 보고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같이 차를 탔는데 서로 목적지가 다를 때,&lt;br /&gt;어디서 내리면 운전자도 덜 부담스럽고 승객도 편할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문에 좌표, 경로, 대중교통 정보를 얹어서 빠르게 답해보는 앱입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들면서 느낀 건, 작은 문제라도 실제 생활의 맥락이 들어가면 설계할 게 꽤 많다는 점이었습니다.&lt;br /&gt;입력은 짧아야 하고, 결과는 바로 비교돼야 하고, 숫자는 신뢰할 수 있어야 하고, 모바일에서 흔들리는 손으로도 쓸 수 있어야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 더 다듬을 부분은 많지만, 문제를 좁게 잡고 구조를 나눠두니 다음 단계로 확장하기는 좋아졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 실제 사용 사례를 더 넣어보면서 추천 품질을 조금씩 올려봐야겠습니다.&lt;/p&gt;</description>
      <category>Develop/SmartDropPoint</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/82</guid>
      <comments>https://smokingbird.tistory.com/82#entry82comment</comments>
      <pubDate>Fri, 15 May 2026 16:40:16 +0900</pubDate>
    </item>
    <item>
      <title>같이 탄 차에서 어디서 내릴지 정해주는 앱</title>
      <link>https://smokingbird.tistory.com/81</link>
      <description>&lt;iframe src=&quot;https://www.youtube.com/embed/Pl-GG24HhdE?controls=0&amp;amp;modestbranding=1&amp;amp;rel=0&amp;amp;playsinline=1&amp;amp;loop=1&amp;amp;playlist=Pl-GG24HhdE&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;
         &lt;/iframe&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같이 차를 타고 가다 보면 은근 애매한 순간이 있습니다.&lt;br /&gt;출발지는 같은데, 도착지는 서로 다를 때요.&lt;br /&gt;친구 차를 얻어 탔거나, 회식 끝나고 동료 차를 탔거나, 여행지에서 일행이랑 택시를 같이 탔는데 숙소가 다른 경우가 그렇습니다.&lt;br /&gt;이럴 때 보통은 이렇게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;그럼 어디서 내리지?&amp;rdquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운전자는 너무 돌아가면 부담스럽고, 승객은 아무 데서나 내리면 집까지 가는 길이 복잡해집니다. 그런데 이걸 차 안에서 지도 앱 여러 개 열어가며 비교하기는 꽤 번거롭습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Smart Drop Point는 이 작은 결정을 줄이기 위해 만든 모바일 웹앱입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어디서 내리면 좋을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Smart Drop Point가 풀려는 문제는 단순합니다.&lt;br /&gt;운전자와 승객이 같은 차에 탔지만, 최종 목적지가 다를 때.&lt;br /&gt;운전자는 덜 돌아가고, 승객은 하차 후 이동이 편한 지점을 찾아주는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 상황입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;강남에서 같이 출발함&lt;/li&gt;
&lt;li&gt;운전자는 송파 쪽으로 감&lt;/li&gt;
&lt;li&gt;승객은 강변역 쪽으로 가야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 그냥 감으로 정하면 &amp;ldquo;잠실역 어때?&amp;rdquo; 정도에서 끝나기 쉽습니다.&lt;br /&gt;물론 그게 맞을 수도 있지만, 정말 운전자가 많이 안 돌아가는지, 승객 입장에서 대중교통이 편한지, 다른 후보는 없는지 바로 알기는 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Smart Drop Point는 이런 후보를 계산해서 보여줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력은 세 가지만&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용법은 복잡하지 않게 잡았습니다.&lt;br /&gt;기본적으로 세 가지 위치만 넣으면 됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;운전자가 출발하는 곳&lt;/li&gt;
&lt;li&gt;운전자가 도착하는 곳&lt;/li&gt;
&lt;li&gt;승객이 최종적으로 가야 하는 곳&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 우회 허용 시간이나 도보 허용 시간, 우선순위 정도만 고르면 됩니다.&lt;br /&gt;예를 들면 &amp;ldquo;운전자는 너무 돌아가지 않게&amp;rdquo;, &amp;ldquo;승객 환승을 줄이게&amp;rdquo;, &amp;ldquo;둘 다 적당히 균형 있게&amp;rdquo; 같은 방향입니다.&lt;br /&gt;차 안에서 잠깐 쓰는 도구라서 입력이 길어지면 안 된다고 봤습니다.&lt;br /&gt;회원가입을 하거나, 여러 단계를 거치거나, 설정을 많이 만지는 앱이면 이 상황에서는 잘 안 쓰게 될 것 같았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 모바일에서 한 손으로 빠르게 입력하고 바로 결과를 볼 수 있는 흐름을 우선했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과는 후보 카드로 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 화면에서는 추천 후보를 카드로 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 후보에는 이런 정보가 들어갑니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;운전자가 얼마나 더 돌아가는지&lt;/li&gt;
&lt;li&gt;승객이 내린 뒤 목적지까지 얼마나 걸리는지&lt;/li&gt;
&lt;li&gt;도보와 대중교통 시간이 어느 정도인지&lt;/li&gt;
&lt;li&gt;환승이 얼마나 필요한지&lt;/li&gt;
&lt;li&gt;왜 이 지점이 괜찮은지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자를 길게 나열하기보다, 후보끼리 바로 비교할 수 있게 보여주는 게 중요했습니다.&lt;br /&gt;특히 이 앱은 &amp;ldquo;가장 정확한 경로 탐색 앱&amp;rdquo;이라기보다 &amp;ldquo;차 안에서 빨리 결정하게 도와주는 앱&amp;rdquo;에 가깝습니다.&lt;br /&gt;그래서 1순위 후보를 크게 보여주고, 나머지 후보는 비교할 수 있게 압축해서 보여주는 방향으로 구성했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 지도 앱과 다른 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오맵이나 네이버지도는 이미 훌륭합니다.&lt;br /&gt;A에서 B까지 가는 길을 찾는 용도라면 그걸 쓰면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Smart Drop Point가 보는 문제는 조금 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 지도 앱은 보통 한 사람의 이동을 기준으로 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내가 어디서 출발해서&lt;/li&gt;
&lt;li&gt;어디까지 갈지&lt;/li&gt;
&lt;li&gt;어떤 경로가 빠른지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Smart Drop Point는 두 사람의 이동을 동시에 봅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;운전자는 자기 목적지로 가야 하고&lt;/li&gt;
&lt;li&gt;승객은 중간 어딘가에서 내려야 하고&lt;/li&gt;
&lt;li&gt;그 지점이 둘 모두에게 납득 가능해야 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;A에서 B로 가는 길&amp;rdquo;이 아니라 &amp;ldquo;A에서 출발한 두 사람이 어디서 자연스럽게 갈라질지&amp;rdquo;를 찾는 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이가 생각보다 큽니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;감 대신 숫자로 말하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 앱에서 중요하게 생각한 건 객관적인 근거입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같이 탄 차에서 하차 지점을 정하는 일은 단순한 길찾기만은 아닙니다.&lt;br /&gt;관계의 문제가 조금 섞여 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 멀리 데려다 달라고 하긴 미안하고,&lt;br /&gt;아무 데서나 내리자니 내 이동이 너무 불편하고,&lt;br /&gt;그렇다고 차 안에서 오래 고민하기도 애매합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 &amp;ldquo;여기가 제일 좋아요&amp;rdquo;라고만 말하면 설득력이 약합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 이렇게 보여주는 편이 낫습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;운전자는 3분만 더 돌아가면 됨&lt;/li&gt;
&lt;li&gt;승객은 하차 후 14분이면 도착함&lt;/li&gt;
&lt;li&gt;환승이 적고 도보가 짧음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 숫자로 보이면 대화가 조금 쉬워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;여기서 내려도 될까요?&amp;rdquo;라는 말에 근거가 생기는 느낌입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모바일 웹앱으로 만든 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Smart Drop Point는 모바일 웹앱으로 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 앱을 쓰는 순간은 대부분 이동 중일 거라고 봤습니다.&lt;br /&gt;차 안에서, 약속이 끝난 직후에, 누군가 &amp;ldquo;같이 타고 가자&amp;rdquo;라고 말한 뒤에 잠깐 열어보는 식입니다.&lt;br /&gt;그래서 설치가 필요하면 안 되고, 데스크톱보다 모바일이 먼저여야 했습니다.&lt;br /&gt;브라우저에서 바로 열고, 필요하면 홈 화면에 추가해서 앱처럼 쓸 수 있는 정도가 적당하다고 봤습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이런 상황에 잘 맞습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Smart Drop Point는 모든 이동 문제를 해결하려는 앱은 아닙니다.&lt;br /&gt;오히려 아주 좁은 상황 하나를 보고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;퇴근길에 동료 차를 같이 탔을 때&lt;/li&gt;
&lt;li&gt;회식 후 누군가 차로 데려다 준다고 했을 때&lt;/li&gt;
&lt;li&gt;여행지에서 같은 택시를 탔지만 숙소가 다를 때&lt;/li&gt;
&lt;li&gt;가족이나 지인이 태워줬는데 목적지가 완전히 같지는 않을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 순간에 &amp;ldquo;어디서 내리는 게 제일 괜찮지?&amp;rdquo;를 빠르게 정하는 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낯선 사람과 카풀을 매칭하거나, 실시간 차량을 추적하거나, 결제를 붙이는 서비스는 아닙니다.&lt;br /&gt;이미 같이 탄 사람들끼리 결정을 쉽게 하는 데 집중했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Smart Drop Point는 길 위에서 자주 생기는 작은 결정을 줄이기 위한 앱입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같이 차를 탔지만 도착지가 다를 때,&lt;br /&gt;운전자에게는 덜 부담스럽고 승객에게는 이동이 편한 하차 지점을 찾아줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 자체는 작아 보이지만, 실제 상황에서는 꽤 자주 마주치는 문제라고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어디서 내릴지 감으로 정하지 않고,&lt;br /&gt;운전자 우회 시간과 승객 이동 시간을 같이 보면서 결정할 수 있으면 대화가 조금 더 편해질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 계속 다듬는 중이지만, &amp;ldquo;같이 탔는데 어디서 내릴까?&amp;rdquo;라는 질문 하나만큼은 빠르게 줄여주는 도구로 만들어보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스는 여기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 주소로 접속하시면 사용할 수 있습니다.&lt;br /&gt;아직은 초기라서 에러가 있을수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sdp.sanguneo.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://sdp.sanguneo.com/&lt;/a&gt;&lt;/p&gt;</description>
      <category>Develop/SmartDropPoint</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/81</guid>
      <comments>https://smokingbird.tistory.com/81#entry81comment</comments>
      <pubDate>Fri, 15 May 2026 16:26:34 +0900</pubDate>
    </item>
    <item>
      <title>사내 그룹웨어 Daou GW 자동화: 브라우저 대신 CLI와 MCP로 연결하기</title>
      <link>https://smokingbird.tistory.com/80</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2026년 5월 11일 오후 05_29_59.png&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbXIlj/dJMcacJRUPi/SEXRleu5oBzmjskysg8Ytk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbXIlj/dJMcacJRUPi/SEXRleu5oBzmjskysg8Ytk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbXIlj/dJMcacJRUPi/SEXRleu5oBzmjskysg8Ytk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbXIlj%2FdJMcacJRUPi%2FSEXRleu5oBzmjskysg8Ytk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1672&quot; height=&quot;941&quot; data-filename=&quot;ChatGPT Image 2026년 5월 11일 오후 05_29_59.png&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 Hermes Agent를 쓰면서 반복되는 작업을 하나씩 스킬로 정리하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그중에서 꽤 실사용에 가까웠던 게 Daou GW 작업이었어요. 메일 확인, 캘린더 조회, 전자결재 상태 확인 같은 그룹웨어 작업을 매번 브라우저로 열고 클릭하는 게 은근 귀찮아서, 아예 CLI와 MCP로 처리할 수 있게 정리해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글은 그 작업을 만들면서 어떤 식으로 접근했는지 기록해보려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;처음 목표는 간단했습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;메일 최신 5개 보여줘&amp;rdquo;라고 말하면 최근 메일을 확인하고,&lt;br /&gt;&amp;ldquo;오늘 일정 보여줘&amp;rdquo;라고 말하면 캘린더 일정을 바로 가져오는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저를 자동화해서 화면을 클릭하는 방식도 생각할 수 있지만, 저는 가능하면 HTTP/API 중심으로 가고 싶었습니다. 브라우저 자동화는 눈에 보이는 화면을 따라가기 때문에 편할 때도 있지만, 사내 시스템처럼 자주 쓰는 작업에는 조금 무겁게 느껴졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 방향을 이렇게 잡았습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저 자동화는 분석이나 임시 확인용으로만 사용&lt;/li&gt;
&lt;li&gt;실제 반복 작업은 CLI/API로 처리&lt;/li&gt;
&lt;li&gt;Hermes에서는 스킬을 통해 &amp;ldquo;어떤 명령을 우선 써야 하는지&amp;rdquo; 기억하게 만들기&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Go 코드에서 TypeScript CLI로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 로컬에 있던 Go 코드로 Daou GW 로그인과 일부 HTTP API 처리를 해보려고 했습니다. 왜냐면 Go는 따로 npm 같은 패키지 설치 없이 사용할수있도록 쉽게 패키징 할수 있었거든요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 그대로 확장할 수도 있었지만, npm 배포와 MCP 연동까지 생각하면 TypeScript 쪽이 더 다루기 편했습니다. 그래서 별도 저장소로 &lt;code&gt;daou-gw-cli&lt;/code&gt;를 만들고, CLI와 MCP를 같이 제공하는 구조로 정리했습니다. 무엇보다, 오픈소스로 확장하기엔 ts만한게 없었지요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 저장소는 &lt;a href=&quot;https://github.com/sanguneo/daou-gw-mcp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub&lt;/a&gt;에 올려두었습니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;https://github.com/sanguneo/daou-gw-mcp&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조는 대략 이렇습니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;bin&quot;: {
    &quot;daou-gw-cli&quot;: &quot;dist/index.js&quot;,
    &quot;daou-gw-mcp&quot;: &quot;dist/mcp.js&quot;
  },
  &quot;scripts&quot;: {
    &quot;build&quot;: &quot;tsup &amp;amp;&amp;amp; node scripts/postbuild.mjs&quot;,
    &quot;test&quot;: &quot;vitest run&quot;,
    &quot;typecheck&quot;: &quot;tsc --noEmit&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLI로는 직접 명령을 실행할 수 있고,&lt;br /&gt;MCP로는 Hermes 같은 에이전트가 도구처럼 호출할 수 있게 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-heading=&quot;개발은 브라우저에서 시작했습니다&quot; data-ke-size=&quot;size26&quot;&gt;개발은 브라우저에서 시작했습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Hermes Agent에서 CDP로 브라우저에 연결했습니다. 이미 로그인된 브라우저 세션을 그대로 활용할 수 있어서, 로그인 흐름을 매번 다시 만들 필요가 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음에는 브라우저에서 실제로 필요한 행동을 해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 메일 목록을 열어보고, 검색을 해보고, 캘린더 화면을 이동해보고, 결재 상태를 확인하는 식입니다. 사용자가 실제로 누르는 흐름을 먼저 브라우저에서 재현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음은 브라우저에서 행동을 하는 동안 Network 요청을 캡처했습니다. 어떤 버튼을 눌렀을 때 어떤 API가 호출되는지, method가 GET인지 POST인지, query string과 request body가 어떻게 생겼는지 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 확인한 요청을 하나씩 HTTP 메소드로 옮겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략 이런 흐름이었습니다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;브라우저에서 실제 동작 수행
&amp;rarr; Network 요청 확인
&amp;rarr; 필요한 URL / method / payload 정리
&amp;rarr; TypeScript HTTP client로 재현
&amp;rarr; CLI 명령으로 감싸기
&amp;rarr; MCP tool로 노출하기
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 &amp;ldquo;어떤 API를 써야 하는지 알아내는 도구&amp;rdquo;로만 쓰고, 실제 반복 작업은 HTTP 요청으로 처리했습니다. 그래서 나중에 실행할 때는 브라우저를 띄우지 않아도 되고, 에이전트 입장에서도 훨씬 가볍게 호출할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스킬에는 &amp;ldquo;어떻게 행동해야 하는지&amp;rdquo;를 적었다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 중요했던 건 단순히 CLI를 만드는 것보다, 에이전트가 헷갈리지 않게 행동 규칙을 정리하는 일이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자가 &amp;ldquo;메일 최신 5개 보여줘&amp;rdquo;라고 했을 때 에이전트가 먼저 저장소를 뒤지거나, 브라우저를 열거나, UI 상태를 추측하기 시작하면 답답합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Daou GW 스킬에는 이런 원칙을 넣었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메일 목록은 &lt;code&gt;mail list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;메일 검색은 &lt;code&gt;mail search&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;메일 삭제는 &lt;code&gt;mail delete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;캘린더는 &lt;code&gt;calendar list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;결재는 전자결재 상태 조회 명령 사용&lt;/li&gt;
&lt;li&gt;실패했을 때만 세션이나 설정을 확인하고 1회 재시도&lt;/li&gt;
&lt;li&gt;비밀번호, 쿠키, 세션 토큰은 절대 출력하지 않기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 별것 아닌 것 같지만, 실제로 에이전트를 쓰다 보면 이런 규칙이 꽤 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람은 &amp;ldquo;메일 보여줘&amp;rdquo;라고 했을 때 바로 결과를 기대하는데, 에이전트는 가끔 너무 착실하게 주변 정보를 먼저 확인하려고 하거든요. 그래서 스킬에 &amp;ldquo;불필요한 탐색을 하지 말라&amp;rdquo;고 명시했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CLI와 MCP는 같은 의미를 갖게 했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLI 명령과 MCP 도구 이름도 최대한 같은 의미로 맞췄습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLI에서는 이런 식입니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;daou-gw-cli mail list --size 5
daou-gw-cli mail search --query AWS --size 20
daou-gw-cli calendar list
daou-gw-cli approval status&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP에서는 이런 도구로 대응됩니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;mail_list
mail_search
mail_delete
calendar_list
approval_status&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 맞춰두면 사람이 터미널에서 쓰는 방식과 에이전트가 MCP로 호출하는 방식이 크게 다르지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 문제가 생겼을 때도 &amp;ldquo;CLI로 재현해보고 MCP 쪽을 확인하는&amp;rdquo; 흐름이 자연스럽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제로 신경 쓴 부분: 세션과 실패 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Daou GW 같은 사내 시스템은 로그인 세션이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 로그인 정보를 다시 입력하는 방식은 불편하고, 그렇다고 인증 정보를 대화에 노출하는 방식은 위험합니다. 그래서 로컬에 세션을 저장하고, 명령 실행 시 세션을 먼저 재사용하도록 정리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션이 유효하면 그대로 명령을 실행하고, 만료된 경우에는 가능한 범위에서 다시 로그인한 뒤 원래 명령을 한 번만 재시도하는 흐름입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 건 무한 재시도를 하지 않는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내 시스템은 실패 원인이 다양합니다. 세션이 만료됐을 수도 있고, 네트워크가 막혔을 수도 있고, API 응답 구조가 바뀌었을 수도 있습니다. 그래서 실패하면 원인을 짧게 보고하고, 필요한 경우에만 상태를 확인하도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세션과 민감 정보는 로컬에만&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 상태는 &lt;code&gt;~/.daou/&lt;/code&gt; 아래에 두는 방식으로 정리했습니다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;~/.daou/config.json
~/.daou/session.json
~/.daou/vault.key&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스킬에도 민감값을 출력하지 말라고 명시했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트가 편해도, 비밀번호나 쿠키 같은 값이 대화에 그대로 나오는 건 피해야 합니다. 이 부분은 도구를 만들 때부터 계속 신경 써야 할 지점인 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;만들고 나니 좋은 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 만들고 나니 그룹웨어 작업을 훨씬 짧게 처리할 수 있게 됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 브라우저를 열고, 로그인 상태를 보고, 메뉴를 찾아가고, 필요한 화면을 찾아야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 그냥 이렇게 말하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;메일 최신 5개 보여줘
AWS 메일 찾아줘
오늘 캘린더 일정 보여줘
결재 상태 확인해줘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Hermes는 Daou GW 스킬을 보고 CLI/MCP를 우선 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 제가 원했던 형태였습니다. 에이전트가 모든 걸 똑똑하게 추론하는 것보다, 자주 쓰는 업무는 명확한 도구와 규칙으로 고정해두는 방식이 훨씬 안정적이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 Daou GW 스킬 개발은 단순히 브라우저 화면을 자동으로 누르는 것이 아니라, 사내 시스템을 에이전트가 안전하고 반복 가능하게 다루도록 만드는 작업에 가까웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들면서 느낀 점은 이렇습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반복 업무는 브라우저 자동화보다 API/CLI가 편하다&lt;/li&gt;
&lt;li&gt;MCP를 붙이면 에이전트가 도구처럼 호출할 수 있다&lt;/li&gt;
&lt;li&gt;스킬에는 기능 설명보다 행동 규칙을 명확히 적는 게 중요하다&lt;/li&gt;
&lt;li&gt;민감 정보는 절대 대화에 노출되지 않게 해야 한다&lt;/li&gt;
&lt;li&gt;실패 케이스도 스킬에 기록해두면 다음에 덜 헤맨다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 더 다듬을 부분은 있지만, 메일이나 일정처럼 자주 확인하는 작업이 말 한마디로 처리되는 건 꽤 만족스럽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 메일 정리나 전자결재, 게시판 글 작성 쪽도 조금 더 안정적으로 정리 해봐야 겠습니다.&lt;/p&gt;</description>
      <category>Challenge</category>
      <category>cli</category>
      <category>Daou GW</category>
      <category>Hermes Agent</category>
      <category>MCP</category>
      <category>Typescript</category>
      <category>그룹웨어 자동화</category>
      <category>사내 그룹웨어</category>
      <category>업무 자동화</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/80</guid>
      <comments>https://smokingbird.tistory.com/80#entry80comment</comments>
      <pubDate>Mon, 11 May 2026 17:31:25 +0900</pubDate>
    </item>
    <item>
      <title>종합소득세 신고 방법: 직장인 개발자의 홈택스 신고가이드 (feat. 크몽)</title>
      <link>https://smokingbird.tistory.com/79</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3bf30e3c-1d4b-4736-bb4e-724763cb05f4.png&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k93z6/dJMcagFtqPB/3Cr8EXiJ1X4YLwFFBZeK20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k93z6/dJMcagFtqPB/3Cr8EXiJ1X4YLwFFBZeK20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k93z6/dJMcagFtqPB/3Cr8EXiJ1X4YLwFFBZeK20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk93z6%2FdJMcagFtqPB%2F3Cr8EXiJ1X4YLwFFBZeK20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;대표이미지 - 제목 기반&quot; loading=&quot;lazy&quot; width=&quot;1672&quot; height=&quot;941&quot; data-filename=&quot;3bf30e3c-1d4b-4736-bb4e-724763cb05f4.png&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직장에 다니면서 크몽에서 개발 외주, 자동화 작업, 웹사이트 제작, 기술 상담 같은 부업을 했다면 매년 5월 종합소득세 신고 대상이 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 직장인 개발자는 이렇게 생각합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 이미 연말정산 했는데, 또 신고해야 하나?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하면, 크몽에서 부업 수익이 있었다면 별도로 종합소득세 신고를 해야 할 수 있습니다.&lt;br /&gt;회사에서 진행한 연말정산은 근로소득에 대한 정산입니다. 즉, 회사에서 받은 월급에 대한 세금 정리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 크몽에서 발생한 수익은 회사 급여와 별개의 소득입니다. 따라서 직장인이라도 크몽 부업 수익이 있다면 종합소득세 신고 때 직접 반영해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 크몽은 일반적인 프리랜서 지급 구조와 조금 다릅니다.&lt;br /&gt;보통 프리랜서 용역은 3.3% 원천징수를 하고 지급됩니다. 이 경우 홈택스에서 사업소득 자료가 자동으로 잡히는 경우가 많습니다.&lt;br /&gt;하지만 크몽은&amp;nbsp; 3.3%를 원천징수해서 신고해주는 구조가 아니라, 플랫폼 수수료를 제외하고 판매자에게 정산해주는 방식입니다.&lt;br /&gt;그래서 홈택스에 들어가도 크몽 수익이 자동으로 깔끔하게 불러와지지 않을 수 있습니다.&lt;br /&gt;즉, 크몽에서 번 돈은 내가 직접 매출을 확인하고, 직접 입력해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 직장인 개발자가 크몽 부업 수익을 종합소득세 신고에 어떻게 반영하면 되는지 정리한 진입 가이드입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세무 전문 글은 아닙니다.&lt;br /&gt;실제 신고 전에는 본인의 상황에 맞게 홈택스 안내와 세무 전문가 의견을 함께 확인하는 것이 좋습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 홈택스에서 종합소득세 신고로 들어갑니다&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-08 154550.png&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;159&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s5vd2/dJMcaiwsV4c/z9oPkMwKrGOvOj3FzCX97k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s5vd2/dJMcaiwsV4c/z9oPkMwKrGOvOj3FzCX97k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s5vd2/dJMcaiwsV4c/z9oPkMwKrGOvOj3FzCX97k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs5vd2%2FdJMcaiwsV4c%2Fz9oPkMwKrGOvOj3FzCX97k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;811&quot; height=&quot;159&quot; data-filename=&quot;스크린샷 2026-05-08 154550.png&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;159&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 홈택스에 접속합니다.&lt;br /&gt;상단 메뉴에서 &lt;b&gt;종합소득세 신고&lt;/b&gt; 화면으로 이동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신고 화면에 들어가면 여러 신고 방식이 보입니다. 처음 신고하는 직장인 부업 개발자라면 우선 &lt;b&gt;모두채움 정기신고&lt;/b&gt;를 눌러 시작하면 됩니다.&lt;br /&gt;모두채움은 국세청이 이미 보유한 근로소득, 일부 사업소득, 공제 정보 등을 미리 채워주는 신고 방식입니다.&lt;br /&gt;직장인의 경우 회사에서 연말정산한 근로소득 정보가 자동으로 들어와 있기 때문에, 처음부터 전부 입력하는 것보다 진입 장벽이 낮습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 앞서 언급한대로&amp;nbsp;크몽 수익은 일반적인 3.3% 원천징수 사업소득으로 잡히지 않아서,&lt;br /&gt;모두채움으로 시작하더라도, 부업 매출은 이후 단계에서 직접 추가로 입력해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 이렇습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두채움 정기신고는 시작점이고, 크몽 수익은 직접 확인해서 보완 입력해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 기본정보 입력에서 내 정보를 불러옵니다&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;62422752-81ff-45b9-b580-f8e0af53de41.png&quot; data-origin-width=&quot;1619&quot; data-origin-height=&quot;971&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vg8RY/dJMcagSY6Ar/uyuXWkQceciQGriH4WvKFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vg8RY/dJMcagSY6Ar/uyuXWkQceciQGriH4WvKFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vg8RY/dJMcagSY6Ar/uyuXWkQceciQGriH4WvKFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVg8RY%2FdJMcagSY6Ar%2FuyuXWkQceciQGriH4WvKFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1619&quot; height=&quot;971&quot; data-filename=&quot;62422752-81ff-45b9-b580-f8e0af53de41.png&quot; data-origin-width=&quot;1619&quot; data-origin-height=&quot;971&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모두채움 정기신고&lt;/b&gt;로 들어가면 먼저 &lt;b&gt;기본정보 입력&lt;/b&gt; 화면이 나옵니다.&lt;br /&gt;여기서 주민등록번호 입력란 오른쪽에 있는 &lt;b&gt;확인&lt;/b&gt; 버튼을 누릅니다.&lt;br /&gt;확인을 누르면 홈택스에 등록된 기본 인적사항이 자동으로 불러와집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계에서 확인할 내용은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이름&lt;/li&gt;
&lt;li&gt;주민등록번호&lt;/li&gt;
&lt;li&gt;주소&lt;/li&gt;
&lt;li&gt;휴대전화번호&lt;/li&gt;
&lt;li&gt;이메일&lt;/li&gt;
&lt;li&gt;거주 구분&lt;/li&gt;
&lt;li&gt;내&amp;middot;외국인 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분 자동으로 채워지지만, 연락처나 이메일이 예전 정보일 수 있으니 한 번 확인해두는 것이 좋습니다.&lt;br /&gt;신고서의 기본 주체가 나인지 확인하고, 다음 단계로 넘어가기 위한 기본 확인 단계입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 종합소득금액에서 사업소득을 추가합니다&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edf471f9-f8ab-4c55-8441-a93af58e4133.png&quot; data-origin-width=&quot;1755&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhUdsK/dJMb997nTJ5/MiuYfa1KwweLqYnH4EMAyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhUdsK/dJMb997nTJ5/MiuYfa1KwweLqYnH4EMAyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhUdsK/dJMb997nTJ5/MiuYfa1KwweLqYnH4EMAyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhUdsK%2FdJMb997nTJ5%2FMiuYfa1KwweLqYnH4EMAyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1755&quot; height=&quot;896&quot; data-filename=&quot;edf471f9-f8ab-4c55-8441-a93af58e4133.png&quot; data-origin-width=&quot;1755&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본정보 입력을 마치고 다음 단계로 넘어가면 &lt;b&gt;종합소득금액&lt;/b&gt;을 작성하는 화면이 나옵니다.&lt;br /&gt;직장인 부업자의 경우 회사에서 받은 &lt;b&gt;근로소득&lt;/b&gt;은 대부분 자동으로 입력되어 있습니다.&lt;br /&gt;회사에서 이미 연말정산을 했기 때문에, 홈택스에서 근로소득 자료를 불러와 보여주는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 누락된 크몽 수익을 직접 입력해야 합니다.&lt;br /&gt;화면에서 &lt;b&gt;사업소득&lt;/b&gt; 항목을 찾은 뒤, &lt;b&gt;소득명세 작성하기&lt;/b&gt; 버튼을 누릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터가 크몽 부업 수익을 신고서에 직접 반영하는 핵심 단계입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 비어 있는 사업소득 명세를 새로 작성합니다&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-08 143011.png&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;798&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7lG9B/dJMcaaeaFfP/NtvNtQKFKPfki7InJT9tEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7lG9B/dJMcaaeaFfP/NtvNtQKFKPfki7InJT9tEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7lG9B/dJMcaaeaFfP/NtvNtQKFKPfki7InJT9tEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7lG9B%2FdJMcaaeaFfP%2FNtvNtQKFKPfki7InJT9tEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1145&quot; height=&quot;798&quot; data-filename=&quot;스크린샷 2026-05-08 143011.png&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;798&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사업소득 - 소득명세 작성하기&lt;/b&gt;를 누르면 사업장명세, 소득금액명세 등 여러 항목이 나오는 화면으로 이동합니다.&lt;br /&gt;처음 들어오면 대부분의 내용이 비어 있을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당황하지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 화면이 비어 있는 것은 자연스러운 상황입니다.&lt;br /&gt;우측에 있는 &lt;b&gt;작성하기&lt;/b&gt; 버튼을 눌러서 크몽 부업 수익에 대한 사업소득 명세를 새로 입력합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터는 내가 크몽에서 정산받은 금액과 업종 정보를 기준으로 직접 사업소득을 만들어 넣는 단계입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 사업장명세에서 사업자등록번호 없음으로 입력합니다&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-08 143023.png&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;617&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JtI7j/dJMcacbYbQv/YN2wEY2Wll3rcCjLpBHupK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JtI7j/dJMcacbYbQv/YN2wEY2Wll3rcCjLpBHupK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JtI7j/dJMcacbYbQv/YN2wEY2Wll3rcCjLpBHupK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJtI7j%2FdJMcacbYbQv%2FYN2wEY2Wll3rcCjLpBHupK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;894&quot; height=&quot;617&quot; data-filename=&quot;스크린샷 2026-05-08 143023.png&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;617&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;작성하기&lt;/b&gt;를 누르면 먼저 &lt;b&gt;사업장명세&lt;/b&gt;를 입력하는 화면이 나옵니다.&lt;br /&gt;직장인 부업자로 크몽에서 개발 외주를 하는 경우, 별도의 사업자등록을 하지 않은 상태일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 사업자등록번호를 입력할 수 없으므로, &lt;b&gt;사업자등록번호 존재여부&lt;/b&gt;에서 &lt;b&gt;없음&lt;/b&gt;을 선택합니다.&lt;br /&gt;&lt;b&gt;없음&lt;/b&gt;을 선택하면 사업자등록번호 입력란은 사라지고, 주로 &lt;b&gt;업종코드&lt;/b&gt;를 입력하는 형태로 바뀝니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업종코드는 본인이 크몽에서 제공한 대표 업무와 가장 유사한 코드를 선택하면 됩니다.&lt;br /&gt;아마 다음 코드를 사용하시게 될거에요.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;업종코드&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;940926&lt;/td&gt;
&lt;td&gt;SW 프리랜서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;940600&lt;/td&gt;
&lt;td&gt;프로그래머&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;940909&lt;/td&gt;
&lt;td&gt;기타 자영업&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 외주, 자동화, 웹 개발, 프로그램 제작 등을 했다면 보통 &lt;code&gt;940926&lt;/code&gt; 또는 &lt;code&gt;940600&lt;/code&gt; 중 본인 업무에 가까운 쪽을 검토하면 됩니다.&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;그 외에 명확히 맞는 코드가 애매하다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;940909&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기타 자영업으로 입력하는 경우도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-08 143039.png&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dzYfTO/dJMcaaFfSCh/uXOdsQ4EmwNAPmgqNLwvjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dzYfTO/dJMcaaFfSCh/uXOdsQ4EmwNAPmgqNLwvjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dzYfTO/dJMcaaFfSCh/uXOdsQ4EmwNAPmgqNLwvjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdzYfTO%2FdJMcaaFfSCh%2FuXOdsQ4EmwNAPmgqNLwvjk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;896&quot; height=&quot;622&quot; data-filename=&quot;스크린샷 2026-05-08 143039.png&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업종코드를 입력한 뒤 &lt;b&gt;검색&lt;/b&gt; 버튼을 누르면 업종명 등 나머지 칸이 자동으로 업데이트됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용이 정상적으로 채워진 것을 확인했다면 &lt;b&gt;저장하기&lt;/b&gt;를 눌러 사업장명세 입력을 완료합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 사업장을 추가한 뒤 업종별 총 수입금액을 입력합니다&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-08 143109.png&quot; data-origin-width=&quot;848&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BvB4u/dJMcaaL0w0h/ClDq2zYSxFYlBgCVvDWkh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BvB4u/dJMcaaL0w0h/ClDq2zYSxFYlBgCVvDWkh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BvB4u/dJMcaaL0w0h/ClDq2zYSxFYlBgCVvDWkh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBvB4u%2FdJMcaaL0w0h%2FClDq2zYSxFYlBgCVvDWkh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;848&quot; height=&quot;750&quot; data-filename=&quot;스크린샷 2026-05-08 143109.png&quot; data-origin-width=&quot;848&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사업장명세를 저장하면 방금 입력한 사업장이 목록에 추가됩니다.&lt;br /&gt;크몽에서 한 부업이 하나의 업종으로 정리된다면, 방금 추가한 사업장 하나만 두고 다음 단계로 넘어가면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 크몽에서 수행한 일이 여러 업종으로 나뉜다고 판단된다면, 같은 방식으로 업종을 추가할 수 있습니다.&lt;br /&gt;예를 들어 개발 외주, 강의&amp;middot;컨설팅, 디자인 등 성격이 다른 수익이 섞여 있다면 각각에 맞는 업종코드를 추가하는 식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 대부분의 직장인 개발자 부업은 하나의 대표 업종으로 정리해도 크게 무리가 없는 경우가 많습니다.&lt;br /&gt;핵심은 본인의 실제 업무 성격과 가장 가까운 업종코드를 선택하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사업장 추가가 끝났다면 이제 &lt;b&gt;업종별 총 수입금액&lt;/b&gt;을 입력합니다.&lt;br /&gt;여기서 말하는 총 수입금액은 크몽에서 발생한 매출을 기준으로 정리해야 합니다.&lt;br /&gt;크몽 정산 내역을 확인해서 해당 과세기간 동안 발생한 수입을 업종별로 합산해 입력하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이 단계에서는 &amp;ldquo;내가 어떤 업종으로 얼마를 벌었는지&amp;rdquo;를 홈택스에 직접 적어 넣는다고 보면 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6-1. 핵심: 크몽 세금계산서의 월합계를 모두 더합니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계가 크몽 부업 신고에서 가장 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크몽 수익은 홈택스에서 자동으로 사업소득으로 불러와지지 않을 수 있기 때문에, 직접 수입금액을 계산해서 입력해야 합니다.&lt;br /&gt;이때 기준으로 삼을 수 있는 자료가 &lt;b&gt;크몽에서 발행된 월별 세금계산서&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세금계산서 화면을 보면 공급가액, 세액, 수수료 등 여러 금액이 보여서 헷갈릴 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서 봐야 할 핵심은 &lt;b&gt;월합계&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 월별 세금계산서에는 해당 월의 합계 금액이 표시되어 있습니다.&lt;br /&gt;이 &lt;b&gt;월합계 금액을 모두 더하면 해당 연도의 크몽 총 수입금액&lt;/b&gt;이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음 순서입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;크몽에서 월별 세금계산서를 확인합니다.&lt;/li&gt;
&lt;li&gt;각 세금계산서에서 &lt;b&gt;월합계&lt;/b&gt; 금액을 찾습니다.&lt;/li&gt;
&lt;li&gt;신고 대상 연도에 해당하는 월합계를 모두 더합니다.&lt;/li&gt;
&lt;li&gt;합산한 금액을 홈택스의 &lt;b&gt;총 수입금액&lt;/b&gt;란에 입력합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 1월부터 12월까지 크몽에서 수익이 있었다면, 각 월의 월합계를 모두 더한 금액이 올해 신고할 크몽 부업 수입금액입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점이 있습니다.&lt;br /&gt;&lt;b&gt;실제로 통장에 들어온 금액만 보면 안 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크몽은 &lt;b&gt;플랫폼 수수료를 차감한 뒤 정산&lt;/b&gt;해주기 때문에, 통장 입금액만 기준으로 보면 매출을 과소 입력할 수 있습니다.&lt;br /&gt;따라서 홈택스에는 통장 입금액이 아니라, 크몽 세금계산서 기준으로 정리한 &lt;b&gt;전체 수입금액&lt;/b&gt;을 입력해야합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6-2. 월합계 계산이 귀찮다면 GPT를 활용해도 됩니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;월별 세금계산서를 하나씩 열어서 월합계를 더하는 작업이 은근히 귀찮습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때는 각 월별 세금계산서 이미지나 금액 내용을 GPT에 넣고 이렇게 요청하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;소득신고에 사용할 총 수입금액을 계산해야하니, 
각 월별 세금계산서의 월합계만 기준으로 모두 더해서 총 수입금액을 계산해줘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 GPT에게 &lt;b&gt;월합계만 기준으로 계산하라&lt;/b&gt;고 명확히 말하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세금계산서에는 공급가액, 세액, 수수료 등 여러 금액이 함께 보이기 때문에, 그냥 &amp;ldquo;합계 계산해줘&amp;rdquo;라고 하면 엉뚱한 금액을 기준으로 계산할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청할 때는 기준을 분명히 잡아주는 것이 좋습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;월합계만 볼 것&lt;/li&gt;
&lt;li&gt;신고 대상 연도에 해당하는 월만 더할 것&lt;/li&gt;
&lt;li&gt;최종 결과는 총 수입금액으로 알려줄 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 최종 신고 금액이기 때문에 GPT가 계산한 결과를 그대로 믿기보다는, 마지막에 한 번은 직접 월별 금액과 합계가 맞는지 확인하는 것이 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6-3. 단순경비율은 일반율로 선택합니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업종별 총 수입금액을 입력하면 경비율을 선택하는 항목이 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 기본적으로 &lt;b&gt;단순경비율 - 일반율&lt;/b&gt;을 선택합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 말하는 &lt;b&gt;경비&lt;/b&gt;는 부업 수익을 만들기 위해 들어간 비용을 의미합니다.&lt;br /&gt;크몽 부업 개발자라면 예를 들어 이런 것들이 경비에 해당할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;크몽 플랫폼 수수료&lt;/li&gt;
&lt;li&gt;크몽 이용료&lt;/li&gt;
&lt;li&gt;작업에 사용한 소프트웨어 구독료&lt;/li&gt;
&lt;li&gt;서버비&lt;/li&gt;
&lt;li&gt;외주 작업에 직접 들어간 기타 비용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 화면에서는 내가 실제 경비를 하나씩 증빙해서 입력하는 방식이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;국세청이 정한 &lt;b&gt;단순경비율&lt;/b&gt;을 적용합니다.&lt;br /&gt;쉽게 말하면 &amp;ldquo;이 업종은 보통 이 정도 비율의 비용이 들어간다고 보고, 일정 비율을 경비로 인정해주는 방식&amp;rdquo;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 별도로 복잡하게 비용을 하나하나 입력하지 않고, 총 수입금액에 단순경비율을 적용해 소득금액을 계산합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직장인 부업자로 크몽 수익을 신고하는 경우에는 자동으로 선택된&amp;nbsp;&lt;b&gt;일반율&lt;/b&gt; 그대로 진행하시면 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 입력완료 후 공제 항목을 확인합니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사업소득 입력까지 끝났다면 &lt;b&gt;입력완료&lt;/b&gt;를 누릅니다.&lt;br /&gt;이후에는 아래 항목들을 차례대로 확인합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소득공제금액&lt;/li&gt;
&lt;li&gt;세액공제&amp;middot;감면금액&lt;/li&gt;
&lt;li&gt;가산세&lt;/li&gt;
&lt;li&gt;기납부세액&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직장인이라면 대부분의 내용은 이미 입력되어 있을 가능성이 높습니다.&lt;br /&gt;회사에서 연말정산을 했고, 홈택스에 관련 자료가 남아 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 그냥 넘기지는 않는 것이 좋습니다.&lt;br /&gt;화면을 보다 보면 항목별로 &lt;b&gt;불러오기&lt;/b&gt; 버튼이 활성화되어 있는 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 혹시 빠진 자료가 있을까 봐, 활성화된 불러오기 버튼은 하나씩 눌러서 다시 확인했습니다.&lt;br /&gt;특히 &lt;b&gt;연말정산 간소화 자료에서 불러오기&lt;/b&gt;가 가능한 항목은 한 번씩 더 불러와서 채워 넣었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 생각보다 빠진 항목이 발견될 수 있습니다.&lt;br /&gt;제 경우에는 기부금 내역이 제대로 반영되지 않은 부분이 있어서, 이 단계에서 추가로 챙길 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크몽 수익 입력만큼이나 공제 항목 확인도 중요합니다.&lt;br /&gt;소득을 정확히 신고하는 것도 중요하지만, 받을 수 있는 공제와 감면을 빠뜨리지 않는 것도 세금을 줄이는 데 차이를 만듭니다.&lt;br /&gt;(혹시 모르니까..?)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 저장 후 작성완료, 그리고 신고까지 진행합니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공제 항목과 기납부세액까지 확인했다면 이제 마지막 단계입니다.&lt;br /&gt;먼저 &lt;b&gt;저장하기&lt;/b&gt;를 눌러 지금까지 입력한 내용을 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장하지 않고 이동하면 입력한 내용이 날아갈 수 있으니, 중간중간 저장하는 습관을 들이는 것이 좋습니다.&lt;br /&gt;저장 후에는 &lt;b&gt;작성완료&lt;/b&gt;를 누릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성완료 단계에서는 지금까지 입력한 신고 내용이 요약되어 표시됩니다.&lt;br /&gt;여기서 아래 내용을 한 번 더 확인합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;근로소득이 정상 반영되어 있는지&lt;/li&gt;
&lt;li&gt;수입금액이 제대로 입력되어 있는지&lt;/li&gt;
&lt;li&gt;소득공제와 세액공제가 빠지지 않았는지&lt;/li&gt;
&lt;li&gt;최종 납부세액 또는 환급세액이 예상과 크게 다르지 않은지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용에 문제가 없다면 최종적으로 &lt;b&gt;신고하기&lt;/b&gt;를 눌러 종합소득세 신고를 제출합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 신고 후 납부서와 접수 내역을 저장합니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신고하기까지 완료했다면 바로 끝내지 말고, &lt;b&gt;납부서&lt;/b&gt;와 &lt;b&gt;신고 접수 내역&lt;/b&gt;을 저장해둡니다.&lt;br /&gt;납부할 세금이 있다면 홈택스에서 납부서를 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 납부서는 PDF로 저장하거나 인쇄물로 보관해두는 것이 좋습니다.&lt;br /&gt;물론 나중에도 홈택스에서 다시 조회할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 막상 필요할 때 메뉴를 다시 찾는 것도 번거롭고, 신고 직후에는 내가 제출한 내용과 납부 금액을 바로 확인할 수 있으니 그때 저장해두는 편이 안전합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 지방소득세 신고까지 이어서 진행합니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종합소득세 신고를 마치면 이어서 &lt;b&gt;지방소득세 신고&lt;/b&gt; 단계로 넘어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터는 비교적 간단합니다.&lt;br /&gt;앞에서 홈택스에 입력한 종합소득세 신고 정보가 지방소득세 신고 화면으로 연동되기 때문에, 대부분의 내용은 이미 채워져 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 화면에 표시된 내용을 확인하면서 안내에 따라 진행하면 됩니다.&lt;br /&gt;확인할 부분은 주로 다음 정도입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인적사항&lt;/li&gt;
&lt;li&gt;종합소득세 신고 내역 연동 여부&lt;/li&gt;
&lt;li&gt;지방소득세 납부세액&lt;/li&gt;
&lt;li&gt;납부 또는 환급 계좌 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용에 이상이 없다면 지방소득세도 그대로 신고합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종합소득세와 마찬가지로, 지방소득세 신고가 끝난 뒤에도 &lt;b&gt;납부서&lt;/b&gt;를 PDF로 저장하거나 인쇄해두는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지방소득세 역시 나중에 조회할 수는 있지만, 신고 직후에 종합소득세 자료와 함께 정리해두면 훨씬 편합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직장인 개발자가 부업을 하면, 신고가 조금 헷갈릴 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 크몽은 3.3% 원천징수로 자동 반영되는 구조가 아니라서 홈택스에서 사업소득이 비어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다시 정리하면.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;홈택스 모두채움 정기신고로 시작합니다.&lt;/li&gt;
&lt;li&gt;근로소득은 자동 입력된 내용을 확인합니다.&lt;/li&gt;
&lt;li&gt;크몽 수익은 사업소득으로 직접 추가합니다.&lt;/li&gt;
&lt;li&gt;크몽 월별 세금계산서의 월합계를 모두 더해 총 수입금액을 입력합니다.&lt;/li&gt;
&lt;li&gt;단순경비율 일반율을 적용합니다.&lt;/li&gt;
&lt;li&gt;공제 항목을 다시 확인합니다.&lt;/li&gt;
&lt;li&gt;종합소득세 신고 후 지방소득세 신고까지 마무리합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 어렵게 느껴지지만, 흐름을 알고 보면 결국 &amp;ldquo;수입금액을 빠뜨리지 않고 직접 입력하는 것&amp;rdquo;이 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부업을 하는 직장인 개발자라면, 5월 종합소득세 신고 때 이 부분만큼은 꼭 챙겨두는 것이 좋습니다.&lt;/p&gt;</description>
      <category>Day by day</category>
      <category>개발자부업</category>
      <category>단순경비율</category>
      <category>부업세금</category>
      <category>사업소득</category>
      <category>세금신고</category>
      <category>종합소득세</category>
      <category>직장인부업</category>
      <category>크몽</category>
      <category>프리랜서세금</category>
      <category>홈택스</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/79</guid>
      <comments>https://smokingbird.tistory.com/79#entry79comment</comments>
      <pubDate>Fri, 8 May 2026 17:30:40 +0900</pubDate>
    </item>
    <item>
      <title>갑자기 끝나버린 오라클 미환불 사태의 결말</title>
      <link>https://smokingbird.tistory.com/78</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;f6392580-7d38-401b-b423-bf3782bb02b3.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d47GMY/dJMcafmdePE/b3hSZRwa00qC4sWiXEBma0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d47GMY/dJMcafmdePE/b3hSZRwa00qC4sWiXEBma0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d47GMY/dJMcafmdePE/b3hSZRwa00qC4sWiXEBma0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd47GMY%2FdJMcafmdePE%2Fb3hSZRwa00qC4sWiXEBma0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;f6392580-7d38-401b-b423-bf3782bb02b3.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난번 글에서 이어지는 글입니다&lt;br /&gt;&amp;gt;&amp;gt; &lt;a href=&quot;https://blog.sanguneo.com/75&quot;&gt;https://blog.sanguneo.com/75&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1778052861065&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;오라클 클라우드 결제 환불 진행중 (feat. 오라클 고객지원, 문의하기)&quot; data-og-description=&quot;이 이야기는 떼인돈을 받으려 고군분투한 어떤 IT너드의 이야기 입니다. 오라클 클라우드 빈자리가 너무 안나서.. PAY TO GO로 업그레이드 하려고 했던 때 였습니다 결제수단을 추가하고 업그레이&quot; data-og-host=&quot;blog.sanguneo.com&quot; data-og-source-url=&quot;https://blog.sanguneo.com/75&quot; data-og-url=&quot;https://blog.sanguneo.com/75&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dXPxt1/dJMb8YpZmjn/oHrONNcLpzRlEvpCcw1K10/img.png?width=800&amp;amp;height=533&amp;amp;face=0_0_800_533,https://scrap.kakaocdn.net/dn/bZ9dYK/dJMb8WMtzJW/kklRcC0QJq9DzvVHwHFjH0/img.png?width=800&amp;amp;height=533&amp;amp;face=0_0_800_533,https://scrap.kakaocdn.net/dn/bt2zgI/dJMb8U8XznE/LR8ebtgzOTuPgaPGDRnonK/img.jpg?width=1080&amp;amp;height=2124&amp;amp;face=0_0_1080_2124&quot;&gt;&lt;a href=&quot;https://blog.sanguneo.com/75&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.sanguneo.com/75&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dXPxt1/dJMb8YpZmjn/oHrONNcLpzRlEvpCcw1K10/img.png?width=800&amp;amp;height=533&amp;amp;face=0_0_800_533,https://scrap.kakaocdn.net/dn/bZ9dYK/dJMb8WMtzJW/kklRcC0QJq9DzvVHwHFjH0/img.png?width=800&amp;amp;height=533&amp;amp;face=0_0_800_533,https://scrap.kakaocdn.net/dn/bt2zgI/dJMb8U8XznE/LR8ebtgzOTuPgaPGDRnonK/img.jpg?width=1080&amp;amp;height=2124&amp;amp;face=0_0_1080_2124');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;오라클 클라우드 결제 환불 진행중 (feat. 오라클 고객지원, 문의하기)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 이야기는 떼인돈을 받으려 고군분투한 어떤 IT너드의 이야기 입니다. 오라클 클라우드 빈자리가 너무 안나서.. PAY TO GO로 업그레이드 하려고 했던 때 였습니다 결제수단을 추가하고 업그레이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.sanguneo.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;지난글에 이어서...&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이후로도 라이브챗을 몇 번 더 해봤는데, 사실 별 소득은 없었습니다.&amp;nbsp;&lt;br /&gt;대신 &lt;code&gt;collections_sg@oracle.com&lt;/code&gt; 쪽에서는 텀이 길긴 했지만 꾸준히 답장이 오고 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b02PUw/dJMcadPoeeu/kdDhtxsV476MzPa9Xb5mwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b02PUw/dJMcadPoeeu/kdDhtxsV476MzPa9Xb5mwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b02PUw/dJMcadPoeeu/kdDhtxsV476MzPa9Xb5mwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb02PUw%2FdJMcadPoeeu%2FkdDhtxsV476MzPa9Xb5mwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;520&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;우리는 당연히 바로 환불처리했고, 은행에 문의해라 우린 더 할거없다.&quot; 라는 형식적인 답변이었고&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;677&quot; data-origin-height=&quot;471&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxx3X2/dJMcafT24dd/3kaD7X9ZOjmW9JtWU0knEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxx3X2/dJMcafT24dd/3kaD7X9ZOjmW9JtWU0knEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxx3X2/dJMcafT24dd/3kaD7X9ZOjmW9JtWU0knEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbxx3X2%2FdJMcafT24dd%2F3kaD7X9ZOjmW9JtWU0knEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;677&quot; height=&quot;471&quot; data-origin-width=&quot;677&quot; data-origin-height=&quot;471&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;아니, 알지 아는데, 한번만 더 봐줘&quot; 하고 답변을 보냈습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;651&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCGg9c/dJMcadhCcbV/MBCm31cLPKlCIvq6HDC9M1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCGg9c/dJMcadhCcbV/MBCm31cLPKlCIvq6HDC9M1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCGg9c/dJMcadhCcbV/MBCm31cLPKlCIvq6HDC9M1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCGg9c%2FdJMcadhCcbV%2FMBCm31cLPKlCIvq6HDC9M1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;651&quot; height=&quot;368&quot; data-origin-width=&quot;651&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그랬더니, 이번에는 &quot; &quot;알겠다. 4~5일이 지났는데도 안 돌아왔다는 거지? &lt;br /&gt;그럼 거래내역을 보내주면 payment team 쪽으로 확인해볼게.&quot; 하고 &lt;br /&gt;드디어.. 사건발생 8일 만에 맘에드는 답변이 왔습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;721&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAQN7l/dJMcabqAxrp/kk0zfaNKfTgkgXvw6wkks0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAQN7l/dJMcabqAxrp/kk0zfaNKfTgkgXvw6wkks0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAQN7l/dJMcabqAxrp/kk0zfaNKfTgkgXvw6wkks0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAQN7l%2FdJMcabqAxrp%2Fkk0zfaNKfTgkgXvw6wkks0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;682&quot; height=&quot;721&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;721&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 보냈습니다. 4월 30일 밤이 었습니다&amp;nbsp;&lt;br /&gt;토스에서 거래내역 캡쳐도 하고, 은행 거래 증빙 PDF도 발급받아서 같이 첨부했습니다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;여기서부터는 또다시 기다림이었습니다. 그런데...&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히 11일 이 되는 5월 4일 오후 4시 41분, 그러니까 사건이 발생하고 정확히 11일이 지난 시점에 갑자기 취소 환불이 들어왔습니다. &lt;span&gt;진짜 말 그대로 아무 예고 없이, &lt;/span&gt;&lt;span&gt;아무 설명 없이, &lt;/span&gt;&lt;span&gt;그냥 갑자기 풀렸습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;163&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U65sE/dJMcaf7x12h/hgjRkdhky9WkyqtggwXOg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U65sE/dJMcaf7x12h/hgjRkdhky9WkyqtggwXOg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U65sE/dJMcaf7x12h/hgjRkdhky9WkyqtggwXOg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FU65sE%2FdJMcaf7x12h%2FhgjRkdhky9WkyqtggwXOg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;509&quot; height=&quot;77&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;163&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그리고 다음날..&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bB8n7y/dJMcadPoe6w/Rxrn6IqzvGX7b1y1HzgPJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bB8n7y/dJMcadPoe6w/Rxrn6IqzvGX7b1y1HzgPJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bB8n7y/dJMcadPoe6w/Rxrn6IqzvGX7b1y1HzgPJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbB8n7y%2FdJMcadPoe6w%2FRxrn6IqzvGX7b1y1HzgPJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;661&quot; height=&quot;352&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;352&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;payment team에 전달해서 검토 요청하겠다&quot; &lt;br /&gt;=&amp;gt; (이제서야..?)&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;후기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 일을 겪으면서 , &lt;/span&gt;&lt;span&gt;생각보다 Oracle 쪽에서도 이 상황을 정확히 모르고 있었을 수도 있었겠다는 생각이 듭니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;제가 계속 문의를 하긴 했지만, &lt;/span&gt;&lt;span&gt;상담 흐름을 보면 실제 결제 세션이나 상태를 바로 조회해서 확인해주는 느낌은 아니었습니다. 국내 지원에서는 상상하기는 어렵지만 정말 형식적인 답변에 지치더라구요. 문의창구 찾는것도 너무 어려웠고요&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제 세션 상태를 즉시 확인하거나, 실제 오라클측에서 release 여부를 확인할 수 있는 창구가 있었다면 훨씬 덜 불안했을 텐데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결국 이번 일은 시간이 지나 해결되긴 했지만, &lt;/span&gt;&lt;span&gt;문제가 해결되는 과정 자체는 꽤 즐겁지 않았네요&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Day by day</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/78</guid>
      <comments>https://smokingbird.tistory.com/78#entry78comment</comments>
      <pubDate>Wed, 6 May 2026 17:07:16 +0900</pubDate>
    </item>
    <item>
      <title>Oracle Cloud A1 Flex 4 cpu / 24GB mem 성공기</title>
      <link>https://smokingbird.tistory.com/77</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오라클 클라우드에는 평생 무료로 쓸 수 있는 VM이 있다고 해서 Free Tier로 &lt;code&gt;VM.Standard.E2.1.Micro&lt;/code&gt; 인스턴스를 하나 만들었다.&lt;br /&gt;스펙은 AMD 1 OCPU / 1GB RAM.&lt;br /&gt;생각보다 잘 돌아가서 열심히 쓰고 있었는데, 어느 날 커뮤니티에서 이런 이야기를 봤다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ARM 인스턴스는 4 OCPU / 24GB RAM까지 무료로 쓸 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람들이 다들 그걸 쓰고 있다고 자랑하고 있었다.&lt;br /&gt;&amp;ldquo;아니, 그런 게 있으면 나도 안 할 수 없지?&amp;rdquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 기존에 쓰던 E2 인스턴스는 그대로 두고, A1 Flex 인스턴스를 새로 하나 만들어서 운영해보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 막상 만들려고 보니 목표였던 4/24 스펙은 바로 잡히지 않았다.&lt;br /&gt;OCI에서 흔히 보는 &lt;code&gt;Out of capacity&lt;/code&gt;가 떴다.&lt;br /&gt;말 그대로 그 시점에는 원하는 자원을 바로 할당받기 어렵다는 뜻이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 사용 중인 춘천 리전에는 자리가 없다는 말이기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 이야기는 처음부터 &amp;ldquo;원하는 스펙을 한 번에 만들고 세팅 끝!&amp;rdquo; 같은 찬란한 이야기는 아니다.&lt;br /&gt;이 이야기는 &lt;code&gt;어떻게 원하는 스펙까지 결국 올렸는가&lt;/code&gt;에 대한 이야기다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2026년 4월 27일 오후 03_57_11.png&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceNC7B/dJMcacXb1vl/Kdo7kqziMSTV4i8SmaMAxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceNC7B/dJMcacXb1vl/Kdo7kqziMSTV4i8SmaMAxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceNC7B/dJMcacXb1vl/Kdo7kqziMSTV4i8SmaMAxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceNC7B%2FdJMcacXb1vl%2FKdo7kqziMSTV4i8SmaMAxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1672&quot; height=&quot;941&quot; data-filename=&quot;ChatGPT Image 2026년 4월 27일 오후 03_57_11.png&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;position: absolute;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. A1 Flex를 새로 만들기로 함&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 블로그 글, Reddit, ChatGPT, Gemini 등 여러 채널을 돌아다니며 A1 Flex 인스턴스를 만드는 방법을 다시 찾아봤다.&lt;br /&gt;방법 자체는 예전에 E2 인스턴스를 만들었던 것과 크게 다르지 않았다.&lt;br /&gt;문제는 4 OCPU / 24GB RAM 스펙이 죽어도 생성되지 않는다는 점이었다.&lt;br /&gt;오라클 데이터센터에 물리적인 가용량이 없어서 그렇고, 결국 될 때까지 시도해야 한다는 이야기가 많았다.&lt;br /&gt;또 Pay-As-You-Go 계정으로 업그레이드하면 바로 만들 수 있다는 이야기도 있어서 계정도 업그레이드했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 오라클 결제 오류 때문에 마음고생한 이야기는 아직 진행 중이다. ㅠㅠ&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 여기서부터였다.&lt;br /&gt;계정을 업그레이드했는데도 여전히 4/24 스펙은 가용량 부족으로 생성되지 않았다.&lt;br /&gt;OCI 콘솔에서는 &lt;b&gt;Out of capacity&lt;/b&gt; 메시지가 떴다.&lt;br /&gt;실제로는 이것보다 훨씬 긴 메시지였지만, 요지는 간단했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 이 리전/가용성 도메인에는 해당 자원이 부족하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계정도 업그레이드했고, 결제 문제로 마음고생까지 했는데, 정작 원하는 스펙을 못 올리는 상황.&lt;br /&gt;이쯤 되니 이상하게 승부욕이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 전략을 바꿔야 했다.&lt;br /&gt;군자의 도리는 그런 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 무리해서 4/24를 만들려고 하지 않고, 일단 &lt;b&gt;생성 가능한 최소 수준으로 먼저 띄우는 방향&lt;/b&gt;으로 가기로 했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;position: absolute;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. A1 Flex를 최소 스펙으로 먼저 만들었다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 A1 Flex를 최소 스펙으로 따로 만들었다.&lt;br /&gt;&lt;b&gt;1 OCPU / 6GB RAM&lt;/b&gt;&lt;br /&gt;최소 스펙이라 그런지 이건 바로 만들어졌다. (운이 좋았던걸수도 있고)&lt;br /&gt;그리고 확보된 인스턴스를 단계적으로 업그레이드하는 전략을 세웠다.&lt;br /&gt;다행히 OCI는 인스턴스의 CPU와 메모리를 나중에 조정할 수 있는 기능을 제공한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 2/12, 3/18, 4/24로 단계적으로 올리기로 했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음부터는 단계적으로 스펙을 올리기로 했다.&lt;br /&gt;한 번에 4/24로 훅 올리는 게 아니라,&lt;br /&gt;&lt;b&gt;2/12 &amp;rarr; 3/18 &amp;rarr; 4/24&lt;/b&gt;&lt;br /&gt;이렇게 나눠서 올리는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 한 이유는 단순했다.&lt;br /&gt;&lt;code&gt;VM.Standard.A1.Flex&lt;/code&gt;는 고정된 서버 상품을 하나 통째로 받는 구조가 아니라, 오라클의 ARM 호스트 풀에서 필요한 만큼 자원을 할당받는 구조라고 봤기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 4 OCPU / 24GB RAM이 한 번에 비는 경우는 드물 수 있다.&lt;br /&gt;하지만 이미 1/6 인스턴스를 만들어둔 상태에서 한 단계씩 올리면, 매번 추가로 필요한 자원은 &lt;b&gt;1 OCPU / 6GB RAM&lt;/b&gt;뿐이다.&lt;br /&gt;한 번에 큰 자리를 기다리는 것보다, 작은 빈자리를 여러 번 잡는 쪽이 가능성이 더 높을 거라고 생각했다.&lt;br /&gt;그래서 일단 인스턴스를 만든 뒤, 인스턴스 편집 화면에서 2/12로 변경하고 저장을 눌렀다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 웹 콘솔에서 CLI 자동화로 전환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 웹 콘솔에서 직접 시도했는데.대여섯 번쯤 누르니 이런 생각이 들었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 언제까지 누르고 있어야 하지?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10번이 넘어가자 괜히 시작했다는 생각도 들었다.&lt;br /&gt;분명 이걸 자동화하는 방법이 있을 것 같았다.&lt;br /&gt;실제로 성공한 사람들의 이야기를 보면, 설정 스택을 저장해두고 Terraform을 이용해서 몇 분마다 4/24 생성 명령을 반복했다고 했다.&lt;br /&gt;그때 본 것이 &lt;code&gt;oci cli&lt;/code&gt;였다.&lt;br /&gt;이걸 이용하면 Python이나 다른 스크립트로 자동화할 수 있겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform은 잘 모르니까.&lt;br /&gt;그래서 이번에는 OpenAI Codex와 연결된 내 에이전트에게 이 작업을 자동화하고, 로그까지 남기는 스크립트를 만들어달라고 했다.&lt;br /&gt;시행착오는 있었지만, 최종적으로 정착한 조건은 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;    재시도 주기: 3분
    성공 시 다음 단계 시도 간격: 30초
    텔레그램 보고: 08시 ~ 23시까지 매시 정각에 실행 상황 브리핑&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간 로그를 보면 재시도 흔적이 꽤 많이 남아 있다.&lt;br /&gt;특히 2/12로 올라갈 때 정말 많이 시도했는데, 로그가 제대로 남아 있지 않은 게 조금 아쉽다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-27 090247.png&quot; data-origin-width=&quot;1263&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xJ8Xh/dJMcagyv1x3/kRNbVxBpmxtDaYBYhBAGe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xJ8Xh/dJMcagyv1x3/kRNbVxBpmxtDaYBYhBAGe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xJ8Xh/dJMcagyv1x3/kRNbVxBpmxtDaYBYhBAGe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxJ8Xh%2FdJMcagyv1x3%2FkRNbVxBpmxtDaYBYhBAGe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1263&quot; height=&quot;192&quot; data-filename=&quot;스크린샷 2026-04-27 090247.png&quot; data-origin-width=&quot;1263&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-27 090422.png&quot; data-origin-width=&quot;1299&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8jfiK/dJMcaaE7pgA/zylpDqGR9klQ8LktaK1Ekk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8jfiK/dJMcaaE7pgA/zylpDqGR9klQ8LktaK1Ekk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8jfiK/dJMcaaE7pgA/zylpDqGR9klQ8LktaK1Ekk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8jfiK%2FdJMcaaE7pgA%2FzylpDqGR9klQ8LktaK1Ekk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1299&quot; height=&quot;528&quot; data-filename=&quot;스크린샷 2026-04-27 090422.png&quot; data-origin-width=&quot;1299&quot; data-origin-height=&quot;528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-27 090506.png&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6awId/dJMcaaE7pgz/ghCWJ2QjCW3TQ5zaMiukGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6awId/dJMcaaE7pgz/ghCWJ2QjCW3TQ5zaMiukGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6awId/dJMcaaE7pgz/ghCWJ2QjCW3TQ5zaMiukGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6awId%2FdJMcaaE7pgz%2FghCWJ2QjCW3TQ5zaMiukGK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1288&quot; height=&quot;400&quot; data-filename=&quot;스크린샷 2026-04-27 090506.png&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 로그로 보면 시도는 꽤 많이 했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 남은 로그를 정리해보면, 변경 시도만 해도 꽤 여러 번 반복됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2/12로 올라갈 때 로그 정리가 잘 안 되어 있어서 정확히 보이지는 않지만, 체감상 거의 400번 넘게 시도했던 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-27 090812.png&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;209&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mRxug/dJMcaf7rMv5/URc1NKntaw6DIQtPdqiadK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mRxug/dJMcaf7rMv5/URc1NKntaw6DIQtPdqiadK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mRxug/dJMcaf7rMv5/URc1NKntaw6DIQtPdqiadK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmRxug%2FdJMcaf7rMv5%2FURc1NKntaw6DIQtPdqiadK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;447&quot; height=&quot;209&quot; data-filename=&quot;스크린샷 2026-04-27 090812.png&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;209&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 결국 총 800번 정도의 시도 끝에 4 OCPU / 24GB에 안착&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로는 &lt;b&gt;4 OCPU / 24GB RAM&lt;/b&gt;까지 올리는 데 성공했다.&lt;br /&gt;처음 1/6으로 시작했을 때는 금방 되겠지 싶었다.&lt;br /&gt;하지만 2/12로 올라갈 때는 &amp;ldquo;이거 아닌가?&amp;rdquo; 싶을 정도로 오래 걸렸다.&lt;br /&gt;그런데 3/18까지 올라간 뒤에는 생각보다 금방 4/24까지 올라갔다.&lt;br /&gt;그 순간은 꽤 짜릿했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면, 이번 작업은 OCI A1 Flex에서 원하는 스펙이 바로 잡히지 않을 때 사용한 단계적 확장기였다.&lt;br /&gt;처음부터 4 OCPU / 24GB RAM을 만들려고 하면 &lt;code&gt;Out of capacity&lt;/code&gt;에 막혔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 먼저 가능한 최소 스펙인 1 OCPU / 6GB RAM으로 인스턴스를 만들고, 이후에 2/12 &amp;rarr; 3/18 &amp;rarr; 4/24 순서로 조금씩 키웠다.&lt;br /&gt;한 번에 큰 자원을 기다리는 대신, 작은 단위의 빈자리를 여러 번 잡는 방식이었다.&lt;br /&gt;결과적으로 약 800번 정도의 시도 끝에 원하는 스펙까지 도달했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OCI A1 Flex는 무료 스펙이 매력적이지만, 리전 상황에 따라 가용량 문제가 꽤 심하다.&lt;br /&gt;그래도 바로 포기하지 않고, 가능한 스펙부터 확보한 뒤 단계적으로 올리는 방식은 충분히 시도해볼 만한 전략이었다.&lt;/p&gt;</description>
      <category>Challenge</category>
      <category>A1 Flex</category>
      <category>ARM 인스턴스</category>
      <category>oci</category>
      <category>OCI CLI</category>
      <category>Oracle Cloud</category>
      <category>Out of capacity</category>
      <category>가용량 부족</category>
      <category>오라클 클라우드</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/77</guid>
      <comments>https://smokingbird.tistory.com/77#entry77comment</comments>
      <pubDate>Mon, 27 Apr 2026 16:02:01 +0900</pubDate>
    </item>
    <item>
      <title>Hermes Agent core 수정 없이 Supertonic2 TTS 붙이기</title>
      <link>https://smokingbird.tistory.com/76</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOtqQU/dJMcadaEjYQ/vLQnkLFxqA32cqOZF80sA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOtqQU/dJMcadaEjYQ/vLQnkLFxqA32cqOZF80sA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOtqQU/dJMcadaEjYQ/vLQnkLFxqA32cqOZF80sA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOtqQU%2FdJMcadaEjYQ%2FvLQnkLFxqA32cqOZF80sA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes에서 TTS 엔진을 조금 바꿔보고 싶어서 작은 프록시 서버를 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소는 여기입니다.&lt;br /&gt;&lt;a href=&quot;https://github.com/sanguneo/supertonic-openai-tts-proxy&quot;&gt;https://github.com/sanguneo/supertonic-openai-tts-proxy&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777006760147&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - sanguneo/supertonic-openai-tts-proxy: OpenAI-compatible local TTS proxy for supertonic&quot; data-og-description=&quot;OpenAI-compatible local TTS proxy for supertonic. Contribute to sanguneo/supertonic-openai-tts-proxy development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/sanguneo/supertonic-openai-tts-proxy&quot; data-og-url=&quot;https://github.com/sanguneo/supertonic-openai-tts-proxy&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dAZ46u/dJMb9kmfirA/0roXYEsHRZk0kknvR9RsOk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/Ennlv/dJMb9jgzQPI/56OpcUyRbtBzZ3zrkD4dBK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/sanguneo/supertonic-openai-tts-proxy&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/sanguneo/supertonic-openai-tts-proxy&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dAZ46u/dJMb9kmfirA/0roXYEsHRZk0kknvR9RsOk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/Ennlv/dJMb9jgzQPI/56OpcUyRbtBzZ3zrkD4dBK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - sanguneo/supertonic-openai-tts-proxy: OpenAI-compatible local TTS proxy for supertonic&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;OpenAI-compatible local TTS proxy for supertonic. Contribute to sanguneo/supertonic-openai-tts-proxy development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 &lt;a href=&quot;https://github.com/supertone-inc/supertonic&quot;&gt;supertone-inc/supertonic&lt;/a&gt;을 실제 음성 합성 엔진으로 사용합니다.&lt;br /&gt;다만 Supertonic을 Hermes에 직접 붙인 건 아니고, 중간에 작은 프록시를 하나 둔 방식입니다.&lt;br /&gt;Hermes 입장에서는 OpenAI TTS API를 호출하는 것처럼 보이지만, &lt;br /&gt;실제로는 로컬에서 실행 중인 Supertonic 프록시로 요청이 갑니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 프록시로 만들었나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Hermes 내부 TTS 코드를 직접 수정하는 방법도 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 그렇게 하면 나중에 Hermes를 업데이트할 때 귀찮아질 수 있습니다.&lt;br /&gt;수정한 부분이 충돌날 수도 있고, 계속 신경 써야 할 부분이 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Hermes core는 그대로 두고, OpenAI 호환 API처럼 동작하는 프록시를 만드는 쪽으로 잡았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes는 이미 &lt;code&gt;tts.provider: openai&lt;/code&gt; 설정을 지원하고, &lt;code&gt;base_url&lt;/code&gt;도 바꿀 수 있습니다.&lt;br /&gt;그러면 Hermes 설정만 바꿔서 로컬 프록시를 바라보게 만들 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;Hermes / client
      │
      │ OpenAI-compatible request
      ▼
/v1/audio/speech
      │
      │ proxy adapter
      ▼
Supertonic
      │
      │ synthesized audio
      ▼
mp3 / ogg / wav&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대충 이런 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes는 그대로 두고,&lt;br /&gt;중간에서 Supertonic을 OpenAI TTS API처럼 보이게 감싸는 방식입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지원하는 기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 넣어둔 기능은 이 정도입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OpenAI 호환 &lt;code&gt;POST /v1/audio/speech&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /v1/models&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /health&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Supertonic 기반 로컬 TTS 합성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mp3&lt;/code&gt;, &lt;code&gt;ogg&lt;/code&gt;, &lt;code&gt;wav&lt;/code&gt; 출력&lt;/li&gt;
&lt;li&gt;긴 문장 분할 처리&lt;/li&gt;
&lt;li&gt;Supertonic quality steps 설정&lt;/li&gt;
&lt;li&gt;브라우저 테스트용 playground&lt;/li&gt;
&lt;li&gt;WSL user systemd 서비스 스크립트&lt;/li&gt;
&lt;li&gt;Hermes 설정/복원 스크립트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 그냥 연결만 해보려고 했는데, 실제로 계속 쓰려면 서비스 등록이나 설정 복원도 필요해서 같이 정리했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요청 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청은 OpenAI TTS와 비슷한 형태로 보냅니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;{
  &quot;model&quot;: &quot;supertonic-2&quot;,
  &quot;voice&quot;: &quot;F1&quot;,
  &quot;input&quot;: &quot;안녕하세요. Supertonic TTS 테스트입니다.&quot;,
  &quot;response_format&quot;: &quot;mp3&quot;,
  &quot;speed&quot;: 1.3,
  &quot;total_steps&quot;: 3
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;total_steps&lt;/code&gt;는 Supertonic의 품질 설정에 해당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 기본값은 &lt;code&gt;3&lt;/code&gt;으로 잡아두었습니다.&lt;br /&gt;빠른 응답을 우선한 설정입니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Low     = 3
Normal  = 6
High    = 10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;품질을 더 올리고 싶으면 &lt;code&gt;total_steps&lt;/code&gt; 값을 올리면 됩니다.&lt;br /&gt;대신 합성 시간은 조금 더 걸릴 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ogg 출력&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 포맷은 &lt;code&gt;mp3&lt;/code&gt;, &lt;code&gt;ogg&lt;/code&gt;, &lt;code&gt;wav&lt;/code&gt;를 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 오디오 파일로 쓰려면 &lt;code&gt;mp3&lt;/code&gt;가 편하고,&lt;br /&gt;원본 확인이나 디버깅에는 &lt;code&gt;wav&lt;/code&gt;가 편합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ogg&lt;/code&gt;는 Telegram voice message 때문에 넣었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Telegram 음성 메시지는 보통 &lt;code&gt;Ogg 컨테이너 + Opus 코덱&lt;/code&gt; 조합을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래처럼 요청하면:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;{
  &quot;response_format&quot;: &quot;opus&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;audio/ogg&lt;/code&gt; 형태로 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들어두면 단순 파일 첨부가 아니라, Telegram에서 음성 메시지처럼 다루기 좋습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hermes 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes 설정은 대략 이렇게 바꾸면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;tts:
  provider: openai
  openai:
    base_url: http://127.0.0.1:8789/v1
    model: supertonic-2
    voice: F1
    speed: 1.3
    max_text_length: 2000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;code&gt;.env&lt;/code&gt;에는 dummy key를 넣습니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;VOICE_TOOLS_OPENAI_KEY=dummy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 OpenAI API key가 필요한 것은 아닙니다.&lt;br /&gt;로컬 프록시를 쓰는 것이기 때문에 값 자체는 dummy여도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Hermes의 OpenAI TTS provider 경로를 타려면 환경 변수가 필요해서 넣어둔 값입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gateway를 쓰고 있다면 설정 변경 후 재시작합니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;hermes gateway restart&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설정 백업과 복원&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes 설정을 바꾸는 작업이라 백업 스크립트도 같이 넣었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 적용은 아래 스크립트로 합니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;./scripts/setup-hermes-config.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 스크립트는 적용 전에 기존 설정을 백업합니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;~/.hermes/config.yaml.bak.&amp;lt;timestamp&amp;gt;
~/.hermes/.env.bak.&amp;lt;timestamp&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복원은 아래처럼 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;./scripts/restore-hermes-config.sh --list
./scripts/restore-hermes-config.sh --latest
./scripts/restore-hermes-config.sh 20260424_105304&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복원할 때도 현재 설정을 바로 덮어쓰지 않고, 먼저 한 번 더 백업합니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;~/.hermes/config.yaml.pre-restore.&amp;lt;timestamp&amp;gt;
~/.hermes/.env.pre-restore.&amp;lt;timestamp&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 파일은 한 번 꼬이면 귀찮아서, 되돌릴 수 있게 만들어두는 편이 마음이 편합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트용 playground&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 서버 안에 간단한 playground도 넣었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 실행한 뒤 아래 주소로 들어가면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://127.0.0.1:8789/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 텍스트, voice, 출력 포맷, 속도, quality steps를 바꿔가며 테스트할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도 프론트엔드 프로젝트를 만든 건 아니고, FastAPI에서 정적 HTML 하나를 서빙하는 정도입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 확인하기에는 이 정도면 충분했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치는 &lt;code&gt;uv&lt;/code&gt; 기준입니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;uv sync&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 서버 실행은 이렇게 합니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;./scripts/run-dev.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 주소는 아래입니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://127.0.0.1:8789&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 확인은 &lt;code&gt;/health&lt;/code&gt;로 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;curl http://127.0.0.1:8789/health&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트는 이렇게 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;uv run pytest -q&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 기준 테스트는 통과하는 상태입니다.&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;13 passed&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업은 Hermes에 Supertonic을 직접 박아 넣는 방식이 아니라, 중간에 OpenAI 호환 프록시를 두는 방식으로 정리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes core를 건드리지 않아도 되고,&lt;br /&gt;문제가 생기면 설정만 되돌리면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 OpenAI TTS API와 비슷한 모양으로 맞춰두면 Hermes 외의 다른 클라이언트에서도 가져다 쓰기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 TTS를 Hermes에 붙여보고 싶거나,&lt;br /&gt;Supertonic을 OpenAI 호환 TTS endpoint처럼 써보고 싶은 분들은 참고해보셔도 좋겠습니다.&lt;/p&gt;</description>
      <category>Study/AI</category>
      <category>Agent</category>
      <category>Ai</category>
      <category>AI에이전트</category>
      <category>fastapi</category>
      <category>Hermes</category>
      <category>OpenAIapi</category>
      <category>Supertonic</category>
      <category>tts</category>
      <category>로컬TTS</category>
      <category>음성합성</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/76</guid>
      <comments>https://smokingbird.tistory.com/76#entry76comment</comments>
      <pubDate>Fri, 24 Apr 2026 14:07:05 +0900</pubDate>
    </item>
    <item>
      <title>오라클 클라우드 결제 환불 진행중 (feat. 오라클 고객지원, 문의하기)</title>
      <link>https://smokingbird.tistory.com/75</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;이 이야기는&amp;nbsp;&amp;nbsp;떼인돈을 받으려 고군분투한 어떤 IT너드의 이야기 입니다. &lt;br&gt;&lt;br&gt;오라클 클라우드 빈자리가 너무 안나서.. PAY TO GO로 업그레이드 하려고 했던 때 였습니다&amp;nbsp;&amp;nbsp;결제수단을 추가하고 업그레이드 하는데 갑자기 프레임 내부 에러가 뜨고, 세션이 만료되고, 그 와중에 138.19 싱가포르 달러가 실제로 빠져나갔습니다. 이전에 한번 시도(해외결제 차단) 으로 실패했을때는 돈이 일단 나갔다 바로 취소프로세스를 타서 들어왓는데, 이번엔 아니었습니다. 체크카드였어서 돈이 현금으로 그냥 빠져나가버리고, 돌아오질 않으니 그때부터는 뭔가 제대로 잘못됐다는 느낌이 왔습니다.&lt;br&gt;&lt;br&gt;처음엔 저도 &quot;승인 보류겠지&quot; 하고 넘기려고 했습니다. 오라클 쪽도 그렇게 말했으니까요. 그런데 카드 명세에는 이미 돈이 빠져나간 것처럼 보였고, 오라클 화면에서는 그 결제 내역은 어디서도 볼수가 없엇습니다.. 이 상태가 사람을 제일 미치게 합니다. 어디서 잘못된 건지, 누구 말을 믿어야 하는지 안 보이거든요...&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;처음엔 그냥 기다리면 풀릴 줄 알았습니다&lt;/h3&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;처음엔 진짜 대수롭지 않게 생각했습니다. 잠깐 잡아두는 보류고, 시간이 지나면 정리되겠지 싶었습니다.&lt;br&gt;그런데 금액은 애매하지 않았습니다. 테스트용으로 넘기기엔 138.19 싱가포르 달러 그러니까 당시 환율로 16 만원이 넘는 금액은, 결코 작은금액도 아니고,&amp;nbsp;&amp;nbsp;보류처럼만 보이지 않았습니다. 무엇보다 결제 과정에서 에러가 먼저 났습니다. 이 조합이면 &quot;괜찮겠지&quot; 하고 넘기기 어려웠습니다.&lt;br&gt;그때부터는 마음이 좀 급해졌습니다. 기다리는 시간 동안에도 계속 카드 명세를 들여다보고, 오라클 화면을 다시 확인하고, 혹시 내가 뭘 잘못 눌렀나 돌아보고, 또 다시 같은 화면을 보는 식이었으니까요.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제를 결제 사고로 봐야 했습니다&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;이게 승인 보류인지, 매출인지&lt;/li&gt;&lt;li&gt;오라클 시스템에 기록이 남았는지&lt;/li&gt;&lt;li&gt;언제 어떤 상태로 바뀌는지&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: justify;&quot;&gt;이 질문으로 들어가니까 대응도 달라졌습니다. 막연히 &quot;도와주세요&quot;가 아니라, 문제를 구조적으로 짚어야 했습니다. 그런데...&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;이 일에서 제일 중요했던 건 문제를 다시 정의한 거였습니다. 그냥 &quot;결제가 안 된다&quot;가 아니라, &quot;이건 결제 사고처럼 보인다&quot;로 봐야 했습니다.&lt;br&gt;그렇게 봐야 해야 할 질문도 또렷해집니다.&lt;/p&gt;&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;오라클에 문의넣는 여정부터 만만치 않았습니다&lt;br&gt;&lt;/h3&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzcUk8/dJMcaiQz25q/d8cn48hsTw3iEqjnM08WQk/tfile.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzcUk8/dJMcaiQz25q/d8cn48hsTw3iEqjnM08WQk/tfile.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzcUk8/dJMcaiQz25q/d8cn48hsTw3iEqjnM08WQk/tfile.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzcUk8%2FdJMcaiQz25q%2Fd8cn48hsTw3iEqjnM08WQk%2Ftfile.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;오라클 문의 방법 찾는 것만 네시간을 허비했습니다.&amp;nbsp;&amp;nbsp;&lt;br&gt;우선 오라클 클라우드 콘솔 내 문의하기는 AI챗봇이고, 실제상담원으로 전환되거나 하는 기능은 전혀 없습니다. 클라우드 안에서는 방법이 없어요... 커스토머 서포트가 있긴 한데 유료회원인지 기업회원인지.. 저같은 개인은 불가능한 일이었어요&lt;br&gt;&lt;br&gt;흘러흘러 my oracle support이라는걸 찾아 가입했습니다. 근데 가입한 다음 할수있는건 아무 것도 없었습니다. 오라클 클라우드 콘솔이랑 연결하는 정도.. 그외는 아무기능도 없는 사이트였어요...&lt;br&gt;&lt;br&gt;그래서 오라클 스토어쪽으로 어떻게 또 타고 들어왔습니다. 여기에는 문의 폼이 있네요 그래서 여기 먼저 남겼습니다.&lt;br&gt; &lt;a href=&quot;https://store-oracle.custhelp.com/app/CP_CP&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://store-oracle.custhelp.com/app/CP_CP&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;Ask a Question&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;What can we help you with? *&quot; data-og-host=&quot;store-oracle.custhelp.com&quot; data-og-source-url=&quot;https://store-oracle.custhelp.com/app/CP_CP&quot; data-og-image=&quot;&quot; data-og-url=&quot;https://store-oracle.custhelp.com/app/CP_CP&quot;&gt;&lt;a href=&quot;https://store-oracle.custhelp.com/app/CP_CP&quot; target=&quot;_blank&quot; data-source-url=&quot;https://store-oracle.custhelp.com/app/CP_CP&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;Ask a Question&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;What can we help you with? *&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;store-oracle.custhelp.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: justify;&quot;&gt;일단 문의를 남겼는데.. 문의가 잘 남겨졌다고 자동응답이 왔네요&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IMaxk/dJMcajvcvCQ/v8vHS8FnfHnBhGL7WJLhEK/tfile.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IMaxk/dJMcajvcvCQ/v8vHS8FnfHnBhGL7WJLhEK/tfile.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IMaxk/dJMcajvcvCQ/v8vHS8FnfHnBhGL7WJLhEK/tfile.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIMaxk%2FdJMcajvcvCQ%2Fv8vHS8FnfHnBhGL7WJLhEK%2Ftfile.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1322&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;br&gt;그리고 우연히 네이버/구글 여기저기 검색중 환불관련 문의는 &lt;br&gt;collections_sg@oracle.com 에 이메일 남기라는 글을 만났습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N9V4Q/dJMcaiQz25s/atldDX473fzgSKVKwJOFiK/tfile.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N9V4Q/dJMcaiQz25s/atldDX473fzgSKVKwJOFiK/tfile.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N9V4Q/dJMcaiQz25s/atldDX473fzgSKVKwJOFiK/tfile.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN9V4Q%2FdJMcaiQz25s%2FatldDX473fzgSKVKwJOFiK%2Ftfile.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1394&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;이메일 보냈는데 여기도 자동응답이네요,&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;830&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kXxCJ/dJMcajvcvCR/O6zvCBUBrtXFzv4nxQVvPk/tfile.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kXxCJ/dJMcajvcvCR/O6zvCBUBrtXFzv4nxQVvPk/tfile.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kXxCJ/dJMcajvcvCR/O6zvCBUBrtXFzv4nxQVvPk/tfile.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkXxCJ%2FdJMcajvcvCR%2FO6zvCBUBrtXFzv4nxQVvPk%2Ftfile.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;830&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;830&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;br&gt;찾다찾다 오라클 한국 전화번호가 있었는데, 여기는 영업일에 전화준대놓고 안줍니다 여긴 포기하세요.&lt;br&gt;&lt;br&gt;그러다 오라클 라이브챗 을 알게되었습니다. 나중에 알게된건데 아까 문의남긴 그 사이트에 있던건데, 저는 구글에서 직통 url을 찾았어요&lt;br&gt;&lt;br&gt;&lt;a href=&quot;https://store-oracle.custhelp.com/app/chat/chat_launch&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://store-oracle.custhelp.com/app/chat/chat_launch&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;&quot; data-og-host=&quot;&quot; data-og-source-url=&quot;https://store-oracle.custhelp.com/app/chat/chat_launch&quot; data-og-image=&quot;&quot; data-og-url=&quot;&quot;&gt; &lt;a href=&quot;&quot; target=&quot;_blank&quot; data-source-url=&quot;https://store-oracle.custhelp.com/app/chat/chat_launch&quot;&gt; 
  &lt;div class=&quot;og-image&quot; style=&quot;background-image: url('')&quot;&gt; 
  &lt;/div&gt; 
  &lt;div class=&quot;og-text&quot;&gt; 
   &lt;p class=&quot;og-title&quot;&gt;&lt;/p&gt; 
   &lt;p class=&quot;og-desc&quot;&gt;&lt;/p&gt; 
   &lt;p class=&quot;og-host&quot;&gt;&lt;/p&gt; 
  &lt;/div&gt;&lt;/a&gt; 
&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;br&gt;요롷고롬 생겼습니다. 기본적인 인적사항과, (company는 그냥 none 해도됨) 문의내용 요약본을 우리의 ai친구에게 부탁해 넣었습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzQsKb/dJMcaiQz25t/I5hfkb5FX88OGNNtpzw5g1/tfile.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzQsKb/dJMcaiQz25t/I5hfkb5FX88OGNNtpzw5g1/tfile.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzQsKb/dJMcaiQz25t/I5hfkb5FX88OGNNtpzw5g1/tfile.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzQsKb%2FdJMcaiQz25t%2FI5hfkb5FX88OGNNtpzw5g1%2Ftfile.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;2124&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;이렇게 넣으면 일단 라이브 챗이 시작되는데 일단은 여기도 챗봇으로 시작은 합니다. &lt;br&gt;근데 이상하게 스타트 챗이 활성화가 안될때가 있어요 , 그럴땐 다른방법이 있습니다.&lt;br&gt;&lt;a href=&quot;https://customercenter.oracle.com/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://customercenter.oracle.com/&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;https://customercenter.oracle.com/&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;&quot; data-og-host=&quot;customercenter.oracle.com&quot; data-og-source-url=&quot;https://customercenter.oracle.com/&quot; data-og-image=&quot;&quot; data-og-url=&quot;https://customercenter.oracle.com/&quot;&gt;&lt;a href=&quot;https://customercenter.oracle.com/&quot; target=&quot;_blank&quot; data-source-url=&quot;https://customercenter.oracle.com/&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;https://customercenter.oracle.com/&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;customercenter.oracle.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;여기 들어가면 우측하단에 메시지 아이콘을 누르면 바로 시작됩니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1079&quot; data-origin-height=&quot;2118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wicYG/dJMcajvcvCS/D4KubncAKwGJTeBa9wgv51/tfile.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wicYG/dJMcajvcvCS/D4KubncAKwGJTeBa9wgv51/tfile.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wicYG/dJMcajvcvCS/D4KubncAKwGJTeBa9wgv51/tfile.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwicYG%2FdJMcajvcvCS%2FD4KubncAKwGJTeBa9wgv51%2Ftfile.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1079&quot; height=&quot;2118&quot; data-origin-width=&quot;1079&quot; data-origin-height=&quot;2118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;근데 말씀드렸듯 둘다 챗봇으로 시작하는데요, 이때 뭐 선택하고 뭐 하고 할필요 없고 바로 한마디 칩니다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;I want to chat with real agent&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;그럼 진짜 원하냐고 물어보는데 yes 를 누르면 잠시후 연결이 됩니다. 이제 문의하면 됩니다. &lt;br&gt;영어 할줄은 아는데 정확하게 안하면 문제될듯 하니, 우리의 ai친구에게 대화를 전달하면서&amp;nbsp;&amp;nbsp;꾹꾹 눌러 써 나갑니다. 저는 지피티한테 시켰는데 중간중간 &quot;[내정보입력]&quot; 이런식으로 적어놨는데 이거 처음에 그냥 보냈다가 당황했어요.. 근데 스무스하게 넘어갔습니다. 어쨋건 사간발생 4시간만에 라이브 상담사와 연결이 되어 이것저것 문의하고 확인요청을 했습니다. 근데 은행에 확인해보라 말하기도 하고... 해서 토스카드 고객센터는 24시간이니 물어보면서 대화했어요&lt;/p&gt;&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;/h3&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;오라클이랑 은행이 서로 다르게 말하더군요&lt;/h3&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;오라클은 승인 보류 쪽으로 설명했고, 은행 쪽에서는 실제 반영 상태를 따져봐야 했습니다. 문제는 이 둘이 같은 말을 하지 않는다는 겁니다.&lt;br&gt;오라클은 &quot;기다려라&quot; 쪽이고, 은행은 &quot;아직 환불전표가 안들어왔다&quot; 쪽이었습니다. 결국 저만 중간에서 계속 확인하고, 또 확인하고, 또 확인해야 했습니다. 이게 제일 피곤합니다.&lt;br&gt;&lt;br&gt;그래서 저는 오히려 질문을 좁혔습니다. &quot;이게 승인 보류냐, 매출이냐&quot;. 이 한 문장이 제일 중요했습니다. 다른 설명보다 이걸 먼저 확인해야 했습니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;아직 끝난 건 아니지만, 방향은 보였습니다.&lt;/h3&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;지금도 완전히 해결된 상태는 아닙니다. 그래서 이 글은 후기가 아니라 진행 기록에 가깝습니다.&lt;br&gt;다만 이번 일로 하나는 확실히 배웠습니다. 결제는 됐는데 기록이 없으면, 그때부터는 그냥 기다리는 문제가 아니더군요. &lt;br&gt;오라클 쪽에서는 3~5일 정도 기다려보라는 식으로 말했지만, 저는 일단 기록을 남겨두고 지켜보는 쪽으로 가려고 합니다. 괜히 혼자만 조급해지면 더 꼬일 것 같아서요.&lt;br&gt;&lt;br&gt;아마 이 글도 한 번에 끝나지는 않을 겁니다. 나중에 진짜로 정리되면, 그때는 결론까지 이어서 다시 적어보려고 합니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s20wR/dJMcaiQz25v/W5xszZ34K5Jl413Ko1CtCK/tfile.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s20wR/dJMcaiQz25v/W5xszZ34K5Jl413Ko1CtCK/tfile.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s20wR/dJMcaiQz25v/W5xszZ34K5Jl413Ko1CtCK/tfile.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs20wR%2FdJMcaiQz25v%2FW5xszZ34K5Jl413Ko1CtCK%2Ftfile.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완료되면 다시 돌아오겠습니다.&lt;/p&gt;</description>
      <category>Day by day</category>
      <category>Cloud</category>
      <category>oci</category>
      <category>Oracle</category>
      <category>문의</category>
      <category>오라클</category>
      <category>오라클환불</category>
      <category>클라우드</category>
      <category>환불</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/75</guid>
      <comments>https://smokingbird.tistory.com/75#entry75comment</comments>
      <pubDate>Thu, 23 Apr 2026 23:41:17 +0900</pubDate>
    </item>
    <item>
      <title>insane-search-hermes를 공개했습니다</title>
      <link>https://smokingbird.tistory.com/74</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 blocked-site retrieval 쪽을 조금 더 실전적으로 써보고 싶어서, &lt;code&gt;insane-search-hermes&lt;/code&gt;라는 스킬을 따로 정리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본은 &lt;code&gt;fivetaku/insane-search&lt;/code&gt;이고, 저는 그 문제의식과 접근 방식을 Hermes 흐름에 맞게 다시 구성해봤습니다. 그냥 웹 검색이 안 되는 순간 포기하는 게 아니라, 어떤 경로로 한 번 더 뚫어볼지 순서를 잡아주는 쪽에 가깝습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떤 걸 하려고 만든 스킬인가요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이트가 막혔을 때 보통은 여기서 끝나는 경우가 많습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;403&lt;/li&gt;
&lt;li&gt;WAF&lt;/li&gt;
&lt;li&gt;CAPTCHA&lt;/li&gt;
&lt;li&gt;빈 SPA&lt;/li&gt;
&lt;li&gt;로그인 벽&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 &lt;code&gt;web_extract&lt;/code&gt; 하나만 보고 끝내지 않고, 다음 순서로 다시 시도해보도록 정리했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Jina Reader&lt;/li&gt;
&lt;li&gt;공개 JSON API&lt;/li&gt;
&lt;li&gt;RSS / Atom&lt;/li&gt;
&lt;li&gt;curl_cffi 기반 TLS impersonation&lt;/li&gt;
&lt;li&gt;browser / CDP escalation&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 네이버, Reddit, X, Hacker News, Bluesky처럼 접근 방식이 조금씩 다른 서비스는 전용 스크립트까지 같이 넣어뒀습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이번에 같이 넣은 것들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서형 스킬로만 끝내지 않고, 실행형 스크립트도 붙였습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;fetch_with_cffi.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;naver_search.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reddit_json.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hn_fetch.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bluesky_fetch.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wayback_lookup.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;insane_router.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;insane_router.py&lt;/code&gt;는 URL, 핸들, 키워드를 구분해서 어떤 경로를 먼저 써볼지 자동으로 실행해보는 라우터 역할을 합니다. 완전히 원본 플러그인처럼 동작하는 수준까지는 아니지만, 실전에서는 꽤 바로 써먹을 수 있는 정도까지는 왔다고 느꼈습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;skills hub에는 왜 바로 못 올렸나&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 조금 아쉬웠던 부분입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes skills hub로 publish도 바로 해보려고 했는데, 스캔 단계에서 막혔습니다. 이유는 대체로 아래 세 가지였습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;없으면 설치하고 계속 가는 auto-install 문구&lt;/li&gt;
&lt;li&gt;스크립트 내부의 subprocess 실행&lt;/li&gt;
&lt;li&gt;bootstrap / env 처리 로직&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실전성은 좋아졌지만, registry publish 관점에서는 위험 판정이 나는 구조였던 거죠. 그래서 지금은 hub 배포는 보류했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대신 GitHub에는 올려뒀습니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 아래에 정리해뒀습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/sanguneo/insane-search-hermes&quot;&gt;https://github.com/sanguneo/insane-search-hermes&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;README도 한글/영문으로 나눠뒀고, 스킬 본문과 reference, scripts까지 같이 넣어뒀습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;설치 방법은&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1776828889765&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;## 자동
hermes skills install sanguneo/insane-search-hermes:skill/SKILL.md

## 수동
git clone https://github.com/sanguneo/insane-search-hermes.git
cp -R ./insane-search-hermes/skill ~/.hermes/skills/research/insane-search-hermes/&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;써보면서 느낀 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 더 다듬을 부분은 남아 있지만, 지금도 막힌 URL을 다룰 때 꽤 유용하게 쓰고 있습니다. 다음에는 browser/CDP 단계까지 조금 더 자연스럽게 이어지는 쪽으로 정리해보려고 합니다.&lt;/p&gt;</description>
      <category>Study/AI</category>
      <category>Ai</category>
      <category>blocked-site</category>
      <category>curl_cffi</category>
      <category>Hermes</category>
      <category>insane-search</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/74</guid>
      <comments>https://smokingbird.tistory.com/74#entry74comment</comments>
      <pubDate>Wed, 22 Apr 2026 11:16:45 +0900</pubDate>
    </item>
    <item>
      <title>Google AI Studio, 구독만으로 더 넓어진 실험 한도</title>
      <link>https://smokingbird.tistory.com/73</link>
      <description>&lt;p&gt;최근 Google이 AI Studio의 진입 장벽을 꽤 낮췄습니다.&lt;/p&gt;&lt;br /&gt;
&lt;p&gt;&lt;img src=&quot;https://storage.googleapis.com/gweb-uniblog-publish-prod/images/g1-ais_keyword_DEV_header_dark.max-1440x810.png&quot; alt=&quot;Google AI Studio 관련 이미지&quot;&gt;&lt;/p&gt;&lt;br /&gt;
&lt;p&gt;Google AI Studio의 사용 한도가 넓어졌습니다.&lt;/p&gt;&lt;br /&gt;
&lt;p&gt;Google AI Pro와 Ultra 구독자는 이제 AI Studio에서 더 높은 사용 한도를 받을 수 있고, Nano Banana Pro와 Gemini Pro 모델도 바로 활용할 수 있게 됐습니다.&lt;/p&gt;&lt;br /&gt;
&lt;p&gt;겉으로 보면 단순히 &amp;quot;쓸 수 있는 범위가 넓어졌다&amp;quot;는 정도로 보일 수 있습니다. 하지만 실제로는 아이디어를 더 자주, 더 오래 실험할 수 있게 해준다는 점에서 의미가 큽니다.&lt;/p&gt;&lt;br /&gt;
&lt;p&gt;예전에는 무료 한도를 넘기면 테스트 흐름이 쉽게 끊겼습니다. 조금 더 써보려는 순간 종량제 API 세팅으로 넘어가야 해서, 초반 탐색이 번거롭게 느껴질 때가 있었거든요.&lt;/p&gt;&lt;br /&gt;
&lt;p&gt;이번 변화는 그 중간을 더 자연스럽게 이어줍니다.&lt;/p&gt;&lt;br /&gt;
&lt;p&gt;구독 환경에서 먼저 충분히 써보고&lt;br&gt;괜찮다 싶으면&lt;br&gt;AI Studio 안에서 API 기반으로 확장하는 흐름이 가능해진 겁니다.&lt;/p&gt;&lt;br /&gt;&lt;br /&gt;
&lt;p&gt;이 구조가 좋은 이유는&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;시작이 쉽습니다.&lt;/strong&gt;&lt;br&gt;  복잡한 결제나 설정을 처음부터 붙잡지 않아도 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;실험이 빨라집니다.&lt;/strong&gt;&lt;br&gt;  떠오른 아이디어를 바로 확인할 수 있어서, 프로토타입 검증 속도가 빨라집니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;확장도 자연스럽습니다.&lt;/strong&gt;&lt;br&gt;  처음부터 운영 구조를 고민하기보다, 먼저 만들어 보고 이후에 정리하는 방식이 가능해집니다.&lt;/p&gt;&lt;br /&gt;
&lt;p&gt;결국 이번 변화는 AI Studio를 단순한 체험 공간이 아니라,&lt;br&gt;&amp;quot;구독만으로도 충분히 실험해볼 수 있는 작업 공간&amp;quot;에 더 가깝게 바꿔준 업데이트라고 볼 수 있습니다.&lt;/p&gt;&lt;br /&gt;
&lt;p&gt;AI를 쓰는 입장에서는 반가운 변화고, 무언가를 만드는 입장에서는 훨씬 가벼운 출발점이 하나 생긴 셈입니다.&lt;/p&gt;&lt;br /&gt;
&lt;p&gt;천천히 써보면서, 어디까지 구독 환경에서 충분히 해결되는지도 한번 확인해봐야겠어요.&lt;/p&gt;</description>
      <category>Study/AI</category>
      <category>Ai</category>
      <category>ai studio</category>
      <category>Gemini</category>
      <category>Google AI Pro</category>
      <category>Google AI Ultra</category>
      <category>Nano Banana Pro</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/73</guid>
      <comments>https://smokingbird.tistory.com/73#entry73comment</comments>
      <pubDate>Tue, 21 Apr 2026 13:55:30 +0900</pubDate>
    </item>
    <item>
      <title>Vercel 보안 이슈 정리</title>
      <link>https://smokingbird.tistory.com/72</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPmpmA/dJMcad2KvQg/hPpMSvD4DdkvRUqYU3K3j0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPmpmA/dJMcad2KvQg/hPpMSvD4DdkvRUqYU3K3j0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPmpmA/dJMcad2KvQg/hPpMSvD4DdkvRUqYU3K3j0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPmpmA%2FdJMcad2KvQg%2FhPpMSvD4DdkvRUqYU3K3j0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;Vercel 보안 이슈 정리&lt;/h2&gt;
&lt;p&gt;Vercel이 최근 보안 공지를 공개했습니다. 평소처럼 조용히 지나가는 이슈가 아니라, 실제로 내부 시스템 일부에 대한 무단 접근이 확인된 사건입니다.&lt;/p&gt;
&lt;p&gt;처음 제목만 보면 “어디가 털렸나?” 싶을 수 있는데, 공식 bulletin을 읽어보면 핵심은 꽤 명확합니다. 시작점은 Vercel 자체 서버가 아니라, &lt;strong&gt;Vercel 직원이 사용하던 서드파티 AI 도구 Context.ai 계정이 침해된 것&lt;/strong&gt;이었습니다. 그 계정 접근을 통해 공격자가 직원의 Google Workspace 계정까지 들어갔고, 그 결과 일부 Vercel 환경과 환경변수에 접근한 것으로 설명하고 있습니다.&lt;/p&gt;
&lt;h3&gt;무슨 일이 있었나&lt;/h3&gt;
&lt;p&gt;Vercel은 4월 보안 공지를 통해 내부 시스템의 일부에 대한 무단 접근을 확인했다고 밝혔습니다. 동시에 외부 보안업체와 법집행기관도 함께 대응 중이라고 했고, 서비스는 현재 운영 중이라고 덧붙였습니다.&lt;/p&gt;
&lt;p&gt;중요한 건 “모든 고객 데이터가 다 유출됐다”는 식의 단정이 아니라는 점입니다. Vercel은 처음에는 &lt;strong&gt;일부 고객만 영향이 있었고&lt;/strong&gt;, 연락을 받은 고객에게는 즉시 자격 증명 교체를 권고했다고 설명합니다. 아직까지는 노출 범위와 실제 반출 데이터가 조사 중입니다.&lt;/p&gt;
&lt;h3&gt;어디가 위험했나&lt;/h3&gt;
&lt;p&gt;공식 문서에서 특히 눈에 들어온 부분은 &lt;strong&gt;환경변수&lt;/strong&gt;입니다. Vercel은 sensitive로 표시되지 않은 환경변수에 접근했을 가능성을 언급했고, sensitive로 표시된 값은 읽기 어렵게 저장되어 있어 현재까지 접근 증거는 없다고 밝혔습니다.&lt;/p&gt;
&lt;p&gt;실무적으로 보면 이게 꽤 중요합니다. 개발자는 종종 API 키, 토큰, DB 비밀번호를 환경변수로만 넣어두고 끝내기 쉬운데, “어디까지 보호되는지”는 서비스마다 다릅니다. 이번 공지는 그 차이를 다시 한 번 보여줍니다.&lt;/p&gt;
&lt;h3&gt;Vercel이 권고한 대응&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;계정과 환경의 activity log 확인&lt;/li&gt;
  &lt;li&gt;환경변수 중 민감한 값은 우선 교체&lt;/li&gt;
  &lt;li&gt;최근 배포 중 수상한 항목 점검&lt;/li&gt;
  &lt;li&gt;Deployment Protection 설정 확인&lt;/li&gt;
  &lt;li&gt;Deployment Protection token 사용 중이면 교체&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;개인적으로는 이 순서가 꽤 현실적이라고 봅니다. “혹시 모르니 다 바꾸자”보다, &lt;strong&gt;로그 → 환경변수 → 최근 배포 → 보호 설정&lt;/strong&gt; 순으로 보는 편이 훨씬 빠릅니다.&lt;/p&gt;
&lt;h3&gt;내가 보는 포인트&lt;/h3&gt;
&lt;p&gt;이번 건은 단순한 한 서비스의 해킹 소식이라기보다, &lt;strong&gt;서드파티 도구와 계정 연결 지점이 얼마나 큰 리스크가 되는지&lt;/strong&gt; 다시 보여준 사례에 가깝습니다.&lt;/p&gt;
&lt;p&gt;특히 개발 환경은 협업 도구, 자동화 도구, 배포 플랫폼, 메신저, 클라우드가 전부 얽혀 있어서 한 군데만 느슨해도 전체가 흔들릴 수 있습니다. 그래서 보안은 늘 “서버만 단단하면 끝”이 아니고, 연결된 계정과 권한까지 같이 봐야 합니다.&lt;/p&gt;
&lt;p&gt;Vercel을 쓰는 팀이라면 이번 공지 한 번쯤 읽어보고, 환경변수와 배포 보호 설정은 바로 점검해두는 게 좋아 보입니다.&lt;/p&gt;
&lt;h3&gt;정리&lt;/h3&gt;
&lt;p&gt;이번 Vercel 보안 이슈는 아직 조사 중인 부분이 남아 있지만, 실무자가 바로 챙길 건 이미 나와 있습니다. 민감한 비밀값은 다시 점검하고, 최근 배포와 접근 로그를 확인하고, 외부 도구에 연결된 권한도 한 번 정리해두는 것. 이 정도만 해도 피해를 꽤 줄일 수 있습니다.&lt;/p&gt;
&lt;p&gt;보안은 늘 귀찮지만, 한 번 터지고 나면 더 귀찮아집니다. 그래서 미리 보는 쪽이 결국 제일 싸게 먹히는 것 같아요.&lt;/p&gt;</description>
      <category>Develop</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/72</guid>
      <comments>https://smokingbird.tistory.com/72#entry72comment</comments>
      <pubDate>Mon, 20 Apr 2026 22:27:16 +0900</pubDate>
    </item>
    <item>
      <title>한국어 TTS 비교 후기: EdgeTTS를 거쳐 Supertonic-2로 정착하기까지</title>
      <link>https://smokingbird.tistory.com/70</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텔레그램에서 바로 보낼 한국어 음성메시지용 TTS가 필요했습니다. 조건은 비교적 분명했습니다. WSL에서 돌아가야 했고, 외장 GPU 없이도 실사용 가능해야 했으며, 무엇보다 한국어가 어색하지 않아야 했습니다. 단순히 음성이 생성되는 정도가 아니라, 실제로 계속 쓸 수 있을 정도의 품질이 필요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 EdgeTTS로 시작했습니다. 붙이기 쉽고 결과도 빨리 나와서 시작점으로는 나쁘지않았습니다. 다만 쓰다 보니 욕심이 생겼습니다. &amp;ldquo;조금 더 자연스럽고, 조금 더 만족스러운 한국어 TTS가 없을까?&amp;rdquo;라는 생각이 들었고, 그다음으로 Kokoro를 직접 붙여봤습니다. 이후 여러 모델 데모를 더 비교해 본 끝에, 최종적으로는 Supertonic-2에 정착하게 됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 ElevenLabs도 꽤 궁금했습니다. 실제로 한 번 써보고 싶기도 했습니다. 다만 GPU가 없는 환경에서 직접 구축해서 계속 돌리는 쪽에 더 무게를 두고 있었기 때문에, 결과적으로는 그 방향 대신 로컬에 가까운 쪽으로 계속 비교하게 됐습니다. 그리고 결론적으로는 지금 정착한 조합에 &lt;b&gt;매우 만족하고 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번에 비교할 때 중요하게 본 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 TTS를 고를 때 제가 중요하게 본 기준은 아래 네 가지였습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WSL에서 무리 없이 돌릴 수 있는가&lt;/li&gt;
&lt;li&gt;GPU 없이도 실사용 가능한가&lt;/li&gt;
&lt;li&gt;한국어가 외국어 억양처럼 들리지 않는가&lt;/li&gt;
&lt;li&gt;텔레그램 음성메시지로 바로 쓰기에 충분한가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 특히 중요했던 건 세 번째였습니다. 많은 TTS가 &amp;ldquo;한국어 지원&amp;rdquo;은 하지만, 실제로 들어보면 자연스러운 한국어와는 거리가 있는 경우가 생각보다 많았습니다. 문서를 보면 좋아 보이는데, 직접 들어보면 바로 탈락하는 경우가 적지 않았습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1단계: EdgeTTS는 빠르게 시작하기에 좋았습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 사용한 것은 EdgeTTS였습니다. 이유는 단순했습니다. 바로 붙이기 쉽고, 결과를 빠르게 볼 수 있기 때문입니다. 한국어 문장을 읽히고, 파일로 뽑고, 음성메시지로 보내는 흐름을 확인하는 데는 정말 편했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2단계: Kokoro는 기대가 컸지만, 한국어 스피치는 아쉬웠습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kokoro는 꽤 기대하고 붙였습니다. 모델 자체에 대한 평도 좋았고, 한 번 직접 비교해 볼 가치가 충분해 보였습니다. 그래서 실제로 WSL 환경에 설치하고, 문장을 넣어서 음성을 생성해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 여기서 느낀 점은 꽤 분명했습니다. &lt;b&gt;모델 평판과 실제 한국어 TTS 경험은 다를 수 있다&lt;/b&gt;는 점이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 단순히 품질이 낮다기보다, 한국어가 자연스러운 한국어처럼 들리지 않는다는 점이었습니다. 전체적으로는 &lt;b&gt;외국인이 한국어를 따라 읽는 느낌&lt;/b&gt;이 강했습니다. 문장이 부드럽게 흘러가기보다, 한국어 음절을 외국어 화자가 조심스럽게 흉내 내는 듯한 인상이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 일본어 계열 보이스는 더 그랬습니다. 거의 한본어에 가까운 느낌이었습니다. 조금 과장해서 말하면, &amp;ldquo;독도&amp;rdquo;라고 넣었을 때 &amp;ldquo;다케시마&amp;rdquo;라고 해도 이상하지 않을 것 같은 수준의 이질감이 있었습니다. 한국어 TTS를 기대하고 들었는데, 실제로는 한국어 발음을 흉내 내는 외국어 보이스에 가까웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 단순한 취향 문제라기보다, 제가 원하는 용도와 맞지 않는 문제였습니다. 저는 실험용 샘플이 아니라, 실제 텔레그램 음성메시지에 바로 붙일 수 있는 결과가 필요했기 때문입니다. 그 기준에서는 Kokoro를 기본 TTS로 정착시키기 어려웠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 Kokoro를 포기했는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kokoro를 제 기준에서 정리하면 이렇습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점: 기대하게 만드는 모델이고, 직접 써볼 가치는 충분했습니다&lt;/li&gt;
&lt;li&gt;단점: 제가 원하는 &amp;ldquo;자연스러운 한국어 TTS&amp;rdquo;와는 거리가 있었습니다&lt;/li&gt;
&lt;li&gt;결론: 비교 대상으로는 의미 있었지만, 기본 TTS로 정착하긴 어려웠습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &quot;모델이 나쁘다&quot;기보다, 제가 필요한 한국어 음성메시지 용도에는 맞지 않았다 에 더 가까웠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그다음은 문서보다 데모를 더 많이 봤습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kokoro에서 한 번 아쉬움을 느끼고 나니 기준이 더 분명해졌습니다. 그 뒤로는 모델 카드 설명이나 평판만 보기보다, 가능한 한 데모를 직접 들어보는 쪽으로 방향을 바꿨습니다. Hugging Face 쪽도 계속 참고했고, 설명이 좋아 보여도 실제 청감이 별로면 바로 제외했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 다시 확인한 건 단순했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;한국어 인식&quot;이라고 적혀 있는 것과&lt;/li&gt;
&lt;li&gt;&quot;실제로 한국어가 자연스럽게 들리는 것&quot;은 전혀 다르다는 점입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서만 보면 괜찮아 보이는 모델도, 실제 샘플을 몇 개만 들어보면 금방 감이 옵니다. 특히 텔레그램처럼 짧은 음성메시지로 바로 들리는 환경에서는 어색한 억양이 더 잘 드러났습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;직접 써본 흐름 기준 비교&lt;/h2&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse;&quot; border=&quot;1&quot; cellspacing=&quot;0&quot; cellpadding=&quot;8&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 12.4419%;&quot;&gt;후보&lt;/th&gt;
&lt;th style=&quot;width: 16.8604%;&quot;&gt;접근 방식&lt;/th&gt;
&lt;th style=&quot;width: 26.6279%;&quot;&gt;장점&lt;/th&gt;
&lt;th style=&quot;width: 43.9535%;&quot;&gt;아쉬운 점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.4419%;&quot;&gt;EdgeTTS&lt;/td&gt;
&lt;td style=&quot;width: 16.8604%;&quot;&gt;가장 먼저 실제 사용&lt;/td&gt;
&lt;td style=&quot;width: 26.6279%;&quot;&gt;붙이기 쉽고 빠르게 결과를 볼 수 있었습니다&lt;/td&gt;
&lt;td style=&quot;width: 43.9535%;&quot;&gt;예민하게 들으면 너무 음성합성 티가 많이나는거같았습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.4419%;&quot;&gt;Kokoro&lt;/td&gt;
&lt;td style=&quot;width: 16.8604%;&quot;&gt;직접 설치 후 한국어 음성 생성 테스트&lt;/td&gt;
&lt;td style=&quot;width: 26.6279%;&quot;&gt;기대하게 만드는 모델이었고 비교 가치는 충분했습니다&lt;/td&gt;
&lt;td style=&quot;width: 43.9535%;&quot;&gt;외국인이 한국어를 읽는 듯한 느낌이 강했고, 특히 일본어 계열 보이스는 &quot;한본어&quot;에 가까웠습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.4419%;&quot;&gt;Supertonic-2&lt;/td&gt;
&lt;td style=&quot;width: 16.8604%;&quot;&gt;여러 데모 비교 후 실제 정착&lt;/td&gt;
&lt;td style=&quot;width: 26.6279%;&quot;&gt;한국어 청감이 가장 납득 가능했고 실사용 방향이 분명했습니다&lt;/td&gt;
&lt;td style=&quot;width: 43.9535%;&quot;&gt;처음부터 바로 도달한 후보는 아니었습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.4419%;&quot;&gt;ElevenLabs&lt;/td&gt;
&lt;td style=&quot;width: 16.8604%;&quot;&gt;관심은 있었지만 이번 방향에서는 보류&lt;/td&gt;
&lt;td style=&quot;width: 26.6279%;&quot;&gt;품질 기대치가 높았습니다&lt;/td&gt;
&lt;td style=&quot;width: 43.9535%;&quot;&gt;당시에는 GPU 없는 환경에서 직접 구축해 운용하는 방향에 더 집중했습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종적으로 Supertonic-2에 정착한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 데모를 듣고 비교한 뒤, 결국 정착한 것은 Supertonic-2였습니다. 이 모델이 좋았던 이유는 단순히 &amp;ldquo;소리가 괜찮다&amp;rdquo; 수준이 아니었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한국어가 상대적으로 훨씬 자연스럽게 들렸습니다&lt;/li&gt;
&lt;li&gt;WSL + GPU 없는 환경에서도 현실적인 선택지였습니다&lt;/li&gt;
&lt;li&gt;텔레그램 음성메시지 용도로 붙였을 때 거슬림이 적었습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇보다 중요했던 건, 처음으로 &amp;ldquo;아, 이건 계속 써도 되겠다&amp;rdquo;는 느낌이 들었다는 점입니다. EdgeTTS는 편했고, 시작점으로도 좋았습니다. Kokoro는 기대를 안고 갔지만 실제 한국어 청감에서는 아쉬움이 컸습니다. 반면 Supertonic-2는 적어도 제가 원하는 사용처에서는 가장 균형이 좋았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 지금 기준으로는 이 조합에 &lt;b&gt;매우 만족하고 있습니다.&lt;/b&gt; 처음에는 단순히 TTS 하나를 붙이는 작업이라고 생각했는데, 막상 해보니 &amp;ldquo;어떤 모델이 실제 내 환경에서 가장 덜 어색한가&amp;rdquo;를 찾는 과정에 가까웠습니다. 그 기준에서 현재까지는 Supertonic-2가 가장 납득 가능한 답이었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 비교를 한 줄로 정리하면&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;EdgeTTS&lt;/b&gt;: 빠르게 시작하기 좋았고 실제로도 만족스러웠습니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kokoro&lt;/b&gt;: 기대는 컸지만, 제 기준의 한국어 TTS로는 외국어처럼 들렸습니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Supertonic-2&lt;/b&gt;: 여러 데모를 비교해 본 끝에 가장 만족스럽게 정착한 선택지였습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 구축에서 얻은 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 가장 크게 느낀 건, 한국어 TTS는 모델 평판보다 실제 청감이 훨씬 중요하다는 점입니다. &amp;ldquo;좋다고 들었다&amp;rdquo;는 말만 믿고 가기엔 실제 결과 차이가 꽤 컸습니다. 특히 한국어는 어색하면 바로 티가 납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 이번 선택은 이렇게 정리할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠르게 시작한 것은 EdgeTTS였습니다&lt;/li&gt;
&lt;li&gt;더 나은 결과를 기대하고 Kokoro까지 갔습니다&lt;/li&gt;
&lt;li&gt;하지만 실제 한국어 청감에서는 실망한 부분이 있었습니다&lt;/li&gt;
&lt;li&gt;그 뒤 여러 데모를 더 비교해 본 끝에 Supertonic-2로 정착했습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 기준에서 제 결론은 명확합니다. &lt;b&gt;WSL에서, GPU 없이, 한국어 음성메시지를 꾸준히 만들려면 결국 &amp;ldquo;제 귀에 가장 덜 어색한 모델&amp;rdquo;이 정답&lt;/b&gt;이었습니다. 그리고 현재 제 기준에서 그 답은 Supertonic-2입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 여기에 더해서 합성된 음성을 따로 플레이버튼 누르지 않아도, 필요할때 &quot;띵동&quot; 하고 말해주는 시스템을 구축해볼 생각입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴글 읽어주셔서 감사합니다.&lt;/p&gt;</description>
      <category>Study/AI</category>
      <category>EdgeTTS</category>
      <category>huggingface</category>
      <category>KOKORO</category>
      <category>Supertonic2</category>
      <category>tts</category>
      <category>WSL</category>
      <category>텔레그램</category>
      <category>한국어TTS</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/70</guid>
      <comments>https://smokingbird.tistory.com/70#entry70comment</comments>
      <pubDate>Thu, 16 Apr 2026 16:04:41 +0900</pubDate>
    </item>
    <item>
      <title>Zlack: Slack을 더 가볍게 쓰기 위한 Tauri 데스크톱 클라이언트</title>
      <link>https://smokingbird.tistory.com/69</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zlack은 Slack을 조금 더 가볍고 안정적으로 쓰기 위해 만든 Tauri 기반 데스크톱 클라이언트입니다. Windows와 macOS에서 모두 사용할 수 있도록 만들었고, Slack 웹을 바탕으로 알림과 멀티 워크스페이스 사용 흐름을 최대한 자연스럽게 유지하는 데 집중했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;윈도우 설치 앱 기준으로 비교했을 때 Slack은 약 1.24GB, Zlack은 6.49MB 정도로 차이가 났습니다. 물론 단순한 숫자만으로 모든 사용성을 설명할 수는 없지만, 처음부터 체감되는 가벼움은 꽤 분명했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 보실 수 있는 링크도 함께 남겨둡니다.&lt;/p&gt;
&lt;figure id=&quot;og_1776240858200&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Zlack: Tauri로 만든 경량 데스크톱 Slack 클라이언트 | GeekNews&quot; data-og-description=&quot;Zlack은 Slack 웹을 기반으로 하면서, Electron 대신 Tauri로 구현한 경량 데스크톱 Slack 클라이언트입니다.웹 Slack의 사용성은 그대로 유지하면서도, 데스크톱 환경에서 필요한 네이티브 알림과 안정적&quot; data-og-host=&quot;news.hada.io&quot; data-og-source-url=&quot;https://news.hada.io/topic?id=25686&quot; data-og-url=&quot;https://news.hada.io/topic?id=25686&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/0nqEa/dJMb8VNwT70/Fr1WggjE0LQeLvgOtA5Zd1/img.png?width=1200&amp;amp;height=630&amp;amp;face=960_354_1001_399,https://scrap.kakaocdn.net/dn/FWJnK/dJMb8TCa7AA/1ySH8XErKHJLqjiem1OuR1/img.png?width=1200&amp;amp;height=630&amp;amp;face=960_354_1001_399&quot;&gt;&lt;a href=&quot;https://news.hada.io/topic?id=25686&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://news.hada.io/topic?id=25686&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/0nqEa/dJMb8VNwT70/Fr1WggjE0LQeLvgOtA5Zd1/img.png?width=1200&amp;amp;height=630&amp;amp;face=960_354_1001_399,https://scrap.kakaocdn.net/dn/FWJnK/dJMb8TCa7AA/1ySH8XErKHJLqjiem1OuR1/img.png?width=1200&amp;amp;height=630&amp;amp;face=960_354_1001_399');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Zlack: Tauri로 만든 경량 데스크톱 Slack 클라이언트 | GeekNews&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Zlack은 Slack 웹을 기반으로 하면서, Electron 대신 Tauri로 구현한 경량 데스크톱 Slack 클라이언트입니다.웹 Slack의 사용성은 그대로 유지하면서도, 데스크톱 환경에서 필요한 네이티브 알림과 안정적&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;news.hada.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1776240855252&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - sanguneo/zlack: A lightweight, optimized desktop wrapper for Slack, built with Tauri.&quot; data-og-description=&quot;A lightweight, optimized desktop wrapper for Slack, built with Tauri. - sanguneo/zlack&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/sanguneo/zlack&quot; data-og-url=&quot;https://github.com/sanguneo/zlack&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Po8SI/dJMb9lMcZBo/gfzZXFTzF0JmwBTWnIUPa0/img.png?width=1200&amp;amp;height=600&amp;amp;face=993_123_1068_205,https://scrap.kakaocdn.net/dn/uez8H/dJMb9iIIyGF/fk101mcWtT3An5Icmcarb1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://github.com/sanguneo/zlack&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/sanguneo/zlack&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Po8SI/dJMb9lMcZBo/gfzZXFTzF0JmwBTWnIUPa0/img.png?width=1200&amp;amp;height=600&amp;amp;face=993_123_1068_205,https://scrap.kakaocdn.net/dn/uez8H/dJMb9iIIyGF/fk101mcWtT3An5Icmcarb1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - sanguneo/zlack: A lightweight, optimized desktop wrapper for Slack, built with Tauri.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A lightweight, optimized desktop wrapper for Slack, built with Tauri. - sanguneo/zlack&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Zlack을 소개하는 글이기도 하지만, 왜 이런 형태로 다시 만들었는지까지 같이 정리해보는 글입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zlack은 Electron 대신 Tauri를 선택하면서 시작했습니다. 이유는 단순했습니다. Slack을 전체 Chromium 번들로 다시 감싸기보다, 필요한 부분만 더 가볍게 가져가고 싶었기 때문입니다. 그 덕분에 메모리와 설치 용량에서 확실한 차이를 낼 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적으로는 Slack 웹 클라이언트를 그대로 활용하되, preload.js와 Rust 백엔드를 연결해 데스크톱 앱으로서 필요한 부분만 보강하는 방식으로 구성했습니다. 알림, 딥링크, 윈도우 포커스 복원 같은 부분이 핵심이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Windows에서는 알림 클릭 후 창이 잘 복원되지 않는 문제가 꽤 까다로웠습니다. 그래서 Rust 쪽에서 시스템 트레이 복원과 포커스 전환을 안정적으로 처리했고, Slack의 네트워크 트래픽에서 Team ID와 Channel ID를 잡아내서 알림이 정확한 곳으로 이동하도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 조금 더 풀어보면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Slack 웹 클라이언트 + preload.js 브리지&lt;/li&gt;
&lt;li&gt;/traces/v1/list_of_spans 트래픽에서 알림 맥락 추출&lt;/li&gt;
&lt;li&gt;Rust 백엔드에서 Windows 토스트 알림 처리&lt;/li&gt;
&lt;li&gt;최소한의 웹뷰 구조로 멀티 워크스페이스 지원&lt;/li&gt;
&lt;li&gt;작은 설치 용량과 빠른 초기 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 아직도 실사용 피드백을 받으면서 계속 다듬어가는 중입니다. 처음 목표는 분명했지만, 실제 사용 환경에서 생기는 자잘한 문제까지 잡아가야 완성도가 올라간다고 생각했습니다.&lt;/p&gt;</description>
      <category>Develop</category>
      <category>DesktopApp</category>
      <category>electron</category>
      <category>productivity</category>
      <category>Rust</category>
      <category>Slack</category>
      <category>tauri</category>
      <category>webview2</category>
      <category>windows</category>
      <category>Zlack</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/69</guid>
      <comments>https://smokingbird.tistory.com/69#entry69comment</comments>
      <pubDate>Wed, 15 Apr 2026 17:11:37 +0900</pubDate>
    </item>
    <item>
      <title>인한사전 InKrKamus v3 소개 + 개발기</title>
      <link>https://smokingbird.tistory.com/68</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인한사전(InKrKamus) v3는 인도네시아어-한국어 사전을 브라우저에서 빠르고 가볍게 사용하실 수 있도록 다시 정리해 만든 버전입니다. 모바일과 PC에서 모두 부담 없이 검색하실 수 있게 만들었고, 오프라인 캐시까지 함께 챙겨서 재방문도 더 빠르게 보실 수 있게 구성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 사용해보실 분은 아래 주소로 들어가시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://inkrkamus.sanguneo.com/&quot;&gt;https://inkrkamus.sanguneo.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1776238929320&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;인한사전 InKrKamus v3&quot; data-og-description=&quot;인도네시아어-한국어 사전 InKrKamus. 빠른 검색, 모바일/PC 반응형, 오프라인 캐시를 지원합니다.&quot; data-og-host=&quot;inkrkamus.sanguneo.com&quot; data-og-source-url=&quot;https://inkrkamus.sanguneo.com/&quot; data-og-url=&quot;https://inkrkamus.sanguneo.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cIspIH/dJMb887aiEF/uh3rKUFhHzjE70m0mAQuzK/img.png?width=240&amp;amp;height=180&amp;amp;face=0_0_240_180,https://scrap.kakaocdn.net/dn/IahKu/dJMb9hC2L7I/q6cYhPIrknK663lBY26zh0/img.png?width=240&amp;amp;height=180&amp;amp;face=0_0_240_180&quot;&gt;&lt;a href=&quot;https://inkrkamus.sanguneo.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://inkrkamus.sanguneo.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cIspIH/dJMb887aiEF/uh3rKUFhHzjE70m0mAQuzK/img.png?width=240&amp;amp;height=180&amp;amp;face=0_0_240_180,https://scrap.kakaocdn.net/dn/IahKu/dJMb9hC2L7I/q6cYhPIrknK663lBY26zh0/img.png?width=240&amp;amp;height=180&amp;amp;face=0_0_240_180');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;인한사전 InKrKamus v3&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;인도네시아어-한국어 사전 InKrKamus. 빠른 검색, 모바일/PC 반응형, 오프라인 캐시를 지원합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;inkrkamus.sanguneo.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 단순한 기능 소개를 넘어서, 제가 이 프로젝트를 어떻게 다시 정리했는지도 함께 적어보는 글입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InKrKamus의 시작은 꽤 오래전입니다. v1은 2010년에 만들었던 윈도우용 아주 작고 가벼운 프로그램이었습니다. 다만 메모리 최적화 같은 부분이 전혀 되어 있지 않아서, 겉보기에는 가벼워 보여도 실제로는 결코 가볍지만은 않은 프로그램이었습니다. 로고와 디자인도 당시에는 하나하나 직접 손으로 다듬었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 2016년에 만든 v2는 두 가지 버전으로 운영되었습니다. 하나는 Electron 기반 데스크탑 앱이었고, 다른 하나는 Heroku 환경의 Express로 운영하던 웹앱이었습니다. 두 버전은 섞여 있지 않았고, 각각 별도의 형태로 존재했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시의 v2는 나름의 역할을 잘 해냈지만, 지금 기준으로 보면 오래된 스택이 되었습니다. 데스크탑 쪽은 Electron, 웹 쪽은 Heroku + Express, 프론트는 Vue1 기반이었기 때문에 유지보수와 확장성 측면에서 한계가 분명했습니다. 그래서 v3에서는 이 구조를 그대로 끌고 가기보다, 새로 설계하는 방향을 택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 레포는 아래에서 보실 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/sanguneo/inkrkamus&quot;&gt;https://github.com/sanguneo/inkrkamus&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1776238943493&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - sanguneo/inkrkamus&quot; data-og-description=&quot;Contribute to sanguneo/inkrkamus development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/sanguneo/inkrkamus&quot; data-og-url=&quot;https://github.com/sanguneo/inkrkamus&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://github.com/sanguneo/inkrkamus&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/sanguneo/inkrkamus&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - sanguneo/inkrkamus&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to sanguneo/inkrkamus development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 버전은 React + Vite + TypeScript + IndexedDB 조합으로 다시 만들었습니다. Electron 의존성을 덜고, 정적 호스팅에 맞는 구조로 바꾸었으며, 로컬 IndexedDB를 먼저 읽어 즉시 화면을 띄운 뒤 뒤에서 최신 데이터를 갱신하는 방식으로 정리했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 화면은 로컬 데이터로 빠르게 띄우기&lt;/li&gt;
&lt;li&gt;재방문은 IndexedDB 캐시로 더 빠르게 처리하기&lt;/li&gt;
&lt;li&gt;원격 데이터 갱신은 백그라운드에서 조용히 반영하기&lt;/li&gt;
&lt;li&gt;검색과 리스트는 캐시, 정규화, 가상 스크롤로 가볍게 유지하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 흐름은 이전 글에서도 이어서 보실 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;v1 소개: &lt;a href=&quot;https://blog.sanguneo.com/35&quot;&gt;https://blog.sanguneo.com/35&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;v2 소개: &lt;a href=&quot;https://blog.sanguneo.com/38&quot;&gt;https://blog.sanguneo.com/38&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 이번 v3는 이전 버전을 단순히 보강한 것이 아니라, 오래된 구조를 정리하고 다시 출발하는 작업이었습니다. 예전 버전들이 없었다면 지금의 v3도 없었을 것이고, 그런 의미에서 이 프로젝트는 계속 이어져 온 제 작업 기록이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;사설&lt;/h2&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 제가 무엇을 위해 개발을 하는지를 한번씩 생각하게 해주는듯 합니다. 지금은 대기업에서 워낙 사전을 잘 만들어놓았고, AI한테 물어보면 인도네시아 어 뿐아니라 자바어도, 수마트라어도, 아주 깔끔하게 잘 나오더라구요. 저부터도 생각안나는 단어 있으면 inkrkamus보다 ai한테 물어보니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 저는 계속 이 프로젝트를 유지하려고 합니다. 앞으로 새로운 뭔가가 나오거나 하면 계속 또 뚝딱뚝딱 만지겠죠, 그리고 내가 왜 개발을 하는지 다시한번 상기할거같습니다. 앞으로도 많은 관심 부탁드립니다 :)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>Develop/inKrKamus</category>
      <category>Dexie</category>
      <category>IndexedDB</category>
      <category>InKrKamus</category>
      <category>React</category>
      <category>Vite</category>
      <category>개발기</category>
      <category>사전</category>
      <category>오프라인</category>
      <category>인한사전</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/68</guid>
      <comments>https://smokingbird.tistory.com/68#entry68comment</comments>
      <pubDate>Wed, 15 Apr 2026 16:21:26 +0900</pubDate>
    </item>
    <item>
      <title>OpenClaw 에이전트로 민방위 인터넷 교육 이수</title>
      <link>https://smokingbird.tistory.com/67</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;OpenClaw 에이전트를 실제 브라우저 작업에 연결해 처음 제대로 검증해본 사례가 있었다. 테스트 페이지 수준이 아니라, 실제로 수행해야 하는 작업에 에이전트를 붙여본 것이다. 이번 대상은 민방위 인터넷 교육 이수였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 작업은 단순 클릭 자동화가 아니라, 브라우저 세션 상태 유지, 화면 전환, 진행 상황 확인이 모두 필요한 흐름이었다. 즉, 에이전트가 &quot;작동한다&quot;는 것과 &quot;실제 작업에 쓸 수 있다&quot;는 것은 다르다는 걸 확인하기에 적절한 사례였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WSL2 환경과 브라우저 연결 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 환경은 WSL2였다. 문제는 WSL2가 윈도우 호스트와 분리된 실행 환경이라는 점이다. 이 때문에 에이전트가 로컬 브라우저 인스턴스에 바로 연결되기 어렵고, 별도의 브리지 구성이 필요했다. WSL내부에서도 동일하게 크롬을 깔고 실행할 수 있지만 굳이 추가로 설치하고싶지도 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 사용한 방식은 PowerShell 기반 포트포워딩과 CDP(Chrome DevTools Protocol) 연결이다. 즉, 윈도우에서 실행 중인 브라우저 디버그 포트를 WSL2 쪽에서 접근 가능하도록 열어두고, 에이전트가 그 세션에 붙도록 만든 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;headful 브라우저 기반 검증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민방위 인터넷 교육은 완전 비가시성 자동화로 끝낼 수 있는 작업이 아니었다. 직접 콘텐츠를 확인해야 하고, 진행 상태도 사람이 봐야 했기 때문에 headless 로는 진행하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;headful 모드의 장점은 명확하다. 에이전트가 어떤 URL에 있는지, 어떤 요소를 조작하는지, 다음 단계로 정상적으로 전환됐는지를 화면에서 바로 확인할 수 있다. 이번 작업에서는 이 가시성이 매우 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 브라우저를 띄워둔 상태에서 에이전트의 동작을 지켜보는 방식으로 진행했다. 백그라운드에서 무작정 돌리는 형태가 아니라, 중간중간 UI 상태와 흐름을 확인하면서 수행했다. + 교육을 듣기는 해야하니까.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능과 반응성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체감상 가장 아쉬웠던 부분은 반응 속도였다. 윈도우-WSL2 환경 특성상 응답이 다소 느렸고, 아직 브라우저 자동화 작업에 대해 최적화가 충분히 이루어지지 않아 불필요한 대기 시간이 꽤 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 단순 성능 문제라기보다 작업 파이프라인의 마찰에 가까웠다. 에이전트가 명령을 받아도 즉시 다음 단계로 이어지지 않는 구간들이 있었고, 그만큼 전체 체감 속도가 떨어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 과정 덕분에 병목이 어디서 생기는지는 명확히 보였다. 브라우저 연결 자체보다는 환경 지연, 세션 전환, 동기화 타이밍이 중요한 포인트였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;확인한 점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WSL2 환경에서는 브라우저 자동화 연결을 별도로 설계해야 한다&lt;/li&gt;
&lt;li&gt;PowerShell 포트포워딩 + CDP 연결이 실사용에 유효하다&lt;/li&gt;
&lt;li&gt;headful 모드에서 상태 확인이 필요한 작업은 에이전트 검증에 적합하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로, 에이전트는 실제 브라우저 기반 업무에 붙일 수 있는 가능성을 보여줬다. 다만 완성도는 환경 최적화와 연결 구조 정리에 크게 좌우된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 계획&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 진행 중인 제주도 여행 계획 짜기에도 같은 흐름을 적용해볼 생각이다. 여행 계획은 여러 웹 페이지를 오가며 정보를 수집하고 비교해야 해서, 브라우저 기반 에이전트의 활용도를 확인하기 좋은 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 민방위 교육이 &quot;연결과 검증&quot;의 첫 실전이었다면, 제주도 여행 계획은 &quot;실사용성&quot;을 보는 다음 단계가 될 것이다.&lt;/p&gt;</description>
      <category>Study/AI</category>
      <category>CDP</category>
      <category>Hermes</category>
      <category>openclaw</category>
      <category>wsl2</category>
      <category>민방위</category>
      <category>브라우저자동화</category>
      <category>에이전트</category>
      <category>제주도여행</category>
      <author>상구너</author>
      <guid isPermaLink="true">https://smokingbird.tistory.com/67</guid>
      <comments>https://smokingbird.tistory.com/67#entry67comment</comments>
      <pubDate>Wed, 15 Apr 2026 15:39:04 +0900</pubDate>
    </item>
  </channel>
</rss>