들어가며

Nest.js는 현대 웹 애플리케이션 개발을 위해 설계된 강력한 프레임워크로, 모듈화와 의존성 주입을 통해 코드의 재사용성과 유지보수성을 높인다. 이러한 구조적 장점 덕분에 Nest.js는 복잡한 애플리케이션을 효율적으로 관리하고 확장할 수 있는 기반을 마련해 놓고 있다. 특히, 인터셉터는 요청과 응답을 가로채어 처리할 수 있는 중요한 기능으로, 성능 최적화, 로깅, 데이터 변환 등 다양한 목적으로 활용할 수 있다. 여기에서는 Nest.js의 인터셉터에 대해 살펴보고자 한다.

Nest.js의 중요성과 인터셉터의 역할

Nest.js는 모듈 시스템을 통해 애플리케이션을 구성하고, 각 모듈은 독립적으로 개발 및 테스트할 수 있다. 인터셉터는 요청과 응답을 가로채고 수정하며, 이는 애플리케이션의 전반적인 성능과 사용자 경험을 향상시키는 데 중요한 역할을 한다. 예를 들어, 인터셉터를 사용하여 요청에 대한 인증을 수행하거나, 응답 데이터를 특정 형식으로 변환함으로써 클라이언트의 요구에 맞춘 데이터를 제공할 수 있게 된다.

인터셉터란 무엇인가?

인터셉터는 Nest.js 애플리케이션에서 요청과 응답을 처리하는 과정에서 개입할 수 있는 기능이다. 요청이 컨트롤러에 도달하기 전에, 또는 응답이 클라이언트에게 전송되기 전에 데이터를 수정하거나 추가할 수 있다. 예를 들어, 인증 토큰을 검사하거나, 응답 데이터를 특정 형식으로 변환하는 작업을 수행한다.

인터셉터와 미들웨어의 차이점

미들웨어는 요청이 서버에 도달하기 전에 처리되는 반면, 인터셉터는 요청이 컨트롤러에 도달한 후에 처리된다. 미들웨어는 요청 로그를 기록하거나 인증을 수행하는 것과 같은, 주로 요청의 흐름을 차단하거나 변경하는 데 사용된다. 반면, 인터셉터는 주로 응답을 조작하는 데 중점을 둔다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 미들웨어 예시
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: Function) {
console.log("Request...");
next();
}
}

// 인터셉터 예시
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
return next.handle().pipe(map((data) => ({ data })));
}
}

Nest.js에서 인터셉터 생성하기

인터셉터는 NestInterceptor 인터페이스를 구현하여 생성한다. 기본적인 인터셉터의 예시를 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
console.log(`Incoming request... ${request.url}`);
return next.handle().pipe(tap(() => console.log(`Outgoing response...`)));
}
}

인터셉터의 용도

인터셉터에서 가장 중요한 용도는 intercept, catch, transform이다. intercept 는 요청을 가로채고, catch 는 에러를 처리하며, transform 은 응답 데이터를 변환한다. 특히, catch 는 에러 처리뿐만 아니라 특정 조건에서 데이터를 변환하는 데도 사용될 수 있다.

intercept

intercept 용도로는 요청과 응답을 가로채고 전처리 및 후처리를 수행한다. 다음의 예는 클라이언트의 요청을 로그로 남기고, 응답이 클라이언트로 전송되기 전에 응답 데이터를 로그에 기록한다. 이를 통해 요청과 응답의 흐름을 쉽게 추적할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
console.log(`Request... ${request.method} ${request.url}`);

return next.handle().pipe(
tap((data) => {
console.log(`Response... ${JSON.stringify(data)}`);
})
);
}
}

transform

transform 은 인터셉터에서 응답 데이터를 변환하는 데 사용된다. 이 용도는 주로 클라이언트에게 반환되는 데이터를 특정 형식으로 조정해야 할 때 유용하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";

@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
// 응답 데이터를 변환하여 새로운 형태로 반환
return {
success: true,
data: data,
};
})
);
}
}

위의 예시에서 TransformInterceptor는 응답 데이터를 successdata라는 키를 가진 객체로 변환한다. 이렇게 하면 클라이언트는 일관된 형식의 응답을 받을 수 있다.

catch

catch 는 에러를 처리하고, 특정 조건에서 데이터를 변환하는 데 사용될 수 있다. 일반적으로 이 용도로는 요청 처리 과정에서 발생한 에러를 잡아내고, 이를 적절한 형식으로 반환하는 데 중점을 둔다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from "@nestjs/common";
import { Observable, throwError } from "rxjs";
import { catchError } from "rxjs/operators";

@Injectable()
export class ErrorHandlingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((err) => {
// 에러를 잡아 특정 형식으로 변환하여 반환
return throwError({
statusCode: err.status || 500,
message: err.message || "Internal server error",
});
})
);
}
}

이 예시에서는 ErrorHandlingInterceptor가 요청 처리 중 발생한 에러를 잡아내고, 에러의 상태 코드와 메시지를 포함하는 객체 형태로 변환하여 클라이언트에 반환한다. 이를 통해 클라이언트는 에러를 일관된 형식으로 수신할 수 있다.

인터셉터 적용하기

인터셉터는 @UseInterceptors() 데코레이터를 사용하여 적용할 수 있다. 여러 인터셉터를 동시에 적용할 수도 있으며, 이때 우선순위를 설정할 수 있다.

1
2
3
@Controller("cats")
@UseInterceptors(LoggingInterceptor, TransformInterceptor)
export class CatsController {}

RxJS와 인터셉터의 관계

Nest.js는 RxJS를 기반으로 하여 비동기 프로그래밍을 지원한다. 인터셉터 내에서 RxJS의 연산자를 사용하여 데이터 흐름을 제어할 수 있다. 다음 예시에서는 map 연산자를 사용하여 응답 데이터를 변환하는 방법을 보여준다.

1
2
3
4
5
6
return next.handle().pipe(
map((data) => {
// 변환 로직
return data;
})
);

마무리

인터셉터는 Nest.js 애플리케이션에서 요청과 응답을 제어하고 수정하는 중요한 도구이다. 이를 통해 애플리케이션의 유지보수성과 확장성을 높일 수 있으며, 성능 최적화와 사용자 경험 향상을 동시에 이룰 수 있다. 인터셉터의 활용은 애플리케이션 개발에 있어 필수적인 요소로 자리 잡고 있으며, 이를 통해 더욱 효율적이고 안정적인 웹 애플리케이션을 구축할 수 있다.