← Back to list

창식이와 함께하는 물류 개발 라이프(with 하네스)

안녕하세요. 펫프렌즈 인터널서비스 물류개발파트 백엔드 개발자 포비입니다. 🙂

pt-pobi in 펫프렌즈 기술블로그 · 2026-05-26 06:08 · 150 claps · 20.8 min read
#ai #harness-engineering #backend #claude #claude-code
Open on Medium ↗
Wiki topics: LLM · Large Language Models AI · AI · General 🌐 · Web Development

창식이와 함께하는 물류 개발 라이프(with 하네스)

펫프렌즈 물류 창고 지킴이 창식이

펫프렌즈 물류 창고 지킴이 창식이

안녕하세요. 펫프렌즈 인터널서비스 물류개발파트 백엔드 개발자 포비입니다. 🙂

저의 두 번째 기술 블로그는 Slack 봇을 실제 운영하면서, 에이전트가 장기·복잡한 작업을 “그럴듯하게”가 아니라 꾸준히 정확하게 수행하게 만드는 설계를 어떻게 적용했는지 정리한 기록입니다.

이번 블로그에서 다루는 내용!

  • 왜 “에이전트 루프”보다 “컨텍스트/피드백 루프”가 더 중요해지는지
  • 지식 베이스, 규칙 인덱스, 교정 로그로 운영 품질을 끌어올리는 방식
  • MCP 채널을 이용해 Slack ↔ Claude Code를 연결한 아키텍처/운영 방식

저희 인터널서비스 물류개발파트는 주문 이후 물류 전반을 담당하는 여러 레포를 관리하며, 팀에서 반복적으로 하는 작업이 몇 가지 있습니다.

Sentry 에러 + Loki 로그 분석

스택트레이스를 확인하고, trace_id로 로그를 추적해 어떤 사용자가 어떤 요청을 했는지 흐름을 재구성하는 작업

레포 영향도 분석

“반품 플로우 수정으로 인한 로직 수정”이 각 레포에 어떤 영향을 미치는지 파악하는 작업

단순 반복처럼 보이지만, 정확한 답을 내려면 도메인 규칙을 알아야 합니다. 박스 분할 로직은 특정 필드 기준으로 계산하고, 재고 데이터는 전용 배치가 관리합니다. 외부 인터페이스 연동은 API 스펙이 달라 각각 다르게 처리해야 합니다.

때마침 OpenAI의 Harness Engineering과 Anthropic의 Effective harnesses for long-running agents, Effective context engineering for AI agents 문서를 읽으며 “하네스 엔지니어링” 개념을 알게 됐습니다. 이를 Slack 봇에 적용해보자는 생각으로 창식이라는 봇을 만들게 되었습니다.

1. 하네스 엔지니어링이란?

정의

하네스 엔지니어링(Harness Engineering)은 AI 에이전트가 복잡한 작업을 안정적으로 수행할 수 있도록 환경, 컨텍스트, 피드백 루프를 설계하는 분야입니다.

에이전트를 단순히 “실행하는 것”을 넘어, 에이전트가 동작하는 조건 자체를 설계하는 것이 핵심입니다.

“소프트웨어 엔지니어링팀의 주된 역할이 코드를 직접 작성하는 것에서, 환경을 설계하고 의도를 명세하고 피드백 루프를 구축하는 것으로 바뀐다.” — OpenAI, Harness Engineering: leveraging Codex in an agent-first world (2026)

왜 하네스가 필요한가?

에이전트를 무턱대고 실행하면 두 가지 문제가 생깁니다.

① 컨텍스트 불일치 (Context Drift)

컨텍스트 윈도우가 채워질수록 에이전트는 초기 목표에서 멀어집니다. 장기 작업에서는 자신의 작업을 과도하게 긍정적으로 평가하는 자기 편향이 생깁니다.

Anthropic 연구에 따르면, 에이전트는 자신의 작업을 평가할 때 과도하게 긍정적으로 판단하는 경향이 있다. 이를 해결하기 위해 작업 담당 에이전트와 평가 담당 에이전트를 분리하는 것이 효과적이다. — Anthropic, Effective harnesses for long-running agents

② 잘못된 전제에서의 출발

에이전트가 시스템 구조, 도메인 규칙, 코딩 컨벤션을 모르면 아무리 정교한 파이프라인도 결과가 틀립니다.

에이전트에게 얼마나 좋은 컨텍스트를 주느냐가 에이전트 루프 설계보다 더 중요합니다. — Anthropic, Effective context engineering for AI agents

OpenAI Codex 하네스 (2026)

OpenAI는 Codex 에이전트만으로 프로덕션 시스템을 구축했습니다. 핵심 설계 원칙은 세 가지입니다.

  • AGENTS.md는 ~100줄 인덱스만 제공하고, docs/ 폴더를 단일 진실 공급원으로 운영
  • CI/linting으로 지식 베이스의 최신성, 교차 링크, 구조를 자동 검증
  • “doc-gardening” 에이전트가 주기적으로 낡은 문서를 정리

Anthropic — 장기 실행 앱 하네스 (2025)

Anthropic 엔지니어링팀은 GAN(Generative Adversarial Network)에서 영감을 얻어 3-에이전트 구조를 설계했습니다.

플래너 (Planner)
  → 사용자 프롬프트를 상세 제품 스펙으로 확장
생성기 (Generator)
  → 스프린트 단위로 기능 구현
평가기 (Evaluator)
  → Playwright로 실행 앱을 직접 테스트, QA 수행
  (평가 통과 전까지 루프 반복)

“에이전트를 평가한다는 것은 하네스와 모델이 함께 동작하는 시스템 전체를 평가하는 것이다.” — Anthropic Engineering

우리 팀의 접근

업계 사례에서 반복되는 원칙은 하나입니다.

파이프라인의 정교함보다, 에이전트가 출발하는 컨텍스트의 품질이 결과를 결정합니다.

저희는 이를 중심으로 설계했습니다.

  • knowledge/를 단일 진실 공급원으로 — OpenAI의 docs/ 구조와 같은 철학
  • CLAUDE.md는 인덱스만 — OpenAI의 ~100줄 AGENTS.md 원칙 따름
  • corrections.md로 점진적 정제 — 실수에서 배우고 규칙화하는 루프

2. 핵심 철학

2–1. 공유 컨텍스트 (Single Source of Truth)

모든 봇이 동일한 ground truth에서 출발하고, 지식 베이스는 두 층으로 나뉩니다.

공유 knowledge

모든 봇이 공통으로 참조하는 규칙과 안전 지침으로 한 곳을 수정하면 모든 봇에 즉시 반영됩니다.

knowledge/
├── corrections.md   # 공용 교정 로그 → 모든 봇이 작업 전 필수 확인
└── safety_rules.md  # 공통 안전 규칙

봇별 knowledge

각 봇의 도메인에 맞는 지식으로 봇 디렉토리 안에서 독립적으로 관리합니다.

bots/changsik/knowledge/
├── outbound.md      # 출고 도메인
├── inbound.md       # 입고 도메인
├── stock.md         # 재고 도메인
├── reverse.md       # 반품 도메인
├── sentry.md        # Sentry 허용 프로젝트 목록
├── loki.md          # Loki 로그 조회 패턴 및 허용 서비스 목록
└── tempo.md         # Tempo 트레이스 조회 패턴

이 구분이 중요합니다. corrections.md는 어느 봇이 실수를 해도 모든 봇이 그 교정을 흡수합니다. 도메인 지식은 봇별로 격리돼 서로 간섭하지 않습니다.

2–2. 규칙 경량화 (Progressive Disclosure)

CLAUDE.md는 인덱스 역할만 하고, 세부 내용은 필요할 때 로드합니다. 보통 CLAUDE.md에 모든 규칙을 작성하기 쉽습니다. 하지만 이렇게 하면 파일이 무거워지고, 에이전트가 매번 전체 컨텍스트를 소비합니다.

에이전트는 작업 중 필요한 시점에 해당 파일만 읽습니다.

→ 컨텍스트 소비 최소화 + 필요한 정보는 빠짐없이 제공

2–3. 점진적 정제 (Permanent Correction)

실수가 규칙이 되고, 시간이 지날수록 시스템이 정확해집니다.

실수 발생
  ↓
사용자 교정
  ↓
corrections.md에 기록 (동일 실수 2회 반복 시 필수)
  ↓
다음 작업부터 자동 적용

규칙 파일은 봇의 시스템 프롬프트에 자동으로 포함됩니다. 재시작하거나 세션이 바뀌어도 유지됩니다.

3. 아키텍처

구조 및 전체 흐름

디렉토리 구조

디렉토리 구조

사용 흐름

사용 흐름

Claude 연동 — MCP 채널

Slack과 Claude Code를 연결하는 핵심은 MCP(Model Context Protocol) 채널입니다. MCP 채널은 Claude Code 외부에서 발생한 이벤트를 세션 안으로 밀어넣는 MCP 서버로, 자세한 스펙은 Claude Code 공식 문서 — Channels Reference에서 확인할 수 있습니다.

일반 MCP 서버와의 차이는 claude/channel capability를 선언하는 것 하나뿐입니다. 이것만으로 Claude Code가 notification 리스너를 등록하고, 서버가 이벤트를 보내면 Claude가 즉시 반응합니다.

일반 MCP 서버:  Claude가 필요할 때 도구를 호출 (pull)
채널 MCP 서버:  외부 이벤트가 Claude 세션으로 push (push)

연결 구조

Claude Code는 시작 시 .mcp.json 설정을 읽고 slack-channel을 subprocess로 직접 실행합니다. 통신은 stdio로만 이루어집니다. 별도 포트나 네트워크 설정 없이, Claude Code가 프로세스를 띄우고 그 stdin/stdout으로 MCP 프로토콜을 주고받습니다.

Claude Code 프로세스
│  (subprocess 실행)
└─ slack-channel 프로세스
     ├─ MCP ↔ Claude Code  (stdio)     ← stdout은 MCP 전용
     ├─ 로그 출력           (stderr)    ← 로그는 stderr로 분리
     └─ Slack API           (WebSocket / HTTPS)

중요한 점은 stdout/stderr 분리입니다. MCP 프로토콜이 stdout을 점유하므로, console.log를 쓰면 통신이 깨집니다. 모든 로그는 process.stderr.write()로 출력해야 합니다.

capability 선언 - 채널 등록

const mcp = new Server(
  { name: 'slack-changsik', version: '0.0.1' },
  {
    capabilities: {
      experimental: { 'claude/channel': {} },
      tools: {},
    },
    instructions:
      'Slack 메시지가 <channel source="slack-changsik" ...> 형태로 옵니다. ' +
      '반드시 reply 툴로 응답하세요.',
  },
)
  • claude/channel: {} — Claude Code에 notification 리스너를 등록합니다. 없으면 일반 MCP 서버로 동작합니다.
  • instructions — Claude의 시스템 프롬프트에 자동 추가되어, 이벤트 형식과 응답 방법을 알려줍니다.

이벤트 push — Slack → Claude

Slack에서 멘션이 오면 세 가지를 처리합니다

① 수신 확인 — 즉시 리액션을 달아 받음 신호를 보냅니다.

② 스레드 히스토리 수집conversations.replies로 스레드의 전체 대화를 가져오고, 첨부 파일이 있으면 다운로드합니다.

③ Claude에 notification 전송

await mcp.notification({
  method: 'notifications/claude/channel',
  params: {
    content: '[스레드 히스토리]\\n[U001]: 창식아 출고 오류 좀 봐줘\\n[bot]: 확인하겠습니다\\n\\n[현재 메시지]\\n@창식 아직도 안 됐어',
    meta: {
      channel: 'C08XXXXX',     // Slack 채널 ID
      thread_ts: '1234.5678',  // 스레드 타임스탬프 (응답 시 필요)
      user: 'U08XXXXX',        // 요청자
      ts: '1234.5679',         // 현재 메시지 타임스탬프
    },
  },
})

Claude는 이 이벤트를 <channel> 태그로 받습니다

<channel source="slack-changsik" channel="C08XXXXX" thread_ts="1234.5678" user="U08XXXXX">
[스레드 히스토리]
[U001]: 창식아 출고 오류 좀 봐줘
[bot]: 확인하겠습니다
[현재 메시지]
@창식 아직도 안 됐어
</channel>

스레드 히스토리 전체가 포함되기 때문에 Claude는 이전 대화 맥락을 완벽히 이해한 상태에서 응답합니다.

응답 — reply 도구

채널로 이벤트를 받기만 하면 단방향입니다. 봇은 양방향이 필요하므로 reply 도구를 MCP 서버에 등록합니다.

mcp.setRequestHandler(CallToolRequestSchema, async req => {
  if (req.params.name === 'reply') {
    const { channel, thread_ts, text } = req.params.arguments
    await web.chat.postMessage({ channel, thread_ts, text })
    return { content: [{ type: 'text', text: 'sent' }] }
  }
})

Claude 입장에서는 “reply 도구를 호출했더니 Slack에 메시지가 갔다”는 것만 알고, 실제 Slack API 호출은 채널 서버가 처리합니다.

4. CLI와 SDK, 두 가지 실행 모드

봇의 특성에 따라 두 가지 모드를 지원하고 있습니다.

CLI 모드 — 깊은 분석에 적합

tmux 세션 → claude CLI 실행 → MCP 채널 서버로 Slack 연결

Claude Code CLI가 단일 세션으로 동작하므로 컨텍스트가 대화 전체에 걸쳐 유지되어 깊은 코드 분석에 유리합니다. 슬래시 커맨드, 훅, 서브에이전트도 사용 가능합니다.

claude --model claude-sonnet-4-6 \\
  --dangerously-load-development-channels \\
  server:slack-channel

SDK 모드 — 동시 처리에 적합

tmux 세션 → python3 runner/main.py → Claude Agent SDK → 멀티세션

Python 기반 공유 런타임으로 스레드별 독립 세션을 생성합니다. 최대 5개 세션을 동시에 처리할 수 있고, APScheduler를 내장해 크론 스케줄도 지원합니다.

런타임 코드는 모든 SDK 봇이 공유하지만, 각 봇마다 별도 프로세스와 별도 venv로 실행돼 완전히 격리됩니다.

5. 봇 구조 — 3계층 메모리

각 봇은 3계층 메모리를 유지합니다.

장기  → CLAUDE.md + knowledge/     영구, 버전 관리됨
중기  → docs/memory.md             세션 중 갱신, 반복 패턴 누적
단기  → Slack 스레드                 멘션 시마다 전체 스레드 히스토리를 컨텍스트에 포함

CLAUDE.md 핵심 구성

봇 디렉토리 구조

bots/{봇이름}/
├── CLAUDE.md              ← 페르소나, 책임, 절대 금지 규칙
├── .claude/
│   └── settings.json      ← 권한 설정 (허용/금지 도구)
├── knowledge/             ← 도메인 지식 (출고, 입고, 재고, 반품 등)
├── docs/
│   └── memory.md          ← 반복 패턴, 팀 피드백 누적
├── log/                   ← 날짜별 처리 이력
└── mcp/                   ← 커스텀 MCP 서버

6. MCP로 확장하기

외부 도구 연결

Sentry, Grafana, 사내 API 등을 MCP 서버로 연결합니다.

{
  "mcpServers": {
    "sentry": {
      "command": "uvx",
      "args": ["mcp-server-sentry", "--auth-token", "..."]
    },
    "grafana": {
      "command": "uvx",
      "args": ["mcp-grafana"],
      "env": {
        "GRAFANA_URL": "<https://grafana.internal>",
        "GRAFANA_SERVICE_ACCOUNT_TOKEN": "..."
      }
    }
  }
}

오픈소스 MCP 서버는 uvx로 바로 실행하고, 사내 API는 FastMCP로 커스텀 서버를 작성합니다.

from mcp.server.fastmcp import FastMCP
mcp = FastMCP("wms")

@mcp.tool()
def list_outbounds(date: str, status: str = "") -> str:
    """출고 목록 조회"""
    # WMS API 호출
    ...

7. 운영 도구

봇 라이프사이클 관리

python3 manage.py start              # 전체 봇 + 대시보드 + 워치독 시작
python3 manage.py status             # 상태 확인
python3 manage.py restart changsik   # 특정 봇 재시작
python3 manage.py attach changsik    # tmux 세션 직접 접속
python3 manage.py watch              # 크래시 감지 + 자동 재시작

bots/ 디렉토리를 스캔해 .env가 있는 봇을 자동 탐지합니다. 봇을 추가할 때 manage.py를 수정할 필요가 없습니다. 또한 30초 간격으로 tmux 세션 상태를 체크합니다. CLI 모드에서는 셸 프롬프트가 보이면 Claude 종료로 판단하고, SDK 모드에서는 Python traceback을 감지합니다.

대시보드

운영은 터미널 기반이지만, 봇 상태를 한눈에 보기 위한 간단한 웹 뷰어를 만들었습니다. 봇뿐 아니라 LLM 자체도 관측 하며 메트릭을 확인하고 있습니다.

밤티나는 대시보드 이쁘게 고쳐줘서 고마워요 테디!

밤티나는 대시보드 이쁘게 고쳐줘서 고마워요 테디!

8. 운영

창식이는 경비견 봇입니다. 물류 창고를 지키는 충직한 진돗개라는 페르소나를 갖고 있습니다. 실제로 어떻게 쓰이는지 몇 가지 사례를 공유합니다.

[사례 1] Sentry 에러 + Loki 로그 통합 분석

창식이는 Sentry MCP로 이슈를 조회하고, Grafana MCP로 로그를 조회하고, 실제 코드를 읽어서 원인을 분석합니다. 단순히 스택트레이스를 보여주는 게 아니라 흐름을 재구성해서 보고합니다.

[사례 2] Tempo + Loki 연계 분석(세 번 틀리고 만든 규칙)

Grafana Tempo가 추가되면서 처음엔 예상치 못한 혼선이 생겼습니다. 한 번에 배운 게 아니라 두 번 틀리고 나서야 제대로 된 규칙이 만들어진 사례입니다.

(1차 실수) 마커를 에러로 단정

팀원: @창식이 trace_id 확인해봐
      a1b2c3d4e5f60789abcdef1234567890
창식: 컨트롤러 스팬에서 에러가 발생했습니다. 마커가 붙어 있습니다.

실제로 Loki에서 같은 trace_id로 확인해보니 응답 200 OK, ERROR 로그 없고, 정상 처리된 요청이었습니다. 에러가 아니라 컨트롤러/주요 스팬을 강조하는 표시였는데 에러로 잘못 해석한 것입니다.

→ 교정 — 에러 단정 금지, 반드시 Loki 교차 확인

(2차 실수)이번엔 반대로 Loki 200만 믿음

같은 trace_id를 팀원이 다시 요청해서 재조회했을 때, 이번엔 교정을 과도하게 적용했습니다.

창식: Loki 확인 결과 응답 200 OK, ERROR 로그 없습니다. 정상 처리로 보입니다.

그런데 Tempo를 다시 보면 컨트롤러 스팬에 [ERROR] 마커와 함께 다음이 찍혀 있었습니다.

- [10:23:14.120 KST] pf-wms-order | POST /api/orders/... | 3842ms [ERROR]
  exception.type: NullPointerException
  exception.message: Cannot invoke method getWarehouseId() on null object
  exception.stacktrace:
    OrderController.createOrder(OrderController.java:87)
    ...

글로벌 예외 핸들러가 NPE를 잡아 200으로 변환했기 때문에 액세스 로그에는 200이 찍혔지만, 실제 출고 생성은 실패한 상태였습니다.

→ 두 번째 교정이 세 개의 규칙으로 완성

## [2026-05-08] Loki 200 OK만 보고 트레이스 에러를 부정함
**규칙:**
1. get_trace 출력에서 [ERROR] 마커와 exception 속성을 반드시 끝까지 확인한다.
2. Loki 액세스 로그 200은 "예외 없음"을 의미하지 않는다.
   글로벌 예외 핸들러가 200으로 변환할 수 있다.
3. Tempo와 Loki가 충돌할 때(트레이스 에러 + Loki 200)
   "예외는 발생했으나 응답은 200으로 변환됨"으로 해석한다.

(3차 실수) exception 속성을 MCP 툴 한계로 단정

2차 교정 이후, 교차 확인하는 절차가 생겼지만 새로운 문제가 나타났습니다.

창식: [ERROR] 마커를 확인했습니다. 다만, exception 상세 정보는
      Tempo MCP 툴로는 조회할 수 없습니다. Loki에서 직접 확인해 주세요.

직접 Grafana 대시보드에서 확인해보니 스팬에 NullPointerException 스택트레이스가 그대로 있었습니다. get_trace는 스팬의 로그 레코드에서 exception.type, exception.message, exception.stacktrace를 파싱해 출력하는데, 봇이 결과를 끝까지 읽지 않고 "툴 한계"라고 단정한 것이었습니다.

→ 네 번째 규칙 추가

4. get_trace는 exception.type / exception.message / exception.stacktrace를 반환한다.
   결과에서 exception 속성이 보이지 않더라도 "조회 불가"라고 단정하지 않는다.
   없으면 해당 스팬에 예외 이벤트가 없는 것이다.

처음엔 Tempo만 믿어서 틀리고, 다음엔 Loki만 믿어서 틀리고, 세 번째엔 도구 한계를 잘못 단정해 틀렸습니다. 세 번의 실수를 거쳐야 이 단순한 원칙에 도달했습니다. corrections.md의 가치는 바로 이것입니다.

[사례 3] 교정 로그

창식이가 Sentry 시각을 UTC 그대로 “새벽 02:26에 발생”이라고 보고한 적이 있습니다. 실제로는 KST 기준 오전 11:26이었습니다. 이 실수를 corrections.md에 기록한 뒤로는 시각을 항상 KST로 변환해서 보고합니다.

## [2026-04-14] Sentry 시각을 UTC 그대로 보고
규칙: Sentry 시각(firstSeen, lastSeen)은 UTC이므로
       Slack 보고 시 반드시 KST(+9)로 변환해서 전달한다.

이런 교정이 쌓이면서 봇은 점점 더 정확해집니다. 사람이 매번 같은 지적을 반복할 필요가 없습니다.

[사례 4] 봇끼리 멘션

Slack 스레드에서 봇끼리 멘션으로 핸드오프할 수 있습니다. 멘션된 봇은 스레드 히스토리 전체를 읽고 이전 봇의 결론을 이어받아 처리합니다.

만득이는 포스트 오더팀의 봇으로 가끔 이렇게 놀기도 합니다.

만득이는 포스트 오더팀의 봇으로 가끔 이렇게 놀기도 합니다.

9. 마무리! 그래서, 하네스란

하네스는 특별한 기술이 아닙니다. 결국 “좋은 프롬프트/운영 원칙”을 코드와 파일 구조로 체계화한 것에 가깝습니다.

다만, 이것들을 봇별로 격리하고, 브랜치로 관리하고, 운영 도구로 감싸는 구조가 차이를 만듭니다.

복잡한 에이전트 루프보다 좋은 컨텍스트와 정제된 규칙이 결과를 결정한다.

처음 말했듯, 모델은 CPU입니다. CPU를 최신 세대로 교체해도 OS가 허술하면 성능이 나오지 않습니다. 같은 모델이라도 하네스가 잘 짜여져 있으면 도메인 전문가처럼 동작하고, 그렇지 않으면 범용 챗봇에 머뭅니다.

현재 창식이는 물류개발파트에서 운영 중이며, 가치개발본부 프로젝트로 확장되어 여러 팀에서 봇들을 도입하고 있습니다.

앞으로 운영 경험과 컨텍스트가 더 쌓이면, 지식 베이스의 “저장/검색 방식”만 단계적으로 개선하는 방향도 열어두고 있습니다. 지금은 knowledge/를 파일로 관리하지만, 도메인 지식이 더 커지면 Obsidian 같은 관리 도구와 연동하거나 벡터 검색 기반 RAG로 전환해 필요한 컨텍스트만 동적으로 주입하는 방식이 자연스러운 다음 단계라고 생각합니다. 하네스의 큰 설계는 유지한 채로 지식 베이스 레이어만 교체할 수 있게 만들어뒀기 때문에, 확장 부담도 크지 않을 것으로 보고 있습니다.

펫프렌즈 가치개발본부는 AI를 활용한 개발 생산성 향상에 관심이 많습니다. 궁금한 점이 있으시면 편하게 문의해 주세요! 긴글 읽어주셔서 감사합니다!

참고 자료


메타데이터
post_id
a2ba2e31c21a
slug
창식이와-함께하는-물류-개발-라이프-with-하네스-a2ba2e31c21a
url
https://techblog.pet-friends.co.kr/%EC%B0%BD%EC%8B%9D%EC%9D%B4%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EB%AC%BC%EB%A5%98-%EA%B0%9C%EB%B0%9C-%EB%9D%BC%EC%9D%B4%ED%94%84-with-%ED%95%98%EB%84%A4%EC%8A%A4-a2ba2e31c21a
canonical_url
https://techblog.pet-friends.co.kr/%EC%B0%BD%EC%8B%9D%EC%9D%B4%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EB%AC%BC%EB%A5%98-%EA%B0%9C%EB%B0%9C-%EB%9D%BC%EC%9D%B4%ED%94%84-with-%ED%95%98%EB%84%A4%EC%8A%A4-a2ba2e31c21a
author_url
https://medium.com/@jt.yoo
status
ok
fetched_at
2026-06-11 12:34:08