콘텐츠로 이동

@gj-kit/toss-payments-nestjs 핵심 개념

DI token은 타입을 싣고 다니지 않습니다. TossPaymentsFor<typeof config>가 그 타입을 되살려, 배선하지 않은 flow는 그대로 컴파일 에러로 남습니다.

DI token은 타입을 싣고 다니지 않습니다. forRoot()가 돌려주는 건 unique symbol에 바인딩된 DynamicModule이라, 주입 지점에 적은 생성자 타입 표기가 유일한 근거로 남습니다 — BillingKeyStore를 배선한 적 없는 config인데도 toss.billing이 타입 검사를 통과하고, 런타임에는 그 프로퍼티가 undefined입니다. 여기에 webhook controller를 붙이면 Nest 기본 body parser가 요청을 이미 객체로 바꿔 놓은 뒤입니다. 다시 직렬화한 JSON으로는 서명 검증을 복구할 수 없습니다.

Nest DI는 core kit type을 보존해야 합니다

섹션 제목: “Nest DI는 core kit type을 보존해야 합니다”

generic DI token은 configuration별 capability를 잃습니다. 이 패키지는 주입 지점에서 등록한 configuration으로 typed core kit을 다시 만듭니다.

webhook 검증에는 raw bytes가 필요합니다

섹션 제목: “webhook 검증에는 raw bytes가 필요합니다”

Nest JSON middleware가 요청을 파싱한 뒤에는 재직렬화로 signature input을 복구할 수 없습니다. raw-body 설정은 선행 조건입니다.

  • 배선 안 한 flow는 프로퍼티가 없습니다 — TossPaymentsFor<typeof config>가 DI 경계 뒤에서도 조건부 kit 타입을 복원하므로, BillingKeyStore 배선 없이 toss.billing을 쓰면 컴파일 에러입니다.
  • rawBody 부재는 조용히 넘어가지 않습니다 — req.rawBody가 없으면 toNestWebhookHandler는 handler를 호출하지 않습니다. 500을 돌려주고 확인할 설정 세 가지를 로그로 남깁니다 — NestFactory.create의 rawBody: true, Fastify의 raw body 지원 설정, 그리고 webhook route 앞에 걸린 JSON 미들웨어.
  • source IP가 wrapper를 통과합니다 — handler가 원본 Node socket을 그대로 전달해 코어의 fail-closed IP 검증이 유지되고, proxy 헤더를 신뢰하려면 sourceIp extractor를 명시해야 합니다.
  • ESM·CJS 어디서든 같은 token — TOSS_PAYMENTS와 getTossPaymentsToken(name)은 Symbol.for 기반이라, 패키지가 ESM/CJS로 이중 로드돼도 provider 바인딩이 하나로 유지됩니다.
  • emitDecoratorMetadata 없이 동작합니다 — InjectTossPayments()는 @Inject(token)에 위임하는 얇은 래퍼이고 src/ 어디에도 design:paramtypes를 읽는 코드가 없습니다. 그래서 패키지는 emitDecoratorMetadata: false로 배포되며, 해당 메타데이터를 아예 만들지 못하는 vitest의 esbuild 변환에서도 Nest DI 테스트가 그대로 해석됩니다.

검증되는 웹훅은 원본 request bytes를 보존하고, 모든 store 의존성을 host Nest module에 명시하세요.

다음은 검증된 레시피 두 개입니다.