<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>삽질 로그</title>
    <link>https://write83676.tistory.com/</link>
    <description>직접 겪고 씁니다 &amp;mdash; 맥미니 한 대로 자동화&amp;middot;AI&amp;middot;홈서버를 굴리며 남기는 시행착오 기록</description>
    <language>ko</language>
    <pubDate>Tue, 25 Aug 2026 02:28:55 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>홈랩지기</managingEditor>
    <image>
      <title>삽질 로그</title>
      <url>https://tistory1.daumcdn.net/tistory/8972294/attach/8f3252b4f4a6429b95e901e0c149fdc2</url>
      <link>https://write83676.tistory.com</link>
    </image>
    <item>
      <title>AI 제목 생성: 프롬프트 예시가 문제였다, SEO 최적화 해결</title>
      <link>https://write83676.tistory.com/11</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cover.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yOgln/dJMcabMfOpH/kzveNj2K9LYX12gHrkG0c0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yOgln/dJMcabMfOpH/kzveNj2K9LYX12gHrkG0c0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yOgln/dJMcabMfOpH/kzveNj2K9LYX12gHrkG0c0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyOgln%2FdJMcabMfOpH%2FkzveNj2K9LYX12gHrkG0c0%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;1200&quot; height=&quot;630&quot; data-filename=&quot;cover.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 자동화 블로그 &lt;code&gt;https://write83676.tistory.com&lt;/code&gt;는 애드센스 승인을 목표로 돌리는 중이다. &lt;code&gt;automoney&lt;/code&gt; 프로젝트의 핵심 파이프라인 중 하나인데, 발행 목표 30개 중 이제 두세 개 겨우 올렸을 때부터 이상한 낌새를 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매일 아침 9시 30분, &lt;code&gt;daily_run.py&lt;/code&gt; 스크립트가 백로그 소재를 가져와 글을 만들고 제목까지 뽑아 발행하는데, 제목이 매번 묘하게 비슷했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AI 제목, 왜 자꾸 &quot;쉬울 줄 알았다&quot;고 할까?  &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &quot;쉬울 줄 알았다&quot; 같은 표현이 계속 튀어나왔다. 'Playwright 삽질기, 쉬울 줄 알았는데&amp;hellip;', '로컬 LLM 설치, 쉬울 줄 알았는데&amp;hellip;' 이런 식이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 내가 우연히 비슷한 주제를 다뤘나 싶었다. 아니면 AI가 요즘 유행하는 밈(meme)을 따라 하는 건가? 그냥 넘어갈까도 했지만, 블로그는 '삽질 로그'라는 정체성을 가지고 솔직한 경험을 담는 곳이다. 클리셰는 안 맞았다.&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;한 일주일쯤 지났을까. 5개 정도의 글 제목에서 이 반복적인 패턴이 보였다. 이건 더 이상 우연이 아니었다. 분명 어딘가 문제가 있었다. 파이프라인의 핵심은 프롬프트 엔지니어링인데, 이걸 내버려 둘 수는 없었다. 그날 오후는 프롬프트 디버깅에 온전히 쏟았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;삽질의 원인은 의외의 곳에: 프롬프트 예시였다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 제목 생성 프롬프트에 '반복적인 표현을 사용하지 마', '더 창의적으로 작성해' 같은 지시어를 추가해봤다. 이런 일반적인 지시는 늘 그렇듯 큰 효과가 없었다. AI는 주어진 지시를 곧이곧대로 듣지만, 본질적인 '왜'를 이해하지 못하는 경우가 많다. 이 과정에서 두세 시간은 족히 보낸 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭘 시도하든 끈질기게 '쉬울 줄 알았다' 류의 제목이 나왔다. 어떤 때는 단어만 살짝 바꿔 '쉽다고 생각했는데&amp;hellip;' 같은 식으로 변주하기도 했다.&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;b&gt;문제의 원인이 지시문이 아니라 '예시'에 있을 수도 있다는 생각이 번개처럼 스쳤다.&lt;/b&gt; AI 모델은 주어진 예시를 모방하는 데 탁월한 능력을 보인다. 그리고 내 예상은 틀리지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목 생성 프롬프트의 예시 목록에, 내가 직접 작성해 넣었던 과거 블로그 글 제목들이 있었다. 거기에는 &quot;맥미니 자동화, 쉬울 줄 알았는데 꿀팁!&quot; 같은 표현이 대놓고 들어가 있었다. 내가 스스로 삽질을 유도한 셈이었다. &lt;b&gt;AI는 내가 넣어준 '좋은 예시'를 충실히 따른 것뿐이었다.&lt;/b&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;그제야 다른 문제도 눈에 들어왔다. 검색 키워드가 제목 뒤쪽에 밀려 있었다. 예를 들어 '로컬 LLM, 쉽게 배울 줄 알았는데 허수아비 춤을 추네' 같은 제목은 '로컬 LLM'이라는 키워드가 앞에 있지만, 그 뒤로 감성적인 표현이 너무 길게 붙어 정작 중요한 정보가 가려졌다. 검색 엔진은 보통 제목의 앞부분을 더 중요하게 본다. 모바일에서 제목이 잘리면 키워드 노출도 불리해진다. 이건 SEO(검색 엔진 최적화) 관점에서 치명적인 약점이었다. 애드센스 승인을 위해서는 검색 유입도 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인을 찾았으니, 해결책은 명확했다.&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;b&gt;1. 예시 교체:&lt;/b&gt; 프롬프트에 있던 모든 클리셰 예시를 싹 지우고, 간결하고 키워드 중심적인 제목 예시들로 교체했다. 예전처럼 '감성팔이'는 최대한 배제했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 키워드 맨 앞으로:&lt;/b&gt; 제목 생성 규칙에 &quot;핵심 검색 키워드는 제목 맨 앞에 배치하라&quot;는 지시를 명확하게 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 40자 제한:&lt;/b&gt; 모바일 환경에서의 가독성과 SEO를 고려해 제목을 40자로 제한하는 규칙을 넣었다. 40자는 모바일에서 잘리지 않고, 한눈에 내용을 파악하기 좋은 길이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 상투구 금지:&lt;/b&gt; &quot;쉬울 줄 알았다&quot;, &quot;알고 보니&quot; 같은 특정 상투적인 표현들을 명시적으로 금지하는 규칙을 추가했다.&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;code&gt;daily_run.py&lt;/code&gt;를 다시 돌렸다. 몇 번의 테스트 끝에, 이제는 '로컬 LLM: Qwen Heretic으로 개발 환경 구축하기', '맥미니 자동화: 반복 작업 5분 단축 스크립트'와 같은 제목들이 나왔다. 클리셰는 사라졌고, 키워드도 제목의 핵심 위치에 자리 잡았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;terminal.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDcS1A/dJMcag7HsIe/EhKVNRifgFlu2xydVVldz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDcS1A/dJMcag7HsIe/EhKVNRifgFlu2xydVVldz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDcS1A/dJMcag7HsIe/EhKVNRifgFlu2xydVVldz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDcS1A%2FdJMcag7HsIe%2FEhKVNRifgFlu2xydVVldz1%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;1100&quot; height=&quot;470&quot; data-filename=&quot;terminal.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;470&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AI는 역시 예시를 따라한다, 계속 점검해야  &lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경험을 통해 AI 모델은 우리가 생각하는 것 이상으로 프롬프트의 '예시'에 민감하다는 걸 다시 한번 깨달았다. 명시적인 지시어만큼이나 '보여주는' 예시가 중요하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;파이프라인은 한번 만들었다고 끝이 아니라는 것. 계속해서 내가 원하는 결과물을 내는지 점검하고, 필요하면 파이프라인의 설계도를 들여다보고 수정해야 한다는 점이다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #888; font-size: 0.9em;&quot; data-ke-size=&quot;size16&quot;&gt;※ 이 글은 직접 겪은 작업 경험을 바탕으로 AI의 도움을 받아 작성했습니다.&lt;/p&gt;</description>
      <category>AI&amp;middot;로컬 LLM</category>
      <category>ai 제목 생성</category>
      <category>SEO</category>
      <category>클리셰 제목</category>
      <category>티스토리 자동 발행</category>
      <category>프롬프트 엔지니어링</category>
      <author>홈랩지기</author>
      <guid isPermaLink="true">https://write83676.tistory.com/11</guid>
      <comments>https://write83676.tistory.com/11#entry11comment</comments>
      <pubDate>Mon, 24 Aug 2026 09:47:19 +0900</pubDate>
    </item>
    <item>
      <title>LLM 지식 그래프 오염: PKM 자동화 시스템 복구 경험</title>
      <link>https://write83676.tistory.com/10</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cover.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WjQpa/dJMcagUdUuC/KzNZZKKICKdVGXk96x4fx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WjQpa/dJMcagUdUuC/KzNZZKKICKdVGXk96x4fx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WjQpa/dJMcagUdUuC/KzNZZKKICKdVGXk96x4fx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWjQpa%2FdJMcagUdUuC%2FKzNZZKKICKdVGXk96x4fx1%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;1200&quot; height=&quot;630&quot; data-filename=&quot;cover.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;automoney&lt;/code&gt; 프로젝트를 시작하며 개인 지식관리 시스템(PKM)을 자동화하는 데 공을 많이 들였다. 궁극적인 목표는 내가 직접 개입하지 않아도 콘텐츠가 생성되고, 관리되고, 발행되는 파이프라인을 구축하는 거였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸로 &lt;a href=&quot;https://write83676.tistory.com&quot;&gt;https://write83676.tistory.com&lt;/a&gt;에 글을 30개 채워 애드센스 승인을 받는 게 당면 과제였다. 이미 #5 라다 스왑 삽질기, #6 플레이라이트 경험 노트 등 몇 개의 글을 발행했지만, 그 뒤에는 복잡한 시스템이 돌아가고 있었다. 로컬 LLM이 자료를 조사하고, 정리하고, 지식 그래프를 업데이트하는 과정이 매일 오전 9시 30분 &lt;code&gt;cron&lt;/code&gt;에 맞춰 &lt;code&gt;publish/daily_run.py&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;내 옵시디언(Obsidian) 노트는 마치 살아있는 유기체처럼 스스로 진화하는 듯했다. 내가 직접 신경 쓸 필요 없이, 시스템이 알아서 지식을 큐레이션하고 연결해준다는 생각에 뿌듯했다.&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;마치 잘 정리된 도서관에 누군가 쓰레기 더미를 던져 넣고 책장 간의 연결을 뒤죽박죽 만들어 놓은 기분이었다. 처음엔 대수롭지 않게 여겼다. 가끔 LLM이 헛소리를 할 때도 있으니, 일시적인 오류겠거니 했다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 시간이 지날수록 그 오염은 심각해졌다. &lt;code&gt;daily_run.py&lt;/code&gt;가 돌 때마다 지식 그래프는 점점 더 복잡하고 엉망진창이 됐다. 유효한 정보보다 알맹이 없는 '스텁 개체'들이 그래프를 가득 채웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스텁 개체는 지식 그래프에서 특정 개념을 나타내는 빈 카드 같은 건데, 여기에 내용이 없으니 마치 제목만 있고 내용은 없는 위키피디아 페이지 수천 개가 내 시스템에 생긴 꼴이었다. 게다가 이 빈 카드들이 다른 유효한 카드와 엉뚱하게 연결되면서, 정보의 흐름을 완전히 방해했다. 관련 자료를 찾으려 해도 엉뚱한 곳으로 안내되니, &lt;b&gt;자동화 시스템의 의미가 퇴색되는 순간이었다.&lt;/b&gt;&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;p data-ke-size=&quot;size16&quot;&gt;몇 시간을 헤매고 나서야 이건 단순한 시스템 오류가 아니라, 내 자동화 로직 어딘가에 심각한 결함이 있다는 걸 직감했다. 스크립트 로그를 뒤지기 시작했다. &lt;code&gt;daily_run.py&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;terminal.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tuSur/dJMcagUdUuD/WdnTadJbcnzYF0cRrkk9QK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tuSur/dJMcagUdUuD/WdnTadJbcnzYF0cRrkk9QK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tuSur/dJMcagUdUuD/WdnTadJbcnzYF0cRrkk9QK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtuSur%2FdJMcagUdUuD%2FWdnTadJbcnzYF0cRrkk9QK%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;1100&quot; height=&quot;436&quot; data-filename=&quot;terminal.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;/figure&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;b&gt;개체 노트 미생성&lt;/b&gt; 문제는 LLM이 특정 개념을 추출했는데, 그 개념에 대한 실제 옵시디언 노트가 제대로 생성되지 않은 경우였다. 지식 그래프에는 '이런 개념이 있다'는 노드(Node)만 생기고, 그 노드가 실제 가리켜야 할 노트 파일은 없는 셈이었다. 당연히 링크를 따라가도 아무것도 나오지 않았다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;별칭 미등록&lt;/b&gt;은 같은 개념인데 다른 이름으로 불리는 경우(예: 'LLM'과 '대규모 언어 모델') 시스템이 한쪽만 인식하고 다른 쪽은 놓치는 경우가 발생했다. 이 때문에 지식 그래프상에서는 별개의 개념으로 인식되어 연결이 끊기거나, 혹은 연결돼야 할 곳에 제대로 연결되지 않는 문제가 생겼다. 별칭 처리가 중요한데, 이를 간과한 거였다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;링크 목표 중복&lt;/b&gt;은 여러 개의 개념이 동일한 목표 링크를 가지게 되는 경우가 있었다. 예를 들어, 서로 다른 두 문서가 'AI 에이전트'라는 동일한 노드를 참조하게 되면서, 시스템 내부에서 어떤 문서가 실제 'AI 에이전트'의 메인 문서인지 혼동하는 상황이었다. 이는 데이터베이스의 유니크 제약 조건을 무시하고 데이터를 넣으려 할 때 발생할 수 있는 문제와 비슷했다. 마치 두 사람이 같은 주민등록번호를 가졌다고 착각하는 셈이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;잡개체 필터 부재&lt;/b&gt;는 LLM이 지식을 추출하는 과정에서 불필요하거나 임시적인 '잡개체(junk object)'까지 모두 지식 그래프에 반영해버렸다. 예를 들어, 단순한 수사적 표현이나 문맥상 의미 없는 단어들이 개념으로 추출되어 독립적인 노드로 박히는 식이었다. 이런 잡개체들이 그래프에 쌓이니 지식의 밀도는 낮아지고, 노이즈만 가득해졌다. SearXNG로 주간 화제를 수집하는 &lt;code&gt;publish/trends.py&lt;/code&gt;에서 불필요한 키워드가 걸러지지 않고 그대로 유입되는 문제도 있었다. SearXNG는 쿼리 간 1.5초 간격으로 호출하며 rate limit에 걸리지 않도록 조심했지만, 그래도 LLM은 때때로 의도치 않은 결과를 만들어냈다.&lt;/li&gt;
&lt;/ul&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;code&gt;daily_run.py&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;&lt;b&gt;개체 노트 미생성&lt;/b&gt; 문제는 노트 생성 전에 해당 개념의 유효성을 다시 한번 검증하고, 노트가 생성되지 않았을 경우 강제로 재생성하거나 경고를 띄우도록 했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;별칭 미등록&lt;/b&gt;은 LLM이 생성한 별칭 목록을 확인하고, 기존 지식 그래프의 별칭과 비교하여 누락된 부분을 채워 넣는 로직을 추가했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;링크 목표 중복&lt;/b&gt;은 새로운 링크를 추가하기 전에 기존 그래프에서 동일한 목표를 가진 링크가 있는지 확인하고, 있다면 기존 링크를 업데이트하거나 적절한 병합 로직을 적용하도록 수정했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;잡개체 필터 부재&lt;/b&gt;는 LLM의 출력물에서 불필요한 개체를 걸러내는 정규식 기반의 필터링 모듈을 추가했다. 특정 단어 길이나 문맥 패턴을 기준으로 필터링하니, 훨씬 깔끔한 지식 그래프가 만들어졌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요한 건, 이미 오염된 과거 데이터를 어떻게 할 것인가였다. 단순히 코드만 수정해서는 지식 그래프에 쌓인 수많은 쓰레기가 사라지지 않았다. 그래서 &lt;code&gt;daily_run.py&lt;/code&gt; 스크립트에 &lt;code&gt;--migrate&lt;/code&gt;라는 마이그레이션 플래그를 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 플래그를 붙여 스크립트를 실행하면, 기존에 저장된 모든 지식 그래프 데이터를 처음부터 다시 스캔하여 위에 언급된 네 가지 문제를 기준으로 클리닝 작업을 수행하도록 했다. 마치 엉망진창이 된 도서관을 폐쇄하고, 모든 책을 다시 분류하고 정리하는 대규모 작업과 비슷했다. 이 작업은 꼬박 12시간 가까이 걸렸지만, 깨끗해진 그래프를 보니 속이 다 시원했다. ✨&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;code&gt;daily_run.py&lt;/code&gt;에는 &lt;code&gt;publish/trends.py&lt;/code&gt;를 통해 수집된 최신 트렌드를 반영하거나, 블로그 발행을 위한 미러링 작업 등 여러 부가 기능이 포함되어 있었다. 이 기능들이 &lt;code&gt;파일 변경 감지&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;이는 매일 오전 9시 30분에 &lt;code&gt;cron&lt;/code&gt;으로 스크립트가 실행되는 것과는 별개로, 스크립트 &lt;i&gt;내부&lt;/i&gt;의 특정 감시 루프가 파일 변경 이벤트를 기다리는 로직으로 설계되었기 때문이었다. 아무런 입력이 없으면 그저 대기 상태로 유지되는 것이다. 이런 상황을 막기 위해 감시 루프에 주기 실행 로직을 병행하도록 수정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 변경이 없더라도 최소한 하루에 한 번은 중요한 미러링이나 트렌드 업데이트 작업을 강제로 실행하게 만든 것이다. 예를 들어, 0.1%의 미러링 누락이 1시간에 3.6초라면, 한 달이면 거의 3분에 가까운 누락이 생기는 셈이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;이런 미세한 차이가 결국 전체 시스템의 신뢰도를 갉아먹는다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 경험을 통해 데이터 무결성의 중요성과 시스템 설계의 견고함에 대해 다시 한번 생각하게 됐다. 특히 AI를 활용한 자동화 시스템에서는 &lt;b&gt;LLM의 유연성 뒤에 숨어있는 데이터 오염 가능성을 늘 염두에 두어야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 단순히 기능을 구현하는 것을 넘어, 시스템이 예상치 못한 상황에서도 안정적으로 동작하도록 다양한 예외 상황과 엣지 케이스를 고려하는 것이 중요하다. 어쨌든 이 삽질 덕분에 &lt;code&gt;automoney&lt;/code&gt; 프로젝트는 한층 더 단단해졌다.&lt;/p&gt;
&lt;p style=&quot;color: #888; font-size: 0.9em;&quot; data-ke-size=&quot;size16&quot;&gt;※ 이 글은 직접 겪은 작업 경험을 바탕으로 AI의 도움을 받아 작성했습니다.&lt;/p&gt;</description>
      <category>AI&amp;middot;로컬 LLM</category>
      <category>automoney</category>
      <category>LLM</category>
      <category>OBSIDIAN</category>
      <category>pkm</category>
      <category>데이터 무결성</category>
      <category>지식 그래프</category>
      <author>홈랩지기</author>
      <guid isPermaLink="true">https://write83676.tistory.com/10</guid>
      <comments>https://write83676.tistory.com/10#entry10comment</comments>
      <pubDate>Sat, 22 Aug 2026 13:25:36 +0900</pubDate>
    </item>
    <item>
      <title>삼성 990 PRO 가짜 SSD, 외장 케이스 인식 못 한 이유</title>
      <link>https://write83676.tistory.com/9</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cover.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCodCW/dJMcadXtik1/MuK29qzBuLkcBpfEzTke1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCodCW/dJMcadXtik1/MuK29qzBuLkcBpfEzTke1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCodCW/dJMcadXtik1/MuK29qzBuLkcBpfEzTke1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCodCW%2FdJMcadXtik1%2FMuK29qzBuLkcBpfEzTke1K%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;1200&quot; height=&quot;630&quot; data-filename=&quot;cover.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼마 전, 중고 마켓에서 삼성 990 PRO 1TB를 좋은 가격에 찾았다. 늘 그렇듯 작업용 외장 스토리지로 쓸 요량이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집에 남아있던 ORICO USB4 외장 케이스에 물릴 생각이었다. 이 케이스는 USB4 40Gbps에 ASMedia ASM2464 컨트롤러를 쓰는 꽤 괜찮은 녀석이었다. 20mm 팬도 달려있고, 최대 8TB까지 지원한다고 해서 망설임 없이 선택했다.&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;SSD 인식이 안 돼서 뜯어봤더니...  &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 막상 조립하고 보니 인식이 안 되는 문제에 부딪혔다. 처음엔 케이스가 불량인가 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외장 케이스는 새 제품이었고, SSD도 판매자가 정상이라고 했으니 둘 중 하나겠지. 드라이버로 케이스를 열고 닫고, SSD를 다시 뺐다 꽂기를 몇 번. 이리저리 끼워봐도 맥북은 아무것도 찾지 못했다. 잠깐 씨름하다 문득 SSD 기판을 자세히 보게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거기서 이상한 점을 발견했다. 내가 산 990 PRO는 기판의 접점 부분이 세 덩이로 나뉘어 있었다. 흔히 말하는 B+M키 방식이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 키 홈이 두 개라는 뜻이다. 하지만 진품 삼성 990 PRO는 M키 방식이라 홈이 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;바로 이걸로 확신했다. 뭔가 잘못됐다. 내가 받은 SSD는 가짜였다.&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;1.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;2000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxLmKp/dJMcaixKg3t/4CBhzFpumHG1IRKLCWADvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxLmKp/dJMcaixKg3t/4CBhzFpumHG1IRKLCWADvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxLmKp/dJMcaixKg3t/4CBhzFpumHG1IRKLCWADvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxLmKp%2FdJMcaixKg3t%2F4CBhzFpumHG1IRKLCWADvk%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;327&quot; height=&quot;436&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;2000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가짜 SSD의 증거들은 또 왜 이렇게 많아?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때부터 여러 증거들이 눈에 들어오기 시작했다. 라벨에는 &lt;code&gt;MZ-V8P1TO&lt;/code&gt;라고 적혀 있었는데, 990 PRO 정품은 &lt;code&gt;MZ-V9P&lt;/code&gt;로 시작하는 모델명이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게다가 마지막 글자도 숫자 0이 아닌 영문 O였다. 같은 라벨에 &lt;code&gt;MZ-V8P1TO&lt;/code&gt;와 &lt;code&gt;RSEC-MZ-V9P1080&lt;/code&gt;이 동시에 적혀 있는 모순도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브랜드 표기는 &lt;code&gt;SAMSUNG&lt;/code&gt;이 아니라 &lt;code&gt;V-NAND SSD&lt;/code&gt;라는 정체불명의 문구였고, &lt;code&gt;PNMZVL21TOHCRMODEL&lt;/code&gt;, &lt;code&gt;RFTE DC+3.3V-2.9A&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;&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;결국 이 SSD는 가짜였다. 그런데 문제는 또 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 가짜 SSD가 설령 SATA 방식이라고 해도 내 ORICO 케이스에는 인식이 될 수 없다는 사실을 뒤늦게 깨달은 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 케이스 상품 설명에는 'B+M키 지원'이라고 되어 있었다. 하지만 여기서 말하는 '지원'은 물리적으로 꽂을 수 있다는 뜻이지, SATA 프로토콜을 지원한다는 뜻이 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 케이스는 NVMe 전용이었다. SATA SSD는 애초에 NVMe 케이스에서 작동하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마치 USB-A 포트에 USB-C 케이블을 꽂을 수는 있지만, 작동하려면 컨버터가 필요하거나 애초에 같은 규격이어야 하는 것과 비슷하다고 보면 된다.&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;이런 외장 케이스의 NVMe/SATA 겸용 여부는 마케팅 문구보다는 내부 컨트롤러 칩을 확인하는 게 정확하다는 걸 배웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;RTL9210B&lt;/code&gt; 칩셋을 쓰면 겸용이고, &lt;code&gt;JMS583&lt;/code&gt;은 NVMe 전용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 ORICO 케이스에 들어있는 &lt;code&gt;ASMedia ASM2464&lt;/code&gt;는 당연히 NVMe 전용이었다. 그러니 가짜 SSD가 SATA 방식이었다면, 원래부터 케이스에서 인식이 불가능했던 셈이다. 이중으로 허탕을 친 기분이었다.&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;code&gt;f3&lt;/code&gt; 같은 전수 쓰기 검사를 돌려봐야 확실히 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 1TB라고 팔았는데 실제로는 128GB만 진짜고 나머지는 계속 덮어쓰는 식이다.&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;중고 거래에서 육안으로 확인할 수 있는 최소한의 물리적 특징이라도 꼭 확인해야 한다는 점을 다시금 깨달았다. 그리고 '지원'이라는 마케팅 문구 하나에도 여러 함정이 있다는 걸 기억해야 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #888; font-size: 0.9em;&quot; data-ke-size=&quot;size16&quot;&gt;※ 이 글은 직접 겪은 작업 경험을 바탕으로 AI의 도움을 받아 작성했습니다.&lt;/p&gt;</description>
      <category>맥&amp;middot;홈서버</category>
      <category>ASM2464</category>
      <category>NVMe 케이스</category>
      <category>Orico</category>
      <category>가짜 SSD</category>
      <category>삼성 990 pro</category>
      <category>외장 SSD</category>
      <author>홈랩지기</author>
      <guid isPermaLink="true">https://write83676.tistory.com/9</guid>
      <comments>https://write83676.tistory.com/9#entry9comment</comments>
      <pubDate>Fri, 21 Aug 2026 10:25:02 +0900</pubDate>
    </item>
    <item>
      <title>Tailscale userspace-networking: 관리자 권한 없는 Mac 원격 접속</title>
      <link>https://write83676.tistory.com/8</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cover.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u8eqr/dJMcah6HrRt/DZPU2zXu9t6lxkdJ2Ye2KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u8eqr/dJMcah6HrRt/DZPU2zXu9t6lxkdJ2Ye2KK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u8eqr/dJMcah6HrRt/DZPU2zXu9t6lxkdJ2Ye2KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu8eqr%2FdJMcah6HrRt%2FDZPU2zXu9t6lxkdJ2Ye2KK%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;1200&quot; height=&quot;630&quot; data-filename=&quot;cover.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&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;보안 팀에 VPN 요청해봐야 절차만 복잡해질 게 뻔하고, 개인용으로 뭔가 설치하고 싶어도 &lt;code&gt;sudo&lt;/code&gt; 비밀번호가 없으니 답답할 노릇. 이래저래 궁리하다가 Tailscale의 &lt;code&gt;userspace-networking&lt;/code&gt; 모드가 눈에 들어왔다. 관리자 권한 없이 일반 사용자 영역에서 VPN처럼 작동할 수 있다는 말에 솔깃했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;관리자 권한 없는 Mac, Tailscale이 답일까?  &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, &lt;code&gt;brew install tailscale&lt;/code&gt;로 CLI 버전을 설치했다. GUI 앱은 &lt;code&gt;cask&lt;/code&gt;로 설치해야 하는데, 그건 또 &lt;code&gt;sudo&lt;/code&gt;가 필요했다. CLI 설치는 다행히 권한 문제 없이 잘 넘어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 그 다음부터였다. &lt;code&gt;tailscaled&lt;/code&gt;를 실행해야 했는데, 기본적으로는 시스템 서비스로 등록되어야 하지만, &lt;code&gt;userspace-networking&lt;/code&gt; 모드는 수동으로 돌려야 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;userspace-networking&lt;/code&gt; 모드로 &lt;code&gt;tailscaled&lt;/code&gt; 백그라운드 실행하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 다음 명령으로 &lt;code&gt;tailscaled&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;terminal.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kFcsv/dJMcaasVYSf/hsbsJL8gZ6HaqiyVeN5PD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kFcsv/dJMcaasVYSf/hsbsJL8gZ6HaqiyVeN5PD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kFcsv/dJMcaasVYSf/hsbsJL8gZ6HaqiyVeN5PD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkFcsv%2FdJMcaasVYSf%2FhsbsJL8gZ6HaqiyVeN5PD0%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;1100&quot; height=&quot;640&quot; data-filename=&quot;terminal.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;nohup tailscaled --tun=userspace-networking --statedir=$HOME/.tailscale --socket=$HOME/.tailscale/tailscaled.sock &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;&lt;code&gt;statedir&lt;/code&gt;와 &lt;code&gt;socket&lt;/code&gt; 경로를 내 홈 디렉토리 밑으로 지정하는 게 핵심이었다.&lt;/b&gt; &lt;code&gt;nohup&lt;/code&gt;으로 백그라운드에 띄웠으니 터미널을 닫아도 돌아가긴 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 문제는 &lt;code&gt;launchd&lt;/code&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;Tailscale CLI, 왜 자꾸 소켓 파일을 못 찾지?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;tailscaled&lt;/code&gt;는 일단 돌아가니, 이제 &lt;code&gt;tailscale&lt;/code&gt; CLI 명령으로 상태를 확인하거나 설정을 바꿔야 했다. &lt;code&gt;tailscale status&lt;/code&gt; 같은 평범한 명령을 쳤는데 계속 에러가 떴다. &quot;&lt;code&gt;소켓 파일을 찾을 수 없다&lt;/code&gt;&quot;는 식이었다. 순간 당황했다. 분명 백그라운드에서 돌아가고 있는데 왜?  &amp;zwj; &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고 보니 &lt;code&gt;tailscale&lt;/code&gt; CLI는 기본적으로 &lt;code&gt;/var/run/tailscale/tailscaled.sock&lt;/code&gt; 같은 시스템 기본 경로에서 소켓 파일을 찾도록 되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;code&gt;userspace-networking&lt;/code&gt; 모드에서는 소켓 파일이 내 홈 디렉토리인 &lt;code&gt;$HOME/.tailscale/tailscaled.sock&lt;/code&gt;에 생성된 것이다. CLI가 이 경로를 자동으로 유추하지 못하니 매번 &lt;code&gt;--socket&lt;/code&gt; 플래그로 명시해줘야 했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;tailscale --socket=$HOME/.tailscale/tailscaled.sock status
tailscale --socket=$HOME/.tailscale/tailscaled.sock ip
tailscale --socket=$HOME/.tailscale/tailscaled.sock set --ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 긴 경로를 매번 타이핑하는 게 여간 귀찮은 일이 아니었다. &lt;code&gt;tailscale status&lt;/code&gt;를 하루에 다섯 번만 쳐도 꽤 시간이 걸리는 일이다. 명령어 뒤에 매번 저 옵션을 붙여야 한다는 게 솔직히 좀 짜증 났다. 내가 원하는 건 그냥 &lt;code&gt;tailscale status&lt;/code&gt; 한 방이었는데 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 맥의 hostname은 &lt;code&gt;me-mac&lt;/code&gt;이고, Tailscale IP는 &lt;code&gt;100.119.37.84&lt;/code&gt;로 잘 할당되었다. 내 계정 &lt;code&gt;inme&lt;/code&gt;으로 기존 tailnet에 &lt;code&gt;desktop-putfua9&lt;/code&gt;와 &lt;code&gt;me-macmini&lt;/code&gt;가 이미 등록되어 있었다. 로그는 &lt;code&gt;~/.tailscale/tailscaled.log&lt;/code&gt;에 남았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;userspace 모드의 치명적인 한계: SOCKS5 프록시 방식  &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tailscale 내장 SSH 기능도 &lt;code&gt;tailscale set --ssh&lt;/code&gt;로 켜봤다. 갤럭시 탭(&lt;code&gt;tab-s9-fe&lt;/code&gt;, &lt;code&gt;100.95.145.34&lt;/code&gt;)에서 &lt;code&gt;me@100.119.37.84&lt;/code&gt;로 접속하니 맥의 SSH가 잘 붙었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;macOS 원격 로그인을 따로 열어줄 필요가 없어져서 이 점은 아주 만족스러웠다. 다만, 맥 자신에서 자기 Tailscale IP로 접속 테스트를 해보니 타임아웃이 났다. 이건 &lt;code&gt;userspace 모드&lt;/code&gt;의 특성상 인바운드 연결만 가능하고 아웃바운드 트래픽은 기본적으로 Tailscale을 통하지 않아서 생기는 현상이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 또 다른 허들을 만났다. &lt;code&gt;userspace-networking&lt;/code&gt; 모드는 완전한 시스템 수준의 VPN이 아니라는 점이었다. 트래픽을 &lt;code&gt;SOCKS5 프록시&lt;/code&gt; 방식으로 통과시키는 구조였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;일반적인 VPN처럼 모든 앱의 트래픽이 자동으로 Tailscale을 타는 게 아니었다.&lt;/b&gt; 특정 앱에서 Tailscale 네트워크를 사용하려면 해당 앱의 프록시 설정을 Tailscale이 제공하는 &lt;code&gt;SOCKS5 프록시&lt;/code&gt; 주소로 명시적으로 바꿔줘야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 웹브라우저에서 Tailscale 네트워크를 통해 특정 내부망 사이트에 접속하려면 브라우저의 프록시 설정을 수정해야 한다는 뜻이다. 처음엔 &quot;분명 VPN인데 왜 브라우저에서 접속이 안 되지?&quot; 하고 한참 삽질했다. 나중에야 이 프록시 방식의 차이점을 깨달았다. &lt;code&gt;SOCKS5 프록시&lt;/code&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;결국, &lt;code&gt;sudo&lt;/code&gt; 권한이 없다는 제약은 Tailscale을 '제대로' 쓰기 위해 몇 가지 타협을 받아들이게 만들었다. CLI 명령에 매번 소켓 경로를 붙여야 하는 불편함. 그리고 앱별로 프록시 설정을 해줘야 하는 번거로움.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재부팅하면 &lt;code&gt;tailscaled&lt;/code&gt;를 다시 실행해야 하는 자동화의 부재. 이런 것들이 모두 관리자 권한 없이는 표준 설치 경로와 시스템 서비스 등록을 사용할 수 없기 때문에 발생한 일이었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;이런 삽질을 겪으며 배운 건, 특정 환경 제약이 있다면 도구의 기본적인 작동 방식까지 깊이 파고들어야 한다는 점이다. 그리고 '공식 가이드'가 항상 내 상황에 딱 들어맞는 건 아니라는 걸 다시 한번 깨달았다.&lt;/blockquote&gt;
&lt;p style=&quot;color: #888; font-size: 0.9em;&quot; data-ke-size=&quot;size16&quot;&gt;※ 이 글은 직접 겪은 작업 경험을 바탕으로 AI의 도움을 받아 작성했습니다.&lt;/p&gt;</description>
      <category>맥&amp;middot;홈서버</category>
      <category>MacOS</category>
      <category>SOCKS5 프록시</category>
      <category>TailScale</category>
      <category>userspace-networking</category>
      <category>관리자 권한</category>
      <category>원격 접속</category>
      <author>홈랩지기</author>
      <guid isPermaLink="true">https://write83676.tistory.com/8</guid>
      <comments>https://write83676.tistory.com/8#entry8comment</comments>
      <pubDate>Thu, 20 Aug 2026 09:33:41 +0900</pubDate>
    </item>
    <item>
      <title>연락처</title>
      <link>https://write83676.tistory.com/pages/%EC%97%B0%EB%9D%BD%EC%B2%98</link>
      <description>&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;middot;방명록&lt;/b&gt;: 각 글의 댓글 또는 블로그 방명록에 남겨주시면 확인 후 답변드립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인정보 관련 문의도 위 경로로 접수됩니다. 보내주신 의견은 블로그 운영에 반영하도록 하겠습니다.&lt;/p&gt;</description>
      <author>홈랩지기</author>
      <guid isPermaLink="true">https://write83676.tistory.com/pages/%EC%97%B0%EB%9D%BD%EC%B2%98</guid>
      <pubDate>Wed, 19 Aug 2026 23:18:07 +0900</pubDate>
    </item>
    <item>
      <title>블로그 소개</title>
      <link>https://write83676.tistory.com/pages/%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%86%8C%EA%B0%9C</link>
      <description>&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;이 블로그는 맥미니 한 대로 자동화&amp;middot;AI&amp;middot;홈서버를 직접 굴리면서 겪은 시행착오를 기록하는 공간입니다. 매끄럽게 정리된 튜토리얼이 아니라, 실제로 부딪히고 깨지면서 알게 된 것들을 담습니다.&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;b&gt;자동화 삽질기&lt;/b&gt; &amp;mdash; 스크립트, 크론, launchd, 봇 등으로 반복 작업을 없애며 겪은 함정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AI&amp;middot;로컬 LLM&lt;/b&gt; &amp;mdash; 로컬에서 모델을 돌리고 활용하며 얻은 실전 노하우&lt;/li&gt;
&lt;li&gt;&lt;b&gt;맥&amp;middot;홈서버&lt;/b&gt; &amp;mdash; 미니PC&amp;middot;홈서버&amp;middot;네트워크를 직접 구성한 경험&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글은 직접 겪은 작업 경험을 바탕으로 하며, 일부는 AI의 도움을 받아 작성됩니다(각 글 하단에 표기). 도움이 되는 기록이 되기를 바랍니다.&lt;/p&gt;</description>
      <author>홈랩지기</author>
      <guid isPermaLink="true">https://write83676.tistory.com/pages/%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%86%8C%EA%B0%9C</guid>
      <pubDate>Wed, 19 Aug 2026 23:17:56 +0900</pubDate>
    </item>
    <item>
      <title>개인정보처리방침</title>
      <link>https://write83676.tistory.com/pages/%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EB%B0%A9%EC%B9%A8</link>
      <description>&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;h3 data-ke-size=&quot;size23&quot;&gt;1. 수집하는 정보&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그는 회원가입 없이 열람할 수 있으며, 별도의 개인정보를 직접 수집하지 않습니다. 다만 댓글&amp;middot;방명록 작성 시 티스토리(카카오) 계정 정보가 이용될 수 있으며, 이는 티스토리의 개인정보처리방침을 따릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 쿠키(Cookie)와 광고&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;Google을 포함한 제3자 광고 사업자는 쿠키를 사용하여 방문자의 이전 방문 기록에 기반한 광고를 게재합니다.&lt;/li&gt;
&lt;li&gt;Google은 광고 쿠키(DoubleClick DART 쿠키 등)를 사용하여 본 블로그 및 다른 사이트 방문에 기반한 광고를 제공합니다.&lt;/li&gt;
&lt;li&gt;방문자는 &lt;a href=&quot;https://policies.google.com/technologies/ads&quot;&gt;Google 광고 설정&lt;/a&gt;에서 맞춤 광고를 비활성화할 수 있습니다.&lt;/li&gt;
&lt;li&gt;제3자 광고 네트워크의 쿠키 사용은 &lt;a href=&quot;https://www.aboutads.info/&quot;&gt;www.aboutads.info&lt;/a&gt;에서 확인&amp;middot;거부할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 방문 분석 도구&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그는 티스토리 자체 통계 및 Google Analytics 등의 분석 도구를 사용할 수 있으며, 이 과정에서 IP 주소&amp;middot;브라우저 정보 등 비식별 정보가 수집될 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 개인정보 보호책임&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인정보 관련 문의는 '연락처' 페이지를 통해 접수할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 방침 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 방침은 법령&amp;middot;정책 변경에 따라 수정될 수 있으며, 변경 시 본 페이지에 공지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;시행일: 2026년 8월 19일&lt;/i&gt;&lt;/p&gt;</description>
      <author>홈랩지기</author>
      <guid isPermaLink="true">https://write83676.tistory.com/pages/%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EB%B0%A9%EC%B9%A8</guid>
      <pubDate>Wed, 19 Aug 2026 23:17:45 +0900</pubDate>
    </item>
    <item>
      <title>AI 영상 복원 검증법: 길이만 봐선 안 되는 이유 (pts, fps)</title>
      <link>https://write83676.tistory.com/4</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cover.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbqMZe/dJMcaaGuzHY/sHdi2rq9q1QLf99n0BFXi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbqMZe/dJMcaaGuzHY/sHdi2rq9q1QLf99n0BFXi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbqMZe/dJMcaaGuzHY/sHdi2rq9q1QLf99n0BFXi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbqMZe%2FdJMcaaGuzHY%2FsHdi2rq9q1QLf99n0BFXi0%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;1200&quot; height=&quot;630&quot; data-filename=&quot;cover.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥미니 한 대로 AI 영상 복원 파이프라인을 돌리고 있다. 오래된 저화질 영상을 업스케일&amp;middot;노이즈 제거하는 작업인데, 한 편에 짧으면 두어 시간, 400분짜리 대작은 하루 가까이 걸린다.&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;실측을 해보니 이랬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비디오 스트림 길이: 7,180.42초&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오디오 스트림 길이: 7,423.98초&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너(파일)에 찍힌 길이: 7,423.98초 &amp;mdash; 원본과 일치&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비디오가 오디오보다 243초, 그러니까 4분 넘게 먼저 끝나 있었다. 그런데도 검증은 통과였다.&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;b&gt;길이만 보는 검증은 이런 불량을 구조적으로 못 잡는다.&lt;/b&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;첫 번째 함정: 길이가 믿을 게 못 되더라  &amp;zwj;♀️&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그날로 검증 스크립트를 갈아엎었다. 지금은 편이 끝날 때마다 &lt;code&gt;ffprobe&lt;/code&gt;로 이렇게 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ffprobe -v error -select_streams v -show_entries stream=duration 결과물.mp4&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ffprobe -v error -select_streams a -show_entries stream=duration 결과물.mp4&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;terminal.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ep9de/dJMcai5qcNq/MRBrVfkLRRaF42OhbL48bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ep9de/dJMcai5qcNq/MRBrVfkLRRaF42OhbL48bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ep9de/dJMcai5qcNq/MRBrVfkLRRaF42OhbL48bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEp9de%2FdJMcai5qcNq%2FMRBrVfkLRRaF42OhbL48bk%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;1100&quot; height=&quot;368&quot; data-filename=&quot;terminal.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, 비디오&amp;middot;오디오&amp;middot;컨테이너 세 가지 길이를 각각 원본과 비교한다. 허용 오차는 0.001초.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, 마지막 비디오 pts를 확인한다. pts는 프레임마다 찍혀 있는 &quot;이 그림은 몇 초 시점에 보여라&quot;라는 도장인데, 마지막 프레임의 pts와 컨테이너 끝의 간격이 0.5초를 넘으면 불합격 처리한다. 위 사례라면 간격이 243.5초로 찍히니 바로 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋째, 프레임 수를 원본과 대조하고, 전체 스트림을 디코드해서 에러가 0인지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심은 &quot;마지막 비디오 pts&quot;다. 비디오가 실제로 어디까지 존재하는지는 이 값만이 말해준다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸로 끝인 줄 알았는데 두 번째 함정이 있었다. 이번엔 길이도 pts도 전부 정상인데, 재생해 보면 소리가 점점 밀리는 파일이 나왔다.&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;h3 data-ke-size=&quot;size23&quot;&gt;두 번째 함정: fps 표기를 믿지 마세요!  &amp;zwj; &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 원본 컨테이너에 적힌 fps(초당 프레임 수)가 실제와 달랐던 것. 파이프라인은 복원한 프레임을 다시 영상으로 조립할 때 이 표기값을 믿는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 실제로는 30fps로 찍힌 영상인데 컨테이너에 29.97로 적혀 있으면, 재조립된 영상은 0.1% 느리게 흘러가고 오디오는 제자리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겨우 0.1%지만 한 시간이면 3.6초가 밀린다. 대화 장면에서 3.6초면 완전히 다른 사람이 말하고 있는 수준이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 불량은 앞부분 1~2분만 확인하는 검증으로는 절대 못 잡는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 뒤로는 메타데이터의 fps를 그대로 믿지 않고, 실제 프레임 수를 재생 길이로 나눠 역산한 값으로 조립한다.&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;덤으로 얻은 꿀팁: faststart는 공짜 보험이다  &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부수적으로 얻은 보험 하나. 결과물은 &lt;code&gt;faststart&lt;/code&gt;(재생 정보를 파일 앞쪽에 배치) 옵션으로 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 mp4는 재생 정보가 파일 끝에 붙어서, 장시간 배치 도중 파일이 잘리면 통째로 못 여는 쓰레기가 된다.&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;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;길이 비교는 검증이 아니라 인사치레다. 비디오&amp;middot;오디오&amp;middot;컨테이너를 따로 재야 한다.&lt;/li&gt;
&lt;li&gt;비디오의 진짜 끝은 마지막 pts가 말해준다. 간격 0.5초 넘으면 불량.&lt;/li&gt;
&lt;li&gt;메타데이터의 fps는 참고사항이지 사실이 아니다. 프레임 수로 역산해서 써라.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;faststart&lt;/code&gt;는 공짜 보험이다. 배치라면 무조건 켜라.&lt;/li&gt;
&lt;/ul&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;blockquote data-ke-style=&quot;style1&quot;&gt;검증 스크립트 몇 줄이 재작업 몇 시간을 막는다. 배치 자동화에서 제일 남는 장사는 언제나 검증 강화였다.&lt;/blockquote&gt;
&lt;p style=&quot;color: #888; font-size: 0.9em;&quot; data-ke-size=&quot;size16&quot;&gt;※ 이 글은 직접 겪은 작업 경험을 바탕으로 AI의 도움을 받아 작성했습니다.&lt;/p&gt;</description>
      <category>AI&amp;middot;로컬 LLM</category>
      <category>AI 영상 복원</category>
      <category>faststart</category>
      <category>FFPROBE</category>
      <category>동영상 검증</category>
      <category>영상 싱크</category>
      <author>홈랩지기</author>
      <guid isPermaLink="true">https://write83676.tistory.com/4</guid>
      <comments>https://write83676.tistory.com/4#entry4comment</comments>
      <pubDate>Wed, 19 Aug 2026 09:50:30 +0900</pubDate>
    </item>
    <item>
      <title>launchd, screen으로 디스코드 봇 24시간 상주시키기</title>
      <link>https://write83676.tistory.com/3</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cover.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nMEIs/dJMcaiRZm2j/qYh8tY0A6Ku65LqZii29d1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nMEIs/dJMcaiRZm2j/qYh8tY0A6Ku65LqZii29d1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nMEIs/dJMcaiRZm2j/qYh8tY0A6Ku65LqZii29d1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnMEIs%2FdJMcaiRZm2j%2FqYh8tY0A6Ku65LqZii29d1%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;1200&quot; height=&quot;630&quot; data-filename=&quot;cover.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&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;이걸 내 맥에서 24시간 내내 돌리고 싶었다. 내 터미널을 점유하지 않으면서, 맥을 껐다 켜도 자동으로 다시 살아나는 방식으로 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;macOS 환경에서 데몬을 띄우는 표준 방식은 &lt;code&gt;launchd&lt;/code&gt;를 쓰는 거다. 그리고 터미널 기반 앱을 백그라운드에서 돌리려면 &lt;code&gt;screen&lt;/code&gt;이 딱이지. 완벽한 조합이라고 생각했다.&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;code&gt;launchd&lt;/code&gt; &lt;code&gt;plist&lt;/code&gt; 파일을 만들었다. 봇 실행 스크립트를 지정하고, &lt;code&gt;KeepAlive&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;code&gt;screen -r&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;terminal.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;946&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZLHJP/dJMcahFtiFp/xRTSY0oiYLyEOdTht5jXB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZLHJP/dJMcahFtiFp/xRTSY0oiYLyEOdTht5jXB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZLHJP/dJMcahFtiFp/xRTSY0oiYLyEOdTht5jXB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZLHJP%2FdJMcahFtiFp%2FxRTSY0oiYLyEOdTht5jXB1%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;1100&quot; height=&quot;946&quot; data-filename=&quot;terminal.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;946&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;봇이 실제로 살아있는지 확인하려고 &lt;code&gt;screen hardcopy&lt;/code&gt; 명령어로 화면을 캡처해봤다. 그런데 웬걸? 파일은 만들어지는데, 내용은 텅 비어 있었다. TUI 화면이 캡처가 안 되는 건지, 아니면 아예 내용이 없는 건지 알 수가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ps&lt;/code&gt;로 보면 프로세스는 분명히 돌고 있었다. 그런데 왜 반응이 없을까? 대체 뭐가 문제일까?&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;p data-ke-size=&quot;size16&quot;&gt;첫째는 &lt;b&gt;작업 디렉토리 신뢰 문제&lt;/b&gt;였다. 내가 만든 봇은 &lt;code&gt;claude&lt;/code&gt; CLI 도구를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 도구가 특정 디렉토리에서 시작할 때, macOS의 보안 정책에 따라 폴더 신뢰 여부를 묻는 프롬프트에 걸리는 경우가 있었다. &lt;code&gt;launchd&lt;/code&gt;는 상호작용이 불가능하니, 이 프롬프트에 무한 대기하고 있었던 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;launchd&lt;/code&gt;는 주로 &lt;code&gt;/&lt;/code&gt;나 &lt;code&gt;~&lt;/code&gt; 같은 기본 디렉토리에서 스크립트를 실행하는데, 내 경우 &lt;code&gt;/Users/me&lt;/code&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;두 번째는 &lt;b&gt;&lt;code&gt;launchd&lt;/code&gt; 환경 변수 문제&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;launchd&lt;/code&gt;가 띄운 프로세스는 우리가 터미널에서 보는 것과 달리, 환경 변수가 굉장히 빈약하다. 특히 &lt;code&gt;TERM&lt;/code&gt;이나 &lt;code&gt;LANG&lt;/code&gt; 같은 변수가 설정되어 있지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;screen&lt;/code&gt;이나 다른 TUI 앱들이 화면을 제대로 그리기 위해서는 이 변수들이 필수적이다. 그래서 화면이 깨지고 글자가 뭉개졌던 거다.&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;b&gt;&lt;code&gt;screen hardcopy&lt;/code&gt;의 한계&lt;/b&gt;였다. &lt;code&gt;hardcopy&lt;/code&gt;는 단순 텍스트 화면을 덤프하는 데는 유용했지만, &lt;code&gt;claude&lt;/code&gt;처럼 좀 더 복잡한 TUI 앱의 동적인 화면은 제대로 잡아내지 못했다.&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;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;code&gt;ps&lt;/code&gt;로 프로세스가 떠 있는 걸 확인해도, 실제 디스코드 메시지에는 응답하지 않는 '먹통' 상태가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;봇이 게이트웨이에 연결만 되어 '온라인'으로 보여도, 내부적으로는 뻗어있을 수 있다는 걸 간과했다.&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;p data-ke-size=&quot;size16&quot;&gt;먼저, &lt;b&gt;작업 디렉토리 문제&lt;/b&gt;는 봇 실행 스크립트에 &lt;code&gt;cd ~/Claudecode&lt;/code&gt; 명령을 맨 앞에 추가하는 것으로 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;봇 관련 파일들이 있는, 신뢰된 디렉토리로 이동한 후에 &lt;code&gt;claude&lt;/code&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;&lt;b&gt;환경 변수 문제&lt;/b&gt;는 스크립트 내에서 &lt;code&gt;export TERM=xterm-256color&lt;/code&gt;와 &lt;code&gt;export LANG=ko_KR.UTF-8&lt;/code&gt;를 명시적으로 설정하고, 혹시 모를 다른 환경 변수 문제까지 대비해 &lt;code&gt;zsh -l&lt;/code&gt; (로그인 셸 환경 로드)로 실행하도록 조정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;screen&lt;/code&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;&lt;b&gt;봇 상태 확인&lt;/b&gt;은 여러 방법을 조합했다. &lt;code&gt;screen hardcopy&lt;/code&gt;가 안 되니, &lt;code&gt;bun server.ts&lt;/code&gt; 프로세스가 실제로 떠 있는지 확인하고, &lt;code&gt;lsof&lt;/code&gt; 명령으로 봇이 디스코드 게이트웨이와 &lt;code&gt;ESTABLISHED&lt;/code&gt; 상태의 TCP 연결을 유지하고 있는지 확인하는 식으로 바꿨다.&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;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;code&gt;launchd&lt;/code&gt;는 &lt;code&gt;KeepAlive&lt;/code&gt; 옵션으로 내가 만든 와치독 스크립트(&lt;code&gt;~/.claude/start-channels.sh&lt;/code&gt;)만 감시하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 와치독 스크립트가 30초마다 &lt;code&gt;claude-channels&lt;/code&gt;라는 &lt;code&gt;screen&lt;/code&gt; 세션 안에서 돌고 있는 봇의 상태를 점검하고, 만약 응답이 없거나 프로세스가 죽어 있다면 &lt;code&gt;screen&lt;/code&gt; 세션을 종료하고 봇을 &lt;code&gt;claude --channels plugin:discord@...&lt;/code&gt; 명령으로 다시 시작하는 구조였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하니 &lt;code&gt;launchd&lt;/code&gt;는 튼튼한 와치독만 관리하고, 와치독은 봇이 제 역할을 하는지 실시간으로 감시하는 이상적인 구조가 완성되었다.&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;p data-ke-size=&quot;size16&quot;&gt;첫째, &lt;code&gt;launchd&lt;/code&gt; 환경은 생각보다 고립되어 있다는 것. 셸 스크립트가 터미널에서 잘 돌아도 &lt;code&gt;launchd&lt;/code&gt;에서는 예상치 못한 환경 문제에 부딪힐 수 있다. &lt;code&gt;PATH&lt;/code&gt;나 환경 변수, 작업 디렉토리에 특히 신경 써야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, TUI(Text User Interface) 애플리케이션은 &lt;code&gt;TERM&lt;/code&gt;, &lt;code&gt;LANG&lt;/code&gt; 같은 환경 변수가 없으면 무용지물이라는 사실. 이런 기본적인 환경 설정이 얼마나 중요한지 다시금 깨달았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋째, &lt;code&gt;screen hardcopy&lt;/code&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;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, &lt;code&gt;launchd&lt;/code&gt;의 &lt;code&gt;KeepAlive&lt;/code&gt;는 어디까지나 '지정한 스크립트'가 살아있는지 감시하는 거지, 그 스크립트가 띄운 '하위 서비스'까지 책임지지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 서비스라면 &lt;code&gt;launchd&lt;/code&gt;는 상위 감시자 역할만 하고, 실제 서비스 재기동은 별도의 와치독 프로세스에게 맡기는 게 훨씬 견고한 구조를 만든다는 걸 배웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동화된 모니터링과 복구 메커니즘은 선택이 아니라 필수라는 걸 다시 한번 되새기며, 다음 파이프라인에는 더 단단한 와치독을 심어야겠다고 다짐했다.&lt;/p&gt;
&lt;p style=&quot;color: #888; font-size: 0.9em;&quot; data-ke-size=&quot;size16&quot;&gt;※ 이 글은 직접 겪은 작업 경험을 바탕으로 AI의 도움을 받아 작성했습니다.&lt;/p&gt;</description>
      <category>맥&amp;middot;홈서버</category>
      <category>launchd</category>
      <category>macOS 자동화</category>
      <category>screen</category>
      <category>디스코드 봇</category>
      <category>백그라운드 실행</category>
      <category>와치독</category>
      <author>홈랩지기</author>
      <guid isPermaLink="true">https://write83676.tistory.com/3</guid>
      <comments>https://write83676.tistory.com/3#entry3comment</comments>
      <pubDate>Tue, 18 Aug 2026 11:49:09 +0900</pubDate>
    </item>
    <item>
      <title>티스토리 자동 발행, Playwright로 구현하기 (오픈API 종료 대응)</title>
      <link>https://write83676.tistory.com/2</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cover.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgCDF4/dJMcac5f39m/dq3VmSwnh4qGbvy0VY7Ol1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgCDF4/dJMcac5f39m/dq3VmSwnh4qGbvy0VY7Ol1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgCDF4/dJMcac5f39m/dq3VmSwnh4qGbvy0VY7Ol1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgCDF4%2FdJMcac5f39m%2Fdq3VmSwnh4qGbvy0VY7Ol1%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;1200&quot; height=&quot;630&quot; data-filename=&quot;cover.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애드센스 승인을 목표로 티스토리 블로그를 꾸준히 운영하기로 했다. 글쓰기 과정을 효율화하면 좋겠다는 생각에 자동화를 구상했다. 처음엔 당연히 공식 API가 있을 줄 알고 찾아봤다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;티스토리 API, 벌써 죽었니?  &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 여기서부터 시작됐다. 있을 거라 확신했던 티스토리 오픈 API는 이미 서비스 종료 상태였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서를 찾아보니 모든 엔드포인트에 &lt;code&gt;deprecated&lt;/code&gt; 딱지가 붙어 있었다. 이걸 확인하는 순간 머리가 띵했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 남은 길은 브라우저 자동화뿐. 바로 Playwright를 꺼내 들었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;예상치 못한 장벽에 부딪혔지만, 뭐, 개발자에게 이런 건 일상이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 새로운 도전을 시작했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;카카오 로그인 세션, 왜 자꾸 말썽일까?  &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Playwright로 처음 시도한 건 헤드리스 모드였다. 백그라운드에서 조용히 작업이 끝나면 좋겠다는 생각이었다.&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;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책은 의외로 간단했다. &lt;code&gt;headless: false&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;&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;/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;결국, 로그인 페이지에 도달하면 우선 카카오 계정 타일이 있는지 확인하고, 있다면 그걸 클릭하는 로직을 추가했다. 타일이 없으면 그때서야 ID/PW를 입력하는 풀 로그인을 시도하게 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 불필요한 로그인 정보 입력 단계를 건너뛸 수 있었다. 인증 흐름을 뜯어보고 나니, &lt;b&gt;각 세션의 생명주기가 달라서 생긴 일이었다.&lt;/b&gt;&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;code&gt;button&lt;/code&gt; 태그나 &lt;code&gt;input[type=&quot;button&quot;]&lt;/code&gt;으로 찾으려 했다. &lt;code&gt;aria-label&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 직접 DOM 탐색 스크립트를 만들었다. 원하는 영역 근처의 모든 요소를 재귀적으로 탐색하면서 태그, ID, 클래스, &lt;code&gt;aria-label&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;terminal.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/quErr/dJMcac5f39n/MkKMvXxOp3ts47oolW9Wbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/quErr/dJMcac5f39n/MkKMvXxOp3ts47oolW9Wbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/quErr/dJMcac5f39n/MkKMvXxOp3ts47oolW9Wbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FquErr%2FdJMcac5f39n%2FMkKMvXxOp3ts47oolW9Wbk%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;1100&quot; height=&quot;674&quot; data-filename=&quot;terminal.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;674&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 돌려보니 &lt;code&gt;div&lt;/code&gt; 태그인데 &lt;code&gt;mceu_0&lt;/code&gt; 같은 클래스와 &lt;code&gt;aria-label=&quot;사진&quot;&lt;/code&gt;을 가진 녀석이 눈에 띄었다. 이놈이 범인이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순한 버튼이 아니라 TinyMCE 같은 위지윅 에디터에서 사용하는 커스텀 UI 컴포넌트였던 것이다. 너무 쉽게 생각했다가 시간을 꽤 날렸다. 눈으로 보이는 게 다가 아니라는 교훈을 얻었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빈 문단에 커서 놓기... &lt;code&gt;Range&lt;/code&gt; API의 신세계!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 겪었던 삽질은 빈 문단에 커서를 놓는 문제였다. 글 내용을 입력하기 전에 특정 빈 &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; 태그에 커서를 두고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Playwright의 &lt;code&gt;click()&lt;/code&gt; 메서드를 호출했는데, 자꾸 &quot;&lt;code&gt;요소를 찾을 수 없거나 클릭할 수 없다&lt;/code&gt;&quot;는 에러가 떴다.&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;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; 태그는 시각적으로 클릭할 만한 영역이 없기 때문이었다. 브라우저가 사용자 클릭을 인식하는 방식과 프로그래밍적으로 캐럿을 위치시키는 방식 사이의 간극이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책은 &lt;code&gt;page.evaluate()&lt;/code&gt;를 사용해 직접 JavaScript의 &lt;code&gt;Range&lt;/code&gt; API를 호출하는 거였다. 특정 &lt;code&gt;p&lt;/code&gt; 태그 안에 선택 영역을 만들고, 그 영역을 시작점이나 끝점으로 축소하여 커서를 위치시키는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;await page.evaluate(selector =&amp;gt; {
    const element = document.querySelector(selector);
    if (element) {
        const range = document.createRange();
        range.selectNodeContents(element);
        range.collapse(false); // 선택 영역을 끝점으로 축소
        const selection = window.getSelection();
        selection.removeAllRanges();
        selection.addRange(range);
    }
}, '.target-empty-paragraph'); // 빈 문단을 식별하는 셀렉터
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 직접 DOM을 조작하니 비로소 원하는 대로 커서를 움직일 수 있었다. 브라우저 자동화가 표면적인 UI 인터랙션만 다루는 것이 아니라, 때로는 &lt;b&gt;내부적인 DOM과 자바스크립트까지 파고들어야 할 때가 있다는 걸 다시 한번 배웠다.&lt;/b&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;티스토리 글쓰기 자동화를 만들면서 겪은 삽질은 하나하나가 소중한 경험이었다. 단순해 보이는 작업도 파고들면 예상치 못한 복잡성을 만난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그 과정을 통해 웹 페이지의 동작 방식, 인증 시스템의 미묘한 차이, 위지윅 에디터의 특성 등 여러 지식을 얻었다. 자동화는 단순 반복 작업 해방을 넘어, 숨겨진 기술적 난관을 해결하는 과정 자체가 재미있는 여정이었다.&lt;/p&gt;
&lt;p style=&quot;color: #888; font-size: 0.9em;&quot; data-ke-size=&quot;size16&quot;&gt;※ 이 글은 직접 겪은 작업 경험을 바탕으로 AI의 도움을 받아 작성했습니다.&lt;/p&gt;</description>
      <category>자동화 삽질기</category>
      <author>홈랩지기</author>
      <guid isPermaLink="true">https://write83676.tistory.com/2</guid>
      <comments>https://write83676.tistory.com/2#entry2comment</comments>
      <pubDate>Tue, 18 Aug 2026 01:40:35 +0900</pubDate>
    </item>
  </channel>
</rss>