로컬 LLM과 n8n으로 구축하는 에러 분석 자동화
- n8n
- LLM
- 최적화

프로젝트 소개
어떤 계기로 프로젝트를 시작하게 되었는가?
워크플로우 자동화 도구인 n8n을 접하고, 직접 경험해보기 위해 실제로 겪었던 불편함을 풀어보기로 했다. 이전 프로젝트에서 Sentry로 에러 모니터링을 진행하며 스택 트레이스와 유저 액션 정보가 디버깅에 큰 도움이 된다는 걸 느꼈다. 여기서 한 발 더 나아가, 에러가 발생하는 즉시 AI가 컨텍스트를 분석하고 초기 가이드라인을 잡아주면 어떨까 라는 아이디어로 이어졌다.
어떤 프로젝트를 진행하고 있었는가?
팀 개발 중 팀원의 코드에서 에러가 발생하면, 스택 트레이스를 분석하고 맥락을 재구성하는 초기 탐색에 반복적으로 시간이 들었다. 이 과정을 자동화해, 에러가 발생하는 즉시 AI가 원인과 수정 방향을 담은 리포트를 자동으로 생성하도록 만들었다.

프론트엔드에서 에러가 발생하면, n8n이 관련 소스 코드와 스택 트레이스를 수집하고 로컬 LLM(Ollama)이 원인 분석 및 수정 제안을 생성해 Notion 데이터베이스에 자동으로 기록하는 파이프라인이다.
기술 선택
어떤 기술들을 선택했는가?
n8n은 최근 주목받고 있는 워크플로우 자동화 도구로, 직접 경험해보기 위해 선택했다. 셀프 호스팅이 가능해 데이터가 외부 서비스를 거치지 않고, 워크플로우를 JSON으로 관리할 수 있어 Git으로 변경 이력을 추적했다.
Docker로 실행 환경을 격리해 다른 프로젝트나 협업 환경으로 확장할 때 설정 부담을 줄였다. Ollama는 LLM을 로컬에서 구동하고 API 형태로 제공해, 코드가 외부 서버로 전송되지 않는 파이프라인을 구성할 수 있었다. API 비용도 발생하지 않는다.
모델은 사용 중인 하드웨어(M4 Pro, 24GB RAM)에서 코드 분석 성능과 추론 속도의 균형이 뛰어난 deepseek-coder-v2:16b를 선택했다. M4 GPU 가속을 최대로 활용하기 위해 Ollama는 Docker 내부가 아닌 로컬 머신에 직접 설치하고, n8n 컨테이너가 host.docker.internal을 통해 접근하도록 구성했다.
파이프라인 구현
어떤 단계로 진행되는가?
에러 하나가 Notion 리포트가 되기까지, 8단계의 파이프라인을 거친다. 전체 흐름은 크게 세 구간으로 나뉜다.
각 단계는 다음 단계에 필요한 데이터만 넘기도록 설계해, 단계 간 결합도를 낮췄다.

(1) 에러 로거 정의

발생한 에러를 AI가 분석하기 좋은 형태의 데이터로 가공하여 n8n 워크플로우로 전달한다.
초기 파이프라인 결정
초기에는 Sentry → Webhook → n8n 구조를 생각했다. 그러나 에러 발생 시점에 필요한 정보를 직접 정규화해 n8n 웹훅으로 전달하면 Sentry를 거칠 필요가 없었다. 호출 지점 스택, error.cause 체인, 환경 정보 등 Sentry 페이로드 스펙에 없는 필드를 자유롭게 포함할 수 있었고, 중간 서버를 거치지 않아 전송 지연도 줄었다.
에러 중복 처리
같은 에러가 짧은 시간 동안 연속으로 발생하면 동일한 기록이 끝없이 쌓인다. 5초 이내에 동일한 에러 키가 다시 들어오면 중복으로 간주하고 무시하도록 TTL 기반 메모리 캐시를 두었다.
// 중복 판정 기준 시간 (5초)
const RECENT_ERROR_TTL = 5000;
// errorKey -> 마지막 발생 타임스탬프
const recentErrors = new Map();
/** 동일 에러가 TTL 내에 재발생하면 true 반환 */
function isDuplicate(errorKey) {
const now = Date.now();
const lastTimestamp = recentErrors.get(errorKey);
// TTL 이내 동일 키 재진입 시, 중복으로 판정
if (lastTimestamp && now - lastTimestamp < RECENT_ERROR_TTL) {
return true;
}
recentErrors.set(errorKey, now);
// TTL 만료 후 해당 키만 제거해 Map이 무한히 커지는 것을 방지
setTimeout(() => recentErrors.delete(errorKey), RECENT_ERROR_TTL);
return false;
}에러 페이로드 설계
AI의 할루시네이션을 줄이려면 에러 정보를 최대한 구조화해서 보내는 것이 중요하다. 에러가 발생한 지점과 실제로 호출한 지점이 다를 수 있기 때문에 error와 callSite를 분리했다.
두 스택을 함께 제공하면 AI가 원인을 추적할 수 있는 범위가 넓어진다. meta에는 URL, userAgent, 타임스탬프, 환경 정보를 담아 어떤 페이지, 어떤 환경에서 발생한 에러인지 리포트에서 바로 파악할 수 있도록 했다.
function createPayload(error, callSiteStack) {
return {
// 에러 객체: 메시지, 스택, cause 체인을 구조화해 담음
error: {
message: error.message,
name: error.name,
stack: error.stack || "",
traces: parseStackTrace(error.stack),
causes: collectCauseChain(error),
},
// 호출 지점: 에러 발생 위치와 별도로 실제 호출한 스택을 함께 기록
callSite: {
stack: callSiteStack,
traces: parseStackTrace(callSiteStack),
},
// 부가 정보: 발생 페이지, 브라우저, 환경을 리포트에서 바로 파악할 수 있도록 담음
meta: {
url: window.location.href,
userAgent: navigator.userAgent.slice(0, 500),
timestamp: new Date().toISOString(),
environment: isDev ? "development" : "production",
},
};
}안정적인 에러 전송
페이지가 닫히거나 전환되는 찰나의 에러도 놓치지 않도록 fetch API의 keepalive: true 옵션을 적용했다. 웹훅 전송은 .catch()로 예외를 격리해 네트워크 오류나 n8n 장애가 발생해도 실제 서비스 동작에는 영향을 주지 않도록 했다.
// n8n 웹훅으로 페이로드 전송
fetch(N8N_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
keepalive: true, // 페이지 언로드 중에도 요청이 완료될 때까지 유지
}).catch((err) => {
// 개발 환경에서만 콘솔 출력 — 프로덕션에서는 조용히 실패
if (isDev) console.warn("[logger] Webhook 전송 실패", err.message);
});(2) 에러 정보 파싱

로거로부터 전달받은 원시 데이터를 AI 모델이 효율적으로 처리할 수 있도록 정제한다.
분석 대상 파일 선정
너무 많은 파일 정보는 AI의 주의력을 분산시키고 토큰을 낭비한다. node_modules나 번들 결과물이 아닌 실제 수정 가능한 코드만 추출하기 위해 src/ 경로를 포함한 파일만 필터링했다. 에러와 가장 밀접한 상위 3개 파일로 분석 대상을 제한해, 로컬 LLM 환경에서도 핵심 맥락에 집중할 수 있도록 했다.
// 모든 트레이스 합치기 (에러 발생 지점이 앞서도록 순서 유지)
const allTraces = [...(error.traces || []), ...(callSite.traces || [])].slice(0, 3);
const seen = new Set();
const srcFiles = [];
for (const trace of allTraces) {
if (!trace.file) continue;
// 'src/' 경로 포함 여부 확인 후 추출
const match = trace.file.match(/(src\/.*)$/);
if (!match) continue;
// 중복 제거: 동일한 파일 경로는 한 번만 기록
const filePath = match[1];
if (seen.has(filePath)) continue;
seen.add(filePath);
srcFiles.push({
file: filePath,
fn: trace.function,
location: `${trace.line}:${trace.column}`,
full: `${filePath}:${trace.line}:${trace.column}`,
});
}AI 친화적 데이터 구조화
가공된 정보는 프롬프트 설계를 염두에 두고 구조화했다. 제목, 에러 상세, 호출 스택, 환경 정보, 타임스탬프를 명시적으로 분리해두면 이후 프롬프트에서 각 섹션에 필요한 데이터만 골라 주입할 수 있다.
return [
{
json: {
// 노션 페이지 제목 및 Ollama 프롬프트 상단에 사용
title: `[${error.name || "Error"}] ${error.message}`,
// 호출 지점 원본 스택 — 프롬프트의 호출 흐름 섹션에 주입
callSiteStack: callSite.stack,
// 에러 상세 — srcFiles, primaryLocation, stackSummary는 파싱 단계에서 추가된 필드
error: {
...error,
srcFiles, // 분석 대상 파일 목록 (최대 3개)
primaryLocation, // 에러 발생 주요 위치 (file:line:column)
stackSummary, // 스택 트레이스 한 줄 요약
},
// 발생 환경 — 프롬프트의 환경 섹션 및 노션 속성에 사용
context: {
url: meta.url,
path: getUrlPath(meta.url), // 쿼리스트링 제거한 경로만 추출
browser: uaInfo.browser,
os: uaInfo.os,
userAgent: meta.userAgent,
env: meta.environment,
},
timestamp: meta.timestamp || new Date().toISOString(),
},
},
];(3) 에러가 발생한 파일 읽기

에러 파싱 단계에서 식별된 파일과 라인 정보를 바탕으로, AI가 분석할 최적의 코드 범위를 추출한다.
윈도우 슬라이싱을 통한 컨텍스트 최적화
파일 전체를 AI에 전달하면 입력 토큰이 기하급수적으로 늘어나고 분석의 초점이 흐려진다. 처음에는 상하 10줄로 시작했지만, 함수가 길거나 컴포넌트 구조가 복잡한 경우 맥락이 잘리는 문제가 있었다. 에러 발생 라인을 기준으로 상하 20줄로 범위를 넓혔고, 40줄 정도면 함수의 전체적인 흐름과 변수 사용처를 파악하기에 충분했다.
AI 가독성을 위한 코드 포맷팅
AI가 에러 위치를 즉각적으로 특정할 수 있도록 두 가지 보조 장치를 추가했다. 각 줄 앞에 라인 번호를 부여해 "몇 번째 줄에 문제가 있다"고 구체적으로 답변할 수 있도록 했고, 실제 에러 발생 지점에는 시각적 마커(>)를 추가해 AI가 해당 지점에 집중하도록 했다.
if (file.location) {
// "22:15" 형식에서 라인 번호만 추출
const targetLine = parseInt(file.location.split(":")[0]);
const lines = sourceCode.split("\n");
// 에러 발생 라인 기준 상하 20줄 — 파일 경계를 벗어나지 않도록 clamp
const start = Math.max(0, targetLine - 21);
const end = Math.min(lines.length, targetLine + 20);
codeSnippet = lines
.slice(start, end)
.map((line, index) => {
const currLineNum = start + index + 1;
// 에러 발생 지점에 시각적 마커 추가 — AI가 해당 라인에 집중하도록 유도
const marker = currLineNum === targetLine ? " > " : " ";
return `${marker}${currLineNum} | ${line}`;
})
.join("\n");
}(4) Ollama 프롬프트 작성

에러 컨텍스트를 바탕으로 AI가 최적의 분석 결과를 도출할 수 있도록 프롬프트를 작성하는 단계다.
프롬프트 설계
시니어 프론트엔드 개발자이자 디버깅 전문가 역할을 부여해 근본 원인 분석에 집중하도록 유도했다. 응답은 원인 분석 / 수정 전략 / 수정 내용 / 수정 코드 / 테스트 단계 5개 섹션으로 구성했다. fixCode는 전체 파일이 아닌 수정이 필요한 함수/블록 단위로만 작성하게 해 출력 토큰 낭비를 막았다.
당신은 시니어 프론트엔드 개발자이자 디버깅 전문가입니다.
제공된 에러 맥락과 소스 코드를 교차 분석하여 근본 원인을 찾고, 노션(Notion) 리포트용 JSON 응답을 생성하세요.
## 1. 에러 맥락
- 에러명: ${data.title}
- 메시지: ${data.error.message}
- 발생 위치: ${data.error.primaryLocation}
- 호출 흐름: ${data.error.stackSummary}
- 환경: ${data.context.os} / ${data.context.browser} (${data.context.env})
## 2. 원인 체인 (error.cause)
${causesSection}
## 3. 관련 소스 코드 (에러 지점 주변부)
${codeContextSection}
## 4. 상세 분석 및 응답 요구사항 (중요)
- **problemAnalysis**: 단순 현상이 아닌 '왜' 발생했는지 분석하세요. 핵심 키워드에 **볼드**를 사용하세요.
- **fixStrategy**: 어떤 방향으로 고칠지 1~2문장으로 요약하세요.
- **fixContent**: 구체적인 변경 사항을 **반드시 한 줄에 하나씩 글머리 기호('-')를 사용하여 리스트 형태로** 작성하세요. 각 줄 사이에는 줄바꿈을 포함하세요.
- **fixCode**: 수정이 필요한 부분만 **작업 단위(함수 또는 블록)**별로 작성하세요. 전체 파일을 작성하지 마세요. 반드시 변경 전/후의 맥락을 알 수 있는 주석을 포함하세요. 줄 번호는 반드시 포함하지 마세요.
- **riskLevel**: LOW, MEDIUM, HIGH 중 하나를 선택하세요.
- **testSteps**: 검증을 위해 필요한 단계를 배열 형태로 작성하세요.
## 5. 응답 형식 (JSON만 출력, 코드 블록 기호 \`\`\`json 금지)
{
"summary": "한 줄 요약",
"problemAnalysis": "근본 원인 상세 분석",
"fixStrategy": "해결을 위한 접근 방식",
"fixContent": "- 변경 사항 1\\n- 변경 사항 2\\n- 변경 사항 3",
"fixFilePath": "${primaryFile?.file || "src/..."}",
"fixCode": "// [수정된 함수/블록명]\\nfunction example() {\\n // ... 기존 코드\\n // FIXED: 에러 방지를 위해 null 체크 추가\\n if (data) { \\n // ... 수정된 로직\\n }\\n}",
"testSteps": ["테스트 단계 1", "단계 2"],
"riskLevel": "LOW | MEDIUM | HIGH",
}LLM 파라미터 튜닝
디버깅 시에는 창의성보다 정확성과 재현성이 중요하다. 같은 에러 입력에 대해 최대한 동일한 분석 결과를 내도록 세 가지 파라미터를 설정했다.
// 정확성과 재현성 우선 — 코딩 태스크에는 0에 가까운 값 권장
const AI_TEMPERATURE = 0.1;
// 낮은 temperature 보완 — 제한된 범위 내에서 다양성 유지
const AI_TOP_P = 0.9;
// 코드 스니펫 3개 + 스택 트레이스가 잘리지 않을 충분한 컨텍스트 크기
const AI_NUM_CTX = 8192;
const body = {
model: "deepseek-coder-v2:16b",
prompt,
stream: false, // 응답 완료 후 한 번에 수신
format: "json", // JSON 형식 강제 — 파싱 실패 방지
options: {
temperature: AI_TEMPERATURE,
top_p: AI_TOP_P,
num_ctx: AI_NUM_CTX,
},
};(5) Ollama 실행

가공된 데이터를 로컬 AI인 Ollama에 전달하여 실제 분석을 수행한다.
Docker-Host 설정
n8n은 Docker 컨테이너에서 격리 실행하고, Ollama는 M4 Pro의 Metal 기반 GPU를 오버헤드 없이 활용하기 위해 macOS 호스트에 직접 설치했다. Docker 내부에서 실행하면 Metal GPU에 접근할 수 없어 CPU 전용으로 동작하기 때문이다.
컨테이너에서는 host.docker.internal을 통해 호스트의 Ollama 서비스(11434 포트)에 접근하도록 구성했다.

타임아웃 확장
모델 규모와 프롬프트 길이에 따라 응답까지 수십 초에서 수 분이 걸릴 수 있어, HTTP Request 노드의 타임아웃을 300000ms(5분)으로 확장했다. 한 번의 요청 안에서 분석 결과를 끝까지 받아올 수 있도록 하기 위해서다.
"options": {
"timeout": 300000
}(6) Ollama 데이터 검증 및 가공

AI로부터 수신한 JSON 응답을 Notion 데이터베이스 스키마에 최적화된 형태로 재구성한다.
JSON 응답 정리
LLM은 할루시네이션으로 인해 필드가 누락되거나 예상과 다른 타입으로 응답할 수 있다. 각 필드에 기본값을 할당해 이후 단계에서 발생할 수 있는 에러를 방지했다. testSteps처럼 배열이어야 하는 필드는 타입을 명시적으로 검증했다.
analysis = {
summary: parsed.summary,
problemAnalysis: parsed.problemAnalysis ?? "",
fixStrategy: parsed.fixStrategy ?? "",
fixContent: parsed.fixContent ?? "",
fixCode: parsed.fixCode,
fixFilePath:
parsed.fixFilePath ?? promptData.error?.primaryLocation?.split(":")[0] ?? "unknown",
testSteps: Array.isArray(parsed.testSteps) ? parsed.testSteps : [],
riskLevel: parsed.riskLevel,
};(7) Notion API 객체 모델로 변환

재구성한 데이터를 Notion 블록 구조로 변환해 리포트 생성을 준비한다.
노션에 맞는 텍스트 / 블록 가공
Notion API는 마크다운을 그대로 받지 않고 자체 블록 구조로 변환해야 한다. Ollama가 반환한 볼드/불릿 등 마크다운 서식을 Notion rich_text 구조로 파싱하는 변환 유틸을 직접 작성했다.
리포트가 단순 기록이 아닌 관리 도구로 기능하도록 Status(진행 상태), Risk Level(위험도), Environment(발생 환경)를 select 타입으로 정의해 Notion 대시보드에서 필터링과 정렬에 활용할 수 있도록 했다.
// 노션 페이지 생성용 JSON 구조
const notionBody = {
properties: {
Name: {
title: [{ text: { content: pageTitle } }],
},
Status: {
select: { name: "Open" },
},
"Risk Level": {
select: { name: data.riskLevel || "MEDIUM" },
},
Environment: {
select: { name: data.context?.env || "unknown" },
},
// ...
}
// ...
}(8) 노션 페이지 생성 및 기록

모든 데이터 처리가 완료된 후, Notion API를 호출해 에러 분석 리포트를 생성한다.
Credential 기반 API 호출
사전에 등록한 Notion API Credential을 참조해 인증 토큰을 노출하지 않고 안전하게 API를 호출한다. 에러 한 건당 Notion 데이터베이스에 새로운 페이지가 생성되고, 그 안에 AI 분석 결과가 구조화된 형태로 기록된다.
{
"parameters": {
"method": "POST",
"url": "https://api.notion.com/v1/pages",
// n8n Credential에 등록된 Notion API 키를 참조 — 토큰 직접 노출 없음
"authentication": "predefinedCredentialType",
"genericAuthType": "notionApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [ ... ] // Notion-Version 헤더 등 추가 설정
},
"sendBody": true,
"specifyBody": "json",
// 이전 단계에서 구성한 Notion 블록 구조를 JSON 직렬화해 전달
"jsonBody": "={{ JSON.stringify($json.notionBody) }}",
"options": {}
},
"name": "Create Notion Issue",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
}최종 결과
에러 한 건이 발생하면 파이프라인이 자동으로 실행되어 Notion 데이터베이스에 새로운 페이지가 생성된다. 리포트는 크게 네 부분으로 구성된다. 에러 요약과 위험도/환경 속성, 상세 스택 트레이스, AI가 제안하는 수정 코드, 검증 체크리스트다.





마무리하며
개선 및 보완할 점
모델 선택, 토큰 최적화, 정확도
현재는 고정된 20줄 범위로 코드를 슬라이싱하고 있지만, 분석 난이도에 따라 컨텍스트 범위를 동적으로 조절하는 방향을 고민하고 있다. 로컬 모델의 분석 정확도 한계도 있어 향후 유료 API로 마이그레이션하는 방향도 생각 중이다. 다만 이 경우 이 프로젝트의 핵심 전제인 로컬 실행이 무너지기 때문에, 코드 마스킹이나 선택적 전송 방식을 먼저 검토한 후 마이그레이션을 결정할 것이다.
예외 상황에 대한 방어적 구성
운영 신뢰성 측면에서는 Ollama 로딩 지연, 네트워크 타임아웃, 일시적인 API 실패 같은 예외 상황에 대한 처리가 아직 미흡하다. HTTP Request 노드의 재시도 전략과 에러 발생 시 알림 워크플로우를 단계적으로 도입할 계획이다.
n8n을 경험하며
n8n으로 워크플로우를 구성하면서, 생각보다 적은 설정만으로 트리거부터 LLM 분석, Notion 페이지 생성까지의 흐름이 자연스럽게 자동화되는 경험이 인상적이었다. 에러 초기 탐색에 5분 이상 걸리던 과정이 20~25초 내로 단축됐고, 그만큼 실제 문제 해결에 집중할 수 있는 시간이 늘었다. 자동화가 단순히 반복 작업을 줄이는 것이 아니라, 사람이 집중해야 할 곳을 명확하게 만들어준다는 걸 직접 체감했다.

