n8n 사용법 설치부터 GitHub·슬랙 자동화까지 (실전 메모)

목차

PR 머지 알림을 GitHub Actions로 짜다가 워크플로우 YAML이 200줄을 넘어서 n8n으로 갈아탔다. n8n 사용법 설치는 Docker 한 줄이면 끝나는데, 정작 발목을 잡은 건 환경변수 두 개와 볼륨 마운트 한 줄이었다.

결국, 프론트엔드 2년을 하다가 백엔드로 넘어온 지 2년 정도 됐다. 백엔드 사람들이 알림이나 ETL 같은 걸 전부 코드로 짜는 게 처음엔 이상해 보였다. 프론트에선 Figma 같은 노드 그래프가 흔하니까. n8n은 그 시각적 워크플로우가 그대로 백엔드 작업에 옮겨진 느낌이라 손이 빨리 붙었다.

오늘 한 것 — GitHub Actions에서 n8n으로 옮긴 이유

그래서, 원래는 PR이 머지될 때 슬랙 채널에 알림을 보내는 걸 .github/workflows/notify.yml로 짰다. 처음엔 50줄이었는데 조건이 늘었다.

  • PR 작성자가 외부 기여자면 다른 채널로 보낸다
  • 라벨이 hotfix면 멘션을 추가한다
  • 머지 메시지에서 이슈 번호를 추출해 링크를 첨부한다
  • 머지 시간이 새벽이면 다음날 오전에 디지스트로 묶어서 발송한다

그러나, 이게 합쳐지니 250줄짜리 YAML이 됐다. if: 조건 중첩이 4단계 들어갔다. 한 줄을 잘못 건드리면 전체가 안 돌아서 매번 dry-run 브랜치를 만들어 테스트했다. 시간이 너무 많이 들었다.

반면, n8n으로 옮긴 뒤에는 노드 6개로 같은 로직이 끝났다. Webhook 노드 → IF 노드 두 개 → Slack 노드 두 개 → Wait 노드 → Slack 노드. 시각적으로 보이니까 어디서 분기되는지 한눈에 들어왔다.

GitHub Actions와 n8n, 언제 뭘 쓰나

그러나, 둘이 겹쳐 보이지만 결이 다르다. 옮기면서 정리한 기준은 이렇다.

항목 GitHub Actions n8n
트리거 git 이벤트 중심 HTTP, 크론, 외부 서비스 다양
분기 처리 YAML if: 조건문 시각적 IF/Switch 노드
외부 API 호출 매번 curl/스크립트 200+ 통합 노드 내장
상태 보관 기본 없음 (artifact) 실행 이력 자동 저장
디버깅 로그 스크롤 노드별 입출력 즉시 확인
비용 무료 분 한도 셀프호스팅 무료

예를 들어, GitHub 저장소 안에서 돌릴 CI 작업은 여전히 Actions가 낫다. 다만 여러 외부 서비스를 엮는 자동화면 n8n 쪽이 유지보수가 편하더라.

새로 알게 된 것 — Docker 설치에서 만난 두 가지 함정

n8n 공식 문서가 권장하는 설치 방법은 Docker다. 한 줄이라고 적혀 있다. 작성 시점(2026-05-23 기준) 안정 버전대는 1.x다. 공식 가이드는 n8n 셀프호스팅 문서에 있다.

이처럼, 처음엔 그대로 따라 했다.

# 공식 문서 첫 예제 — 이게 함정이다
docker run -it --rm \
  --name n8n \
  -p 5678:5678 \
  docker.n8n.io/n8nio/n8n

localhost:5678에 들어가서 워크플로우를 만들고, 컨테이너를 한 번 재시작했더니 전부 사라졌다. --rm 플래그 때문에 컨테이너가 지워지면서 SQLite 파일도 같이 날아간 거다. 한 시간 짜둔 워크플로우가 통째로 증발했다.

함정 1 — 볼륨 마운트 빼먹지 마라

실제로 쓸 명령은 이렇다.

# 데이터를 호스트에 보존하는 구성
docker volume create n8n_data

docker run -d \
  --name n8n \
  --restart unless-stopped \
  -p 5678:5678 \
  -v n8n_data:/home/node/.n8n \
  -e GENERIC_TIMEZONE="Asia/Seoul" \
  -e TZ="Asia/Seoul" \
  docker.n8n.io/n8nio/n8n

-v n8n_data:/home/node/.n8n가 핵심이다. 워크플로우 정의, 자격증명, 실행 이력이 전부 이 경로 아래 SQLite에 저장된다. 호스트 디렉토리에 직접 마운트할 수도 있지만, 그러면 권한 문제가 종종 생긴다. n8n 컨테이너가 UID 1000으로 도는데 호스트 디렉토리 소유자가 다르면 부팅 단계에서 죽는다. named volume이 속 편하다.

함정 2 — N8N_SECURE_COOKIE 환경변수

위 명령으로 띄워서 외부 IP로 접속하려니 로그인 폼에서 입력이 안 먹혔다. 정확히는 쿠키가 세팅이 안 됐다. 브라우저 콘솔을 보니 Cookie "n8n-auth" has been rejected because it is in a cross-site context and its "SameSite" is "Lax" 비슷한 경고가 떴다.

예를 들어, n8n 1.0 이후로 HTTPS가 아닌 환경에서는 보안 쿠키가 막힌다. 로컬 개발이나 사내 IP로 접근할 거면 환경변수로 꺼야 한다.

docker run -d \
  --name n8n \
  -p 5678:5678 \
  -v n8n_data:/home/node/.n8n \
  -e N8N_SECURE_COOKIE=false \
  -e N8N_HOST=192.168.0.42 \
  -e WEBHOOK_URL=http://192.168.0.42:5678/ \
  docker.n8n.io/n8nio/n8n

WEBHOOK_URL이 또 중요한데, 이게 없으면 외부에서 호출할 Webhook 주소가 localhost로 잡혀서 GitHub 쪽 콜백이 못 들어온다. 이 두 줄을 빼먹어서 30분을 더 헤맸다.

그런데, :::tip 운영용으로 외부에 노출할 거면 Caddy나 Nginx 앞단에서 HTTPS를 끝낸 뒤 N8N_SECURE_COOKIE는 다시 true로 돌려둬라. 그게 기본값이고 안전하다. :::

워크플로우 코드 — GitHub PR 머지를 슬랙으로 보내기

물론, GUI에서 만든 워크플로우는 JSON으로 내보낼 수 있다. 한 번 만들어두면 다른 인스턴스에도 그대로 임포트된다. 코드 베이스에 같이 커밋해두면 변경 이력도 git으로 따라온다.

예를 들어, GitHub 저장소 Settings → Webhooks에 n8n의 Webhook URL을 등록하면 절반은 끝난다. n8n에서 Webhook 노드를 추가하면 Test URLProduction URL 두 개가 보이는데, 이게 처음엔 헷갈렸다.

  • Test URL: 워크플로우 편집기를 열어둔 상태에서만 동작한다. 노드를 클릭해 "Listen for test event"를 누른 뒤에야 유효하다.
  • Production URL: 워크플로우를 활성화(Active 토글 ON)했을 때만 동작한다. 실제 운영용이다.

예를 들어, GitHub 등록은 Production URL로 하면 된다. Test URL로 등록해두고 왜 안 오나 한참을 봤다.

전체 워크플로우 JSON에서 핵심 노드만 옮긴다.

{
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "github-pr",
        "responseMode": "onReceived"
      },
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1
    },
    {
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{$json[\"body\"][\"action\"]}}",
              "operation": "equal",
              "value2": "closed"
            },
            {
              "value1": "={{$json[\"body\"][\"pull_request\"][\"merged\"]}}",
              "operation": "equal",
              "value2": "true"
            }
          ]
        }
      },
      "name": "IF merged",
      "type": "n8n-nodes-base.if"
    },
    {
      "parameters": {
        "channel": "={{$json[\"body\"][\"pull_request\"][\"labels\"].find(l => l.name === \"hotfix\") ? \"#hotfix\" : \"#deploys\"}}",
        "text": "=PR #{{$json[\"body\"][\"number\"]}} merged by {{$json[\"body\"][\"pull_request\"][\"user\"][\"login\"]}}"
      },
      "name": "Slack",
      "type": "n8n-nodes-base.slack"
    }
  ]
}

{{ }} 안에 자바스크립트 표현식을 그대로 박을 수 있다는 게 의외였다. 라벨로 채널을 분기하는 로직을 한 줄에 끝낸다. 백엔드 코드로 짜면 if/else 블록 5~6줄짜리가 표현식 하나로 압축된다. 프론트에서 JSX 안에 삼항연산자 박던 감각이랑 거의 똑같아서, 전환자 입장에선 이 부분이 제일 익숙했다.

자격증명은 어떻게 들어가나

그런데, 슬랙 토큰 같은 비밀값은 노드별로 따로 등록한다. Credentials 메뉴에서 한 번 만들어두면 여러 워크플로우가 공유한다. SQLite에 암호화되어 들어가는데, 키는 N8N_ENCRYPTION_KEY 환경변수로 고정해야 한다. 안 정하면 컨테이너 첫 부팅 때 자동 생성되는데, 이게 바뀌면 기존 자격증명을 못 푼다.

# 처음 띄울 때 한 번 정해두기
-e N8N_ENCRYPTION_KEY="$(openssl rand -hex 32)"

이 값은 어딘가에 따로 백업해두자. 이걸 잃으면 슬랙 토큰부터 GitHub PAT까지 전부 다시 등록해야 한다.

메모 — 운영하며 알게 된 것

결국, 워크플로우 3개를 운영하면서 적어둔 것들이다.

실행 이력은 자동으로 쌓이는데 의외로 무겁다. 기본 설정으로 두면 모든 실행 데이터가 SQLite에 들어간다. 외부 API를 1분마다 폴링하는 워크플로우 하나를 돌렸더니 일주일 만에 DB가 600MB를 넘었다. EXECUTIONS_DATA_PRUNE=trueEXECUTIONS_DATA_MAX_AGE=168 같은 환경변수로 보존 기간을 줄여야 한다.

노드별 입출력은 디버깅의 90%다. 각 노드를 클릭하면 직전 실행의 입출력 JSON이 그대로 보인다. 백엔드 코드에서 print 박으면서 추적하던 걸 클릭 한 번으로 끝낸다. 이게 제일 좋다.

즉, JS 표현식이 강력한 만큼 사고도 잘 친다. 표현식 안에서 한 번 던진 예외는 노드 실행 전체를 중단시킨다. || 폴백을 넣어두는 게 안전하다. {{$json.body.user?.name || "unknown"}} 같은 식으로.

n8n 워크플로우 자체를 git으로 관리하고 싶으면 n8n CLIexport:workflowimport:workflow를 쓸 수 있다. 환경별로 워크플로우를 분리할 때 유용했다.

지금 당장 따라 해볼 만한 건 세 가지다. 첫째, 위의 docker run 한 줄을 named volume 옵션 그대로 띄워보기. 둘째, Webhook 노드 만들고 Production URL을 GitHub Webhooks에 붙여 PR 이벤트를 받아보기. 셋째, N8N_ENCRYPTION_KEY를 미리 정해 비밀번호 매니저에 박아두기. 이 세 개만 해두면 나중에 멀쩡한 SQLite 통째로 잃는 사고를 피한다.

개인적으로는 복잡한 분기와 외부 서비스가 5개 이상 엮이는 자동화라면 코드보다 n8n이 더 나은 것 같다.

관련 글