Next.js에서 NextAuth를 사용하여 인증 시스템을 구축하던 중, 다음과 같은 TypeScript 에러를 만났습니다.
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string; // ❌ 에러!
// Property 'id' does not exist on type
}
return session;
}
session.user에 id 속성을 추가하려고 했지만, TypeScript는 이 속성이 존재하지 않는다고 불평합니다. NextAuth의 기본 타입에는 name, email, image만 있고 id가 없기 때문이죠.
이 문제를 해결하면서 TypeScript의 강력한 기능인 Declaration Merging과 Module Augmentation을 배우게 되었습니다.
Declaration Merging이란?
Declaration Merging은 TypeScript의 독특한 특성으로, 같은 이름의 인터페이스를 여러 번 선언하면 자동으로 합쳐지는 기능입니다.
기본 예시
interface User {
name: string;
}
interface User {
email: string;
}
// 결과: User 타입은 자동으로 병합됩니다
const user: User = {
name: "홍길동",
email: "hong@example.com" // 둘 다 필요!
};
두 개의 User 인터페이스가 하나로 합쳐져서, 최종적으로 name과 email 모두를 가진 타입이 됩니다.
왜 유용한가?
- 점진적 타입 확장: 여러 파일에 걸쳐 타입을 확장할 수 있습니다
- 플러그인 시스템: 라이브러리 사용자가 타입을 확장할 수 있게 합니다
- 코드 분리: 관련 타입을 여러 곳에 나눠서 정의할 수 있습니다
Module Augmentation
Module Augmentation은 외부 라이브러리의 타입을 확장할 수 있게 해주는 기능입니다. 라이브러리 코드를 직접 수정하지 않고도 타입을 추가할 수 있죠.
문법
declare module "라이브러리-이름" {
// 여기서 타입 확장
}
NextAuth 실전 예시
처음에 만난 문제를 해결하기 위해 다음과 같이 작성했습니다.
파일: src/types/next-auth.d.ts
import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
} & DefaultSession["user"];
}
interface User {
id: string;
}
}
declare module "next-auth/jwt" {
interface JWT {
id?: string;
}
}
이렇게 하면:
- next-auth 모듈의 Session 인터페이스에 id가 추가됩니다
- 기존의 name, email, image 속성은 DefaultSession["user"]로 유지됩니다
- User와 JWT 타입에도 id를 추가합니다
결과
// 이제 에러 없이 작동합니다!
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string; // ✅ 정상 작동!
}
return session;
}
Java/Spring과의 비교
Java나 Spring에 익숙하다면, 이것이 상속과 비슷해 보일 수 있습니다. 하지만 중요한 차이가 있습니다.
Java의 상속
// 기존 클래스
public class User {
private String name;
private String email;
}
// 새로운 클래스 생성
public class AdminUser extends User {
private String id; // 새 속성
}
→ 새로운 타입이 만들어집니다. User와 AdminUser는 다른 타입입니다.
TypeScript의 Declaration Merging
// 기존 인터페이스
interface User {
name: string;
email: string;
}
// 같은 인터페이스에 추가
interface User {
id: string;
}
→ 기존 타입 자체가 확장됩니다. 여전히 하나의 User 타입입니다.
비교 표
특성 Java 상속 TypeScript Merging
| 새 타입 생성 | ✅ 예 (AdminUser) | ❌ 아니오 |
| 원본 수정 | ❌ 아니오 | ✅ 예 (User 확장) |
| 타입 이름 | 다름 (User ≠ AdminUser) | 같음 (User) |
| 사용 방법 | 새 타입으로 사용 | 기존 타입으로 사용 |
실전 활용 가이드
언제 사용하면 좋을까?
Module Augmentation이 필요한 경우:
- 라이브러리 타입이 부족할 때
- // Express의 Request에 사용자 정보 추가 declare module "express-serve-static-core" { interface Request { user?: { id: string; email: string; } } }
- 커스텀 속성을 추가할 때
- // Material-UI 테마에 커스텀 색상 추가 declare module "@mui/material/styles" { interface Theme { status: { danger: string; } } }
- 플러그인 타입을 확장할 때
- // Vue Router에 메타 필드 추가 declare module "vue-router" { interface RouteMeta { requiresAuth?: boolean; } }
주의사항
1. .d.ts 파일 위치
타입 선언 파일은 프로젝트 어디든 둘 수 있지만, 관례적으로:
src/
├── types/
│ ├── next-auth.d.ts
│ ├── express.d.ts
│ └── ...
2. import/export가 있으면 Global Scope가 아님
// ❌ 이렇게 하면 Module Augmentation이 작동 안 함
export {}; // export가 있으면 모듈이 됨
declare module "next-auth" {
// ...
}
하지만 import가 필요하다면:
// ✅ 올바른 방법
import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
} & DefaultSession["user"];
}
}
3. tsconfig.json 확인
TypeScript가 .d.ts 파일을 인식하려면:
{
"include": [
"src/**/*"
]
}
4. 재시작 필요
타입 파일을 추가한 후에는:
- TypeScript 서버 재시작 (VS Code: Cmd/Ctrl + Shift + P → "TypeScript: Restart TS Server")
- 개발 서버 재시작
실전 예제: Express + TypeScript
실무에서 자주 사용하는 또 다른 예시를 살펴보겠습니다.
문제 상황
Express에서 미들웨어로 사용자 정보를 req.user에 추가했지만, TypeScript는 이를 인식하지 못합니다.
// middleware.ts
app.use((req, res, next) => {
req.user = { id: "123", email: "user@example.com" };
// ^^^^ Property 'user' does not exist
next();
});
해결 방법
파일: src/types/express.d.ts
declare global {
namespace Express {
interface Request {
user?: {
id: string;
email: string;
role: string;
}
}
}
}
export {}; // 파일을 모듈로 만들기
결과
// 이제 타입 안전하게 사용 가능!
app.use((req, res, next) => {
req.user = {
id: "123",
email: "user@example.com",
role: "admin"
};
next();
});
app.get("/profile", (req, res) => {
if (req.user) {
res.json({
id: req.user.id, // ✅ 자동완성 작동!
email: req.user.email // ✅ 타입 체크 작동!
});
}
});
TypeScript의 Declaration Merging과 Module Augmentation은 처음에는 낯설 수 있지만, 실무에서 매우 유용한 기능입니다.
- Declaration Merging: 같은 이름의 인터페이스는 자동으로 합쳐진다
- Module Augmentation: declare module로 외부 라이브러리 타입을 확장할 수 있다
- 실용성: NextAuth, Express, Vue Router 등 실무에서 자주 사용
- 타입 안전성: 런타임 에러를 컴파일 타임에 잡을 수 있다
추가 학습 자료
'개발 & IT > 프론트엔드' 카테고리의 다른 글
| 웹 빌드 도구 비교: Webpack vs Vite vs Rollup 그리고 etc (0) | 2025.10.26 |
|---|---|
| Vite: 차세대 프론트엔드 개발 도구 (0) | 2025.10.20 |
| Vue3 Carousel에서 화면 줄어들 때 이전 슬라이드가 겹쳐 보이는 문제 해결하기 (2) | 2025.08.18 |
| Vue + Tailwind에서 반응형 메뉴 구현 시 흔히 겪는 문제와 해결법 (0) | 2025.07.15 |
| Capacitor에서 앱 아이콘과 스플래시 이미지 자동 생성하기 (0) | 2025.07.02 |