HEYDLIFE / n8n 자동화

n8n 메모 500자 검사: 가족 이모지 100개가 거절되는 이유와 재현 코드

UTF-16·코드 포인트·grapheme의 차이를 같은 입력으로 비교하고 실제 n8n에서 다섯 경계값을 시험했습니다. 입력 검사와 저장 권한을 구분합니다.

“500자까지”라는 안내만으로는 서버와 화면의 길이 기준을 맞출 수 없습니다. n8n 입력 검사에서 😀 500개는 통과했지만, 가족 이모지 100개는 거절됐습니다. 같은 검사 코드가 유니코드 코드 포인트를 세기 때문입니다.

입력UTF-16 길이코드 포인트눈에 보이는 묶음검사 결과
a × 500500500500READY
😀 × 5001000500500READY
😀 × 5011002501501HOLD
👨‍👩‍👧‍👦 × 1001100700100HOLD
e + 결합 악센트 × 50010001000500HOLD

“눈에 보이는 묶음”은 Intl.Segmenter의 grapheme 단위입니다. 문자·바이트 제한과 다른 개념입니다. 위 입력은 2026년 10월 5일 n8n 2.40.7의 실제 CLI 실행에서 확인했으며, 길이 세 방식은 같은 입력을 Node.js에서 비교했습니다. 모든 결과는 saved=false입니다. 고객 메모를 저장하거나 AI Agent를 연결한 실험은 아닙니다.

왜 같은 이모지인데 수가 다를까요?

note.length는 JavaScript의 UTF-16 코드 단위를 셉니다. [...note].length는 코드 포인트를 셉니다. 가족 이모지는 여러 인물과 연결 문자로 구성되어 코드 포인트 7개지만 화면에서는 한 묶음으로 보일 수 있습니다. e와 결합 악센트도 두 코드 포인트입니다. 따라서 프런트엔드와 서버가 서로 다른 방식을 쓰면 “100자만 넣었는데 실패했다”는 상황이 생깁니다.

내 서비스의 제한을 먼저 한 문장으로 정하세요

요구사항검사 기준함께 정할 조건
화면에 보이는 글자 묶음 500개Intl.Segmenter의 grapheme서버 런타임 지원·정규화·결합 문자 시험
코드 포인트 500개[...text].length현재 예제의 기준
저장 컬럼 또는 API 바이트 제한인코딩 후 바이트 수UTF-8인지·최대 바이트·초과 시 처리

화면의 표시 개수 제한과 저장소의 바이트 제한을 둘 다 적용할 수 있습니다. 한쪽을 늘렸다고 다른 쪽도 안전하다고 판단하지 마세요. 이 글의 n8n 예제는 기존 계약을 유지해 코드 포인트 500개로 검사하며, grapheme 방식으로 자동 변경하지 않았습니다.

다섯 입력으로 확인하는 수동 시험

  1. 새 n8n 수동 워크플로에 Manual Trigger → Code(입력) → Code(검사)를 연결합니다.
  2. 두 Code 노드는 JavaScript / Run Once for All Items로 설정합니다. 아래 입력 생성 코드와 검사 코드를 각각 복사합니다.
  3. 마지막 출력 순서가 READY, READY, HOLD, HOLD, HOLD인지 확인합니다. 모든 saved는 false여야 합니다.
const notes = ['a'.repeat(500), '😀'.repeat(500), '😀'.repeat(501),
  '👨‍👩‍👧‍👦'.repeat(100), 'e\u0301'.repeat(500)];
return notes.map((note,i)=>({json:{
  request_id:'REQ-'+String(i+1).padStart(3,'0'), account_id:'C-001', note
}}));
입력 검사 전체 코드
// Educational dry run. Never writes or sends a request.
const allowed = new Set(['request_id', 'account_id', 'note']);
const demoAccounts = new Set(['C-001']);
return $input.all().map((item, index) => {
  const x = item.json;
  let reason = null;
  if (!x || typeof x !== 'object' || Array.isArray(x)) reason = 'INVALID_OBJECT';
  else if (Object.keys(x).some(k => !allowed.has(k))) reason = 'EXTRA_FIELD';
  else if (typeof x.request_id !== 'string' || !/^REQ-[0-9]{3}$/.test(x.request_id)) reason = 'INVALID_REQUEST_ID';
  else if (typeof x.account_id !== 'string' || !demoAccounts.has(x.account_id)) reason = 'UNKNOWN_ACCOUNT';
  else if (typeof x.note !== 'string' || !x.note.trim() || [...x.note.trim()].length > 500) reason = 'INVALID_NOTE';
  // Preserve a result for every input; HOLD must never reach a production writer.
  const result = reason
    ? { status: 'HOLD', reason, saved: false }
    : { status: 'READY', saved: false, payload: {
        request_id: x.request_id, account_id: x.account_id, note: x.note.trim()
      } };
  return { json: result, pairedItem: { item: index } };
});
세 가지 길이를 비교하는 Node.js 코드
const s = '👨‍👩‍👧‍👦'.repeat(100);
const segmenter = new Intl.Segmenter('ko', {granularity:'grapheme'});
console.log({utf16:s.length, codePoints:[...s].length,
  graphemes:[...segmenter.segment(s)].length});
// 결과: {utf16:1100, codePoints:700, graphemes:100}

이 예제는 허용 필드도 제한합니다. 정상 입력에 role 또는 address를 추가하면 EXTRA_FIELD로 거절합니다. 2026-10-01의 실제 n8n 경계 시험에서는 role 추가가 HOLD였고, 기존 정상·오류 7건은 READY 1건/HOLD 6건이었습니다. 이전 경계 시험 결과에서 대조할 수 있습니다.

READY를 저장 권한으로 해석하면 안 됩니다

C-001은 가상 고객 목록입니다. 로그인 사용자 확인, 고객 접근 권한, 메모 저장, 중복 방지 기능이 없습니다. READY는 문자열과 필드 검사만 통과했다는 뜻입니다. 실제 서버는 요청자의 고객 접근 권한을 별도로 검사하고, 저장 후 결과를 읽어 확인해야 합니다. API 키나 실제 고객 기록은 이번 코드에 넣을 필요가 없습니다.

정정: 이전 글의 Agent 도구 제작 소개를 제거하고 실제 시험한 입력 검사로 범위를 좁혔습니다. 자료: n8n Code node 및 데이터 구조 문서, MDN Intl.Segmenter·String.length, 확인 2026-10-05. 자체 생성 입력이며 의료·고객 자료가 아닙니다.

관련 실험: 빈 입력과 0원 주문 · 재시도와 중복 저장

작성·실험 정리: HEYDLIFE 운영자와 AI 작성 보조. 개정: 2026년 10월 5일. 가상 자료로 수행한 시험과 실제 서비스 사용 성과를 구분했습니다.