한 클라이언트의 요구사항은 간단해 보였다. 백엔드 DB는 따로 있지만, 슬랙 특정 채널에 수년간 쌓인 메시지와 이모지 반응, 스레드를 추적해서 DB에 백필해달라.
한 문장이었다. 그런데 막상 들어가 보니 일은 세 겹이었다.
1막 — 코드가 아니라 권한이 먼저였다
본격적인 개발 전에 사람의 일이 절반이었다.
- 클라이언트 워크스페이스에 초대받아 채널 접근 확보
- 워크스페이스에 봇 생성
- 어드민과 협의해 대상 채널에 봇 초대
- 필요한 스코프(
conversations.history,conversations.replies) 권한 부여
여기까지 코드는 한 줄도 안 썼다.
2막 — 채널을 통째로, 안전하게 긁어오기
단순히 메시지를 읽는 게 아니었다. 대량 데이터를 레이트리밋 아래에서 빠짐없이 가져오는 설계가 필요했다.
- 두 API를 엮었다 — top-level 메시지는
conversations.history로 페이지네이션하고, 답글이 있는 것만 골라conversations.replies로 스레드를 따로 수집했다(부모 메시지는 중복 제거). - 레이트리밋이 진짜 벽이었다 — 분당 ~50회 제한에 스레드만 8천 건.
429가 오면Retry-After만큼 기다렸다 재시도하게 했다. 느려질지언정 실패하지 않게. - 한 번만 받고, 가공은 로컬에서 — API 호출이 비싸니 원본을 그대로 로컬에 저장(18MB)하고, 변환·매핑 같은 반복 작업은 전부 로컬 파일로 돌렸다. 다시는 긁지 않았다.
- 중단·재개 — 수집 단위를 한 줄씩 append+flush로 적고, 재실행하면 이미 받은 건 건너뛰게 했다. 8천 건 도중에 끊겨도 그 자리에서 이어받았다.
쌓인 데이터의 모양
원본은 가공하지 않고 세 개의 파일로 떨어뜨렸다. 역할을 분리한 게 핵심이다.
users.json : 작성자 id → { name, email } 매핑 (한 번만 받으면 끝)
messages.json : 채널 top-level 메시지 원본 배열 (history 그대로)
threads.jsonl : 스레드 한 개 = 한 줄
threads.jsonl은 이렇게 한 줄에 부모 ts와 그 답글 원본을 통째로 담았다.
{"parent_ts": "<원글ts>", "replies": [
{"user": "<uid>", "ts": "<답글ts>", "text": "...", "reactions": [...]}
]}
여기서 의도한 게 두 가지다.
- 왜 messages는 json, threads는 jsonl인가 — 본문은 받고 끝나는 한 덩어리라 통배열 json이면 충분했다. 반면 스레드는 8천 번을 돌며 조금씩 쌓는 비싼 루프라, "한 줄 = 한 스레드"인 jsonl이 append만으로 안전하게 늘어나고 그대로 재개 체크포인트가 된다.
- 왜 원본 그대로 두는가 —
reactions,reply_users같은 필드를 미리 버리지 않고 통째로 남겨두면, 나중에 "엑셀로 뽑기" "담당자 추론" "타임라인 복원" 같은 가공이 전부 이 로컬 파일 위에서 재료를 다시 긁지 않고 돌아간다.
3막 — "넣기"는 적재가 아니라 복원이었다
진짜 작업은 여기였다. 긁어온 메시지를 CRM의 실제 데이터와 연결하는 것.
원글은 이미 CRM에 있는 문의였고, 스레드 답글은 거기 달린 상담 기록이었다. 즉 답글 한 건 = 문의 타임라인의 메모 한 줄. 문제는 두 시스템 사이에 공통 ID가 없다는 거였다.
그래서 본문에서 전화번호를 파싱·정규화하고 생성 시각이 가장 가까운 문의에 매칭했다. 깨끗한 1:1만 자동 적용하고, 애매한 건 CSV로 빼서 사람이 확인하게 했다. 메모의 작성 시각은 원래 답글 시각으로 넣어 타임라인 순서를 그대로 복원했다 — 현재 시각으로 박으면 대화 흐름이 무너지니까.
마지막은 멱등성이었다. 대상 테이블에 중복 방지 키가 없어서, 재실행하면 데이터가 두 배가 된다. dry-run으로 생성될 건수와 샘플을 먼저 확인하고, 단일 트랜잭션으로 딱 한 번만 INSERT했다.
남는 것
"슬랙 채널을 DB에 넣어줘"는 한 문장이었다. 그런데 실제 난이도는 코드가 아니라 권한 협의, 레이트리밋 아래의 대량 수집, 비싼 호출을 한 번만 하는 캐싱·재개, 그리고 공통 키도 없는 두 시스템을 잇는 데이터 매칭에 있었다.
결국 이건 적재(load)가 아니라 복원(reconstruct)에 가까운 일이었다.