📅 Sense Stock 개발 일지 (2025-08-11)
n8n, 사용자 질의 기반 경제/시장/주식 분석 자동화 파이프라인 구축 중 진행한 작업들을 정리합니다.
오늘은 "Data Search" 노드 Error를 해결하고, 비효율적인 '최근 질문 조회' 방식을 개선하며 워크플로우 최적화 작업을 정리했습니다.
지난번에는 사용자 경험 개선을 위한 큰 그림의 아키텍처를 구상했었다. 이번에는 그 구상을 현실로 만들게 한 두 가지 핵심 과제, 'Data Search 노드 Error 해결'과 '최근 질문 조회' 최적화 과정을 상세히 기록한다.
STEP1 - Data Search 노드 Error (질문 유효성 검사)
문제 발생
워크플로우를 테스트하던 중, 특정 질문에 대해 Data Search 노드가 에러가 발생하는 현상을 발견했다. 원인은 간단했다. 앞단의 Best Keyword Extract Agent가 분석이 불가능한 질문에 대해 결과값으로 빈 값(Empty Value)을 뱉어냈기 때문이다.
- "AI 행보"라고 질문했는데, Best Keyword Extract Agent에서 Status: unidentifiable
- 지침 프롬프트
분석 불가 시 "status"는 "unidentifiable"로, derived_data의 모든 값은 비워두거나 null(배열은 [])로 설정
- 지침 프롬프트
- 질문 자체에서 특정 키워드(S&P500)를 유추하지 못하면 빈값을 Output으로 가져오게된다.
처음엔 이 빈 값만 잘 처리하면 될 줄 알았다. 하지만 우리 워크플로우는 '단순 요약'과 '디테일 분석'으로 스트림이 나뉘는데,
특히 '디테일 분석'은 중간에 'KYC Agent'까지 실행되는 등 구조가 더 복잡해서 간단한 예외 처리로는 해결이 어려웠다.
문득 "질문이라는 게 굉장히 어려운데, 정형화되어 있지 않은 입력을 어떻게 안정적으로 처리할까?" 라는 근본적인 고민에 부딪혔다. 그래서 개별 에러를 땜질하는 대신, 1차 분석 직후 '결과 유효성'을 먼저 검사하는 방식으로 구조 자체를 바꾸기로 했다.
왜 이 방식이 최선인가?
이 구조는 단순히 에러를 막는 것을 넘어, 사용자에게 훨씬 더 나은 경험을 제공한다.
- 헛된 기대를 주지 않는다
- 나쁜 경험
시스템이 질문이 나쁘다는 걸 알면서도 "상세 분석해 드릴까요?"라고 묻는다. 사용자는 기대하며 버튼을 누르지만, 결국 "분석 불가" 답변만 받는다. 이는 사용자를 기만하는 경험이다. - 좋은 경험
시스템은 질문을 받자마자 "분석 불가"로 판단하고, "질문이 너무 광범위합니다. 구체적인 산업을 알려주시겠어요?"라고 즉시 응답한다. 사용자는 왜 안되는지 바로 이해하고 다음 행동을 결정할 수 있다.
- 나쁜 경험
- 빠르고 명확한 피드백을 준다
- 사용자는 자신의 질문이 좋은지 나쁜지 즉시 알 수 있다. 잘못된 질문이라면 어떻게 개선해야 할지에 대한 가이드를
바로 받아, 더 수준 높은 질문으로 다시 시도할 수 있다. 시간을 절약해주고 서비스가 스마트하게 작동한다는 인상을 준다.
- 사용자는 자신의 질문이 좋은지 나쁜지 즉시 알 수 있다. 잘못된 질문이라면 어떻게 개선해야 할지에 대한 가이드를
- 가치 있는 선택권을 제공한다
- 사용자가 "단순 요약 / 디테일 분석" 버튼을 보게 된다면, 그 선택지는 이미 AI가 "이 질문은 분석할 가치가 있다" 고
1차 검증을 마쳤음을 의미한다. 따라서 사용자는 어떤 버튼을 눌러도 의미 있는 결과를 얻을 수 있다는 신뢰를 갖게 된다.
- 사용자가 "단순 요약 / 디테일 분석" 버튼을 보게 된다면, 그 선택지는 이미 AI가 "이 질문은 분석할 가치가 있다" 고
🧱 해결
'선(先)검사' 로 Data Search 노드 Error 원천 차단
Data Search 노드 에러의 근본 원인은 '알 수 없는 질문'이 여과 없이 흘러들어왔기 때문이다.
그래서 워크플로우 가장 앞단에 '질문 유효성 검사' 게이트를 만들었다.
Agent의 역할 분리 - Slack Trigger Workflow
기존의 'Best Keyword Extract Agent'가 유효성 검사까지 겸하게 할까도 생각했다. 하지만 역할을 명확히 분리하는 것이 더 좋다고 판단했다. '유효성 검사 Agent'는 오직 질문의 통과/실패 여부만 판단하고, 'Best Keyword Extract Agent'는 통과된 질문에서 키워드를 뽑는 데만 집중하는 것이 더 깔끔하고 유지보수에도 유리하다.
<Slack Trigger Workflow>
- Before: '질문' ➔ 키워드 추출 (실패) ➔ Data Search (Error!)
- After: '질문' ➔ 유효성 검사 (실패, 즉시 Slack Message 전달) ➔ 워크플로우 종료
➔ 유효성 검사 (성공) ➔ 기존 Workflow 진행(디테일분석/단순요약)
이제 '알 수 없는 질문' 은 Data Search 노드에 도달조차 하지 못하므로 에러는 원천적으로 발생하지 않는다.
STEP 2 - 최근 질문 조회
문제 발생
사용자가 봇과 대화하는 흐름을 생각해보자.
- 사용자가 질문한다.
- 봇이 "디테일 분석 / 간단 요약" 버튼을 보여준다.
- 사용자가 버튼을 클릭한다.
여기서 3번 단계에서 봇은 1번 단계의 '원본 질문'이 무엇이었는지 알아야 분석을 시작할 수 있다.
처음에는 가장 단순 무식한 방법을 썼다. 바로 DB에서 해당 유저의 질문 기록을 모조리 가져와, 코드에서 가장 마지막 데이터를 추출하는 것이었다. 이 방식은 사용자 데이터가 적을 때는 문제가 없지만, 기록이 많아질수록 기하급수적으로 비효율적이 된다. 10,000개의 질문 기록을 가진 사용자는 버튼을 누르고 한참을 기다려야 할지도 모르는 일이다.
🧱 해결
'데이터 직접 전달' 로 최근 질문 조회 제거
"필요할 때마다 과거(DB)를 돌아보지 말고, 필요한 정보를 직접 전달."
해결책은 Slack Block Kit 안에 숨어있었다. 바로 Type: button의 value 속성이라는 '보이지 않는 주머니'를 사용하는 것이었다.
Slack Block 코드
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "\n*`{{ $json.real_name }}`님, 어떤 `분석`을 원하시나요?*"
}
},
{
"type": "actions",
"block_id": "select_mode",
"elements": [
{
"type": "button",
"action_id": "detailed_click",
"text": {
"type": "plain_text",
"text": ":mag: 디테일 분석",
"emoji": true
},
// 이부분이 핵심
"value": "{\"action\":\"detailed\", \"execution_id\":\"{{ $execution.id }}\", \"chatInput\":\"{{ $json.chatInput }}\"}"
},
{
"type": "button",
"action_id": "simple_click",
"text": {
"type": "plain_text",
"text": ":newspaper: 간단 요약",
"emoji": true
},
// 이부분이 핵심
"value": "{\"action\":\"simple\", \"execution_id\":\"{{ $execution.id }}\", \"chatInput\":\"{{ $json.chatInput }}\"}"
}
]
}
]
}
💨 데이터 흐름 정리
- 데이터 포장하기: 사용자가 질문을 하면, 응답으로 버튼을 보내주기 전에 chatInput(원본 질문), action(버튼 종류), execution_id(추적 ID)를 하나의 JSON 문자열로 묶는다.
- 보이지 않는 주머니에 넣기: 이 JSON 문자열을 각 버튼의 value 필드에 할당한다. 사용자는 그냥 평범한 버튼으로 보게 된다.
- 데이터 회수하기: 사용자가 버튼을 클릭하면, Slack이 이 value 값을 n8n으로 보내준다. n8n의 Action Parsing 노드는 이 값을 받아 JSON을 해석하고, 단 한 번의 DB 조회 없이 원본 질문을 즉시 확보한다.
이 방식으로 워크플로우는 더 이상 과거 기록에 의존하지 않는, 가볍고 빠른 구조가 되었다.
🚨Issue
'디테일 분석' 기능을 더 구체화하기 위해, 사용자에게 여러 분석 항목(이미지)을 고르게 하고 싶었다. 이를 위해 Slack의 multi_static_select (멀티 선택 메뉴)를 도입했다.
사용자가 이 메뉴에서 원하는 항목들을 선택하고 '확인' 버튼을 누르면, Webhook이 다시 한번 실행된다.
그런데 이때도 마찬가지로 사용자의 원본 질문(chatInput)과 추적 ID(execution_id)가 반드시 필요했다.
이 문제를 해결하는 가장 깔끔하고 안정적인 방법은 block_id를 활용하는 것이다.
Slack Block 코드(multi_static_select)
{
"blocks": [
{
"type": "section",
// block_id에 데이터를 JSON 문자열로 숨겨서 전달합니다.
"block_id": "{\"execution_id\":\"{{ $execution.id }}\", \"chatInput\":\"{{ $json.chatInput }}\"}",
"text": {
"type": "mrkdwn",
"text": "*🧠 아래 질문에 답해주세요. 더 정확한 `투자 성향 분석`에 도움이 됩니다.*\n(최대 3개)"
},
"accessory": {
"type": "multi_static_select",
"action_id": "analysis_multi_select",
"placeholder": {
"type": "plain_text",
"text": "분석 항목 선택"
},
"options": [
// ...(옵션 내용은 기존과 동일)...
],
"max_selected_items": 3
}
}
]
}
✔ 추가 내용
1. 불안정한 Execute Node를 위한 안전장치 추가
웹 스크래핑처럼 외부 API를 호출하는 Execute Node는 응답이 10초 이상 걸리거나 네트워크 문제로 실패하는 경우가 종종 있었다. 이럴 경우 워크플로우 전체가 멈추거나 비정상적인 데이터를 다음 단계로 넘기는 문제가 발생했다.
이를 해결하기 위해 Execute Node 바로 뒤에 If 노드를 추가했다. 이 If 노드는 실행 결과를 체크해서,
만약 결과값이 비어있거나 에러가 발생하면 재시도 실행
2. Timezone 설정 확인
n8n 워크플로우 설정(Settings)에서 직접 변경 가능한 것을 확인했다. Timezone을 Asia/Seoul로 명확히 지정하여 앞으로 생성될 모든 시간 관련 데이터의 정합성을 확보할 수 있겠다.
❓ 다음 단계에서 고민 중인 것들
- Data Search 노드의 결과값 분리
현재는 기업과 매칭되는 데이터를 'context'라는 하나의 키에 전부 담아서 추출(Output)하고 있다. 앞으로 프롬프트에서 '기업 개요', '최신 뉴스'처럼 필요한 정보만 개별로 참조할 수 있도록, 결과값을 명확히 구분해서 추출하는 구조로 변경해야 한다. - Best Keyword Extract 프롬프트 수정
이전에 질문의 '유효성 검사' 기능을 별도의 Agent로 분리했기 때문에, 기존 Agent의 역할을 재정의해야 한다. 이제 이 Agent는 유효성 검사 없이, 이미 검증된 질문에서 순수하게 핵심 키워드를 추출하는데만 집중하도록 프롬프트를 수정할 필요가 있다. - Keyword Parsing Node 에러 해결
Best Keyword Extract Agent에서 결과값이 정상적으로 나왔음에도, 바로 다음 노드인 Keyword Parsing Node에서 에러가 발생하고 있다. 두 노드 사이에 오고 가는 데이터의 형식이 맞지 않는 것으로 추정됨.
'Automation Tool > n8n Project' 카테고리의 다른 글
💣 프로젝트 트러블슈팅 및 삽질 기록 (4) | 2025.08.12 |
---|---|
Sense Stock 개발 회고, 세 번째 (3) | 2025.08.12 |
Sense Stock, D+23 (2) | 2025.08.11 |
Sense Stock, D+22 (4) | 2025.08.06 |
Sense Stock, D+21 (4) | 2025.08.04 |
댓글