iBetter Books
수정

Ch 03. 서드파티 라이브러리 타입 다루기

npm에는 수십만 개의 패키지가 있습니다. 그 중 일부는 TypeScript로 작성되어 타입이 내장되어 있고, 일부는 별도의 @types 패키지로 타입을 제공하고, 나머지는 타입이 전혀 없습니다. 각 상황을 올바르게 처리하는 방법을 알아봅니다.

타입이 내장된 라이브러리

TypeScript로 작성되었거나 .d.ts 파일을 함께 배포하는 라이브러리는 별도 작업 없이 바로 사용할 수 있습니다.

npm install zodnpm install axios
// 파일: src/external/built-in-types.tsimport * as z from "zod";   // zod는 타입 내장import axios from "axios";  // axios도 타입 내장// zod 스키마 정의 — 타입 자동 추론const UserSchema = z.object({  id: z.number(),  name: z.string().min(1),  email: z.string().email(),});type User = z.infer<typeof UserSchema>;// axios — 제네릭으로 응답 타입 지정async function fetchUser(id: number): Promise<User> {  const response = await axios.get<User>(`/api/users/${id}`);  return response.data; // response.data는 User 타입}

@types 패키지와 DefinitelyTyped

JavaScript로 작성된 라이브러리의 타입은 DefinitelyTyped 레포지토리에서 @types/패키지명 형태로 제공됩니다.

# lodash는 JS 라이브러리 — 타입은 별도 설치npm install lodashnpm install -D @types/lodash# express도 동일npm install expressnpm install -D @types/express# node.js API 타입npm install -D @types/node
// 파일: src/external/types-package.tsimport _ from "lodash";import express, { Request, Response } from "express";// @types/lodash 덕분에 타입 안전하게 사용const numbers = [1, 2, 3, 4, 5];const doubled = _.map(numbers, (n) => n * 2); // number[]const sum = _.sum(numbers);                   // numberconst grouped = _.groupBy(numbers, (n) => n % 2 === 0 ? "even" : "odd");// { even: number[], odd: number[] }// @types/express 덕분에 타입 안전const app = express();app.get("/users", (req: Request, res: Response) => {  const page = Number(req.query.page) || 1;  res.json({ page, users: [] });});

타입이 없는 라이브러리 처리하기

@types 패키지도 없는 라이브러리를 만났을 때 세 가지 방법이 있습니다.

방법 1: any로 빠르게 처리 (임시방편)

// 파일: src/external/no-types-any.ts// eslint-disable-next-line @typescript-eslint/no-var-requiresconst someLib = require("some-lib") as any;// 또는declare module "some-lib";// 이렇게 하면 import 시 any 타입이 됨

방법 2: 직접 선언 파일 작성 (권장)

// 파일: src/types/some-lib.d.tsdeclare module "some-lib" {  export interface SomeOptions {    timeout?: number;    retries?: number;    onError?: (error: Error) => void;  }  export interface SomeResult {    data: unknown;    statusCode: number;    headers: Record<string, string>;  }  export function connect(url: string, options?: SomeOptions): Promise<void>;  export function request(path: string): Promise<SomeResult>;  export function close(): void;  const defaultExport: {    connect: typeof connect;    request: typeof request;    close: typeof close;  };  export default defaultExport;}

방법 3: 래퍼 모듈 작성

라이브러리를 직접 타이핑하기 어려울 때, 실제로 사용하는 기능만 래핑합니다.

// 파일: src/lib/some-lib-wrapper.ts// eslint-disable-next-line @typescript-eslint/no-var-requiresconst rawLib = require("some-lib");export interface ConnectionOptions {  timeout: number;  retries: number;}export async function connect(url: string, options: ConnectionOptions): Promise<void> {  await rawLib.connect(url, options);}export async function sendData(path: string, data: Record<string, unknown>): Promise<void> {  await rawLib.send(path, JSON.stringify(data));}

라이브러리 타입 확장하기

라이브러리 타입이 있지만 특정 사용 케이스에서 불완전할 때, 선언 병합으로 확장할 수 있습니다.

// 파일: src/types/express-extensions.d.tsimport "express";// Express Request에 커스텀 속성 추가declare module "express" {  interface Request {    user?: {      id: number;      name: string;      role: "admin" | "user";    };    requestId: string;  }}
// 파일: src/middleware/auth.tsimport { Request, Response, NextFunction } from "express";export function authMiddleware(  req: Request,  res: Response,  next: NextFunction): void {  const token = req.headers.authorization;  if (!token) {    res.status(401).json({ error: "인증 토큰이 없습니다" });    return;  }  // req.user는 이제 타입이 정의됨  req.user = { id: 1, name: "Alice", role: "user" };  next();}export function requireAdmin(  req: Request,  res: Response,  next: NextFunction): void {  // req.user가 타입 안전하게 접근 가능  if (req.user?.role !== "admin") {    res.status(403).json({ error: "관리자 권한이 필요합니다" });    return;  }  next();}

타입 버전 불일치 문제

라이브러리 버전과 @types 버전이 맞지 않을 때 생기는 문제입니다.

# 버전을 맞춰 설치npm install [email protected]npm install -D @types/[email protected]# 특정 버전 고정npm install -D @types/node@20  # Node.js 20 버전 타입
// package.json — 버전 범위 지정{  "devDependencies": {    "@types/lodash": "^4.17.0",    "@types/node": "^20.0.0",    "@types/express": "^4.17.0"  }}

skipLibCheck 설정

타입 정의 파일에서 오류가 발생할 때 임시로 건너뛸 수 있습니다.

// tsconfig.json{  "compilerOptions": {    "skipLibCheck": true  // .d.ts 파일의 타입 검사를 건너뜀  }}

skipLibCheck는 편리하지만 타입 오류를 숨길 수 있으므로 장기적으로는 근본 원인을 해결하는 것이 좋습니다.

서드파티 라이브러리 타입을 올바르게 다루는 것은 TypeScript 실전 개발의 중요한 부분입니다. 좋은 타입 정의는 자동 완성과 오류 감지를 가능하게 해서 개발 생산성을 크게 높여줍니다. 타입이 없는 라이브러리는 직접 .d.ts 파일을 작성해 팀 전체가 타입 안전성의 혜택을 누릴 수 있도록 기여하는 것이 좋습니다.