<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>즐거운 개발 세상</title>
    <link>https://egg-stone.tistory.com/</link>
    <description>즐거운 상상이 발전의 토대가 된다고 믿습니다.</description>
    <language>ko</language>
    <pubDate>Sun, 12 Jul 2026 19:55:05 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>블체.</managingEditor>
    <image>
      <title>즐거운 개발 세상</title>
      <url>https://tistory1.daumcdn.net/tistory/4414172/attach/823855b30ca3405bb3124947492e6eac</url>
      <link>https://egg-stone.tistory.com</link>
    </image>
    <item>
      <title>SpeakSH: Claude 로 Linux 서버 작업</title>
      <link>https://egg-stone.tistory.com/50</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사 업무 중 서버작업을 해야할 일이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 서버에 붙어서 작업을 해보니 여러가지 오류가 발생했다. &lt;br /&gt;인증서&amp;nbsp;문제,&amp;nbsp;패키지&amp;nbsp;버전문제&amp;nbsp;등등..&amp;nbsp;해결이&amp;nbsp;가능한&amp;nbsp;문제들이었다. &lt;br /&gt;다만&amp;nbsp;문제는&amp;nbsp;시간이겠지. &lt;br /&gt;마침&amp;nbsp;내가&amp;nbsp;전에&amp;nbsp;만들어둔&amp;nbsp;&quot;서버&amp;nbsp;-&amp;nbsp;Claude&quot;&amp;nbsp;를&amp;nbsp;가능하게&amp;nbsp;해주는&amp;nbsp;미들웨어를&amp;nbsp;사용하기로&amp;nbsp;했다. &lt;br /&gt;문제는&amp;nbsp;아주&amp;nbsp;빠르게&amp;nbsp;해결이&amp;nbsp;되었다.&amp;nbsp;로그도&amp;nbsp;따로&amp;nbsp;남겼고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1587&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNkt8S/dJMcaiih8Wi/Tql2IAymtAEU2ShE4TcGdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNkt8S/dJMcaiih8Wi/Tql2IAymtAEU2ShE4TcGdk/img.png&quot; data-alt=&quot;Claude 에서 작성하는 명령어와 서버의 콘솔로그도 모두 출력된다. 내가 직접 작업도 가능하고.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNkt8S/dJMcaiih8Wi/Tql2IAymtAEU2ShE4TcGdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNkt8S%2FdJMcaiih8Wi%2FTql2IAymtAEU2ShE4TcGdk%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;1587&quot; height=&quot;1038&quot; data-origin-width=&quot;1587&quot; data-origin-height=&quot;1038&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Claude 에서 작성하는 명령어와 서버의 콘솔로그도 모두 출력된다. 내가 직접 작업도 가능하고.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1436&quot; data-origin-height=&quot;972&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWux6v/dJMcaflzXbO/5WzbNgrKIavBatlYRmDKuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWux6v/dJMcaflzXbO/5WzbNgrKIavBatlYRmDKuk/img.png&quot; data-alt=&quot;서버의 로그를 Claude 도 확인가능해서 이슈에 대해 대화가 가능하다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWux6v/dJMcaflzXbO/5WzbNgrKIavBatlYRmDKuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWux6v%2FdJMcaflzXbO%2F5WzbNgrKIavBatlYRmDKuk%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;1436&quot; height=&quot;972&quot; data-origin-width=&quot;1436&quot; data-origin-height=&quot;972&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서버의 로그를 Claude 도 확인가능해서 이슈에 대해 대화가 가능하다.&lt;/figcaption&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;서버에 명령어를 실행하고, 그에 대한 로그를 가지고 대화가 가능했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 내용을 내가 파악하면서도 빠르게 진행할 수 있었다.&lt;/p&gt;</description>
      <category>AI</category>
      <category>ai</category>
      <category>claude</category>
      <category>linux</category>
      <category>llm</category>
      <category>MCP</category>
      <category>server</category>
      <category>SpeakSH</category>
      <author>블체.</author>
      <guid isPermaLink="true">https://egg-stone.tistory.com/50</guid>
      <comments>https://egg-stone.tistory.com/50#entry50comment</comments>
      <pubDate>Wed, 18 Mar 2026 00:05:11 +0900</pubDate>
    </item>
    <item>
      <title>GitLab &amp;rarr; GitHub 미러링 연동 구축기</title>
      <link>https://egg-stone.tistory.com/49</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;취약점 점검 서비스는 github 에 연동해서 검사가 가능했는데, 우리팀은 gitlab 을 사용하고 있었다.&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;gitlab의 프로젝트를 연동시키기 위해서 처음에 고려했던 건 &lt;b&gt;Repository mirroring&lt;/b&gt; 이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 하나 있었는데, 특정 브랜치만 연동할 방법이 없다는 것이었다. 따라서 CI/CD 파이프라인을 선택할 수 밖에 없었다.&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;따라서 GitLab서버에 GitLab Runner를 설치하고 프로젝트 루트 디렉토리에 .gitlab-ci.yml을 작성해서 특정 브랜치에 push가 발생하면 GitHub으로 자동 미러링되도록 했다.&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;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&gt;GitLab 서버&lt;/td&gt;
&lt;td&gt;Rocky Linux 9.6, dev.test.co.kr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitLab Runner&lt;/td&gt;
&lt;td&gt;v16.11.0 (shell executor)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동기화 대상 브랜치&lt;/td&gt;
&lt;td&gt;v1_test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub 레포지토리&lt;/td&gt;
&lt;td&gt;test-lab/dev01&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;pre class=&quot;stata&quot;&gt;&lt;code&gt;GitLab (dev.test.co.kr)
  └── push to v1_test
        └── CI/CD Pipeline 실행
              └── git push &amp;rarr; GitHub (test-lab/dev01) main 브랜치
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GitLab Runner 설치 및 등록&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Runner 설치 확인&lt;/h3&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;which gitlab-runner &amp;amp;&amp;amp; gitlab-runner --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Runner 등록 명령어&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;gitlab-runner register \
  --non-interactive \
  --url https://dev.test.co.kr \
  --token glrt-xxxxxxxxxxxxxx \
  --executor shell \
  --description git-runner
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Runner 서비스 등록 및 시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이너리로 직접 설치한 경우 systemd 서비스 등록이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;gitlab-runner install --user root --working-directory /root
gitlab-runner start
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;.gitlab-ci.yml 최종 구성&lt;/h2&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;stages:
  - sync

github_sync:
  stage: sync
  only:
    - v1_test
  variables:
    GIT_DEPTH: 0
  script:
    - git push --force https://test-lab:${GITHUB_TOKEN}@github.com/test-lab/dev01.git HEAD:main
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GitLab CI/CD 변수 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 &amp;rarr; CI/CD &amp;rarr; Variables에서 아래와 같이 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Key Value 옵션&lt;/p&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&gt;GITHUB_TOKEN&lt;/td&gt;
&lt;td&gt;GitHub PAT 토큰&lt;/td&gt;
&lt;td&gt;Masked ✅, Protected ❌, Expanded ❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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; Masked를 반드시 체크해야 로그에 토큰이 노출되지 않는다.&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;h3 data-ke-size=&quot;size23&quot;&gt;1. TLS 인증서 오류 &amp;mdash; Legacy Common Name&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오류 메시지&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;tls: failed to verify certificate: x509: certificate relies on legacy Common Name field, use SANs instead
&lt;/code&gt;&lt;/pre&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;Go 1.15 이후부터 SAN(Subject Alternative Name)이 없는 Legacy Common Name 방식의 인증서를 거부한다. 내부망 자체 서명 인증서가 SAN 없이 발급되어 있었다.&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;기존 인증서를 백업하고 SAN을 포함한 새 인증서를 재발급한다.&lt;/p&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;# 백업
mkdir -p /etc/gitlab/ssl/backup_$(date +%Y%m%d)
cp /etc/gitlab/ssl/dev.test.co.kr.crt /etc/gitlab/ssl/backup_$(date +%Y%m%d)/
cp /etc/gitlab/ssl/dev.test.co.kr.key /etc/gitlab/ssl/backup_$(date +%Y%m%d)/

# SAN 포함 인증서 재발급
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
  -keyout /etc/gitlab/ssl/dev.test.co.kr.key \
  -out /etc/gitlab/ssl/dev.test.co.kr.crt \
  -subj '/C=KR/ST=seoul/L=seoul/O=test/OU=dev/CN=dev.test.co.kr' \
  -addext 'subjectAltName=DNS:dev.test.co.kr'

# nginx 재시작
gitlab-ctl restart nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. TLS 인증서 오류 &amp;mdash; Unknown Authority&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오류 메시지&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;tls: failed to verify certificate: x509: certificate signed by unknown authority
&lt;/code&gt;&lt;/pre&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;자체 서명 인증서가 시스템 CA 신뢰 저장소에 등록되어 있지 않다.&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;Rocky Linux의 CA 신뢰 저장소에 인증서를 등록한다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;cp /etc/gitlab/ssl/dev.test.co.kr.crt /etc/pki/ca-trust/source/anchors/
update-ca-trust
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. apk 명령어를 찾을 수 없음&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오류 메시지&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;bash: apk: 명령어를 찾을 수 없음
&lt;/code&gt;&lt;/pre&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;.gitlab-ci.yml에 image: alpine:latest와 apk add 명령이 작성되어 있었다. image 키는 Docker executor 전용 설정이며, shell executor에서는 무시된다. 따라서 서버 OS인 Rocky Linux에서 Alpine 전용 패키지 매니저 apk를 실행하려 해서 실패한다.&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;shell executor 환경에서는 서버에 이미 git이 설치되어 있으므로 image와 before_script를 제거한다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;# 변경 전
image: alpine:latest
before_script:
  - apk add --no-cache git

# 변경 후 &amp;mdash; 불필요하므로 제거
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. GitHub 인증 실패&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오류 메시지&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;remote: Invalid username or token. Password authentication is not supported for Git operations.
&lt;/code&gt;&lt;/pre&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;GITHUB_TOKEN 변수에 Protected 옵션이 설정되어 있었다. Protected 변수는 Protected 브랜치에서만 주입되므로, 해당 브랜치가 Protected로 설정되지 않은 경우 변수 값이 빈 문자열로 전달된다.&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;GITHUB_TOKEN 변수 편집 &amp;rarr; Protected 체크 해제&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 토큰 평문 노출&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오류 메시지&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;error: 레퍼런스를 'https://test-lab:ghp_xxxxxxxxxxxx'에 푸시하는데 실패했습니다
&lt;/code&gt;&lt;/pre&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;GITHUB_TOKEN 변수에 Expanded 옵션이 활성화되어 있어 URL 파싱이 깨지면서 토큰 값이 에러 메시지에 평문으로 출력됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조치&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;GitHub에서 노출된 토큰을 즉시 폐기(revoke)하고 새 PAT를 발급한다.&lt;/li&gt;
&lt;li&gt;GitLab CI/CD 변수를 아래 옵션으로 재등록한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션 설정&lt;/p&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&gt;Masked&lt;/td&gt;
&lt;td&gt;✅ 체크 (로그 마스킹)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protected&lt;/td&gt;
&lt;td&gt;❌ 해제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expanded&lt;/td&gt;
&lt;td&gt;❌ 해제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. shallow clone으로 인한 push 실패&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오류 메시지&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;remote: fatal: did not receive expected object
error: 리모트 묶음 풀기 실패: index-pack failed
&lt;/code&gt;&lt;/pre&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;GitLab Runner의 기본 GIT_DEPTH가 20으로 설정되어 있어 커밋 히스토리를 20개만 가져온다. GitHub에 push할 때 참조하는 커밋 객체가 shallow clone 범위 밖에 있어 실패한다.&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;.gitlab-ci.yml에 GIT_DEPTH: 0을 설정해 전체 히스토리를 clone한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;variables:
  GIT_DEPTH: 0
&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;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&gt;1&lt;/td&gt;
&lt;td&gt;TLS Legacy CN 오류&lt;/td&gt;
&lt;td&gt;SAN 없는 자체 서명 인증서&lt;/td&gt;
&lt;td&gt;SAN 포함 인증서 재발급&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;TLS Unknown Authority&lt;/td&gt;
&lt;td&gt;CA 신뢰 저장소 미등록&lt;/td&gt;
&lt;td&gt;update-ca-trust&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;apk 명령어 없음&lt;/td&gt;
&lt;td&gt;shell executor에서 Alpine 명령 사용&lt;/td&gt;
&lt;td&gt;image, before_script 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;GitHub 인증 실패&lt;/td&gt;
&lt;td&gt;변수 Protected 옵션 활성화&lt;/td&gt;
&lt;td&gt;Protected 해제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;토큰 평문 노출&lt;/td&gt;
&lt;td&gt;변수 Expanded 옵션 활성화&lt;/td&gt;
&lt;td&gt;토큰 폐기 후 재발급, Expanded 해제, Masked 체크&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;push 실패 (객체 누락)&lt;/td&gt;
&lt;td&gt;shallow clone (depth=20)&lt;/td&gt;
&lt;td&gt;GIT_DEPTH: 0 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>GIT</category>
      <category>CI/CD</category>
      <category>GIT</category>
      <category>github</category>
      <category>GitLab</category>
      <author>블체.</author>
      <guid isPermaLink="true">https://egg-stone.tistory.com/49</guid>
      <comments>https://egg-stone.tistory.com/49#entry49comment</comments>
      <pubDate>Tue, 17 Mar 2026 20:00:47 +0900</pubDate>
    </item>
    <item>
      <title>GPL-2.0 라이선스 취약점 조치</title>
      <link>https://egg-stone.tistory.com/48</link>
      <description>&lt;h1&gt;: javax.mail &amp;rarr; jakarta.mail 교체&lt;/h1&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;Package License Category Severity Target&lt;/p&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&gt;javax.mail:mail&lt;/td&gt;
&lt;td&gt;GPL-2.0-with-classpath-exception&lt;/td&gt;
&lt;td&gt;restricted&lt;/td&gt;
&lt;td&gt;HIGH&lt;/td&gt;
&lt;td&gt;Java&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;GPL-2.0 라이선스란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPL-2.0(GNU General Public License v2.0)은 오픈소스 라이선스 중에서도 강력한 의무사항을 지닌 라이선스다. 핵심 의무사항은 다음과 같다.&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; : GPL 2.0 코드를 사용하여 개발된 결과물을 배포할 경우, 수정된 부분을 포함한 전체 소스 코드를 반드시 제공해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;라이선스 유지&lt;/b&gt; : 파생된 프로그램은 반드시 GPL 2.0 라이선스를 유지해야 한다.&lt;/li&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;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GPL의 전염성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPL 라이선스의 가장 중요한 특징은 &lt;b&gt;전염성(Copyleft)&lt;/b&gt; 이다. GPL로 된 코드를 포함하거나 결합한 프로그램은 &lt;b&gt;전체가 GPL로 공개되어야&lt;/b&gt; 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, javax.mail:mail 라이브러리를 사용하는 솔루션은 전체 소스코드를 외부에 공개해야 하는 치명적인 리스크를 안게 된다.&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;build.gradle에서 다음 의존성이 확인되었다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'javax.mail:mail:1.4.7'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 라이브러리가 GPL-2.0 라이선스 취약점의 원인이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;조치 방법: jakarta.mail로 교체&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 라이브러리를 교체한다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;// 기존
implementation 'javax.mail:mail:1.4.7'

// 변경
implementation 'org.eclipse.angus:jakarta.mail:2.0.5'
&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;jakarta.mail 역시 GPL-2.0 라이선스를 포함하고 있다. 그러나 동시에 &lt;b&gt;GNU General Public License version 2 + Classpath Exception&lt;/b&gt; 라이선스도 함께 보유하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Classpath Exception&lt;/b&gt;이 핵심이다. 이 예외 조항은 GPL 코드를 클래스패스(classpath)로 연결하여 사용할 경우, GPL의 전염성이 적용되지 않도록 해준다. 즉, 해당 라이브러리를 사용하더라도 &lt;b&gt;전체 소스코드를 공개하지 않아도 된다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조: &lt;a href=&quot;https://jakartaee.github.io/mail-api/JakartaMail-License&quot;&gt;Jakarta Mail License 공식 문서&lt;/a&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;구분 javax.mail:mail:1.4.7 org.eclipse.angus:jakarta.mail:2.0.5&lt;/p&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&gt;라이선스&lt;/td&gt;
&lt;td&gt;GPL-2.0&lt;/td&gt;
&lt;td&gt;GPL-2.0 + Classpath Exception&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;소스코드 공개 의무&lt;/td&gt;
&lt;td&gt;&lt;b&gt;있음&lt;/b&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;위험&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;GPL 라이선스 계열 라이브러리를 도입할 때는 반드시 &lt;b&gt;Classpath Exception 포함 여부&lt;/b&gt;를 확인해야 한다. 예외 조항 하나의 유무가 소스코드 전체 공개 여부를 결정짓는 만큼, 라이선스 검토는 개발 초기 단계부터 꼼꼼히 이루어져야 한다.&lt;/p&gt;</description>
      <category>WEB</category>
      <category>GPL-2.0</category>
      <category>gradle</category>
      <category>Jakarta</category>
      <category>javax</category>
      <category>취약점</category>
      <author>블체.</author>
      <guid isPermaLink="true">https://egg-stone.tistory.com/48</guid>
      <comments>https://egg-stone.tistory.com/48#entry48comment</comments>
      <pubDate>Thu, 12 Mar 2026 19:00:18 +0900</pubDate>
    </item>
    <item>
      <title>gemini-cli 사용하는 법</title>
      <link>https://egg-stone.tistory.com/47</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1. npm install -g @google/gemini-cli &lt;br /&gt;2.&amp;nbsp;터미널에&amp;nbsp;&quot;gemini&quot;&amp;nbsp;입력 &lt;br /&gt;3.&amp;nbsp;&lt;a href=&quot;https://aistudio.google.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://aistudio.google.com/&lt;/a&gt;&amp;nbsp;의&amp;nbsp;api&amp;nbsp;키를&amp;nbsp;복사해서&amp;nbsp;로그인&amp;nbsp;혹은&amp;nbsp;구글&amp;nbsp;아이디로&amp;nbsp;로그인&lt;/p&gt;</description>
      <category>AI</category>
      <category>ai</category>
      <category>Gemini</category>
      <category>gemini-cli</category>
      <author>블체.</author>
      <guid isPermaLink="true">https://egg-stone.tistory.com/47</guid>
      <comments>https://egg-stone.tistory.com/47#entry47comment</comments>
      <pubDate>Wed, 11 Mar 2026 15:05:34 +0900</pubDate>
    </item>
    <item>
      <title>Docker 환경에서 OpenClaw와 Ollama 연결</title>
      <link>https://egg-stone.tistory.com/46</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;OpenClaw와 Ollama를 각각 Docker 컨테이너로 실행할 때, 두 컨테이너가 서로 통신하도록 네트워크를 연결하는 과정을 정리했다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OS: Windows 11 + WSL2 (Ubuntu)&lt;/li&gt;
&lt;li&gt;Docker Desktop&lt;/li&gt;
&lt;li&gt;OpenClaw: docker-compose로 실행 (/mnt/c/clawdbot)&lt;/li&gt;
&lt;li&gt;Ollama: 별도 Docker 컨테이너로 실행&lt;/li&gt;
&lt;/ul&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;OpenClaw와 Ollama를 각각 독립된 Docker 컨테이너로 실행하면, 기본적으로 서로 다른 네트워크에 속하게 된다. 이 상태에서는 OpenClaw가 Ollama에 접근할 수 없어 연결 오류가 발생한다.&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;h3 data-ke-size=&quot;size23&quot;&gt;1단계: 네트워크 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 현재 Docker 네트워크 상태를 확인한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker network ls
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;NETWORK ID     NAME                    DRIVER    SCOPE
b0b244886b91   bridge                  bridge    local
8eade16da4b1   clawdbot_default        bridge    local
2889821e834b   clawdbot_openclaw-net   bridge    local
8bc80a2e5471   host                    host      local
14980b8324ab   none                    null      local
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenClaw는 clawdbot_openclaw-net 네트워크를 사용하고 있었다. Ollama 컨테이너가 이 네트워크에 포함되어 있는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker network inspect clawdbot_openclaw-net | grep -A 5 &quot;Containers&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;&quot;Containers&quot;: {
    &quot;0d103a074adf...&quot;: {
        &quot;Name&quot;: &quot;clawdbot-openclaw-gateway-1&quot;,
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenClaw 컨테이너만 있고 &lt;b&gt;Ollama는 없었다.&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;2단계: Ollama를 OpenClaw 네트워크에 연결&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker network connect clawdbot_openclaw-net ollama
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 후 OpenClaw 컨테이너 내부에서 Ollama에 접근 가능한지 테스트한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker exec -it clawdbot-openclaw-gateway-1 curl http://ollama:11434/api/tags
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{&quot;models&quot;:[{&quot;name&quot;:&quot;llama3.2:latest&quot;,&quot;model&quot;:&quot;llama3.2:latest&quot;,...}]}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 연결은 성공했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계: openclaw.json 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 연결 후에도 채팅이 작동하지 않았다. 원인은 openclaw.json 설정 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시행착오 1: api 필드 누락&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 설정:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;ollama&quot;: {
  &quot;baseUrl&quot;: &quot;http://ollama:11434/v1&quot;,
  &quot;apiKey&quot;: &quot;ollama-local&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류:&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;Error: No API provider registered for api: undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api 필드가 없으면 OpenClaw가 어떤 API 방식을 사용할지 알 수 없어서 크래시가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시행착오 2: openai-responses API 타입 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;api&quot;: &quot;openai-responses&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 오류가 반복됐다. OpenClaw의 Ollama 통합은 native Ollama API를 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최종 설정:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;&quot;models&quot;: {
  &quot;providers&quot;: {
    &quot;ollama&quot;: {
      &quot;baseUrl&quot;: &quot;http://ollama:11434&quot;,
      &quot;apiKey&quot;: &quot;ollama-local&quot;,
      &quot;api&quot;: &quot;ollama&quot;,
      &quot;models&quot;: [
        {
          &quot;id&quot;: &quot;llama3.2:latest&quot;,
          &quot;name&quot;: &quot;Llama 3.2&quot;,
          &quot;input&quot;: [&quot;text&quot;],
          &quot;reasoning&quot;: false,
          &quot;cost&quot;: {&quot;input&quot;: 0, &quot;output&quot;: 0},
          &quot;contextWindow&quot;: 32000,
          &quot;maxTokens&quot;: 4096
        }
      ]
    }
  }
},
&quot;agents&quot;: {
  &quot;defaults&quot;: {
    &quot;model&quot;: {
      &quot;primary&quot;: &quot;ollama/llama3.2:latest&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&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;baseUrl에서 /v1 제거 &amp;rarr; native Ollama API 사용&lt;/li&gt;
&lt;li&gt;&quot;api&quot;: &quot;ollama&quot; 명시 필수&lt;/li&gt;
&lt;li&gt;models 배열 필수 (없으면 config validation 오류 발생)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4단계: docker-compose.yml에 ollama 추가 (영구 설정)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker network connect 명령은 컨테이너 재시작 시 초기화된다. 영구적으로 같은 네트워크를 공유하려면 docker-compose.yml에 ollama를 추가한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;services:
  openclaw-gateway:
    networks:
      - openclaw-net

  ollama:
    image: ollama/ollama
    container_name: ollama
    ports:
      - &quot;11434:11434&quot;
    volumes:
      - ollama:/root/.ollama
    networks:
      - openclaw-net
    restart: unless-stopped

networks:
  openclaw-net:
    driver: bridge

volumes:
  ollama:
    external: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 재시작:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;docker compose down
docker compose up -d
&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;OpenClaw 로그에서 다음과 같이 모델이 정상 인식됨을 확인:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[gateway] agent model: ollama/llama3.2:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅 시 Ollama 컨테이너 CPU가 약 400%까지 올라가며 추론이 실행되는 것을 확인할 수 있다.&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;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&gt;curl: (6) Could not resolve host: ollama&lt;/td&gt;
&lt;td&gt;컨테이너 간 네트워크 분리&lt;/td&gt;
&lt;td&gt;docker network connect로 같은 네트워크 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No API provider registered for api: undefined&lt;/td&gt;
&lt;td&gt;api 필드 누락&lt;/td&gt;
&lt;td&gt;&quot;api&quot;: &quot;ollama&quot; 명시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;config validation 오류&lt;/td&gt;
&lt;td&gt;models 배열 누락&lt;/td&gt;
&lt;td&gt;models 배열 추가 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재시작 후 네트워크 끊김&lt;/td&gt;
&lt;td&gt;임시 네트워크 연결&lt;/td&gt;
&lt;td&gt;docker-compose.yml에 ollama 서비스 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>AI</category>
      <category>ai</category>
      <category>docker</category>
      <category>Ollama</category>
      <category>openclaw</category>
      <author>블체.</author>
      <guid isPermaLink="true">https://egg-stone.tistory.com/46</guid>
      <comments>https://egg-stone.tistory.com/46#entry46comment</comments>
      <pubDate>Tue, 24 Feb 2026 00:15:30 +0900</pubDate>
    </item>
    <item>
      <title>Docker 환경에서 Ollama + OpenClaw(Clawdbot) 설치하기</title>
      <link>https://egg-stone.tistory.com/45</link>
      <description>&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;환경&lt;/b&gt;: Windows 10/11, Docker Desktop, WSL2(Ubuntu)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목표&lt;/b&gt;: Ollama 로컬 LLM 모델을 AI 백엔드로 사용하는 OpenClaw를 Docker로 구성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;참고&lt;/b&gt;: Clawdbot &amp;rarr; Moltbot &amp;rarr; OpenClaw 순서로 이름이 변경됨 (2026년 기준 공식 명칭은 OpenClaw)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. WSL2 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PowerShell을 &lt;b&gt;관리자 권한&lt;/b&gt;으로 실행 후:&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;wsl --install
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 완료 후 PC 재시작. 재시작 후 Ubuntu 터미널이 자동으로 열리면 사용자 이름과 비밀번호를 설정한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Docker Desktop 설치&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.docker.com/products/docker-desktop&quot;&gt;https://www.docker.com/products/docker-desktop&lt;/a&gt; 에서 다운로드&lt;/li&gt;
&lt;li&gt;설치 완료 후 PC 재시작&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WSL2 연동 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Desktop 실행 후:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;⚙️ Settings &amp;rarr; &lt;b&gt;Resources &amp;rarr; WSL Integration&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Enable integration with my default WSL distro&lt;/b&gt; 체크&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ubuntu 토글 켜기&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Apply &amp;amp; Restart&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WSL2에서 Docker 권한 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu 터미널에서 실행:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;sudo usermod -aG docker $USER
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PowerShell에서 WSL2 재시작:&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;wsl --shutdown
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu 터미널을 다시 열고 정상 동작 확인:&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;docker compose version
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Ollama 설치 (Docker)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨테이너 실행&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker run -d --name ollama -p 11434:11434 -v ollama:/root/.ollama ollama/ollama&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모델 다운로드&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker exec -it ollama ollama pull llama3.2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 http://localhost:11434 접속 시 Ollama is running 이 출력되면 정상.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. OpenClaw 설치 (Docker)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;소스코드 클론&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;cd /mnt/c/
git clone https://github.com/openclaw/openclaw.git clawdbot
cd /mnt/c/clawdbot
&lt;/code&gt;&lt;/pre&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;: 이후 모든 명령어는 Git Bash가 아닌 &lt;b&gt;Ubuntu 터미널(WSL2)&lt;/b&gt; 에서 실행해야 한다.&lt;br /&gt;Git Bash는 화살표 키 등 인터랙티브 기능을 제대로 지원하지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치 스크립트 실행&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;./docker-setup.sh
&lt;/code&gt;&lt;/pre&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;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&gt;보안 경고&lt;/td&gt;
&lt;td&gt;y + Enter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Onboarding mode&lt;/td&gt;
&lt;td&gt;QuickStart &amp;rarr; Enter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모델 선택&lt;/td&gt;
&lt;td&gt;목록에 Ollama 없음 &amp;rarr; 아무거나 선택 후 진행 (나중에 설정 파일 수정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skills 설치&lt;/td&gt;
&lt;td&gt;Skip for now&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;5. OpenClaw 대시보드 접속&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 토큰 확인&lt;/h3&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;cat ~/.openclaw/openclaw.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 내용 중 gateway.auth.token 값을 복사한다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;gateway&quot;: {
  &quot;auth&quot;: {
    &quot;mode&quot;: &quot;token&quot;,
    &quot;token&quot;: &quot;여기가 실제 토큰&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;브라우저 접속&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://localhost:18789/?token=여기에토큰값
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 트러블슈팅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;device token mismatch&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;disconnected (1008): unauthorized: device token mismatch
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널 출력 토큰과 설정 파일 토큰이 다른 경우 발생한다.&lt;br /&gt;반드시 ~/.openclaw/openclaw.json 의 gateway.auth.token 값을 사용해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;pairing required&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;disconnected (1008): pairing required
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 디바이스 인증 단계에서 막히는 경우다. 설정 파일에 dangerouslyDisableDeviceAuth 옵션을 추가하면 우회할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;nano ~/.openclaw/openclaw.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gateway 섹션을 아래와 같이 수정:&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;&quot;gateway&quot;: {
  &quot;port&quot;: 18789,
  &quot;mode&quot;: &quot;local&quot;,
  &quot;bind&quot;: &quot;loopback&quot;,
  &quot;auth&quot;: {
    &quot;mode&quot;: &quot;token&quot;,
    &quot;token&quot;: &quot;여기에토큰값&quot;
  },
  &quot;controlUi&quot;: {
    &quot;dangerouslyDisableDeviceAuth&quot;: true
  },
  &quot;tailscale&quot;: {
    &quot;mode&quot;: &quot;off&quot;,
    &quot;resetOnExit&quot;: false
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장 후 컨테이너 재시작:&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;docker compose -f /mnt/c/clawdbot/docker-compose.yml restart openclaw-gateway
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Ollama 모델 연결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 파일에 Ollama provider를 추가한다:&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;nano ~/.openclaw/openclaw.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최상위 레벨에 models 항목 추가, agents.defaults.model 수정:&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;models&quot;: {
    &quot;providers&quot;: {
      &quot;ollama&quot;: {
        &quot;baseUrl&quot;: &quot;http://host.docker.internal:11434/v1&quot;,
        &quot;apiKey&quot;: &quot;ollama&quot;
      }
    }
  },
  &quot;agents&quot;: {
    &quot;defaults&quot;: {
      &quot;model&quot;: {
        &quot;primary&quot;: &quot;ollama/llama3.2&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&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;: Docker 컨테이너 내부에서 호스트 PC의 Ollama에 접근할 때는 localhost 대신 &lt;b&gt;host.docker.internal&lt;/b&gt; 을 사용해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재시작:&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;docker compose -f /mnt/c/clawdbot/docker-compose.yml restart openclaw-gateway
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 최종 구성도&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;브라우저 (http://localhost:18789)
        &amp;darr;
OpenClaw Gateway (Docker 컨테이너)
        &amp;darr;
Ollama (Docker 컨테이너, 포트 11434)
        &amp;darr;
llama3.2 등 로컬 LLM 모델
&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;pre class=&quot;awk&quot;&gt;&lt;code&gt;# 컨테이너 상태 확인
docker ps

# OpenClaw 로그 확인
docker compose -f /mnt/c/clawdbot/docker-compose.yml logs -f openclaw-gateway

# OpenClaw 재시작
docker compose -f /mnt/c/clawdbot/docker-compose.yml restart openclaw-gateway

# OpenClaw 중지
docker compose -f /mnt/c/clawdbot/docker-compose.yml down

# OpenClaw 시작
docker compose -f /mnt/c/clawdbot/docker-compose.yml up -d

# Ollama 모델 목록 확인
docker exec -it ollama ollama list

# Ollama 모델 추가
docker exec -it ollama ollama pull 모델명
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>AI</category>
      <category>ai</category>
      <category>docker</category>
      <category>moltbot</category>
      <category>openclaw</category>
      <author>블체.</author>
      <guid isPermaLink="true">https://egg-stone.tistory.com/45</guid>
      <comments>https://egg-stone.tistory.com/45#entry45comment</comments>
      <pubDate>Mon, 23 Feb 2026 00:01:26 +0900</pubDate>
    </item>
    <item>
      <title>sQuiry: AI 서버와 통신하는 프론트엔드 개발</title>
      <link>https://egg-stone.tistory.com/44</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;서론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전글: &lt;a href=&quot;https://egg-stone.tistory.com/43&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://egg-stone.tistory.com/43&lt;/a&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;이전 글에서는 sQuiry의 백엔드 구조와 RAG 기반 SQL 생성 엔진에 대해 다뤘다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기술 스택과 선택 이유&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;바닐라 JavaScript&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React나 Vue 같은 프레임워크를 사용하지 않고 바닐라 JavaScript를 선택했다.&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;프로젝트 규모가 작음 (3개 페이지, 독립적인 기능)&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;프레임워크는 강력하지만 모든 프로젝트에 필요한 것은 아니다. sQuiry는 페이지별로 독립적인 기능을 제공하므로 단순한 구조가 적합했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FastAPI + Jinja2&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 사이드 렌더링을 위해 FastAPI와 Jinja2 템플릿 엔진을 사용했다.&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;/li&gt;
&lt;li&gt;SEO 친화적&lt;/li&gt;
&lt;li&gt;FastAPI와 자연스럽게 통합&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;pre class=&quot;axapta&quot;&gt;&lt;code&gt;frontend/
├── routes.py           # 페이지 라우팅
├── templates/          # Jinja2 템플릿
│   ├── base.html      # 공통 레이아웃
│   ├── index.html     # 메인 페이지
│   ├── query.html     # 쿼리 생성 페이지
│   └── history.html   # 히스토리 페이지
└── static/            # 정적 파일
    ├── css/           # 스타일시트
    └── js/            # JavaScript
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CSS 변수 기반 스타일링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS 변수로 디자인 토큰을 정의하여 일관된 디자인 시스템을 구축했다. 향후 다크 모드나 테마 변경이 쉽도록 설계했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;전체 아키텍처&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레이어 구조&lt;/h3&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;사용자
  &amp;darr;
[프론트엔드 - HTML/CSS/JS]
  &amp;darr; HTTP Request (JSON)
[FastAPI 라우터]
  &amp;darr;
[백엔드 API] (/api/v1/*)
  &amp;darr;
[Vanna Engine + DB]
  &amp;darr; HTTP Response (JSON)
[프론트엔드 - JavaScript]
  &amp;darr;
DOM 업데이트 &amp;rarr; 사용자에게 표시
&lt;/code&gt;&lt;/pre&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; /, /query, /history &amp;rarr; HTML 응답&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백엔드 API:&lt;/b&gt; /api/v1/* &amp;rarr; JSON 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 분리하면 향후 모바일 앱이나 다른 클라이언트에서도 동일한 API를 사용할 수 있다.&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;메인 페이지 (/):&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;/li&gt;
&lt;li&gt;쿼리 생성 페이지로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;쿼리 생성 페이지 (/query):&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;SQL 생성 및 편집&lt;/li&gt;
&lt;li&gt;SQL 실행 및 결과 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;히스토리 페이지 (/history):&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;질문, SQL, 실행 결과 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;백엔드와의 데이터 통신&lt;/b&gt;&lt;/h2&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;b&gt;1. SQL 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;verilog&quot;&gt;&lt;code&gt;POST /api/v1/generate-sql
요청: { &quot;question&quot;: &quot;지난 달 매출은?&quot; }
응답: { &quot;question&quot;: &quot;...&quot;, &quot;sql&quot;: &quot;SELECT ...&quot;, &quot;execution_time&quot;: 1.5 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. SQL 실행&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;POST /api/v1/execute-sql
요청: { &quot;sql&quot;: &quot;SELECT * FROM orders&quot; }
응답: { &quot;success&quot;: true, &quot;data&quot;: [...], &quot;row_count&quot;: 10 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 히스토리 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /api/v1/query-history
응답: { &quot;history&quot;: [ { &quot;question&quot;: &quot;...&quot;, &quot;sql&quot;: &quot;...&quot;, &quot;timestamp&quot;: &quot;...&quot; } ] }
&lt;/code&gt;&lt;/pre&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;쿼리 생성 및 실행 플로우:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 사용자가 자연어 질문 입력
   &amp;darr;
2. JavaScript: POST /api/v1/generate-sql
   &amp;darr;
3. 백엔드: Vanna 엔진이 SQL 생성
   &amp;darr;
4. 프론트엔드: 생성된 SQL 화면에 표시
   &amp;darr;
5. 사용자가 &quot;실행&quot; 버튼 클릭 (또는 SQL 편집 후 실행)
   &amp;darr;
6. JavaScript: POST /api/v1/execute-sql
   &amp;darr;
7. 백엔드: MariaDB에서 쿼리 실행
   &amp;darr;
8. 프론트엔드: 결과를 테이블로 렌더링
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JavaScript 구조&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;common.js&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;API 호출 헬퍼 (fetch 래퍼)&lt;/li&gt;
&lt;li&gt;알림 메시지 표시&lt;/li&gt;
&lt;li&gt;테이블 생성 함수&lt;/li&gt;
&lt;li&gt;SQL 포맷팅&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;query.js&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;SQL 생성 요청&lt;/li&gt;
&lt;li&gt;SQL 실행 요청&lt;/li&gt;
&lt;li&gt;SQL 편집 기능&lt;/li&gt;
&lt;li&gt;복사 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;history.js&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;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기 통신 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 API 호출은 async/await로 처리하여 UI 블록을 방지했다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// API 호출 패턴
async function generateSQL(question) {
    try {
        // 로딩 표시
        showLoading();
        
        // API 호출
        const response = await fetch('/api/v1/generate-sql', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ question })
        });
        
        const data = await response.json();
        
        // 결과 표시
        displaySQL(data.sql);
        
    } catch (error) {
        // 에러 처리
        showError(error.message);
    } finally {
        // 로딩 숨김
        hideLoading();
    }
}
&lt;/code&gt;&lt;/pre&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;&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;/li&gt;
&lt;li&gt;전역 상태 없음&lt;/li&gt;
&lt;li&gt;필요한 데이터는 API에서 다시 가져옴&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;pre class=&quot;ceylon&quot;&gt;&lt;code&gt;let currentSql = '';  // 현재 생성된 SQL
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 변수로 현재 SQL을 관리한다. 사용자가 편집하면 이 변수를 업데이트하고, 실행 시 이 변수의 값을 API로 전송한다.&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;/li&gt;
&lt;li&gt;페이지 로드 시마다 API에서 최신 데이터 가져옴&lt;/li&gt;
&lt;li&gt;새로고침 버튼으로 수동 업데이트 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;사용자 경험 설계&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단계별 UI 표시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리 생성 페이지는 사용자의 작업 단계에 따라 섹션을 순차적으로 표시한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 질문 입력 섹션 (항상 표시)
   &amp;darr;
2. SQL 생성 클릭
   &amp;darr;
3. 생성된 SQL 섹션 표시
   &amp;darr;
4-A. 실행 클릭 &amp;rarr; 결과 섹션 표시
4-B. 편집 클릭 &amp;rarr; 편집 섹션 표시
&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 호출 중 로딩 스피너 표시&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;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;에러 발생 시 빨간색 알림&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;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;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에러 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 API 호출에서 에러를 캐치하고 사용자에게 명확한 메시지를 표시한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;try {
    const response = await apiCall('/api/v1/generate-sql', ...);
} catch (error) {
    showAlert(error.message, 'error');
}
&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;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;성능 고려사항&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최소한의 JavaScript&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript 파일 크기를 최소화했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;common.js: 약 2KB&lt;/li&gt;
&lt;li&gt;query.js: 약 5KB&lt;/li&gt;
&lt;li&gt;history.js: 약 3KB&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 라이브러리를 사용하지 않아 추가 로딩 시간이 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DOM 조작 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블이나 히스토리 목록처럼 여러 요소를 렌더링할 때, HTML 문자열을 한 번에 생성한 후 innerHTML로 삽입한다. 여러 번 DOM을 조작하는 것보다 훨씬 빠르다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기 로딩&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 로드와 API 호출을 분리하여 초기 로딩 속도를 개선했다. 히스토리 페이지는 빈 페이지를 먼저 표시하고, JavaScript로 데이터를 비동기 로드한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발 과정의 설계 결정&lt;/h2&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;선택: 페이지별 독립 상태&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;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;Redux나 Vuex 같은 상태 관리 라이브러리 불필요&lt;/li&gt;
&lt;li&gt;각 페이지는 필요한 데이터를 API에서 직접 가져옴&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지가 많아지거나 복잡한 상태 공유가 필요하면 재고려할 수 있다.&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;&lt;b&gt;선택: RESTful API&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드와 백엔드 API를 명확히 분리했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/api/v1/* - JSON API&lt;/li&gt;
&lt;li&gt;그 외 - HTML 페이지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 분리하면 API 버전 관리가 쉽고, 다른 클라이언트 (모바일 앱 등)에서도 동일한 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;FastAPI는 프론트엔드와 백엔드를 하나의 애플리케이션으로 서빙한다.&lt;/p&gt;
&lt;pre class=&quot;capnproto&quot;&gt;&lt;code&gt;# main.py
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from frontend.routes import router as frontend_router
from backend.api.routes.query import router as api_router

app = FastAPI()

# API 라우트 등록
app.include_router(api_router)

# 프론트엔드 라우트 등록
app.include_router(frontend_router)

# 정적 파일 서빙
app.mount(&quot;/static&quot;, StaticFiles(directory=&quot;frontend/static&quot;), name=&quot;static&quot;)
&lt;/code&gt;&lt;/pre&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;/li&gt;
&lt;li&gt;CORS 문제 없음&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;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;하지만 sQuiry는 소규모 프로젝트라 문제없음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;향후 개선 방향&lt;/h2&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;Vite로 모듈 번들링&lt;/li&gt;
&lt;li&gt;TypeScript 도입&lt;/li&gt;
&lt;li&gt;CSS 전처리기 (Sass)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현재는 복잡도 대비 이점이 크지 않다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebSocket 지원&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 쿼리 실행 상태나 진행률을 표시하려면 WebSocket이 필요할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;사용자 &amp;rarr; 쿼리 실행 요청
서버 &amp;rarr; WebSocket: &quot;쿼리 파싱 중...&quot;
서버 &amp;rarr; WebSocket: &quot;데이터베이스 연결 중...&quot;
서버 &amp;rarr; WebSocket: &quot;쿼리 실행 중... (10%)&quot;
서버 &amp;rarr; WebSocket: &quot;완료!&quot;
&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;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 거의 백엔드개발자에 가깝다. 다행히 Claude 의 도움으로 썩 괜찮아 보이는 웹서비스를 만들 수 있었다.&lt;br /&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;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; - 프로젝트 규모에 맞는 도구를 사용했다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;명확한 레이어 분리&lt;/b&gt; - 프론트엔드와 백엔드를 독립적으로 설계했다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관된 API 통신&lt;/b&gt; - 모든 API 호출에 동일한 패턴을 적용했다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자 중심 설계&lt;/b&gt; - 단계별 UI와 명확한 피드백을 중점으로 개발했다.&lt;/li&gt;
&lt;/ol&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>AI</category>
      <author>블체.</author>
      <guid isPermaLink="true">https://egg-stone.tistory.com/44</guid>
      <comments>https://egg-stone.tistory.com/44#entry44comment</comments>
      <pubDate>Tue, 10 Feb 2026 00:10:09 +0900</pubDate>
    </item>
    <item>
      <title>sQuiry: 오픈소스 LLM과 RAG 기반 자연어-SQL 변환 서비스</title>
      <link>https://egg-stone.tistory.com/43</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;서론&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;자연어로 쿼리를 생성하자&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스를 다루다 보면 비개발자가 원하는 데이터를 조회하기 위해 개발자에게 요청하거나, SQL을 배워야 하는 상황이 발생한다. &quot;지난 달 매출이 가장 높은 상위 10개 제품을 보여줘&quot;라는 간단한 요청도 복잡한 SQL 쿼리로 변환해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제를 해결하기 위해 자연어를 SQL로 변환하는 시스템 sQuiry를 개발했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 상용 API 대신 Huggingface의 오픈소스 모델을 로컬에서 실행하고, RAG 기법으로 데이터베이스 스키마와 예제를 학습시켜 정확도를 높이는 것이 핵심이다.&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;b&gt;프로젝트 개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sQuiry는 자연어 질문을 SQL 쿼리로 변환하고 실행하는 API 서비스다. 사용자는 &quot;지난 주 주문 수는?&quot;과 같이 자연어로 질문하면, 시스템이 이를 SQL로 변환하여 실행하고 결과를 반환한다.&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;오픈소스 LLM을 로컬에서 실행 (비용 절감)&lt;/li&gt;
&lt;li&gt;RAG 기반 스키마 학습 (정확도 향상)&lt;/li&gt;
&lt;li&gt;Few-shot learning 지원&lt;/li&gt;
&lt;li&gt;FastAPI 기반 RESTful API&lt;/li&gt;
&lt;/ul&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;Python 3.x&lt;/li&gt;
&lt;li&gt;FastAPI - 웹 프레임워크&lt;/li&gt;
&lt;li&gt;Vanna.ai - SQL 생성 프레임워크&lt;/li&gt;
&lt;li&gt;Ollama - 로컬 LLM 실행 환경&lt;/li&gt;
&lt;li&gt;ChromaDB - 벡터 데이터베이스&lt;/li&gt;
&lt;li&gt;MariaDB - 타겟 데이터베이스&lt;/li&gt;
&lt;li&gt;PyMySQL - DB 커넥터&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;&lt;b&gt;아키텍처 설계&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sQuiry의 전체 아키텍처는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;사용자 질문 (자연어)
    &amp;darr;
FastAPI (엔드포인트)
    &amp;darr;
Vanna Engine (SQL 생성)
    &amp;darr; (유사 예제 검색)
ChromaDB (RAG)
    &amp;darr; (프롬프트 구성)
Ollama (LLM 추론)
    &amp;darr;
생성된 SQL
    &amp;darr;
MariaDB (실행)
    &amp;darr;
결과 반환 (JSON)
&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 자연어 질문을 API로 전송&lt;/li&gt;
&lt;li&gt;ChromaDB에서 유사한 질문-SQL 쌍 검색 (RAG)&lt;/li&gt;
&lt;li&gt;데이터베이스 스키마 정보 조회&lt;/li&gt;
&lt;li&gt;검색된 예제와 스키마로 프롬프트 구성&lt;/li&gt;
&lt;li&gt;Ollama를 통해 LLM에 전송&lt;/li&gt;
&lt;li&gt;생성된 SQL을 MariaDB에서 실행&lt;/li&gt;
&lt;li&gt;결과를 JSON으로 반환&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;핵심 구현 내용&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Vanna.ai 커스터마이징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vanna.ai는 SQL 생성을 위한 프레임워크다. 하지만 기본적으로 OpenAI API를 사용하기 때문에, Ollama를 사용하도록 커스터마이징했다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;class OllamaLLM:
    &quot;&quot;&quot;Ollama LLM 래퍼&quot;&quot;&quot;
    
    def __init__(self, host: str, model: str):
        self.host = host
        self.model = model
    
    def generate(self, prompt: str) -&amp;gt; str:
        &quot;&quot;&quot;Ollama API를 통해 텍스트 생성&quot;&quot;&quot;
        try:
            url = f&quot;{self.host}/api/generate&quot;
            payload = {
                &quot;model&quot;: self.model,
                &quot;prompt&quot;: prompt,
                &quot;stream&quot;: False
            }
            
            response = requests.post(url, json=payload, timeout=120)
            response.raise_for_status()
            
            result = response.json()
            return result.get('response', '')
            
        except Exception as e:
            raise Exception(f&quot;Ollama 생성 실패: {str(e)}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ollama는 HTTP API를 제공하므로 간단한 POST 요청으로 LLM을 호출할 수 있다. 타임아웃은 120초로 설정하여 대용량 쿼리 생성도 안정적으로 처리했다.&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. ChromaDB 기반 RAG 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChromaDB는 벡터 데이터베이스로, 임베딩된 질문-SQL 쌍을 저장하고 유사도 검색을 수행한다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;class VannaGenerator(ChromaDB_VectorStore):
    &quot;&quot;&quot;Vanna.ai 기반 SQL 생성기 (ChromaDB + Ollama)&quot;&quot;&quot;
    
    def __init__(self, config=None):
        ChromaDB_VectorStore.__init__(self, config=config)
        self.llm = OllamaLLM(
            host=config.get('ollama_host', 'http://localhost:11434'),
            model=config.get('ollama_model', 'llama3.1:8b')
        )
    
    def generate_sql(self, question: str, **kwargs) -&amp;gt; str:
        &quot;&quot;&quot;자연어 질문으로부터 SQL 생성&quot;&quot;&quot;
        # 1. 관련 문서/스키마 검색 (RAG)
        similar_docs = self.get_similar_question_sql(question)
        
        # 2. 데이터베이스 스키마 정보 가져오기
        schemas = db_connection.get_table_schemas()
        
        # 3. 프롬프트 구성
        context = &quot;&quot;
        if similar_docs:
            context = &quot;\n참고할 수 있는 유사한 예제:\n&quot;
            for doc in similar_docs[:3]:
                context += f&quot;Q: {doc.get('question', '')}\n&quot;
                context += f&quot;SQL: {doc.get('sql', '')}\n\n&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG의 핵심은 유사한 질문-SQL 쌍을 검색하여 프롬프트에 포함시키는 것이다. 이렇게 하면 LLM이 데이터베이스 구조와 쿼리 패턴을 더 잘 이해할 수 있다.&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. 프롬프트 엔지니어링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확한 SQL을 생성하기 위해 프롬프트를 세밀하게 설계했다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;prompt = f&quot;&quot;&quot;당신은 SQL 전문가입니다. 사용자의 자연어 질문을 SQL 쿼리로 변환하세요.

{schema_text}
{context}

데이터베이스: MariaDB
질문: {question}

중요한 규칙:
1. 반드시 위에 명시된 테이블과 컬럼만 사용하세요
2. 존재하지 않는 테이블이나 컬럼을 절대 사용하지 마세요
3. 테이블명과 컬럼명을 정확히 확인하세요
4. SQL 쿼리만 반환하세요 (설명이나 다른 텍스트 없이)
5. 마크다운 코드 블록 없이 순수 SQL만 작성하세요
6. SELECT, INSERT, UPDATE, DELETE 등 표준 SQL을 사용하세요

SQL:&quot;&quot;&quot;
&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;b&gt;스키마 정보&lt;/b&gt; - 사용 가능한 테이블과 컬럼 목록&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유사 예제&lt;/b&gt; - RAG로 검색된 질문-SQL 쌍&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;특히 &quot;반드시 명시된 테이블만 사용&quot;과 같은 규칙으로 LLM이 존재하지 않는 테이블을 생성하는 것을 방지했다.&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. 자동 스키마 학습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스의 모든 테이블 스키마를 자동으로 학습시키는 기능을 구현했다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def train_on_database_schema(self) -&amp;gt; bool:
    &quot;&quot;&quot;데이터베이스 스키마를 자동으로 학습&quot;&quot;&quot;
    try:
        schemas = db_connection.get_table_schemas()
        
        for table_name, columns in schemas.items():
            # DDL 형식으로 변환
            ddl = f&quot;CREATE TABLE {table_name} (\n&quot;
            column_defs = []
            
            for col in columns:
                col_def = f&quot;  {col['column_name']} {col['column_type']}&quot;
                
                if col['is_nullable'] == 'NO':
                    col_def += &quot; NOT NULL&quot;
                
                if col['column_key'] == 'PRI':
                    col_def += &quot; PRIMARY KEY&quot;
                
                if col['extra']:
                    col_def += f&quot; {col['extra']}&quot;
                
                if col['comment']:
                    col_def += f&quot; COMMENT '{col['comment']}'&quot;
                
                column_defs.append(col_def)
            
            ddl += &quot;,\n&quot;.join(column_defs)
            ddl += &quot;\n);&quot;
            
            self.train_on_ddl(ddl)
        
        return True
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스키마 정보를 DDL 형식으로 변환하여 ChromaDB에 저장한다. 이를 통해 LLM이 테이블 구조를 정확히 파악할 수 있다.&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;5. Few-shot Learning&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 질문-SQL 쌍을 직접 학습시킬 수 있는 기능을 제공했다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def train_on_sql(self, question: str, sql: str) -&amp;gt; bool:
    &quot;&quot;&quot;질문-SQL 쌍을 학습시킨 (Few-shot learning)&quot;&quot;&quot;
    try:
        self.vanna.train(question=question, sql=sql)
        return True
    except Exception as e:
        return False
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 사용되는 복잡한 쿼리나 도메인 특화 쿼리를 학습시키면, 유사한 질문에 대해 더 정확한 SQL을 생성할 수 있다.&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;6. SQL 후처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 생성한 응답에서 순수한 SQL만 추출하는 로직을 구현했다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def _extract_sql(self, text: str) -&amp;gt; str:
    &quot;&quot;&quot;응답에서 SQL 추출&quot;&quot;&quot;
    import re
    
    # 마크다운 코드 블록 제거
    text = re.sub(r'```sql\n?', '', text)
    text = re.sub(r'```\n?', '', text)
    
    # 앞뒤 공백 제거
    text = text.strip()
    
    # SQL 키워드로 시작하는 라인만 추출
    lines = text.split('\n')
    sql_lines = []
    for line in lines:
        line = line.strip()
        if any(line.upper().startswith(kw) for kw in ['SELECT', 'INSERT', 'UPDATE', 'DELETE']):
            sql_lines.append(line)
        elif sql_lines:
            sql_lines.append(line)
    
    if sql_lines:
        return ' '.join(sql_lines)
    
    return text
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 종종 마크다운 코드 블록이나 설명 텍스트를 포함하여 응답하기 때문에, 이를 제거하고 순수한 SQL만 추출했다.&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;b&gt;API 설계&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RESTful API는 FastAPI로 구현했다. 주요 엔드포인트는 다음과 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. SQL 생성 (실행 없이)&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;@router.post(&quot;/generate-sql&quot;, response_model=SQLResponse)
async def generate_sql(request: QuestionRequest):
    &quot;&quot;&quot;자연어 질문으로부터 SQL을 생성합니다.&quot;&quot;&quot;
    try:
        start_time = time.time()
        sql = sql_generator.generate_sql(request.question)
        execution_time = time.time() - start_time
        
        return SQLResponse(
            question=request.question,
            sql=sql,
            execution_time=execution_time
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 생성된 SQL을 검토한 후 직접 실행하고 싶을 때 사용한다.&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. SQL 생성 및 실행&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;@router.post(&quot;/execute&quot;, response_model=QueryResult)
async def execute_query(request: QuestionRequest):
    &quot;&quot;&quot;자연어 질문으로부터 SQL을 생성하고 실행하여 결과를 반환합니다.&quot;&quot;&quot;
    try:
        result = sql_generator.generate_and_execute(request.question)
        
        return QueryResult(
            question=result[&quot;question&quot;],
            sql=result[&quot;sql&quot;],
            data=result[&quot;data&quot;],
            row_count=result[&quot;row_count&quot;],
            execution_time=result[&quot;execution_time&quot;]
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 생성과 실행을 한 번에 처리한다.&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. 스키마 학습&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;@router.post(&quot;/train/schema&quot;)
async def train_schema():
    &quot;&quot;&quot;데이터베이스 스키마를 자동으로 학습합니다.&quot;&quot;&quot;
    try:
        result = vanna_trainer.train_from_database()
        
        if result[&quot;status&quot;] == &quot;error&quot;:
            raise HTTPException(status_code=500, detail=result[&quot;message&quot;])
        
        return result
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))
&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;4. 예제 학습&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;@router.post(&quot;/train/examples&quot;)
async def train_examples(request: TrainingRequest):
    &quot;&quot;&quot;질문-SQL 쌍을 학습시켜 Few-shot learning을 수행합니다.&quot;&quot;&quot;
    try:
        results = []
        for example in request.examples:
            success = vanna_trainer.train_question_sql(
                question=example.question,
                sql=example.sql
            )
            results.append({
                &quot;question&quot;: example.question,
                &quot;success&quot;: success
            })
        
        return {
            &quot;status&quot;: &quot;success&quot;,
            &quot;trained_count&quot;: len([r for r in results if r[&quot;success&quot;]]),
            &quot;total_count&quot;: len(results),
            &quot;details&quot;: results
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;h3 data-ke-size=&quot;size23&quot;&gt;1. 상용 API vs 오픈소스 모델&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 OpenAI API를 사용하려 했다. 하지만 비용 문제와 데이터 프라이버시를 고려하여 오픈소스 모델을 선택했다.&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;/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;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;하드웨어 요구사항 (GPU 권장)&lt;/li&gt;
&lt;li&gt;모델 성능이 GPT-4보다 낮을 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ollama를 사용함으로써 로컬에서 Llama 3.1, Mistral 등의 오픈소스 모델을 간편하게 실행할 수 있었다.&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. RAG의 중요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 RAG 없이 스키마 정보만 프롬프트에 포함시켰다. 하지만 복잡한 조인이나 집계 쿼리에서 정확도가 떨어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG를 도입한 후:&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;Few-shot learning으로 도메인 특화 쿼리 개선&lt;/li&gt;
&lt;li&gt;컬럼명 오타나 존재하지 않는 테이블 사용 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChromaDB는 벡터 검색이 빠르고 설정이 간단하여 RAG 구현에 적합했다.&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. 환각 방지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM은 종종 존재하지 않는 테이블이나 컬럼을 사용한다. 이를 방지하기 위해:&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; - 사용 가능한 테이블과 컬럼을 명확히 나열&lt;/li&gt;
&lt;li&gt;&lt;b&gt;엄격한 규칙&lt;/b&gt; - &quot;명시된 테이블만 사용하라&quot;는 지시&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SQL 검증&lt;/b&gt; - 생성된 SQL을 실행 전에 구문 검사&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 피드백&lt;/b&gt; - 실패한 쿼리를 학습 데이터로 활용&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 프롬프트 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트 설계가 SQL 품질에 가장 큰 영향을 미쳤다. 여러 번의 실험을 통해:&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;b&gt;적절한 컨텍스트&lt;/b&gt; - 너무 많은 정보는 혼란을 초래&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 스키마 + 상위 3개 예제 조합이 최적이었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 모델 선택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 오픈소스 모델을 테스트했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 크기 속도 SQL 정확도 비고&lt;/p&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&gt;llama3.1:8b&lt;/td&gt;
&lt;td&gt;4.7GB&lt;/td&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;mistral:7b&lt;/td&gt;
&lt;td&gt;4.1GB&lt;/td&gt;
&lt;td&gt;매우 빠름&lt;/td&gt;
&lt;td&gt;양호&lt;/td&gt;
&lt;td&gt;간단한 쿼리용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llama3.1:70b&lt;/td&gt;
&lt;td&gt;40GB&lt;/td&gt;
&lt;td&gt;느림&lt;/td&gt;
&lt;td&gt;매우 우수&lt;/td&gt;
&lt;td&gt;GPU 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;codellama:13b&lt;/td&gt;
&lt;td&gt;7.4GB&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;td&gt;우수&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;b&gt;llama3.1:8b&lt;/b&gt;가 속도와 정확도의 균형이 가장 좋아 기본 모델로 선택했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;성능 최적화&lt;/b&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;FastAPI의 async/await를 활용하여 동시 요청 처리 성능을 개선했다.&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;Ollama 요청에 120초 타임아웃을 설정하여 무한 대기를 방지했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;향후 개선 계획&lt;/b&gt;&lt;/h2&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;h3 data-ke-size=&quot;size23&quot;&gt;결과 시각화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 결과를 차트나 그래프로 자동 시각화하는 기능을 추가할 계획이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;멀티 데이터베이스 지원&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 MariaDB만 지원하지만, PostgreSQL, MySQL, Oracle 등 다양한 데이터베이스를 지원하도록 확장할 예정이다.&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;SQL 인젝션 방지&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;h3 data-ke-size=&quot;size23&quot;&gt;Fine-tuning&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 특화 데이터로 오픈소스 모델을 Fine-tuning하여 정확도를 더 높일 수 있을 것 같다.&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;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sQuiry를 개발하면서 오픈소스 LLM과 RAG를 활용한 실용적인 서비스 구축의 가능성을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상용 API 대신 로컬 LLM을 사용함으로써 비용을 절감하고 데이터 프라이버시를 지킬 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 RAG를 통한 Few-shot learning이 SQL 생성 정확도에 큰 영향을 미친다는 것을 알게 되었다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트 엔지니어링에 대해서도 많이 배울 수 있었다. 단순히 페르소나를 부여하는 것 뿐만 아니라, AI가 데이터를 다루는 방식이나 속도에도 큰 영향을 끼친다는 것을 배웠다. 생각보다 훨씬 중요한 부분이었다.&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;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;GitHub Repository:&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Vanna.ai Documentation: &lt;a href=&quot;https://vanna.ai/docs&quot;&gt;https://vanna.ai/docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ollama: &lt;a href=&quot;https://ollama.ai&quot;&gt;https://ollama.ai&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ChromaDB: &lt;a href=&quot;https://www.trychroma.com&quot;&gt;https://www.trychroma.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;FastAPI: &lt;a href=&quot;https://fastapi.tiangolo.com&quot;&gt;https://fastapi.tiangolo.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Huggingface: &lt;a href=&quot;https://huggingface.co/models&quot;&gt;https://huggingface.co/models&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI</category>
      <category>ai</category>
      <category>DB</category>
      <category>llm</category>
      <category>SQL</category>
      <category>sQuiry</category>
      <category>자연어</category>
      <author>블체.</author>
      <guid isPermaLink="true">https://egg-stone.tistory.com/43</guid>
      <comments>https://egg-stone.tistory.com/43#entry43comment</comments>
      <pubDate>Mon, 9 Feb 2026 00:10:45 +0900</pubDate>
    </item>
    <item>
      <title>SpeakSH:  Claude AI로 서버 관리 (시나리오)</title>
      <link>https://egg-stone.tistory.com/42</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdu62n/dJMcagEpuXr/ZhkWnPB9d3ywtmTHN96pRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdu62n/dJMcagEpuXr/ZhkWnPB9d3ywtmTHN96pRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdu62n/dJMcagEpuXr/ZhkWnPB9d3ywtmTHN96pRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbdu62n%2FdJMcagEpuXr%2FZhkWnPB9d3ywtmTHN96pRK%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;400&quot; height=&quot;400&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;서론&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 SpeakSH를 어떻게 사용할 수 있는지에 대한 몇 가지 시나리오를 만들어서 공유하는 것에 목적을 둔다.&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555; text-align: start;&quot;&gt;이전글:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://egg-stone.tistory.com/40&quot;&gt;https://egg-stone.tistory.com/41&lt;/a&gt;&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;b&gt;개발환경 구축&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mz4Eh/dJMcagEpu5b/IkFnM8wKziPKyFFMFEZlp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mz4Eh/dJMcagEpu5b/IkFnM8wKziPKyFFMFEZlp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mz4Eh/dJMcagEpu5b/IkFnM8wKziPKyFFMFEZlp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmz4Eh%2FdJMcagEpu5b%2FIkFnM8wKziPKyFFMFEZlp1%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;1400&quot; height=&quot;926&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xKVXh/dJMcahJ5eRU/QzgbUD0ZUucUQxDGuOVCZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xKVXh/dJMcahJ5eRU/QzgbUD0ZUucUQxDGuOVCZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xKVXh/dJMcahJ5eRU/QzgbUD0ZUucUQxDGuOVCZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxKVXh%2FdJMcahJ5eRU%2FQzgbUD0ZUucUQxDGuOVCZk%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;1502&quot; height=&quot;1038&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;1038&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsxPCg/dJMcaac6NNB/AbkblyLCAI8lYHkxG7njq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsxPCg/dJMcaac6NNB/AbkblyLCAI8lYHkxG7njq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsxPCg/dJMcaac6NNB/AbkblyLCAI8lYHkxG7njq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdsxPCg%2FdJMcaac6NNB%2FAbkblyLCAI8lYHkxG7njq1%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;860&quot; height=&quot;190&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;283&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;보안상태 점검&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvKkmc/dJMcajnwtcs/3bgg83nHEfZyPZpQsQDyEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvKkmc/dJMcajnwtcs/3bgg83nHEfZyPZpQsQDyEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvKkmc/dJMcajnwtcs/3bgg83nHEfZyPZpQsQDyEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvKkmc%2FdJMcajnwtcs%2F3bgg83nHEfZyPZpQsQDyEK%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;1400&quot; height=&quot;926&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LL0qC/dJMcacBZuO2/SArXc1CR8im1mf549mz9qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LL0qC/dJMcacBZuO2/SArXc1CR8im1mf549mz9qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LL0qC/dJMcacBZuO2/SArXc1CR8im1mf549mz9qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLL0qC%2FdJMcacBZuO2%2FSArXc1CR8im1mf549mz9qk%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;1400&quot; height=&quot;926&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7lWwA/dJMcacWgR0V/4hZj3118STz9kke4k1TOg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7lWwA/dJMcacWgR0V/4hZj3118STz9kke4k1TOg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7lWwA/dJMcacWgR0V/4hZj3118STz9kke4k1TOg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7lWwA%2FdJMcacWgR0V%2F4hZj3118STz9kke4k1TOg0%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;1400&quot; height=&quot;926&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P0ShZ/dJMcab38qSz/zNhV05s4YKTIAwB9xlogc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P0ShZ/dJMcab38qSz/zNhV05s4YKTIAwB9xlogc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P0ShZ/dJMcab38qSz/zNhV05s4YKTIAwB9xlogc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP0ShZ%2FdJMcab38qSz%2FzNhV05s4YKTIAwB9xlogc0%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;1502&quot; height=&quot;1038&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;1038&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;서비스 제거&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u3yqM/dJMcagLazs0/i3awKs4qnJ0W5vivX3rg9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u3yqM/dJMcagLazs0/i3awKs4qnJ0W5vivX3rg9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u3yqM/dJMcagLazs0/i3awKs4qnJ0W5vivX3rg9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu3yqM%2FdJMcagLazs0%2Fi3awKs4qnJ0W5vivX3rg9k%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;1282&quot; height=&quot;417&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;417&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3SsQK/dJMcabC5BPj/ZUe7uNQzobMsdbKPCC77cK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3SsQK/dJMcabC5BPj/ZUe7uNQzobMsdbKPCC77cK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3SsQK/dJMcabC5BPj/ZUe7uNQzobMsdbKPCC77cK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3SsQK%2FdJMcabC5BPj%2FZUe7uNQzobMsdbKPCC77cK%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;1400&quot; height=&quot;926&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EMfrq/dJMcabiNuya/HBqf18d18Yqaxa5wfeWhnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EMfrq/dJMcabiNuya/HBqf18d18Yqaxa5wfeWhnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EMfrq/dJMcabiNuya/HBqf18d18Yqaxa5wfeWhnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEMfrq%2FdJMcabiNuya%2FHBqf18d18Yqaxa5wfeWhnk%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;1502&quot; height=&quot;1038&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;1038&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/znUO6/dJMcadt4GT8/p5FKHSVqqPzeBQEPEb8tXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/znUO6/dJMcadt4GT8/p5FKHSVqqPzeBQEPEb8tXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/znUO6/dJMcadt4GT8/p5FKHSVqqPzeBQEPEb8tXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FznUO6%2FdJMcadt4GT8%2Fp5FKHSVqqPzeBQEPEb8tXK%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;1400&quot; height=&quot;926&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4LmSi/dJMcaivoGtQ/zpBrn9WPGj0dw95is8Oce0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4LmSi/dJMcaivoGtQ/zpBrn9WPGj0dw95is8Oce0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4LmSi/dJMcaivoGtQ/zpBrn9WPGj0dw95is8Oce0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4LmSi%2FdJMcaivoGtQ%2FzpBrn9WPGj0dw95is8Oce0%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;1502&quot; height=&quot;1038&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;1038&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dh9QMQ/dJMcai9Zof7/4qCSebESjYX41V6DRuDkpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dh9QMQ/dJMcai9Zof7/4qCSebESjYX41V6DRuDkpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dh9QMQ/dJMcai9Zof7/4qCSebESjYX41V6DRuDkpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdh9QMQ%2FdJMcai9Zof7%2F4qCSebESjYX41V6DRuDkpk%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;1282&quot; height=&quot;232&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;232&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;로그확인&lt;/b&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAbAl9/dJMcabpy4eT/o4qgAIn3geYllx11Ok7DKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAbAl9/dJMcabpy4eT/o4qgAIn3geYllx11Ok7DKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAbAl9/dJMcabpy4eT/o4qgAIn3geYllx11Ok7DKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAbAl9%2FdJMcabpy4eT%2Fo4qgAIn3geYllx11Ok7DKK%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;1400&quot; height=&quot;926&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PrfGV/dJMcabpy4eX/cRhk8fMPUgH3I8M6dklLLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PrfGV/dJMcabpy4eX/cRhk8fMPUgH3I8M6dklLLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PrfGV/dJMcabpy4eX/cRhk8fMPUgH3I8M6dklLLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPrfGV%2FdJMcabpy4eX%2FcRhk8fMPUgH3I8M6dklLLk%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;1400&quot; height=&quot;926&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&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;</description>
      <category>AI</category>
      <category>ai</category>
      <category>claude</category>
      <category>linux</category>
      <category>llm</category>
      <category>MCP</category>
      <category>server</category>
      <category>SpeakSH</category>
      <author>블체.</author>
      <guid isPermaLink="true">https://egg-stone.tistory.com/42</guid>
      <comments>https://egg-stone.tistory.com/42#entry42comment</comments>
      <pubDate>Sat, 7 Feb 2026 00:10:22 +0900</pubDate>
    </item>
    <item>
      <title>SpeakSH: Claude Desktop 연동</title>
      <link>https://egg-stone.tistory.com/41</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMRKt1/dJMcabJR18V/TttlAsczIDbS7ZXXHwuCe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMRKt1/dJMcabJR18V/TttlAsczIDbS7ZXXHwuCe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMRKt1/dJMcabJR18V/TttlAsczIDbS7ZXXHwuCe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMRKt1%2FdJMcabJR18V%2FTttlAsczIDbS7ZXXHwuCe1%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;400&quot; height=&quot;400&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;서론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 SpeakSH를 실제로 Claude Desktop 에 연동하는 방법에 대해서 다룬다.&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;a href=&quot;https://egg-stone.tistory.com/40&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://egg-stone.tistory.com/40&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. MCP 등록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #555555; text-align: start;&quot;&gt;좌측 하단 &quot;설정&quot; 아이콘을 클릭하면 연동 설정창이 뜨게된다.&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;1502&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/berm2m/dJMcagxBcnW/KMxq73xA2FKYHV8kDaSui0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/berm2m/dJMcagxBcnW/KMxq73xA2FKYHV8kDaSui0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/berm2m/dJMcagxBcnW/KMxq73xA2FKYHV8kDaSui0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fberm2m%2FdJMcagxBcnW%2FKMxq73xA2FKYHV8kDaSui0%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;1502&quot; height=&quot;1038&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;1038&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;Claude Desktop 연동&quot; 을 클릭하면 진행여부를 체크한다. 더 진행해보면..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BR3LT/dJMcaaYsQWt/vakaT0lBI5H9ZkTLLYOhJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BR3LT/dJMcaaYsQWt/vakaT0lBI5H9ZkTLLYOhJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BR3LT/dJMcaaYsQWt/vakaT0lBI5H9ZkTLLYOhJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBR3LT%2FdJMcaaYsQWt%2FvakaT0lBI5H9ZkTLLYOhJK%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;1502&quot; height=&quot;1038&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;1038&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bV60Y8/dJMb99ZAIsC/zSKiY1tKHA9c5OAk5kLRO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bV60Y8/dJMb99ZAIsC/zSKiY1tKHA9c5OAk5kLRO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bV60Y8/dJMb99ZAIsC/zSKiY1tKHA9c5OAk5kLRO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbV60Y8%2FdJMb99ZAIsC%2FzSKiY1tKHA9c5OAk5kLRO0%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;1502&quot; height=&quot;1038&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;1038&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1667&quot; data-origin-height=&quot;893&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C7RlZ/dJMcadgAfsn/LlSMbKUiY7HL9XUlBSeiDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C7RlZ/dJMcadgAfsn/LlSMbKUiY7HL9XUlBSeiDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C7RlZ/dJMcadgAfsn/LlSMbKUiY7HL9XUlBSeiDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC7RlZ%2FdJMcadgAfsn%2FLlSMbKUiY7HL9XUlBSeiDK%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;1667&quot; height=&quot;893&quot; data-origin-width=&quot;1667&quot; data-origin-height=&quot;893&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;claude_desktop_config.json 에 &quot;speaksh&quot; 가 성공적으로 등록된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 claude desktop 에서도 확인을 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A5xrM/dJMcaiCaQSk/nE1uVYrX7xvzw69USksEKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A5xrM/dJMcaiCaQSk/nE1uVYrX7xvzw69USksEKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A5xrM/dJMcaiCaQSk/nE1uVYrX7xvzw69USksEKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA5xrM%2FdJMcaiCaQSk%2FnE1uVYrX7xvzw69USksEKk%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;1400&quot; height=&quot;800&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnJPG2/dJMcaiCaQSA/ZV6o3JK4Ulk7zddm7BgNFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnJPG2/dJMcaiCaQSA/ZV6o3JK4Ulk7zddm7BgNFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnJPG2/dJMcaiCaQSA/ZV6o3JK4Ulk7zddm7BgNFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnJPG2%2FdJMcaiCaQSA%2FZV6o3JK4Ulk7zddm7BgNFK%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;1400&quot; height=&quot;800&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;800&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;연결 성공!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. 서버추가&lt;/h3&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SpeakSH의 서버 연결 테스트를 위해서 테스트서버를 만들어놨다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brwLy3/dJMcaferr2t/AsG1BxO6dVayrUBbSh6PpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brwLy3/dJMcaferr2t/AsG1BxO6dVayrUBbSh6PpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brwLy3/dJMcaferr2t/AsG1BxO6dVayrUBbSh6PpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrwLy3%2FdJMcaferr2t%2FAsG1BxO6dVayrUBbSh6PpK%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;1282&quot; height=&quot;460&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;460&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;&amp;nbsp;&lt;/p&gt;
&lt;p 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;1502&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cap6cK/dJMcaiWti2L/K0pLPzyAN4Pg4eTGOHwPx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cap6cK/dJMcaiWti2L/K0pLPzyAN4Pg4eTGOHwPx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cap6cK/dJMcaiWti2L/K0pLPzyAN4Pg4eTGOHwPx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcap6cK%2FdJMcaiWti2L%2FK0pLPzyAN4Pg4eTGOHwPx0%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;1502&quot; height=&quot;1038&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;1038&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwoP8K/dJMcacaV2sS/p2p2QMKAQamfc1SEyUh9fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwoP8K/dJMcacaV2sS/p2p2QMKAQamfc1SEyUh9fk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwoP8K/dJMcacaV2sS/p2p2QMKAQamfc1SEyUh9fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwoP8K%2FdJMcacaV2sS%2Fp2p2QMKAQamfc1SEyUh9fk%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;1502&quot; height=&quot;1038&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;1038&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;이제 Claude Desktop 에서 서버가 추가되었는지 확인을 해보고 연결까지 진행해보겠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ETzsg/dJMcaiPGt08/1xREYARPDyAuwVWMcvmShK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ETzsg/dJMcaiPGt08/1xREYARPDyAuwVWMcvmShK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ETzsg/dJMcaiPGt08/1xREYARPDyAuwVWMcvmShK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FETzsg%2FdJMcaiPGt08%2F1xREYARPDyAuwVWMcvmShK%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;1400&quot; height=&quot;926&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MF09i/dJMcadHCwpw/kdejUbyRd9CZu8KxseB0KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MF09i/dJMcadHCwpw/kdejUbyRd9CZu8KxseB0KK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MF09i/dJMcadHCwpw/kdejUbyRd9CZu8KxseB0KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMF09i%2FdJMcadHCwpw%2FkdejUbyRd9CZu8KxseB0KK%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;1400&quot; height=&quot;926&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;926&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;이렇게 MCP 등록 및 서버 등록/연결 과정까지 완료했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제로 Claude Desktop 을 통해서 서버에 대한 작업을 할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 내가 정의해본 시나리오 몇 가지를 진행해보려고 한다.&lt;/p&gt;</description>
      <category>AI</category>
      <category>ai</category>
      <category>claude</category>
      <category>linux</category>
      <category>llm</category>
      <category>MCP</category>
      <category>server</category>
      <category>SpeakSH</category>
      <category>자동화</category>
      <author>블체.</author>
      <guid isPermaLink="true">https://egg-stone.tistory.com/41</guid>
      <comments>https://egg-stone.tistory.com/41#entry41comment</comments>
      <pubDate>Fri, 6 Feb 2026 00:10:56 +0900</pubDate>
    </item>
  </channel>
</rss>