[개발][n8n] 구글 시트 대신 MySQL! 자동화 블로그 구축 삽질기 (feat. Merge 교착상태 & 루프 끊김 해결)
AI 자동화 뉴스를 시작하고, 얼마전 가게부를 만들면서 MySQL이 아닌 Google Sheets 를 이용하는 것은 좋지 못하다고 느껴졌다
수천 행의 시트 데이터가 발생하게 된다면 매번 읽어오는 속도 저하도 문제고 금액에 대한 부분, 무엇보다 안정성에 대해서 의문이 생겼다.
그래서 결심했다. "데이터베이스의 정석, MySQL로 이사 가자!"
하지만 단순한 DB 교체 작업인 줄 알았던 이 과정에서, DB 설계부터 AI 로직 변경, 그리고 n8n의 작동 원리를 뼈저리게 배우게 되는 거대한 삽질을 겪게 된다.
1. MySQL 테이블 설계 (Synology & Docker)
가장 먼저 시놀로지 NAS에 Docker로 띄워둔 MySQL에 접속해 뉴스를 저장할 테이블을 만들었다.
기존 구글 시트의 컬럼을 그대로 가져오되, 조회 속도를 위해 URL에 인덱스를 걸었다.
CREATE TABLE `post_history` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`original_title` varchar(255) NOT NULL COMMENT '뉴스 원문 제목',
`original_url` varchar(500) NOT NULL COMMENT '중복 체크용 URL',
`original_content` text DEFAULT NULL COMMENT '뉴스 요약 내용',
`ghost_post_id` varchar(100) DEFAULT NULL COMMENT '발행된 Ghost 글 ID',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_url` (`original_url`) -- 검색 속도를 위해 인덱스 추가
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
이제 구글 시트의 Lookup 노드 대신, n8n의 MySQL 노드 가 이 테이블을 조회하게 된다.
2. AI 중복 체크 로직의 변화 (System Prompt)
DB를 도입하면서 AI에게 "이 글이 중복인지 판단해줘"라고 요청하는 방식도 더 스마트하게 바꿨다.
- [기존 방식]: 단순히 구글 시트의 전체 행을 가져와서 비교 (데이터가 많아지면 토큰 낭비 심함)
- [변경된 방식]: MySQL에서 최근 발행된 글 50개의 제목과 요약본만 텍스트로 합쳐서(CONCAT) AI에게 '참고 자료'로 던져준다.
1) Context 생성 쿼리
SELECT
CONCAT('제목: ', original_title, '\n요약: ', IFNULL(original_content, '내용 없음')) AS combined_info
FROM post_history
ORDER BY id DESC
LIMIT 50;
2) AI 에이전트 프롬프트 수정
위 쿼리 결과를 {{ $json.history_list }} 변수로 받아 프롬프트에 심어주었다.
[System Prompt]
당신은 블로그 콘텐츠의 중복 여부를 판단하는 냉철한 에디터입니다.[분석할 데이터]
- 새로운 뉴스 제목: {{ $json.title }}
- 최근 발행 목록: {{ $json.history_list }}
[판단 기준]
- 중복(True): 핵심 사건과 주제가 동일하거나, 제목이 달라도 메인 정보가 이미 발행된 글(
history_list)에 존재하는 경우.- 신규(False): 키워드는 같아도 다른 사건을 다루거나, 비교할 과거 데이터가 없는 경우.
이렇게 하니 AI가 DB에 저장된 과거 기록을 바탕으로 훨씬 정확하게 중복 여부를 가려내기 시작했다.
3. 첫 번째 삽질: 영원히 오지 않는 신호 (Merge Node Deadlock)
로직 변경 후 야심 차게 실행 버튼을 눌렀지만, 워크플로우가 무한 대기(Running...) 상태에 빠졌다.
범인은 습관적으로 사용한 Merge 노드였다.
- 설계: RSS 데이터와 SQL 중복 체크 결과를
Merge노드로 합치려 함. - 문제:
If노드에서 **'중복(False)'**으로 판명되면 해당 데이터 흐름이 끊긴다. - 결과:
Merge노드는 "두 입력이 모두 와야 실행"되는데, 한쪽이 끊겨버리니 영원히 기다리다 교착 상태(Deadlock)에 빠짐.
✅ 해결:
Merge 노드를 과감히 삭제했다. n8n에서는 노드를 굳이 합치지 않아도 {{ $('Node Name').item.json.field }} 문법으로 상위 데이터를 어디서든 가져올 수 있기 때문이다.
4. 두 번째 삽질: 루프가 왜 돌다 말지? (Loop 끊김 현상)
Merge 문제를 해결하고 돌려보니, 이번엔 5개의 뉴스 중 1개만 검사하고 워크플로우가 1초 만에 종료되는 현상이 발생했다.
분명 Limit 5를 걸었는데 왜 루프가 안 돌지?
🚨 원인 분석
n8n의 SplitInBatches (Loop) 노드는 "자신에게 신호가 다시 돌아와야" 다음 아이템을 꺼내준다.
나는 If 노드에서 다음과 같이 설정했었다.
- True (신규 글): 글 작성 -> 알림 -> Loop로 연결 (O)
- False (중복 글): 연결 없음 (끊김) -> Loop로 신호 안 감 (X)
첫 번째 뉴스가 이미 DB에 있는 글이라 False로 빠졌는데, 그 뒤에 연결된 선이 없으니 "할 일 끝!" 하고 루프 자체가 죽어버린 것이다.
✅ 해결: 모든 길은 루프로 통하게 하라
중복인 경우에도 "이건 패스하고 다음 뉴스 줘!"라고 루프 노드에게 알려줘야 한다.
- 루프의 마지막 지점에
Replace Me(No Operation) 노드를 하나 둔다. If노드의 False(중복) 출력선을 이Replace Me노드에 연결한다.Replace Me노드는 다시Loop노드의 입력으로 연결한다.
이렇게 하니 글을 쓰든(True), 중복이라 넘기든(False) 무조건 루프로 신호가 돌아가서 5개의 뉴스를 완벽하게 순회하게 되었다.
💡 결론
구글 시트에서 MySQL로의 전환은 단순히 저장소를 바꾸는 것을 넘어, 데이터 처리 로직과 AI 프롬프트 최적화까지 고민하게 만든 좋은 계기였다.
특히 n8n을 다룰 때 다음 두 가지는 잊지 못할 것 같다.
- Merge 노드 주의: 조건부 분기 뒤에는 쓰지 말자.
- Loop 연결 필수: '실패/중복' 경로도 반드시 Loop로 되돌려줘야 프로세스가 멈추지 않는다.
이제 내 홈 서버의 MySQL은 묵묵히 중복을 걸러내고, AI 에디터는 더 똑똑하게 뉴스를 큐레이션 하고 있다.
"자동화는 구축하는 것보다, 예외 처리를 잡는 게 90%다" 라는 말을 실감한 하루였다.