첫 번째 workflow: 수동 트리거 → 필드 편집 → 코드
견적 문제는 제가 표에서 2년 동안 손으로 계산했습니다: 수량 곱하기 단가, 운송비 더하기, 다시 나눠서 개당 원가 계산. 봉투 200개, 단가 1.85위안, 운송비 60위안, 한 세트에 430위안, 개당 2.15위안. 숫자는 복잡하지 않은데, 문제는 제가 운송비를 두 번 빠뜨렸고 고객 쪽에는 견적서가 이미 발송된 상태였습니다.
나중에 이 계산법을 n8n으로 옮겼습니다. 노드 세 개, 숫자 하나 바꾸고 다시 실행하면 2초 만에 새 가격이 나옵니다. 이 글은 빈 캔버스에서 실행까지의 세 단계이며, 제가 처음에 430을 37060으로 계산한 그 함정도 포함합니다.
사전 준비는 두 가지뿐입니다: n8n이 이미 설치되어 열 수 있어야 하고(설치하지 않은 분은《n8n 설치 방법》참고), 캔버스, 노드 패널, 실행 기록 이 세 가지가 익숙해야 합니다(익숙하지 않은 분은《인터페이스 안내》참고). 코드를 작성할 줄 몰라도 됩니다. 코드 부분은 통째로 복사하면 됩니다.
- 세 노드가 각각 한 가지씩 담당합니다
- 노드 사이에 전달되는 것은 무엇인가
- 빈 캔버스에서 세 노드까지
- 필드 편집: 데이터 하나에 필드 네 개 추가하기
- 코드 노드: 하나 받아서, 한 번 계산하고, 하나 돌려주기
- 한 번 실행해서 출력 확인하기
- 부록: 전체 워크플로 JSON
- 정말로 제대로 실행되는지 확인하는 방법
- 실제로 마주치게 되는 다섯 가지 오류
- Code 노드의 세 가지 경계
세 노드가 각각 한 가지 일을 담당한다
초보자가 가장 쉽게 범하는 실수는 세 노드를 「세 단계」로 여기는 것이다. 그것들은 단계가 아니라 세 가지 역할이다.
- Manual Trigger(수동 트리거): 빈 데이터를 하나 만들어 아래로 보낸다. 데이터베이스를 조회하지 않고, 요청을 받지 않고, 시간을 보지 않으며, 순전히 「네가 한 번 클릭하면 한 번 실행된다」. 첫 워크플로우에서 이것을 쓰는 이유는 변수를 하나로 줄이기 위해서다——데이터가 어디서 오는지는 먼저 걱정하지 않아도 된다.
- Edit Fields(필드 편집): item에 필드를 넣거나 기존 필드를 바꾼다. 개수는 변하지 않으며, 들어온 하나가 나가도 하나다. 옛 튜토리얼에서는 Set이라고 불렀고, 이름이 바뀐 지 꽤 되었으며, 2.43.7의 노드 패널에는 Edit Fields (Set)라고 적혀 있어 두 이름 모두 검색된다.
- Code(코드): 유일하게 개수를 바꿀 수 있는 단계. 하나를 여러 개로 나눌 수 있고, 여러 개를 하나로 압축할 수 있다. 따라서 반환값은 반드시 배열이어야 한다——이것이 다른 노드와 가장 큰 차이이며, 오류가 가장 많이 나는 지점이기도 하다.
제 인스턴스는 2.43.7이고, 인터페이스를 중국어 간체로 전환했습니다. 한 가지 알려드리자면: 중국어 인터페이스는 설명 텍스트와 버튼만 번역했고, 노드 이름과 파라미터 이름은 여전히 영어입니다. 「编辑字段」로 검색하면 아무것도 안 나오고, edit fields로 검색해야 합니다. 인터넷의 튜토리얼들이 쓰는 「Set 노드」「Set 노드의 Keep Only Set Fields」는 2.x 인터페이스에서는 역시 맞지 않으니, 자신을 의심하지 마세요.
노드 사이에서 전달되는 것은 대체 무엇인가
이것을 명확히 하지 않으면, 이후의 모든 오류는 추측에 의존할 수밖에 없다.
n8n에서 노드 사이를 흐르는 모든 데이터는 하나의 배열이며, 배열의 각 요소는 이렇게 생겼다:
[
{ "json": { "qty": 200 } },
{ "json": { "qty": 150 } }
]
바깥쪽 대괄호는 「하나의 배치」이고, 안의 각 객체는 하나의 데이터이며 n8n은 이를 item이라고 부른다. 실제 비즈니스 필드는 json 이 키 안에 들어 있다. 따라서 「200건의 주문 처리」는 이 모델에서 200번 반복하는 것이 아니라, 한 번에 노드에 200개의 item을 주고 노드가 스스로 하나씩 처리하는 것이다.
이 설계는 두 가지 결과를 가져온다. 좋은 소식은 노드가 자연스럽게 배치로 작업하므로 루프를 작성할 필요가 없다는 것이다. 나쁜 소식은 코드를 작성할 때 손에 들어오는 것이 단일 객체가 아니라 배열이라는 것이다——row.qty 이런 작성 방식은 배열에 직접 사용할 수 없으며, 먼저 첫 번째 항목을 꺼내야 한다.
빈 캔버스에서 세 개의 노드까지
- 워크플로 목록 오른쪽 위의 「새 워크플로」를 누르거나, 주소창에 직접
/workflow/new을 입력한다. 나는 후자를 선호한다. 빠르기 때문이다. - 빈 캔버스에서 오른쪽 위의 +를 클릭한다(캔버스 가운데의 더하기를 클릭해도 된다). 패널에 「다음에 무슨 일이 일어날까요?」가 뜬다.
- 검색창에
manual trigger을 입력하고 Manual Trigger를 선택한다. - 마우스를 노드 오른쪽의 작은 원 위로 옮기면 +이 나타나면 클릭하고,
edit fields을 검색한 뒤 Edit Fields (Set)를 선택한다. 이 방식으로 추가하면 연결선이 자동으로 연결되어 수동으로 끌 필요가 없다. - 다시 한 번 반복한다, 검색
code, Code를 선택한다. - 노드 이름은 기본적으로 영어이다. 제목을 더블클릭하면 변경할 수 있으며, 나는 중국어로 바꿨다: 수동 트리거, 견적서 작성, 금액 계산. 노드가 많아지면 영어 이름은 한눈에 Trigger, Set, Code만 보이므로, 중국어로 바꾸는 것은 순전히 눈을 편하게 하기 위함이며 기능적으로는 아무런 영향이 없다.

필드 편집: 하나의 데이터에 네 개의 필드를 추가한다
「견적서 작성」을 더블클릭하여 패널을 열면, 매개변수는 두 곳만 수정하면 된다.
Mode 유지 Manual Mapping, 이것이 폼 작성 방식이다. 다른 옵션인 JSON Output은 JSON을 직접 작성하는 것으로, 중첩 구조를 만들 때 더 편리하지만, 입문 단계에서는 먼저 사용하지 않는 것이 좋다.
Fields to Set 에 네 줄을 추가한다. 각 줄의 왼쪽은 필드 이름, 가운데는 타입 드롭다운, 오른쪽은 값이다:
| 필드 이름 | 타입 | 值 |
|---|---|---|
| product | 문자열 | 진공 압축팩 60x80cm |
| qty | 숫자 | 200 |
| unit_price | 숫자 | 1.85 |
| freight | 숫자 | 60 |
유형 드롭다운의 기본값은 「문자열」이며, qty, unit_price, freight 이 세 가지는 반드시 수동으로 「숫자」로 변경해야 합니다. 인터페이스는 막지 않으며, 잘못 선택해도 오류를 보고하지 않고 조용히 200을 "200"으로 저장합니다. 이 함정이 얼마나 아픈지는 나중에 계산해서 보여드리겠습니다.

row.unit_price, 한 글자라도 빠지면 undefined가 됩니다.패널 맨 아래에 있는 Include Other Input Fields 스위치는 기본적으로 켜져 있으며, 의미는 「입력에서 내가 건드리지 않은 필드는 그대로 출력한다」입니다. 이 흐름의 입력은 비어 있어서 스위치를 켜든 끄든 같습니다. 하지만 데이터가 있는 흐름으로 바꾸면, 이걸 끄는 것은 업스트림 필드를 전부 잘라내고 직접 입력한 네 개만 남기는 것과 같습니다——그때 코드에서 product를 가져올 수 없다고 해서 코드가 잘못 작성됐다고 생각하지 마세요.
코드 노드: 하나를 가져와서, 한 번 계산하고, 하나를 돌려준다
「금액 계산」을 더블클릭한다.Mode 유지 Run Once for All Items(전체 배열을 한 번 실행),Language JavaScript를 유지한다. 그런 다음 아래 내용을 편집 영역에 붙여넣는다:
const row = items[0].json;
const amount = row.qty * row.unit_price + row.freight;
const unitCost = Number((amount / row.qty).toFixed(2));
return [{
json: {
product: row.product,
qty: row.qty,
amount,
unitCost,
summary: row.product + ' ' + row.qty + ' 个,含运费合计 ' + amount + ' 元,单个成本 ' + unitCost + ' 元',
},
}];
한 줄씩 설명하되, 문제가 될 수 있는 부분만 말한다.
items[0].json 첫 번째 항목을 가져온다. 여기에는 데이터가 하나뿐이므로 [0]을 취하면 충분하다. 여러 개인 경우에는 items.map(...)를 작성해야 하는데, 그건 다음 글에서 다룬다.items 는 예전 방식이며 2.x에서도 여전히 사용할 수 있다; 새로운 방식은 $input.first().json이며, 두 가지 모두 이 인스턴스에서 실행한 결과가 완전히 같았다. 내가 items를 남긴 이유는 「전체 배열을 한 번 실행」 모드에서는 애초에 전체 배열을 가지고 있기 때문에 이 이름이 더 정직하기 때문이다.
toFixed(2) 보기 좋으라고 하는 것이 아니다. 430을 200으로 나누면 정확히 2.15라서 깔끔해 보인다; 운송비를 55로 바꾸면 2.175가 되고, 부동소수점 연산 결과는 2.1749999999999998이 될 수 있다. 표에 적거나 고객에게 보내면 이런 꼬리가 보기 좋지 않다. 바깥쪽 Number() 문자열에서 다시 숫자로 바꾸는 것이다—toFixed 반환되는 것은 문자열이라, 변환하지 않으면 unitCost가 표에서 왼쪽 정렬된다.
끝부분의 그 부분 return 이 핵심이다.배열을 반환하고, 각 요소를 json으로 한 겹 감싼다. n8n은 이 json 키로 「이것은 비즈니스 데이터」와 「이것은 바이너리 첨부 파일」을 구분한다. 형식이 틀리면 노드가 바로 빨갛게 뜬다.

한 번 실행해서 출력을 본다
캔버스 아래쪽의 주황색 「실행」을 클릭한다. 1~2초면 세 노드의 테두리가 초록색으로 바뀌고, 각 노드 오른쪽 위에 작은 체크 표시가 생기며, 연결선 중간에 「1个项目」이 표시된다.

「금액 계산」을 더블클릭하고, 오른쪽 출력 영역에서 JSON 탭으로 전환한다:

참고로 오해하기 쉬운 점 하나: 이 「실행」 버튼은 클릭할 때만 실행된다. Manual Trigger 워크플로는 게시되었다고 해서 스스로 동작하지 않으며, 「스스로 동작하는」 메커니즘이 없다. 정기 실행을 하려면 Schedule Trigger 같은 트리거로 바꿔야 한다.
첨부: 전체 워크플로 JSON
노드를 클릭하기 귀찮다면 전체를 가져올 수 있다. 아래 내용을 복사한 뒤 캔버스 빈 곳에서 Ctrl+V, 세 개의 노드가 바로 캔버스에 배치됩니다. 메뉴로도 가능합니다: 오른쪽 상단 ⋯ → 가져오기 → 파일에서 가져오기, 먼저 내용을 .json。
이 부분은 제가 직접 빈 워크플로에 붙여넣어 한 번 실행해봤습니다: 노드 세 개, 연결선 두 개, 위치 모두 맞고, 실행 후 amount도 430입니다.
{
"name": "第一个工作流",
"nodes": [
{"parameters":{},"id":"m1","name":"手动触发","type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[-368,0]},
{"parameters":{"assignments":{"assignments":[
{"id":"a1","name":"product","value":"真空压缩袋 60x80cm","type":"string"},
{"id":"a2","name":"qty","value":200,"type":"number"},
{"id":"a3","name":"unit_price","value":1.85,"type":"number"},
{"id":"a4","name":"freight","value":60,"type":"number"}
]},"includeOtherFields":true,"options":{}},"id":"m2","name":"填写报价单","type":"n8n-nodes-base.set","typeVersion":3.5,"position":[-144,0]},
{"parameters":{"jsCode":"const row = items[0].json;nconst amount = row.qty * row.unit_price + row.freight;nconst unitCost = Number((amount / row.qty).toFixed(2));nnreturn [{n json: {n product: row.product,n qty: row.qty,n amount,n unitCost,n summary: row.product + ' ' + row.qty + ' 个,含运费合计 ' + amount + ' 元,单个成本 ' + unitCost + ' 元',n },n}];","mode":"runOnceForAllItems"},"id":"m3","name":"计算金额","type":"n8n-nodes-base.code","typeVersion":2,"position":[80,0]}
],
"connections": {
"手动触发": {"main": [[{"node": "填写报价单", "type": "main", "index": 0}]]},
"填写报价单": {"main": [[{"node": "计算金额", "type": "main", "index": 0}]]}
},
"settings": {"executionOrder": "v1"}
}
정말로 제대로 실행됐는지 판단하는 방법
네 가지이며, 모두 직접 확인할 수 있고, 하나라도 빠지면 완료가 아닙니다.
- 세 노드에 초록 테두리와 체크 표시가 있고, 두 연결선 모두 「1个项目」으로 표시됩니다. 숫자가 1이 아니면 중간 노드가 항목 수를 변경한 것이므로 다시 확인해야 합니다.
- 코드 노드 출력 JSON에서 amount는 430, unitCost는 2.15입니다.
- 실행 기록에서 이번 실행을 찾을 수 있습니다. 수동으로 클릭한 것도 기록이 남으니, 예약 작업만 목록에 들어간다고 생각하면 안 됩니다.
- freight를 60에서 100으로 변경하고 다시 실행하면 amount가 470이 되어야 합니다. 입력 하나를 바꾸고 출력이 따라 변하는지 보는 것, 이 항목이 앞의 세 가지보다 더 확실합니다——430을 한 번만 보는 것은 우연일 수 있습니다.
네 번째는 매번 하는 것을 권장합니다. 한 번 실행되는 것과 올바르게 실행되는 것 사이에는 차이가 있습니다.
실제로 마주치게 되는 다섯 가지 오류
1. Code doesn't return items properly
문자열을 반환했습니다. 제가 실제로 테스트했을 때 한 줄만 작성했더니 return "共 430 元";, 노드가 즉시 빨간색으로 변하고 오른쪽 아래에 이 메시지가 뜬다.

흥미로운 점은,return { amount: 430 } 배열로 감싸지 않고 json으로 감싸지 않은 이런 작성 방식이실행된다는 것이다. Code 노드가 자동으로 json 키와 대괄호를 보충해 주며, 문서에도 이 동작이 명시되어 있다. 따라서 이 오류는 「형식이 표준에 맞지 않음」이 아니라 「반환한 것이 데이터가 아님」이다 — 문자열, 숫자, null은 보충할 수 없다.
2. items is not defined [line 1]
Mode가 Run Once for Each Item으로 선택되어 있는데, 코드에서는 여전히 items를 사용하고 있다. 항목별 모드에서 n8n은 한 번에 하나씩만 입력하므로, 현재 항목은 $json이며, 개수를 가져오려면 $json.qty로 작성해야 한다. 두 모드의 차이는 한 문장으로: 전체 배열을 한 번 실행하느냐, 각 항목을 한 번씩 실행하느냐이다. 코드를 작성하기 전에 이 드롭다운을 먼저 확인하자.
3. 하나의 워크플로우에는 "Manual Trigger" 노드를 하나만 허용한다
이미 트리거가 있는 워크플로우에 트리거를 하나 더 추가했다. 이번에는 더 어리석게 실수했다: 트리거가 포함된 JSON을 기존 워크플로우 캔버스에 붙여넣자 「노드를 삽입할 수 없음」이 팝업되고 그 아래에 이 메시지가 표시되었다. 비교해 보려면 새 워크플로우를 먼저 열고 붙여넣자.
4. undefined를 출력하지만 오류는 나지 않음
Edit Fields에서 필드 이름은 unit_price인데, 코드에서는 row.unitPrice로 작성함. 대소문자와 밑줄이 한 글자 차이인데, n8n도 신경 쓰지 않고 JS도 신경 쓰지 않아서 결과적으로 undefined가 계속 전달되어 summary에 「운임 포함 합계 undefined 원」이 나타남. 이런 오류는 빨간색도 뜨지 않고 소리도 없이 숫자만 틀리기 때문에 찾기 가장 어려움.
5. amount가 "37060"이 됨
필드 타입을 문자열로 선택함. 60과 "60"은 JS에서 같아 보이지만, + 문자열을 만나면 연결로 바뀜: 370에 "60"을 더하면 430이 아니라 "37060"이 됨.

다섯 번째가 이 글에서 가장 값진 항목임. 처음 이 숫자가 나왔을 때 37060을 반 분 동안 바라보다가 문자열 연결이라는 걸 알아차림 — 코드는 전혀 문제 없어 보이니까, 문제는 이전 노드의 타입 드롭다운에 있었음.
Code 노드의 세 가지 경계
Code 노드는 n8n 자체 프로세스에서 실행되지만 샌드박스가 있음: 로컬 파일을 읽을 수 없고 HTTP 요청을 보낼 수 없음. 파일을 다루려면 Read/Write File From Disk 노드를 쓰고, 요청을 보내려면 HTTP Request 노드를 씀. 이건 결함이 아니라 「아무렇게나 붙여넣은 JS 한 조각이 할 수 있는 일의 크기」를 최소로 줄인 것임.
하지만 막지 못하는 세 가지는 직접 관리해야 함.
남이 준 워크플로 JSON을 함부로 붙여넣지 말 것.붙여넣는 순간 남이 작성한 JS가 자신의 인스턴스에서 실행됨. 내 인스턴스는 JSON을 붙여넣으면 확인 창도, 출처 질문도 없이 바로 노드에 반영됨. 정말로 시험하려면 빈 테스트 인스턴스에 먼저 넣고, 코드를 확인한 뒤 실행 여부를 결정할 것.
키를 코드에 하드코딩하지 말 것.워크플로는 통째로 내보내서 동료에게 보내고 단체 채팅방에 붙일 수 있다. 코드에 하드코딩된 api key는 JSON과 함께 따라간다. credential 노드에 넣어서 n8n이 암호화해 저장하게 해야 한다.
셀프 호스팅은 서드파티 npm 패키지를 설치할 수 있다.기본적으로 꺼져 있고, 환경 변수를 수동으로 켜야 한다. 켜면 Code 노드에 임의 코드 실행 구멍을 열어주는 셈이다. 혼자 쓰는 인스턴스는 마음대로 해도 되지만, 여러 명이 공유하는 인스턴스는 계속 꺼두는 걸 권한다.
소결
노드 세 개, 체인 하나: 트리거가 데이터를 만들고, 필드 편집이 데이터를 보충하고, 코드가 데이터를 계산한다. 진짜로 기억해야 할 것은 이 세 노드를 어떻게 클릭하는가가 아니라 그 "배열 안에 json" 모양이다. 뒤의 IF, Merge, 루프, 오류 분기는 전부 이 모양의 변형이다.
코드는 가능하면 쓰지 않는 게 좋다. 이 견적은 Edit Fields에 표현식을 더해도 계산할 수 있고,{{ $json.qty * $json.unit_price + $json.freight }} 한 줄이면 끝나며 노드도 하나 줄어든다. 내가 최종적으로 Code를 고른 이유는 뒤에서 세 가지 기준의 견적을 동시에 계산해야 하는데, 노드에 표현식을 쌓는 것이 코드를 쓰는 것보다 유지보수가 더 어렵기 때문이다. 숫자 하나만 계산한다면 코드를 쓰지 마라.
타입이라는 함정은, 지금 당장 일부러 freight를 문자열로 바꿔서 한 번 실행해 보길 권한다. 430이 37060으로 바뀌는 걸 직접 눈으로 봐라. 글을 열 번 읽는 것보다 직접 한 번 틀리는 게 낫다.
계속해서
이전 글은《n8n, Zapier, Make 어떻게 선택할까》였다. 노드, 연결, 실행 이 세 가지가 JSON에서 어떻게 생겼는지는이 글에서 뜯어봤다. 언젠가 서비스가 뜨지 않거나 포트가 충돌하면《n8n 시작 오류와 포트 점유 어떻게 확인할까》를 봐라.. 설치 참고이 글, 인터페이스가 익숙하지 않으면이 글. 전체 코스 목차는여기。
참고 자료
- Manual Trigger: 실행을 클릭할 때만 실행되며 자동 트리거를 제공하지 않음; 하나의 workflow에는 하나만 허용됨
- Edit Fields (Set): Manual Mapping과 JSON Output 두 가지 모드, Include in Output의 역할
- Code: 두 Mode의 차이, 언어 옵션, 파일 시스템에 접근할 수 없고 HTTP 요청을 보낼 수 없음
- Understand n8n's data structure: 배열 안에 json이 중첩된 구조의 유래, 그리고 Code 노드가 json 키를 자동으로 보충하는 설명
- 내장 단축키 사용: 표현식 내장 변수와 메서드 중 Code 노드에서 적용되지 않는 것들
최종 업데이트: 2026년 10월 28일, n8n 2.43.7 사용.

