발단 : 로컬에 셋팅한 PaperClip을 스마트폰으로도 접속해서 오더를 내리고 관리하고 싶다.
PaperClip은 최근 공개된 오픈소스 AI 에이전트 오케스트레이션 플랫폼이다. 여러 AI 에이전트(Claude Code, Cursor 등)를 마치 회사의 팀원처럼 조직화하고 관리할 수 있게 해주는 도구로, 목표 정렬, 비용 관리, 거버넌스, 스케줄링 등의 기능을 제공한다. npx paperclipai onboard --yes 한 줄이면 로컬에 설치되고, 임베디드 PostgreSQL과 함께 localhost:3100에서 바로 실행된다.
이렇게 로컬에 셋팅해둔 PaperClip을 스마트폰으로 접속할 수 있게 외부 도메인(abc.mydomain.com)을 붙이고 싶었다.
이 작업은 처음부터 끝까지 Claude Code(Anthropic의 CLI 에이전트)와 대화하면서 진행했다. 내가 "도메인으로 외부에서 접속할 수 있게 해줘"라고 요청하자, Claude Code가 현재 환경을 분석하고, 선택지를 제시하고, 설정 파일을 작성하고, 서비스를 등록하는 것까지 전부 처리해줬다. 그 과정을 시간순으로 기록한다.
(PaperClip 자체에 대한 소개와 탐구는 다음 글에서 자세히 다룰 예정이다.)
1단계: 어떤 방법으로 외부 접속을 열 것인가?
Claude Code가 가장 먼저 물어본 것은 "어떤 방식으로 외부 접속을 구성할 것인가"였다. 네 가지 선택지를 제시해줬다:
| 방법 | 장점 | 단점 |
|---|---|---|
| Cloudflare Tunnel | 무료, 포트 개방 불필요, SSL 자동 | Cloudflare 계정 필요, NS 이전 필요 |
| Nginx 리버스 프록시 | 전통적이고 검증된 방식 | 고정 IP 필요, SSL 별도 설정 |
| ngrok | 설치 즉시 사용 가능 | 무료는 랜덤 URL, 커스텀 도메인은 유료 |
| Tailscale Funnel | Tailscale 네트워크 활용 | Tailscale 계정 필요, 커스텀 도메인 제한 |
나는 이미 도메인이 있었고, Nginx가 익숙하기도 해서 Nginx 리버스 프록시를 선택했다. SSL은 Let's Encrypt로 처리하면 될 것 같았다.
2단계: 동적 IP 문제 해결
그런데 맥북은 노트북이다. 카페에서, 집에서, 사무실에서 — 접속할 때마다 IP가 바뀐다. Nginx 리버스 프록시 방식은 도메인의 A 레코드에 서버 IP를 고정해야 하는데, IP가 계속 바뀌면 그때마다 DNS를 수정해줘야 한다. 현실적으로 불가능하다.
이 문제를 Claude Code에 공유하자, 다시 선택지를 정리해줬다:
- ngrok — 무료 플랜에서 고정 도메인 1개 제공. 기존 DNS에서 CNAME으로 연결 가능. 하지만 커스텀 도메인은 유료.
- Cloudflare Tunnel + 기존 DNS 유지 — Cloudflare Free 플랜에서는 서브도메인 위임(partial setup)을 지원하지 않아 실질적으로 불가.
- Cloudflare로 NS 이전 후 Tunnel 사용 — 기존 DNS 레코드를 Cloudflare가 자동 스캔해서 복제. 관리 화면만 바뀔 뿐 기능은 동일. 가장 안정적이고 무료.
Cloudflare Tunnel의 핵심 원리는 이렇다: 맥북에서 Cloudflare 엣지 서버로 아웃바운드 연결을 맺는다. 외부에서 들어오는 요청은 Cloudflare가 이 터널을 통해 맥북으로 전달한다. 즉, 맥북의 IP가 뭐든 상관없다. 포트 포워딩도 필요 없고, 방화벽도 신경 쓸 필요 없다.
3단계: DNS를 Cloudflare로 옮겨야 하는가?
사실 이 부분이 가장 고민됐다. 나는 기존에 dnszi라는 무료 DNS 관리 서비스를 사용하고 있었다. 여기에 A 레코드, TXT(SPF) 레코드 등이 설정되어 있었는데, 이걸 통째로 Cloudflare로 옮겨야 한다는 거다.
Claude Code가 먼저 현재 DNS 상태를 dig 명령으로 조회해서 백업해줬다:
dig mydomain.com ANY +noall +answer
| 레코드 타입 | 값 |
|---|---|
| NS | dnszi.com 네임서버 5개 |
| A | 기존 서버 IP |
| TXT (SPF) | SPF 레코드 |
| MX | 없음 |
백업을 확인한 뒤, Cloudflare로 이전하기로 결정했다. 어차피 Cloudflare가 기존 레코드를 자동 스캔해서 가져와 주고, 이후에도 같은 방식으로 관리할 수 있으니까.
4단계: Cloudflare 도메인 등록 및 dnszi에서 이전
Cloudflare 계정이 없어서 새로 만들었다. 이후 과정은 다음과 같다:
- dash.cloudflare.com에서 "Add a site" → 도메인 입력
- Free 플랜 선택
- Cloudflare가 기존 DNS 레코드를 자동 스캔 — dnszi에 설정했던 A 레코드, TXT 레코드 등이 그대로 복제됨
- Cloudflare에서 제공하는 네임서버 2개를 확인
- 도메인 등록기관(도메인을 구매한 곳)에서 네임서버를 dnszi의 것에서 Cloudflare 것으로 변경
네임서버 전파에는 보통 10분~1시간이 걸리고, 최대 48시간까지 소요될 수 있다. 전파되는 동안 터널 설정을 먼저 진행했다.
5단계: Cloudflare Tunnel 설정 — Claude Code가 전부 처리
여기서부터는 Claude Code가 명령어를 직접 실행하면서 진행했다. 내가 한 건 cloudflared tunnel login 실행 후 브라우저에서 도메인 인증을 클릭한 것뿐이다.
5-1. cloudflared 설치
brew install cloudflared
5-2. Cloudflare 계정 인증
cloudflared tunnel login
# 브라우저가 열리고, 도메인을 선택(authorize)하면 완료
# 인증서가 ~/.cloudflared/cert.pem에 저장됨
5-3. 터널 생성
cloudflared tunnel create paperclip
# Tunnel credentials written to ~/.cloudflared/<터널ID>.json
# Created tunnel paperclip with id <터널ID>
5-4. DNS 라우팅 연결
cloudflared tunnel route dns paperclip abc.mydomain.com
# Cloudflare DNS에 CNAME 레코드가 자동 추가됨
5-5. 설정 파일 작성
Claude Code가 ~/.cloudflared/config.yml을 자동으로 생성해줬다:
tunnel: <터널ID>
credentials-file: /Users/<username>/.cloudflared/<터널ID>.json
ingress:
- hostname: abc.mydomain.com
service: http://127.0.0.1:3100
- service: http_status:404
5-6. 터널 실행 및 확인
cloudflared tunnel run paperclip
서울(ICN) Cloudflare 엣지에 4개 커넥션이 연결되었다. NS 전파가 끝나면 바로 접속 가능해진다.
6단계: 맥북 재부팅에도 자동 실행되도록 설정
터널이 잘 작동하는 걸 확인한 후, 재부팅해도 자동으로 뜨도록 macOS LaunchAgent에 등록했다. 여기서도 Claude Code가 처리해줬는데, 두 가지 서비스를 모두 등록해야 했다.
6-1. Cloudflare Tunnel 자동 시작
cloudflared service install
이 명령으로 LaunchAgent가 등록되지만, 한 가지 함정이 있었다. 자동 생성된 plist 파일의 ProgramArguments에 tunnel run paperclip 인자가 빠져 있어서, 서비스가 시작은 되지만 터널이 열리지 않았다. Claude Code가 로그를 확인하고 바로 plist를 수정해줬다:
<!-- ~/Library/LaunchAgents/com.cloudflare.cloudflared.plist -->
<array>
<string>/opt/homebrew/bin/cloudflared</string>
<string>tunnel</string>
<string>run</string>
<string>paperclip</string>
</array>
6-2. PaperClip 서비스 자동 시작
PaperClip 자체도 자동 시작이 등록되어 있지 않았다. Claude Code가 ps로 현재 실행 방식을 확인한 뒤, LaunchAgent plist를 새로 작성해줬다:
<!-- ~/Library/LaunchAgents/com.paperclip.server.plist -->
<key>ProgramArguments</key>
<array>
<string>/path/to/npx</string>
<string>paperclipai</string>
<string>onboard</string>
<string>--yes</string>
</array>
등록 후 launchctl load로 서비스를 시작하고, curl http://127.0.0.1:3100으로 HTTP 200 응답을 확인했다.
7단계: 트러블슈팅 — 포트가 두 개?
서비스 등록 직후 이상한 걸 발견했다. localhost:3100과 localhost:3101 둘 다 PaperClip이 열려 있었다. Claude Code가 lsof로 원인을 바로 찾아줬다:
| 포트 | 원인 |
|---|---|
| 3100 | 아까 수동으로 실행했던 원래 프로세스 |
| 3101 | LaunchAgent가 띄운 새 인스턴스 (3100이 점유되어 자동으로 다음 포트 사용) |
해결은 간단했다. 수동 프로세스를 종료하고 서비스를 재시작하면 새 인스턴스가 3100 포트를 잡는다:
kill <수동 실행 PID>
launchctl unload ~/Library/LaunchAgents/com.paperclip.server.plist
launchctl load ~/Library/LaunchAgents/com.paperclip.server.plist
최종 결과
| 항목 | 설정 |
|---|---|
| PaperClip | localhost:3100, LaunchAgent으로 자동 시작 + 크래시 시 자동 재시작 |
| Cloudflare Tunnel | abc.mydomain.com → localhost:3100, LaunchAgent으로 자동 시작 |
| SSL | Cloudflare 자동 제공 (별도 인증서 설정 불필요) |
| 동적 IP | 아웃바운드 터널이므로 IP 변경과 무관 |
| DNS | dnszi → Cloudflare로 자동 이전 완료 |
맥북을 재부팅하더라도 PaperClip과 Cloudflare Tunnel이 자동으로 시작되며, 어디서든 https://abc.mydomain.com으로 접속할 수 있다.
회고: Claude Code와 함께한 인프라 작업
솔직히 이 작업을 혼자 했다면 한참 걸렸을 것이다. ngrok으로 로컬에 띄운 웹서비스를 외부도메인에 연결할수 있다는 정도의 사전지식만 있었을 뿐 Cloudflare Tunnel이라는 선택지 자체도 몰랐다.
Claude Code가 좋았던 점은 단순히 명령어를 대신 쳐준 것이 아니라, 각 단계에서 선택지를 제시하고 장단점을 설명해준 것이다. "Nginx로 할까, Cloudflare Tunnel로 할까", "기존 DNS를 유지할까, 이전할까" — 이런 의사결정 포인트에서 판단 근거를 함께 제공해줬기 때문에 빠르게 결정할 수 있었다.
또한 plist에 인자가 빠진 버그나, 포트 중복 문제 같은 트러블슈팅도 로그를 확인하고 즉시 원인을 파악해서 고쳐줬다. 전체 과정이 하나의 대화 세션에서 약 20분 만에 끝났다.
로컬 서비스를 도메인으로 열어야 하는 비슷한 상황이라면, Cloudflare Tunnel을 추천한다. 특히 고정 IP가 없는 환경에서는 사실상 유일한 무료 해결책이다. (단, 당연하게도 맥북이 켜져있고 인터넷에 연결되어 있어야만 가능)