발행일: 2026-10-06(화) 11:44
홈›도서›하루 30분 n8n ④›첫 번째 커스텀 노드 만들기
DAY 23★★★

첫 번째 커스텀 노드 만들기

📖 이론5분
🛠️ 실습20분
🎯 미션5분

"만들어보지 않은 것은 이해한 것이 아니다."

— 리처드 파인만 (Richard Feynman, 노벨물리학상 수상자)
🎯 오늘의 학습 목표
  • 크리덴셜 파일과 노드 파일의 구조를 이해하고 작성할 수 있다.
  • 알리고 SMS API를 호출하는 실전 커스텀 노드를 완성할 수 있다.
  • 에러 핸들링과 continueOnFail 패턴을 적용할 수 있다.
실전 프로젝트: 알리고 SMS 발송 노드

국내 SMS 서비스 '알리고'의 API를 n8n 노드로 만들어봅니다. 크리덴셜 파일과 노드 실행 파일 두 가지를 작성합니다.

크리덴셜 파일 — credentials/AligoApi.credentials.ts
typescript
import { ICredentialType, INodeProperties } from 'n8n-workflow';

export class AligoApi implements ICredentialType {
  name = 'aligoApi';
  displayName = '알리고 SMS API';
  documentationUrl = 'https://www.aligo.in/api/doc';
  properties: INodeProperties[] = [
    {
      displayName: 'API Key',
      name: 'apiKey',
      type: 'string',
      typeOptions: { password: true },
      default: '',
      description: '알리고 관리자 페이지에서 발급받은 API Key',
    },
    {
      displayName: '사용자 ID',
      name: 'userId',
      type: 'string',
      default: '',
      description: '알리고 계정 아이디',
    },
    {
      displayName: '발신 번호',
      name: 'senderPhone',
      type: 'string',
      default: '',
      placeholder: '01012345678',
      description: '알리고에 사전 등록된 발신자 번호 (숫자만)',
    },
  ];
}
노드 파일 — nodes/AligoSms/AligoSms.node.ts
typescript
import {
  IExecuteFunctions,
  INodeExecutionData,
  INodeType,
  INodeTypeDescription,
  IHttpRequestOptions,
  NodeOperationError,
} from 'n8n-workflow';

export class AligoSms implements INodeType {
  description: INodeTypeDescription = {
    displayName: '알리고 SMS',
    name: 'aligoSms',
    icon: 'file:aligoSms.svg',
    group: ['communication'],
    version: 1,
    description: '알리고 API를 통해 SMS를 발송합니다',
    defaults: { name: 'SMS 발송' },
    inputs: ['main'],
    outputs: ['main'],
    credentials: [{ name: 'aligoApi', required: true }],
    properties: [
      {
        displayName: '수신 번호',
        name: 'receiverPhone',
        type: 'string',
        default: '',
        required: true,
        placeholder: '01098765432',
        description: '수신자 전화번호 (숫자만, 다수는 쉼표로 구분)',
      },
      {
        displayName: '메시지 내용',
        name: 'message',
        type: 'string',
        typeOptions: { rows: 4 },
        default: '',
        required: true,
        description: '발송할 SMS 내용 (한글 45자/영문 90자 이하)',
      },
    ],
  };

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

    for (let i = 0; i < items.length; i++) {
      try {
        const credentials = await this.getCredentials('aligoApi');
        const receiverPhone = this.getNodeParameter('receiverPhone', i) as string;
        const message = this.getNodeParameter('message', i) as string;

        const requestOptions: IHttpRequestOptions = {
          method: 'POST',
          url: 'https://apis.aligo.in/send/',
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
          body: new URLSearchParams({
            key: credentials.apiKey as string,
            user_id: credentials.userId as string,
            sender: credentials.senderPhone as string,
            receiver: receiverPhone,
            msg: message,
            testmode_yn: 'N',
          }).toString(),
        };

        const response = await this.helpers.httpRequest(requestOptions);
        const result = typeof response === 'string' ? JSON.parse(response) : response;

        if (result.result_code !== '1') {
          throw new NodeOperationError(
            this.getNode(),
            `SMS 발송 실패: ${result.message} (코드: ${result.result_code})`,
            { itemIndex: i }
          );
        }

        returnData.push({
          json: {
            success: true,
            msgId: result.msg_id,
            receiver: receiverPhone,
            message: message,
            remainingCount: result.remain_count,
            sentAt: new Date().toISOString(),
          },
          pairedItem: { item: i },
        });
      } catch (error) {
        if (this.continueOnFail()) {
          returnData.push({
            json: { success: false, error: (error as Error).message },
            pairedItem: { item: i },
          });
          continue;
        }
        throw error;
      }
    }
    return [returnData];
  }
}
빌드 및 테스트
bash
npm run build
🛠️ DAY 23 실습
Step 1크리덴셜 파일 작성
  1. credentials/ 폴더에 AligoApi.credentials.ts 파일 생성
  2. 위 코드를 복사 붙여넣기
  3. npm run build 로 컴파일 오류 없는지 확인
Step 2노드 파일 작성
  1. nodes/AligoSms/ 폴더 생성
  2. AligoSms.node.ts 파일 작성
  3. aligoSms.svg 아이콘 파일 준비 (빈 SVG 파일이라도 OK)
Step 3로컬 테스트
  1. npm run build 성공 확인
  2. npx n8n start 로 로컬 n8n 실행
  3. 노드 패널에서 '알리고 SMS' 노드 검색 및 확인
🚀 DAY 23 미션

알리고 SMS 커스텀 노드를 완성하고, 로컬 n8n에서 노드가 정상적으로 나타나는 것을 확인하세요. 실제 알리고 계정이 없다면 testmode_yn: 'Y' 로 설정하여 테스트 모드로 발송을 테스트할 수 있습니다.

☑️ 미션 체크리스트
  • AligoApi.credentials.ts 작성 및 빌드 성공
  • AligoSms.node.ts 작성 및 빌드 성공
  • 로컬 n8n 노드 패널에서 '알리고 SMS' 노드 확인
  • 워크플로우에 노드 추가 후 속성 패널 정상 표시 확인
💡 힌트: SVG 아이콘이 없으면 빌드 오류가 납니다. 임시로 기존 ExampleNode.svg를 복사해서 aligoSms.svg로 이름을 바꿔 사용하세요.
🎯 성공 기준: 로컬 n8n 노드 패널에서 '알리고 SMS' 노드가 표시되고, 속성(수신번호, 메시지 내용)이 보이는 상태
← 이전
커스텀 노드 개발 환경 세팅
다음 →
커스텀 노드 고급 패턴과 npm 배포
← 목차로 돌아가기