TypeScript에서 호출 시그니처로 간단한 Mini Day.js 클론 및 Logger Middleware 만들기

발행: (2026년 2월 4일 오후 02:15 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

위의 링크에 포함된 전체 텍스트를 제공해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다.
(코드 블록, URL, 마크다운 형식 등은 그대로 유지됩니다.)

먼저, 함수가 객체임을 이해하세요

함수 프로토타입의 **typeofobject**임을 알아야 합니다 — 여기서 말하는 것은 프로토타입이며, typeof function 자체가 아니라는 점을 강조합니다.

프로토타입에 익숙하지 않다면, 계속 진행하기 전에 먼저 프로토타입에 대해 읽어볼 것을 강력히 권합니다.

우리가 해결하고자 하는 문제

함수를 구성하고 그 구성으로 실행하려면 어떻게 해야 할까요?

이는 클로저와 다소 비슷하지만, 클로저는 변수 스코프를 사용해 상태를 캡슐화합니다. 호출 시그니처를 사용하면 상태가 외부에 노출되어 필요에 따라 속성을 변경할 수 있습니다.

이 패턴은 dayjs와 같은 라이브러리나 로거 미들웨어를 구현할 때 마주칠 수 있습니다. TypeScript에서 이를 올바르게 정의하려면 어떻게 해야 할까요?

TypeScript의 호출 서명

JavaScript에서는 함수가 호출 가능할 뿐만 아니라 속성을 가질 수 있습니다. 하지만 일반적인 함수 타입 구문으로는 속성을 선언할 수 없습니다. 호출 가능하면서 속성을 가진 무언가를 설명하고 싶다면 객체 타입 안에 호출 서명을 사용할 수 있습니다.

호출 서명은 클로저와 비슷하지만, 핵심 차이점은 상태가 함수 외부에 존재한다는 점이며, 따라서 수정이 가능합니다.

예제 1 — Mini Day.js 클론

type DayJS = {
  timezone: string;
  description: string;
  (date: string | number | Date): string;
};

const dayjs = function (date: string | number | Date) {
  const tz = dayjs.timezone;

  const formatter = new Intl.DateTimeFormat("en-US", {
    timeZone: tz,
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
  });

  return formatter.format(new Date(date));
} as DayJS;

dayjs.timezone = "Asia/Ho_Chi_Minh";
dayjs.description = "Mini DayJS clone";

console.log(dayjs("2026-02-04"));
// 02/04/2026, 07:00:00 AM

여기서 dayjs는 다음 두 가지 역할을 합니다:

  • 함수처럼 호출 가능
  • 속성을 통해 설정 가능

이것이 바로 호출 서명이 가능하게 하는 기능입니다.

예제 2 — 로거 미들웨어

이제 다음과 같은 설정 가능한 속성을 가진 로거 미들웨어를 구현해 보겠습니다:

  • isEnabled (프로덕션 환경 여부에 따라)
  • 로그 타입 (warning, error, info)
  • 메시지 페이로드
type LoggerData = { message: string; metadata?: unknown };

type LoggerType = "warning" | "error" | "info";

type LoggerMiddleware = {
  isEnabled: boolean;
  type: LoggerType;
  (data: LoggerData): void;
};

const initializedSomeService = () => {
  const warningLogger: LoggerMiddleware = function (data) {
    if (!warningLogger.isEnabled) return;

    console.warn(`TYPE:${warningLogger.type}`, { data });
  };

  // 속성 설정
  warningLogger.isEnabled = process.env.NODE_ENV === "production";
  warningLogger.type = "warning";

  // 함수처럼 실행
  warningLogger({
    message: "This is a warning message",
    metadata: { id: "ab85592d-b7cf-4623-9884-aa70f40814f7" },
  });
};

initializedSomeService();

결과:

TYPE:warning {
  data: {
    message: 'This is a warning message',
    metadata: { id: 'ab85592d-b7cf-4623-9884-aa70f40814f7' }
  }
}

언제 호출 시그니처를 사용해야 할까요?

Use them when you want something that is:

  • ✅ 함수처럼 호출 가능
  • ✅ 속성을 통해 구성 가능
  • ✅ 강력한 타입 지정

Typical real‑world use cases:

  • 로거 유틸리티
  • 분석 트래커
  • 피처 플래그 실행기
  • SDK 스타일 API (예: dayjs와 같은 라이브러리)

클로저 vs 호출 시그니처 (빠른 인사이트)

클로저호출 시그니처
상태 처리상태를 캡슐화함상태를 노출함
변경 가능성변경으로부터 안전함변경 가능
스타일보다 함수형보다 라이브러리‑스타일
공통 패턴HooksSDK 설계

둘 다 더 낫다 할 수는 없으며, 설계 목표에 따라 선택하세요.

  • 불변성을 원한다면 → 클로저를 선호하세요.
  • 구성 가능성을 원한다면 → 호출 시그니처가 빛을 발합니다.

최종 생각

콜 시그니처는 작은 TypeScript 기능이지만, 이를 이해하면 많은 라이브러리에서 이 패턴을 발견하게 됩니다. 이는 JavaScript에 자연스럽게 어울리는 API를 설계하는 강력한 방법을 제공합니다 — 구성 가능하고, 호출 가능하며, 강력하게 타입이 지정된.

당신의 TypeScript 도구 상자에 추가할 가치가 확실히 있습니다 🚀

Back to Blog

관련 글

더 보기 »

TypeScript 또는 눈물

프론트엔드 품질 게이트 또 보기: 백엔드 품질 게이트 백엔드 린터는 async footguns를 잡아냅니다. Type checkers는 runtime explosions를 방지합니다. 이제는 프론트엔드의…

Deno 샌드박스

번역하려는 텍스트를 제공해 주시겠어요? 텍스트를 알려주시면 한국어로 번역해 드리겠습니다.