TypeScript에서 호출 시그니처로 간단한 Mini Day.js 클론 및 Logger Middleware 만들기
Source: Dev.to
위의 링크에 포함된 전체 텍스트를 제공해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다.
(코드 블록, URL, 마크다운 형식 등은 그대로 유지됩니다.)
먼저, 함수가 객체임을 이해하세요
함수 프로토타입의 **typeof가 object**임을 알아야 합니다 — 여기서 말하는 것은 프로토타입이며, 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 호출 시그니처 (빠른 인사이트)
| 클로저 | 호출 시그니처 | |
|---|---|---|
| 상태 처리 | 상태를 캡슐화함 | 상태를 노출함 |
| 변경 가능성 | 변경으로부터 안전함 | 변경 가능 |
| 스타일 | 보다 함수형 | 보다 라이브러리‑스타일 |
| 공통 패턴 | Hooks | SDK 설계 |
둘 다 더 낫다 할 수는 없으며, 설계 목표에 따라 선택하세요.
- 불변성을 원한다면 → 클로저를 선호하세요.
- 구성 가능성을 원한다면 → 호출 시그니처가 빛을 발합니다.
최종 생각
콜 시그니처는 작은 TypeScript 기능이지만, 이를 이해하면 많은 라이브러리에서 이 패턴을 발견하게 됩니다. 이는 JavaScript에 자연스럽게 어울리는 API를 설계하는 강력한 방법을 제공합니다 — 구성 가능하고, 호출 가능하며, 강력하게 타입이 지정된.
당신의 TypeScript 도구 상자에 추가할 가치가 확실히 있습니다 🚀