댓글이 저장되면 알림을 보내고, 회원 정보가 바뀌면 부가 처리를 하고 싶을 때가 있습니다. 이런 기능을 저장 함수에 계속 추가하면 코어가 커지고 프로젝트별 수정도 관리하기 어려워집니다. Plextype의 Trigger는 핵심 로직을 유지하면서 이벤트에 후속 기능을 연결하는 장치입니다.

이 글은 기존 가이드에서 확인한 구현을 기준으로 핵심 흐름과 댓글 로그 예제를 정리한 것입니다. 설치 버전에 따라 이벤트 이름과 전달 데이터가 달라질 수 있으므로 적용 전 자신의 코드를 확인하세요.

1. 트리거의 역할과 실행 흐름

사건의 이름을 이벤트, 이벤트를 발생시키는 동작을 디스패치, 연결되어 실행되는 함수를 핸들러라고 합니다.

댓글 저장 → 권한·입력 검증 → DB 저장
→ comment.saved 디스패치 → 데이터 매핑 → 핸들러 실행

이것은 PostgreSQL의 CREATE TRIGGER와 다릅니다. 애플리케이션에서 dispatchTrigger를 호출해야 실행되며, SQL이나 Prisma Query만 호출한다고 자동 발생하지 않습니다.

댓글·문서 액션의 triggerWrapper.ts는 본래 액션의 결과에 result.success 또는 result.id가 truthy일 때 디스패치합니다. 다만 후속 호출을 await하지 않으므로 사용자에게 결과가 반환되어도 핸들러는 아직 실행 중일 수 있습니다. 별도 파일인 withTrigger.ts의 before/after/both 콜백 래퍼와는 구분해야 합니다.

2. 이벤트와 전달 데이터 이해하기

이벤트 이름만큼 중요한 것이 어떤 데이터가 전달되는가입니다. 특히 saved를 신규 생성 전용으로 해석하면 안 됩니다.

  • comment.saved: 댓글 생성·수정에 사용합니다. 결과 데이터는 context.result.data에 있습니다.

  • document.saved / document.removed: 문서 저장·삭제에 사용합니다. 삭제 이벤트는 삭제 전 데이터 등 실제 호출부를 확인하세요.

  • user.saved: 회원 객체가 context.result에 직접 들어갑니다. 회원 ID는 id, 신규 여부는 _isNew로 매핑합니다.

허브는 handler(mappedData, context) 형태로 함수를 호출합니다. 첫 번째 인자는 설정의 map으로 고른 데이터, 두 번째 인자는 원본 실행 문맥입니다. 로그인 사용자 정보가 자동으로 추가되는 것은 아닙니다.

// 댓글 저장 결과를 단순화한 예
const context = {
  result: {
    success: true,
    data: { id: 42, userId: 7 }
  }
};
// map의 "data.id" → 42
// map의 "data.userId" → 7

map의 경로는 context.result를 기준으로 찾습니다. 따라서 댓글 ID는 data.id이지 result.data.id가 아닙니다. 문자열 템플릿 {{...}}도 사용할 수 있지만, 존재하지 않는 경로가 문자열 그대로 남거나 템플릿에서 0·false가 빈 문자열이 되는 경우가 있으므로 핸들러에서 값을 검증해야 합니다. 중첩 객체 내부는 재귀적으로 매핑하지 않습니다.

이벤트마다 구조가 같다고 가정하지 마세요. 기존 가이드에서 확인한 user.removed는 전달 형태가 달랐고, 설정에 이름만 적힌 이벤트나 export되지 않은 핸들러도 실제로 동작하지 않습니다.

3. 실전: 댓글 저장 로그 연결하기

외부 서비스나 DB 추가 없이, 댓글 식별자만 서버 로그에 남기는 예제입니다. 핸들러 작성 → export 연결 → 이벤트 설정 순서로 작업합니다.

① 핸들러 작성
src/extensions/comment-audit/trigger/handlers.ts를 만들고 전달값을 검증합니다.

type CommentAuditData = {
  commentId?: unknown;
  actorId?: unknown;
};

export async function projectAuditCommentSaved(
  data: CommentAuditData,
  _context: unknown
): Promise<void> {
  const { commentId, actorId } = data;
  if (
    typeof commentId !== "number" ||
    !Number.isInteger(commentId) || commentId <= 0 ||
    typeof actorId !== "number" ||
    !Number.isInteger(actorId) || actorId <= 0
  ) {
    console.warn("[comment-audit] Invalid event payload");
    return;
  }
  console.info("[comment-audit] comment.saved", {
    commentId, actorId
  });
}

② export 연결
src/extensions/trigger/index.ts에 아래 줄을 추가합니다. 기존 export는 보존합니다.

export { projectAuditCommentSaved }
  from "../comment-audit/trigger/handlers";

③ 이벤트 설정
src/extensions/trigger/trigger.json의 comment.saved 배열에 아래 항목을 추가합니다. 전체 파일을 덮어쓰거나 같은 이벤트 키를 중복 작성하지 않습니다.

{
  "handler": "projectAuditCommentSaved",
  "map": {
    "commentId": "data.id",
    "actorId": "data.userId"
  }
}

엔진은 src/extensions/triggerRegistry.ts가 내보내는 설정과 핸들러를 사용합니다. 위 연결을 마친 뒤 개발 환경에서 댓글을 생성·수정하고 [comment-audit] comment.saved 로그를 확인하세요. 권한 거부·저장 실패 때 실행되지 않는지도 확인해야 합니다. 이 예제는 기능 검증용 로그이며 영속적인 감사 기록은 아닙니다.

4. 운영 전에 알아둘 경계

  • 설정은 추가되고, 같은 함수 이름은 덮어쓸 수 있습니다. 이벤트 설정은 코어 배열 뒤에 확장 배열이 붙습니다. 반면 핸들러 이름이 겹치면 확장 함수가 코어 함수를 대체할 수 있어 고유한 이름을 권장합니다.

  • 후속 처리 실패가 DB 저장을 되돌리지는 않습니다. 허브는 개별 핸들러 오류를 기록하고 다음 핸들러를 실행합니다. 매핑 단계의 오류는 별도로 전파될 수 있습니다. 결제처럼 원자성이 필요한 업무는 핵심 액션의 트랜잭션에서 처리하세요.

  • 재시도와 완료 보장은 별도 설계가 필요합니다. 현재 흐름은 영속 큐가 아니며 중복 방지·재시도·타임아웃을 자동 제공하지 않습니다. 외부 호출에는 제한 시간을 두고, 반드시 전달할 작업은 outbox나 worker 등을 검토하세요.

  • 순환 호출과 정보 노출을 피하세요. 댓글 저장 핸들러에서 다시 같은 저장 액션을 호출하면 순환할 수 있습니다. 원본 context 전체 대신 필요한 식별자만 사용하고, 토큰·개인정보를 로그나 외부로 보내지 않습니다.

알림도 트리거 실행만으로 전달이 보장되지는 않습니다. sendNotification 이후 알림 액션에서 수신자와 수신 설정, 저장·전송 과정을 처리하므로 문제 발생 시 그 경로까지 확인해야 합니다.

5. 확장 배포와 확인 사항

기능을 배포할 때는 핸들러뿐 아니라 설정 조각과 export 연결 안내를 함께 제공합니다. 임의의 폴더를 복사하는 것만으로 등록되는 구조는 아니며, extensions:sync는 라우트 어댑터를 처리하는 도구이지 트리거 등록이나 DB 마이그레이션을 대신하지 않습니다.

설정은 모듈 수준에서 캐시되므로 운영 환경에서는 변경사항을 포함한 빌드·배포와 프로세스 재시작을 확인하세요. 필요한 DB 변경도 별도로 관리해야 합니다.

  • 액션이 발생시키는 이벤트 이름과 설정 키가 일치하는가?

  • 핸들러가 정확한 이름으로 export되고, 데이터 경로와 타입이 검증되는가?

  • 생성·수정·실패 및 중복 실행을 테스트했는가?

  • 배포에 설정·코드·필요한 DB 변경이 모두 반영되었는가?

6. 맺음말과 참고 자료

Plextype Trigger의 핵심은 이벤트, 전달 데이터, 실패의 영향 범위입니다. 작은 로그 핸들러로 연결을 확인한 뒤 필요한 후속 기능으로 확장하세요. 트리거는 핵심 업무를 대신하는 기능이 아니라, 코어와 확장 기능의 책임을 나누는 도구입니다.

확장 구조와 관련 개념은 아래 문서를 참고하세요. Node.js 이벤트와 PostgreSQL 트리거는 비교를 위한 자료이며 Plextype의 JSON 기반 Trigger와 동일한 구현은 아닙니다.