본문으로 건너뛰기
개발 머꼬
개발 노트JavaScript
hohyeon.dev27

Express Request에 커스텀 필드를 추가했는데 타입 에러가 난 이유

  • #Engineering Note
  • #TypeScript

문제 발생

인증 미들웨어에서 req.user에 로그인한 사용자 정보를 담아두고, 이후 핸들러에서 req.user를 읽으려고 했는데 TypeScript가 "Request 타입에 user 속성이 없다"는 에러를 냈습니다.

app.use((req, res, next) => {
  req.user = getUserFromToken(req); // 에러: user 속성이 Request에 없음
  next();
});

원인 분석

Express의 Request 타입 정의(@types/express가 제공)에는 애초에 user라는 필드가 없습니다. 미들웨어가 런타임에 req.user = ...로 실제 객체에 필드를 추가하는 것은 JavaScript 입장에서는 아무 문제 없이 동작하지만, TypeScript는 여전히 원본 라이브러리가 선언한 타입 정의만 보고 검사하므로 user라는 필드가 있다는 사실을 전혀 모릅니다.

해결 방안

TypeScript는 이런 상황을 위해 **모듈 보강(module augmentation)**과 **선언 병합(declaration merging)**을 지원합니다 — 이미 존재하는 모듈의 인터페이스에 새 필드를 "추가로 선언"하면, TypeScript가 원본 정의와 자동으로 합쳐줍니다.

// types/express.d.ts
import "express";

declare module "express" {
  interface Request {
    user?: { id: string; role: string };
  }
}

이 파일이 프로젝트에 포함되어 있으면(보통 tsconfig.jsoninclude에 잡히는 위치), Request 인터페이스는 원본 필드에 user가 추가된 형태로 확장됩니다 — 원본 @types/express 패키지 파일을 직접 수정하지 않고도, 프로젝트 전역에서 req.user를 타입 에러 없이 쓸 수 있습니다.

  1. import "express"; 한 줄이 중요합니다 — 이게 없으면 TypeScript가 이 파일을 "완전히 새로운 독립 모듈 선언"으로 취급해 원본 express 모듈과 병합하지 않고 별개의 것으로 처리할 수 있습니다. 기존 모듈을 보강하려는 의도라면 그 모듈을 최소 한 번은 import(또는 export)해서 파일이 모듈임을 명시해야 합니다.
  2. interface는 같은 이름으로 여러 번 선언하면 자동으로 병합되지만(type은 그렇지 않습니다) — 이게 모듈 보강이 인터페이스 확장에서만 자연스럽게 동작하는 이유입니다.
  3. 필드를 user?: ...처럼 옵셔널로 선언하는 것이 안전합니다 — 인증 미들웨어를 거치지 않은 요청에서는 실제로 user가 없을 수 있으므로, 필수 필드로 선언하면 오히려 다른 곳에서 잘못된 타입 안전성을 주게 됩니다.
  4. Next.js, NextAuth 등 많은 라이브러리가 세션 타입을 확장하는 데 정확히 같은 패턴(공식 문서에 next-auth.d.ts 같은 예시로 제공)을 씁니다 — 이 패턴을 한 번 이해하면 여러 라이브러리에서 반복적으로 활용할 수 있습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.