n8n 고급 활용

커스텀 노드 개발, 서브워크플로우 모듈화, Docker 운영 배포, 보안 강화, 성능 최적화, 모니터링까지 — n8n을 프로덕션 수준으로 운영하는 완전 가이드

서브워크플로우 & 모듈화

대규모 자동화 시스템을 구축하면 워크플로우가 복잡해집니다. 서브워크플로우(Sub-Workflow)는 공통 로직을 별도 워크플로우로 분리하고 재사용하는 모듈화 패턴입니다.

서브워크플로우 구조

메인 워크플로우 Webhook Trigger 데이터 검증 Execute Workflow AI 처리 서브워크플로우 Execute Workflow 알림 서브워크플로우 응답 AI 처리 서브워크플로우 Execute Workflow Trigger → AI LLM Chain → 결과 포맷 → Respond to Workflow 알림 서브워크플로우 Execute Workflow Trigger → IF (긴급 여부) → Slack / Email → DB 로그

서브워크플로우 모듈화: 메인 워크플로우에서 Execute Workflow 노드로 서브워크플로우 호출

서브워크플로우 만들기

# 서브워크플로우 구성 (별도 워크플로우)

# 1. 트리거: Execute Workflow Trigger
노드: Execute Workflow Trigger
  # 이 노드가 있어야 서브워크플로우로 호출 가능
  # 입력 파라미터를 $json으로 받음# 2. 처리 로직
Basic LLM Chain
  User Message: {{ $json.prompt }}
  ↓

Code 노드 (결과 가공)
  ↓

# 3. 결과 반환
# 마지막 노드의 출력이 자동으로 호출자에게 반환됨
# 메인 워크플로우에서 서브워크플로우 호출

Execute Workflow 노드
  Source: Database (워크플로우 이름/ID로 선택)
  Workflow: "AI 처리 서브워크플로우"

  # 데이터 전달 방법
  Wait For Sub-Workflow: true   # 완료까지 대기 (동기)

  Input Data:
    prompt: {{ $json.userPrompt }}
    language: {{ $json.language }}
    maxTokens: 2048

서브워크플로우 패턴

# 패턴 1: 공통 알림 서브워크플로우
# 모든 워크플로우에서 재사용
Input: { channel, message, severity, metadata }

Switch (severity)
  "error" → PagerDuty + Slack #alerts
  "warning" → Slack #monitoring
  "info" → Slack #general + DB 로그

---

# 패턴 2: 데이터 검증 서브워크플로우
Input: { data, schema }

Code 노드 (JSON Schema 검증)
  const Ajv = require('ajv');
  const ajv = new Ajv();
  const valid = ajv.validate($json.schema, $json.data);

  if (!valid) {
    throw new Error(`검증 실패: ${ajv.errorsText()}`);
  }

  return [{ json: { valid: true, data: $json.data } }];

---

# 패턴 3: Rate Limiter 서브워크플로우
Input: { key, maxRequests, windowSeconds }

Redis 노드 (GET 현재 카운트)
  ↓
IF 노드 (카운트 < maxRequests)
  True → Redis INCR + EXPIRE → 통과
  False → 에러 반환 (429 Too Many Requests)

에러 처리 & 안정성

에러 처리 전략

전략 적용 방법 사용 시나리오
Continue on Fail 노드 설정 → "Continue on Fail" ON 에러 무시하고 계속 진행 (비중요 작업)
Error Workflow Settings → Error Workflow 지정 워크플로우 실패 시 알림/복구
Try/Catch 패턴 Code 노드 + IF 노드 조합 노드별 세밀한 에러 처리
재시도 설정 노드 → Retry on Fail 일시적 API 오류, 네트워크 불안정

에러 워크플로우 패턴

# 에러 워크플로우 구성

Error Trigger
  ↓

Code 노드 (에러 정보 파싱)
  const { execution } = $json;
  const errorInfo = {
    workflowName: execution.workflowData.name,
    workflowId: execution.workflowData.id,
    executionId: execution.id,
    errorNode: execution.lastNodeExecuted,
    errorMessage: execution.data?.resultData?.error?.message || "Unknown",
    errorStack: execution.data?.resultData?.error?.stack || "",
    startedAt: execution.startedAt,
    failedAt: new Date().toISOString(),
    inputData: JSON.stringify(
      execution.data?.resultData?.runData?.[execution.lastNodeExecuted]?.[0]?.data?.main?.[0]?.[0]?.json
    ).slice(0, 500), // 처음 500자만
  };

  return [{ json: errorInfo }];
  ↓

IF 노드 (심각도 분류)
  Condition: {{ $json.errorMessage }} Contains "ECONNREFUSED"
    → "인프라 에러" (긴급)
  Default → "일반 에러"
  ↓ (긴급)

PagerDuty 노드 (즉시 호출)
  + Slack #incidents 알림
  ↓ (일반)

Slack #errors 알림
  + PostgreSQL 에러 로그 저장

재시도 + 지수 백오프

// Code 노드 — 재시도 로직 구현
async function withRetry(fn, maxRetries = 3, baseDelay = 1000) {
  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      if (attempt === maxRetries) break;

      // 지수 백오프 + 지터
      const delay = baseDelay * Math.pow(2, attempt) +
        Math.random() * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw lastError;
}

// 사용 예시
const result = await withRetry(async () => {
  const response = await $helpers.httpRequest({
    method: 'POST',
    url: 'https://api.anthropic.com/v1/messages',
    body: { /* ... */ },
  });
  return response;
}, 3, 2000);

return [{ json: result }];

커스텀 노드 개발

기본 제공 노드로 해결되지 않는 경우, TypeScript로 커스텀 노드를 개발하여 n8n에 등록할 수 있습니다.

개발 환경 설정

# n8n 커스텀 노드 프로젝트 초기화
npx n8n-node-dev init

# 또는 수동 설정
mkdir n8n-nodes-mycustom
cd n8n-nodes-mycustom
npm init -y

npm install -D typescript \
  @types/node \
  n8n-workflow \
  @n8n/n8n-node-dev-utils

# tsconfig.json
{
  "compilerOptions": {
    "target": "ES2019",
    "module": "commonjs",
    "strict": true,
    "outDir": "./dist",
    "declaration": true,
    "sourceMap": true
  },
  "include": ["nodes/**/*.ts", "credentials/**/*.ts"]
}

커스텀 노드 코드

// nodes/MyApiNode/MyApiNode.node.ts
import { IExecuteFunctions, INodeExecutionData, INodeType,
         INodeTypeDescription, NodeOperationError } from 'n8n-workflow';

export class MyApiNode implements INodeType {
  description: INodeTypeDescription = {
    // 기본 정보
    displayName: 'My Custom API',
    name: 'myApiNode',
    icon: 'file:my-icon.svg',
    group: ['transform'],
    version: 1,
    description: 'My Custom API Node',

    // 기본값
    defaults: {
      name: 'My API',
    },

    // 입출력 설정
    inputs: ['main'],
    outputs: ['main'],

    // 자격증명
    credentials: [
      {
        name: 'myApiCredentials',
        required: true,
      },
    ],

    // 노드 파라미터 (UI 설정 필드)
    properties: [
      {
        displayName: 'Operation',
        name: 'operation',
        type: 'options',
        noDataExpression: true,
        options: [
          { name: 'Get User', value: 'getUser' },
          { name: 'Create User', value: 'createUser' },
        ],
        default: 'getUser',
      },
      {
        displayName: 'User ID',
        name: 'userId',
        type: 'string',
        required: true,
        displayOptions: {
          show: { operation: ['getUser'] },
        },
        default: '',
        placeholder: 'user-123',
      },
    ],
  };

  // 실행 로직
  async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    const items = this.getInputData();
    const returnData: INodeExecutionData[] = [];

    // 자격증명 가져오기
    const credentials = await this.getCredentials('myApiCredentials');
    const apiKey = credentials.apiKey as string;

    for (let i = 0; i < items.length; i++) {
      try {
        const operation = this.getNodeParameter('operation', i) as string;

        if (operation === 'getUser') {
          const userId = this.getNodeParameter('userId', i) as string;

          const response = await this.helpers.httpRequest({
            method: 'GET',
            url: `https://api.myservice.com/users/${userId}`,
            headers: {
              'Authorization': `Bearer ${apiKey}`,
            },
          });

          returnData.push({ json: response });
        }
      } catch (error) {
        if (this.continueOnFail()) {
          returnData.push({
            json: { error: (error as Error).message },
            pairedItem: { item: i },
          });
          continue;
        }
        throw new NodeOperationError(this.getNode(), error as Error, {
          itemIndex: i,
        });
      }
    }

    return [returnData];
  }
}

커스텀 노드 설치

# 빌드
npm run build

# n8n에 등록 (패키지로 설치)
cd ~/.n8n
mkdir custom
cp -r /path/to/n8n-nodes-mycustom/dist ./custom/node_modules/n8n-nodes-mycustom

# package.json 수정
{
  "n8n": {
    "nodes": ["dist/nodes/MyApiNode/MyApiNode.node.js"],
    "credentials": ["dist/credentials/MyApiCredentials.credentials.js"]
  }
}

# 환경 변수로 커스텀 노드 경로 지정
N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom

# n8n 재시작
docker compose restart n8n

프로덕션 배포

운영용 Docker Compose

# docker-compose.prod.yml
version: '3.8'

services:
  n8n:
    image: n8nio/n8n:latest
    restart: always
    ports:
      - "127.0.0.1:5678:5678"   # 로컬 바인딩 (Nginx 뒤에 위치)
    environment:
      # 보안
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - N8N_USER_MANAGEMENT_JWT_SECRET=${JWT_SECRET}

      # 인증 (n8n v1 이후: 이메일/패스워드 기반)
      - N8N_USER_MANAGEMENT_DISABLED=false
      - N8N_DIAGNOSTICS_ENABLED=false
      - N8N_VERSION_NOTIFICATIONS_ENABLED=false

      # 외부 URL
      - N8N_HOST=n8n.yourdomain.com
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://n8n.yourdomain.com

      # 데이터베이스
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_DATABASE=n8n
      - DB_POSTGRESDB_USER=n8n
      - DB_POSTGRESDB_PASSWORD=${DB_PASSWORD}

      # 실행 이력 관리
      - EXECUTIONS_DATA_MAX_AGE=720     # 30일
      - EXECUTIONS_DATA_PRUNE=true
      - EXECUTIONS_DATA_PRUNE_MAX_COUNT=50000

      # 성능
      - N8N_CONCURRENCY_PRODUCTION_LIMIT=20
      - QUEUE_BULL_REDIS_HOST=redis
      - EXECUTIONS_MODE=queue           # 대용량 처리 시

      # 타임존
      - GENERIC_TIMEZONE=Asia/Seoul
      - TZ=Asia/Seoul
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - n8n-network
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:5678/healthz"]
      interval: 30s
      timeout: 10s
      retries: 3

  n8n-worker:          # Queue 모드 사용 시 워커
    image: n8nio/n8n:latest
    command: worker
    restart: always
    environment:
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_DATABASE=n8n
      - DB_POSTGRESDB_USER=n8n
      - DB_POSTGRESDB_PASSWORD=${DB_PASSWORD}
      - QUEUE_BULL_REDIS_HOST=redis
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
    networks:
      - n8n-network
    scale: 3               # 3개 워커 인스턴스

  postgres:
    image: postgres:15-alpine
    restart: always
    environment:
      - POSTGRES_DB=n8n
      - POSTGRES_USER=n8n
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./postgres-init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - n8n-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U n8n"]
      interval: 10s
      retries: 5

  redis:
    image: redis:7-alpine
    restart: always
    command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    networks:
      - n8n-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      retries: 5

volumes:
  n8n_data:
  postgres_data:
  redis_data:

networks:
  n8n-network:
    driver: bridge

Nginx 리버스 프록시

# /etc/nginx/sites-available/n8n
server {
    listen 80;
    server_name n8n.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name n8n.yourdomain.com;

    # SSL 인증서 (Certbot)
    ssl_certificate /etc/letsencrypt/live/n8n.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/n8n.yourdomain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    # 보안 헤더
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;

    # n8n 프록시
    location / {
        proxy_pass http://127.0.0.1:5678;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket 타임아웃
        proxy_read_timeout 3600;
        proxy_send_timeout 3600;
    }

    # 최대 요청 크기 (파일 업로드)
    client_max_body_size 100M;
}

보안 설정

인증 & 접근 제어

# n8n v1 이후 이메일/패스워드 인증 (권장)
환경 변수:
  N8N_USER_MANAGEMENT_DISABLED=false

# 첫 실행 시 관리자 계정 생성 프롬프트 표시
# 이후 Settings → Users에서 팀원 추가

# IP 기반 접근 제한 (Nginx)
location / {
    allow 10.0.0.0/8;        # 내부 네트워크
    allow 192.168.0.0/16;    # 로컬 네트워크
    allow 203.0.113.0/24;    # 회사 IP 대역
    deny all;
    proxy_pass http://127.0.0.1:5678;
}

# Webhook 경로는 외부 허용 (선택적)
location /webhook/ {
    proxy_pass http://127.0.0.1:5678;
}

자격증명 보안

# 암호화 키 생성 (최초 설정, 절대 분실 금지)
openssl rand -hex 32
# → 저장: .env 파일에 N8N_ENCRYPTION_KEY=<생성된 값>

# .env 파일 예시
N8N_ENCRYPTION_KEY=a1b2c3d4e5f6...  # 64자 hex
JWT_SECRET=your-jwt-secret
DB_PASSWORD=strong-random-password

# .env 파일 권한 설정
chmod 600 .env
chown root:root .env

# 자격증명 백업 (암호화된 상태)
# Settings → Setup → Download backup 또는
docker exec n8n n8n export:credentials --all --output=/backup/credentials.json

Webhook 보안

// Webhook 서명 검증 (Code 노드)
const crypto = require('crypto');

const payload = JSON.stringify($json.body);
const signature = $json.headers['x-signature-256'];
const secret = $env.WEBHOOK_SECRET;

const expectedSignature = 'sha256=' + crypto
  .createHmac('sha256', secret)
  .update(payload)
  .digest('hex');

if (!crypto.timingSafeEqual(
  Buffer.from(signature),
  Buffer.from(expectedSignature)
)) {
  throw new Error('Invalid webhook signature');
}

return [{ json: $json.body }];

네트워크 격리

# Docker 네트워크 격리
networks:
  n8n-internal:
    internal: true     # 외부 인터넷 차단
  n8n-external:
    driver: bridge     # 인터넷 접근 허용

# n8n은 내부+외부 둘 다, DB/Redis는 내부만
services:
  n8n:
    networks: [n8n-internal, n8n-external]
  postgres:
    networks: [n8n-internal]  # 내부 전용
  redis:
    networks: [n8n-internal]  # 내부 전용

성능 최적화

Queue 모드 (대용량 처리)

기본 실행 모드는 단일 프로세스로 처리합니다. 대량 워크플로우를 동시에 처리하려면 Queue 모드를 사용하세요.

n8n 메인 서버 UI / 스케줄러 Webhook 수신 작업 큐잉 실행 이력 관리 Redis Queue Bull Queue 작업 대기열 Worker 1 워크플로우 실행 Worker 2 워크플로우 실행 Worker 3 워크플로우 실행 PostgreSQL 워크플로우 정의 실행 이력 자격증명 (암호화) 사용자 계정

Queue 모드: 메인 서버는 작업을 Redis Queue에 넣고, 여러 워커가 병렬로 처리

# Queue 모드 설정
environment:
  - EXECUTIONS_MODE=queue         # queue 또는 regular
  - QUEUE_BULL_REDIS_HOST=redis
  - QUEUE_BULL_REDIS_PORT=6379
  - QUEUE_BULL_REDIS_DB=0

# 워커 실행
docker run -d n8nio/n8n worker \
  -e EXECUTIONS_MODE=queue \
  -e QUEUE_BULL_REDIS_HOST=redis

성능 튜닝 포인트

# 1. 병렬 실행 제한 (과부하 방지)
N8N_CONCURRENCY_PRODUCTION_LIMIT=10  # 동시 실행 워크플로우 수

# 2. 실행 이력 자동 정리
EXECUTIONS_DATA_PRUNE=true
EXECUTIONS_DATA_MAX_AGE=720      # 720시간 = 30일
EXECUTIONS_DATA_PRUNE_MAX_COUNT=100000

# 3. PostgreSQL 인덱스 최적화
-- 실행 이력 조회 성능 향상
CREATE INDEX idx_execution_finished ON execution_entity (finished, stopped_at);
CREATE INDEX idx_execution_workflow ON execution_entity (workflow_id, started_at);

# 4. Redis 메모리 설정
maxmemory 1gb
maxmemory-policy allkeys-lru

# 5. Node.js 힙 메모리 증가 (대용량 데이터 처리 시)
NODE_OPTIONS=--max-old-space-size=4096

대량 데이터 처리 최적화

// Code 노드 — 청크 분할 처리
function chunkArray<T>(array: T[], size: number): T[][] {
  const chunks: T[][] = [];
  for (let i = 0; i < array.length; i += size) {
    chunks.push(array.slice(i, i + size));
  }
  return chunks;
}

const allItems = $input.all().map(item => item.json);
const chunks = chunkArray(allItems, 50);  // 50개씩 묶음

return chunks.map(chunk => ({ json: { chunk, count: chunk.length } }));
# 청크 단위 병렬 처리 패턴

Code 노드 (배열 → 청크 분할)
  ↓
Loop Over Items (각 청크마다)
  ↓
  SplitInBatches (청크 내 아이템 병렬 처리)
    ↓
    [병렬] HTTP Request × N
    ↓
  Merge
  ↓
Wait 노드 (Rate Limit 방지, 1초 대기)
  ↓
다음 청크...

모니터링 & 운영

헬스체크 & 모니터링

# n8n 헬스체크 엔드포인트
GET https://n8n.yourdomain.com/healthz
# 응답: {"status":"ok"}

# Prometheus 메트릭 활성화
N8N_METRICS=true
N8N_METRICS_PREFIX=n8n_

# 메트릭 엔드포인트
GET http://localhost:5678/metrics

# 주요 메트릭
n8n_workflow_success_total       # 성공 실행 수
n8n_workflow_failed_total        # 실패 실행 수
n8n_execution_duration_seconds   # 실행 시간 분포
n8n_active_executions_count      # 현재 실행 중인 수

Grafana 대시보드 설정

# docker-compose에 Prometheus + Grafana 추가
services:
  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana
    ports:
      - "3000:3000"
    volumes:
      - grafana_data:/var/lib/grafana
# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'n8n'
    static_configs:
      - targets: ['n8n:5678']
    metrics_path: '/metrics'

알림 설정

# 실패율 급증 알림 워크플로우 (n8n으로 모니터링 구축)

Schedule Trigger (5분마다)
  ↓

PostgreSQL 노드
  Query: |
    SELECT
      COUNT(*) FILTER (WHERE finished AND NOT stopped) AS success,
      COUNT(*) FILTER (WHERE finished AND stopped) AS failed,
      COUNT(*) FILTER (WHERE NOT finished) AS running
    FROM execution_entity
    WHERE started_at > NOW() - INTERVAL '5 minutes'
  ↓

Code 노드 (실패율 계산)
  const { success, failed, running } = $json;
  const total = success + failed;
  const failureRate = total > 0 ? failed / total : 0;
  const alert = failureRate > 0.3 && total > 10; // 30% 이상, 10건 이상

  return [{ json: { success, failed, running, failureRate, alert } }];
  ↓

IF 노드 (alert === true)
  ↓ (True)

Slack 노드
  Channel: #n8n-alerts
  Message: |
    ⚠️ n8n 실패율 급증
    최근 5분간 실패율: {{ Math.round($json.failureRate * 100) }}%
    성공: {{ $json.success }}, 실패: {{ $json.failed }}

백업 & 복구

# 워크플로우 내보내기 (정기 백업)
docker exec n8n n8n export:workflow \
  --all \
  --output=/backup/workflows-$(date +%Y%m%d).json

# 자격증명 내보내기
docker exec n8n n8n export:credentials \
  --all \
  --output=/backup/credentials-$(date +%Y%m%d).json

# PostgreSQL 백업
docker exec postgres pg_dump -U n8n n8n | gzip \
  > /backup/n8n-db-$(date +%Y%m%d).sql.gz

# 복구
docker exec -i postgres psql -U n8n n8n < /backup/n8n-db-20260101.sql

docker exec n8n n8n import:workflow \
  --input=/backup/workflows-20260101.json

docker exec n8n n8n import:credentials \
  --input=/backup/credentials-20260101.json
# 자동 백업 스크립트 (cron)
#!/bin/bash
BACKUP_DIR=/opt/backups/n8n
DATE=$(date +%Y%m%d-%H%M)

mkdir -p $BACKUP_DIR

# 워크플로우 백업
docker exec n8n n8n export:workflow --all --output=/tmp/wf.json
docker cp n8n:/tmp/wf.json $BACKUP_DIR/workflows-$DATE.json

# DB 백업
docker exec postgres pg_dump -U n8n n8n | gzip \
  > $BACKUP_DIR/db-$DATE.sql.gz

# 30일 이전 백업 삭제
find $BACKUP_DIR -mtime +30 -delete

echo "백업 완료: $DATE"

# crontab 등록 (매일 새벽 3시)
# 0 3 * * * /opt/scripts/n8n-backup.sh >> /var/log/n8n-backup.log 2>&1

워크플로우 관리 & 거버넌스

버전 관리 (Git 연동)

# n8n Enterprise: Source Control (Git 통합)
Settings → Source Control → Connect to Git

# 오픈소스 대안: 정기적으로 워크플로우 내보내기 후 Git 커밋

#!/bin/bash — 워크플로우 Git 저장 스크립트
REPO_DIR=/opt/n8n-workflows

# 워크플로우 내보내기
docker exec n8n n8n export:workflow --all \
  --output=$REPO_DIR/workflows.json --pretty

# Git 커밋
cd $REPO_DIR
git add workflows.json
git diff --staged --quiet || git commit -m "workflow: auto-export $(date +%Y-%m-%d)"
git push origin main

네이밍 컨벤션

# 워크플로우 명명 규칙
[트리거 타입] [목적] [버전]

예시:
  [Webhook] 고객 문의 AI 분류 v2
  [Schedule] 일일 매출 리포트 발송
  [Manual] 데이터 마이그레이션
  [Sub] 공통 알림 발송 모듈
  [Sub] AI 응답 생성 모듈

# 노드 명명 규칙
동사 + 목적어

예시:
  Load Customer Data
  Validate Input Format
  Call Claude API
  Parse AI Response
  Send Slack Alert
  Save to Database

워크플로우 테스트

# 테스트용 더미 데이터 생성 (Code 노드)
// 실제 데이터 없이 워크플로우 테스트
const testData = [
  {
    id: "test-001",
    email: "test@example.com",
    message: "결제가 안 됩니다. 도와주세요!",
    createdAt: new Date().toISOString(),
  }
];

return testData.map(item => ({ json: item }));
# 스테이징 환경 분리

# 환경 변수로 환경 구분
N8N_ENVIRONMENT=production  # 또는 staging, development

# 워크플로우에서 환경 체크
IF 노드
  Condition: {{ $env.N8N_ENVIRONMENT }} Equal "production"
  True → 실제 Slack 알림 발송
  False → 콘솔 로그만 (테스트 모드)

트러블슈팅

자주 발생하는 문제

증상 원인 해결 방법
Webhook이 수신되지 않음 WEBHOOK_URL 미설정 또는 방화벽 WEBHOOK_URL=https://n8n.yourdomain.com 설정, 포트 443/80 개방
자격증명 복호화 실패 N8N_ENCRYPTION_KEY 변경 최초 설정한 키 복원 (절대 변경 금지)
메모리 부족 (OOM) 대용량 데이터 처리, 실행 이력 누적 실행 이력 정리, NODE_OPTIONS 메모리 증가
실행이 멈춤 (Stuck) 무한 루프, 응답 없는 API 노드별 타임아웃 설정, 실행 취소 후 워크플로우 수정
AI Agent 무한 루프 도구 설명 불명확, 모델 혼란 최대 반복 횟수 설정, 도구 설명 개선
PostgreSQL 연결 실패 네트워크 분리, 자격증명 오류 Docker 네트워크 확인, DB 자격증명 재확인
Webhook 중복 실행 타임아웃 재시도, 외부 서비스 재전송 Idempotency Key 구현, 중복 체크 로직 추가

디버깅 팁

# 실행 로그 확인
docker logs n8n --tail 100 -f

# 특정 워크플로우 실행 이력 DB 조회
docker exec postgres psql -U n8n -c "
  SELECT id, finished, stopped, started_at, stopped_at,
         (data::json->'resultData'->'error'->>'message') as error
  FROM execution_entity
  WHERE workflow_id = '워크플로우ID'
  ORDER BY started_at DESC
  LIMIT 20;
"

# n8n 디버그 모드 실행
N8N_LOG_LEVEL=debug n8n start

# Code 노드에서 디버깅
console.log('현재 데이터:', JSON.stringify($json, null, 2));
console.log('이전 노드 출력:', JSON.stringify($input.all(), null, 2));

관련 가이드

📚 n8n 학습 경로
  1. n8n이란? — 기초 개념, 설치, 첫 워크플로우
  2. AI 워크플로우 구축 — Claude/OpenAI/RAG 연동
  3. 고급 활용 (현재 페이지) — 운영, 보안, 성능
🔗 연관 주제