<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>NestJS &amp;mdash; The Heartade Space</title>
    <link>https://blog.heartade.dev/tag:NestJS</link>
    <description>life as a continuous loop of building and breaking</description>
    <pubDate>Sun, 03 May 2026 12:39:49 +0000</pubDate>
    <item>
      <title>NestJS에서 응답 객체에 Class Transform 적용하기</title>
      <link>https://blog.heartade.dev/nestjseseo-eungdab-gaegcee-class-transform-jeogyonghagi</link>
      <description>&lt;![CDATA[표지 사진&#xA;&#xA;오늘의 #뚝딱뚝딱 도 #NestJS 이야기입니다.&#xA;&#xA;DB에서 사용자 정보를 가져와서 전송하는 API가 필요하다고 생각해 보세요. return await this.userRepository.findOne({where: {id: userId}}); 정도를 작성하고 커밋을 누르기만 하면 된다면 얼마나 좋을까요?&#xA;&#xA;!--more--&#xA;&#xA;하지만 그렇게 했다가는 응답 객체에 들어가면 안 되는 내용까지 들어가게 되겠죠.&#xA;&#xA;{&#xA;  &#34;id&#34;: &#34;heartade&#34;,&#xA;  &#34;name&#34;: &#34;Dani&#34;,&#xA;  &#34;email&#34;: &#34;soohan@heartade.dev&#34;,&#xA;  &#34;secret&#34;: &#34;진짜진짜비밀&#34;&#xA;}&#xA;&#xA;물론 return await this.userRepository.findOne({where: {id: userId}}) as PickUser, &#34;id&#34;|&#34;name&#34;|&#34;email&#34;; 도 소용이 없습니다. 타입스크립트의 타입 체크는 컴파일 타임에만 동작하니까요.&#xA;&#xA;이런 상황을 예방하는 가장 직관적인 방법은 아래와 같은 모양일 거예요.&#xA;&#xA;// UserResponseDto extends PickType(User, [&#34;id&#34;, &#34;name&#34;, &#34;email&#34;])&#xA;async getUser(userId: string): PromiseUserResponseDto {&#xA;  const user = await this.userRepository.findOne({where: {id: userId}});&#xA;  // 클라이언트에게 보여줘도 되는 정보만 한땀한땀 골라서 전달&#xA;  // 코드에 다른 필드를 넣으면 &#34;Object literal may only specify known properties&#34; 오류 발생&#xA;  return {id: user.id, name: user.name, email: user.email};&#xA;}&#xA;&#xA;하지만 응답에 들어가는 필드가 많아질수록 이런 방식은 귀찮아집니다. 심지어 Object literal을 반환하는 게 아니라면 추가로 들어간 필드가 있어도 타입 체크에 걸리지도 않죠. 이왕 UserResponseDto를 정의한 김에 그냥 알아서 반환값을 끼워맞춰 주는, 좀 더 예쁘고 깔끔한 방법은 없는 걸까요?&#xA;&#xA;그래서 class-transformer를 사용해 보기로 했습니다.&#xA;&#xA;class-transformer를 적용하는 인터셉터 구현&#xA;&#xA;NestInterceptor에서 class-transformer를 사용하면 컨트롤러가 응답을 반환하기 전에 응답 객체를 특정한 타입으로 변환하도록 할 수 있습니다. 그리고 우리가 클라이언트에게 보낼 필드만 담긴 타입을 정의해서 인터셉터 생성자의 인수로 주면 되는 거죠.&#xA;&#xA;// response.interceptor.ts&#xA;import {&#xA;  CallHandler,&#xA;  ExecutionContext,&#xA;  Injectable,&#xA;  InternalServerErrorException,&#xA;  NestInterceptor,&#xA;} from &#39;@nestjs/common&#39;&#xA;import { instanceToPlain, plainToInstance } from &#39;class-transformer&#39;&#xA;import { validate } from &#39;class-validator&#39;&#xA;import { switchMap } from &#39;rxjs&#39;&#xA;&#xA;@Injectable()&#xA;export class ResponseInterceptorT extends object&#xA;  implements NestInterceptorany, T&#xA;{&#xA;  constructor(private readonly dto: new () =  T) {}&#xA;&#xA;  intercept(context: ExecutionContext, next: CallHandler) {&#xA;    // 컨트롤러가 handle된 뒤에 pipe를 실행합니다.&#xA;    return next.handle().pipe(&#xA;      switchMap(async (data) =  {&#xA;        // instanceToPlain(data)를 실행해서 plain object로 변환한 다음,&#xA;        // plainToInstance(this.dto, ...)를 실행해서 plain object로 변환된 data를 dto 타입으로 변환합니다.&#xA;        const transformedData = plainToInstance(this.dto, instanceToPlain(data))&#xA;        // class-validator로 이상한 값이 없는지 점검도 한 번 해 줍니다.&#xA;        const errors = await validate(transformedData)&#xA;        if (errors.length   0) {&#xA;          throw new InternalServerErrorException(&#xA;            &#34;RESPONSE VALIDATION FAILED&#34;,&#xA;          )&#xA;        }&#xA;        return transformedData&#xA;      }),&#xA;    )&#xA;  }&#xA;}&#xA;&#xA;이제 클라이언트에게 보여줄 사용자 정보가 담긴 UserResponseDto를 정의하고...&#xA;&#xA;// user-response.dto.ts&#xA;import { PickType } from &#39;@nestjs/swagger&#39;&#xA;import { User } from &#39;src/entities/user.entity&#39;&#xA;&#xA;export class UserResponseDto extends PickType(User, [&#xA;  &#39;id&#39;,&#xA;  &#39;name&#39;,&#xA;  &#39;email&#39;,&#xA;] as const) {}&#xA;&#xA;컨트롤러에 인터셉터를 적용하고...&#xA;&#xA;// user.controller.ts&#xA;// ...&#xA;export class UserController {&#xA;  // ...&#xA;  @Get(&#39;:id&#39;)&#xA;  @UseInterceptors(new ResponseInterceptor(UserResponseDto))&#xA;  async getUser(@Param(&#39;id&#39;) userId: string) {&#xA;    await this.userService.getUser(userId);&#xA;  }&#xA;}&#xA;&#xA;실행해 보면... 아까랑 같은 결과가 나옵니다. 왠지 여전히 secret 필드를 넣어서 보내 주고 있는 거죠.&#xA;&#xA;@Expose() 사용하기&#xA;&#xA;이런 문제가 생기는 원인은 plainToInstance가 변환 대상이 되는 클래스에 없는 필드가 plain object에 있다고 해서 삭제하지는 않기 때문입니다.&#xA;&#xA;특정 필드를 변환 과정에서 삭제하려면 그 필드를 exclude해야 합니다. class-transformer는 기본적으로 클래스나 필드에 달 수 있는 @Exclude()와 @Expose() 데코레이터를 제공하고, plainToInstance의 설정 기본값은 @Exclude()가 붙어 있지 않은 모든 필드를 노출하는 거죠.&#xA;&#xA;이 기본 설정을 그대로 둔 상태로 User 클래스의 secret에 @Exclude()를 적용하는 방법도 있겠지만, secret을 반환해야 하는 API를 따로 만들 일이 있을지도 모르니 그리 마음에 들지 않습니다. 그러니 plainToInstance의 strategy를 기본값인 exposeAll 대신 excludeAll로 바꿔 주고, 각 DTO 클래스에서 어떤 필드를 클라이언트에게 노출할지 @Expose()로 알려 주는 게 좋겠어요.&#xA;&#xA;// response.interceptor.ts&#xA;// ...&#xA;@Injectable()&#xA;export class ResponseInterceptorT extends object&#xA;  implements NestInterceptorany, T&#xA;{&#xA;  constructor(private readonly dto: new () =  T) {}&#xA;&#xA;  intercept(context: ExecutionContext, next: CallHandler) {&#xA;    // ...&#xA;        const transformedData = plainToInstance(&#xA;          this.dto,&#xA;          instanceToPlain(data),&#xA;          {&#xA;            // 변환 시 @Expose()가 붙어 있지 않은 모든 필드를 삭제합니다.&#xA;            strategy: &#39;excludeAll&#39;,&#xA;            // nested 객체도 변환을 적용합니다.&#xA;            enableImplicitConversion: true,&#xA;          },&#xA;        )&#xA;// ...&#xA;&#xA;// user-response.dto.ts&#xA;import { PickType } from &#39;@nestjs/swagger&#39;&#xA;import { Expose } from &#39;class-transformer&#39;&#xA;import { User } from &#39;src/entities/user.entity&#39;&#xA;&#xA;export class UserResponseDto extends PickType(User, [&#xA;  &#39;id&#39;,&#xA;  &#39;name&#39;,&#xA;  &#39;email&#39;,&#xA;] as const) {&#xA;  @Expose()&#xA;  id: string;&#xA;  @Expose()&#xA;  name: string;&#xA;  @Expose()&#xA;  email: string;&#xA;}&#xA;&#xA;여기까지 하고 나니 계획대로 userRepository에서 반환된 User 객체를 컨트롤러에서 그대로 반환해도 클라이언트에는 id, name, email만 전송되게 되었습니다. 하지만 이번엔 UserResponseDto가 너무 길어져서 처음이 더 나을 지경이네요.&#xA;&#xA;그래서 정해진 필드 몇 개만 모아서 @Expose()할 수 있는 데코레이터를 하나 더 만들었습니다.&#xA;&#xA;@ExposeFields() 데코레이터&#xA;&#xA;간단하게 필드 이름의 목록을 인수로 넣으면 그 필드에 @Expose()를 붙여 주는 데코레이터입니다.&#xA;&#xA;// expose-fields.decorator.ts&#xA;import { Expose } from &#39;class-transformer&#39;&#xA;&#xA;export default function ExposeFields&lt;&#xA;  T extends object,&#xA;  // 필드 이름이 아닌 값을 넣지 못하도록 keyof T를 extend해 줍니다.&#xA;  // 단순히 keyof T를 사용하면 숫자 인덱스 접근자도 포함하는데,&#xA;  // PropertyDecorator는 (target: Object, propertyKey: string | symbol) =  void로 정의되기 때문에&#xA;  // 필드에 Expose()를 적용하는 과정에서 타입이 충돌합니다.&#xA;  K extends keyof T &amp; (string | symbol),&#xA;  (fields: readonly K[]) {&#xA;  return function (constructor: new (...args: any[]) =  T) {&#xA;    fields.forEach((field) =  {&#xA;      Expose()(constructor.prototype, field)&#xA;    })&#xA;  }&#xA;}&#xA;&#xA;그러면 이제 UserResponseDto를 훨씬 간결하게 만들 수 있죠.&#xA;&#xA;// user-response.dto.ts&#xA;import { PickType } from &#39;@nestjs/swagger&#39;&#xA;import { ExposeFields } from &#39;src/decorators/expose-fields.decorator&#39;&#xA;import { User } from &#39;src/entities/user.entity&#39;&#xA;&#xA;@ExposeFields([&#39;id&#39;, &#39;name&#39;, &#39;email&#39;] as const)&#xA;export class UserResponseDto extends PickType(User, [&#xA;  &#39;id&#39;,&#xA;  &#39;name&#39;,&#xA;  &#39;email&#39;,&#xA;] as const) {}&#xA;&#xA;사실 이제는 PickType을 쓰지 않고 바로 User를 extend해도 알아서 id, name, field만 걸러져서 응답 데이터로 전송됩니다! 이젠 DTO에 @ExposeFields를 달고 요청 핸들러에 @UseInterceptors를 달기만 하면 서비스 구현체에서 User를 바로 반환하더라도 문제없어요. 다만 그렇게 하면 Swagger 문서에 나오는 필드와 실제 반환되는 필드가 서로 달라질테니 저는 PickType을 그대로 두기로 했습니다.&#xA;&#xA;// user-response.dto.ts&#xA;import { PickType } from &#39;@nestjs/swagger&#39;&#xA;import { ExposeFields } from &#39;src/decorators/expose-fields.decorator&#39;&#xA;import { User } from &#39;src/entities/user.entity&#39;&#xA;&#xA;@ExposeFields([&#39;id&#39;, &#39;name&#39;, &#39;email&#39;] as const)&#xA;export class UserResponseDto extends User {}&#xA;&#xA;!--more--&#xD;&#xA;---&#xD;&#xA;Dani Soohan Park (@heartade)&#xD;&#xA;&#xD;&#xA;Follow this blog at Fediverse:&#xD;&#xA;@heartade@blog.heartade.dev&#xD;&#xA;&#xD;&#xA;Follow my shorter shoutouts at Fediverse:&#xD;&#xA;@heartade@social.silicon.moe&#xD;&#xA;&#xD;&#xA;Follow me at Bluesky:&#xD;&#xA;@heartade.dev]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://blog.heartade.dev/img/posts/nestjseseo-eungdab-gaegcee-class-transform-jeogyonghagi.png" alt="표지 사진"></p>

<p>오늘의 <a href="https://blog.heartade.dev/tag:%EB%9A%9D%EB%94%B1%EB%9A%9D%EB%94%B1" class="hashtag"><span>#</span><span class="p-category">뚝딱뚝딱</span></a> 도 <a href="https://blog.heartade.dev/tag:NestJS" class="hashtag"><span>#</span><span class="p-category">NestJS</span></a> 이야기입니다.</p>

<p>DB에서 사용자 정보를 가져와서 전송하는 API가 필요하다고 생각해 보세요. <code>return await this.userRepository.findOne({where: {id: userId}});</code> 정도를 작성하고 커밋을 누르기만 하면 된다면 얼마나 좋을까요?</p>



<p>하지만 그렇게 했다가는 응답 객체에 들어가면 안 되는 내용까지 들어가게 되겠죠.</p>

<pre><code class="language-json">{
  &#34;id&#34;: &#34;heartade&#34;,
  &#34;name&#34;: &#34;Dani&#34;,
  &#34;email&#34;: &#34;soohan@heartade.dev&#34;,
  &#34;secret&#34;: &#34;진짜진짜비밀&#34;
}
</code></pre>

<p>물론 <code>return await this.userRepository.findOne({where: {id: userId}}) as Pick&lt;User, &#34;id&#34;|&#34;name&#34;|&#34;email&#34;&gt;;</code> 도 소용이 없습니다. 타입스크립트의 타입 체크는 컴파일 타임에만 동작하니까요.</p>

<p>이런 상황을 예방하는 가장 직관적인 방법은 아래와 같은 모양일 거예요.</p>

<pre><code class="language-typescript">// UserResponseDto extends PickType(User, [&#34;id&#34;, &#34;name&#34;, &#34;email&#34;])
async getUser(userId: string): Promise&lt;UserResponseDto&gt; {
  const user = await this.userRepository.findOne({where: {id: userId}});
  // 클라이언트에게 보여줘도 되는 정보만 한땀한땀 골라서 전달
  // 코드에 다른 필드를 넣으면 &#34;Object literal may only specify known properties&#34; 오류 발생
  return {id: user.id, name: user.name, email: user.email};
}
</code></pre>

<p>하지만 응답에 들어가는 필드가 많아질수록 이런 방식은 귀찮아집니다. 심지어 Object literal을 반환하는 게 아니라면 추가로 들어간 필드가 있어도 타입 체크에 걸리지도 않죠. 이왕 <code>UserResponseDto</code>를 정의한 김에 그냥 알아서 반환값을 끼워맞춰 주는, 좀 더 예쁘고 깔끔한 방법은 없는 걸까요?</p>

<p>그래서 <code>class-transformer</code>를 사용해 보기로 했습니다.</p>

<h2 id="class-transformer-를-적용하는-인터셉터-구현" id="class-transformer-를-적용하는-인터셉터-구현"><code>class-transformer</code>를 적용하는 인터셉터 구현</h2>

<p><code>NestInterceptor</code>에서 <code>class-transformer</code>를 사용하면 컨트롤러가 응답을 반환하기 전에 응답 객체를 특정한 타입으로 변환하도록 할 수 있습니다. 그리고 우리가 클라이언트에게 보낼 필드만 담긴 타입을 정의해서 인터셉터 생성자의 인수로 주면 되는 거죠.</p>

<pre><code class="language-typescript">// response.interceptor.ts
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  InternalServerErrorException,
  NestInterceptor,
} from &#39;@nestjs/common&#39;
import { instanceToPlain, plainToInstance } from &#39;class-transformer&#39;
import { validate } from &#39;class-validator&#39;
import { switchMap } from &#39;rxjs&#39;

@Injectable()
export class ResponseInterceptor&lt;T extends object&gt;
  implements NestInterceptor&lt;any, T&gt;
{
  constructor(private readonly dto: new () =&gt; T) {}

  intercept(context: ExecutionContext, next: CallHandler) {
    // 컨트롤러가 handle된 뒤에 pipe를 실행합니다.
    return next.handle().pipe(
      switchMap(async (data) =&gt; {
        // instanceToPlain(data)를 실행해서 plain object로 변환한 다음,
        // plainToInstance(this.dto, ...)를 실행해서 plain object로 변환된 data를 dto 타입으로 변환합니다.
        const transformedData = plainToInstance(this.dto, instanceToPlain(data))
        // class-validator로 이상한 값이 없는지 점검도 한 번 해 줍니다.
        const errors = await validate(transformedData)
        if (errors.length &gt; 0) {
          throw new InternalServerErrorException(
            &#34;RESPONSE VALIDATION FAILED&#34;,
          )
        }
        return transformedData
      }),
    )
  }
}
</code></pre>

<p>이제 클라이언트에게 보여줄 사용자 정보가 담긴 <code>UserResponseDto</code>를 정의하고...</p>

<pre><code class="language-typescript">// user-response.dto.ts
import { PickType } from &#39;@nestjs/swagger&#39;
import { User } from &#39;src/entities/user.entity&#39;

export class UserResponseDto extends PickType(User, [
  &#39;id&#39;,
  &#39;name&#39;,
  &#39;email&#39;,
] as const) {}
</code></pre>

<p>컨트롤러에 인터셉터를 적용하고...</p>

<pre><code class="language-typescript">// user.controller.ts
// ...
export class UserController {
  // ...
  @Get(&#39;:id&#39;)
  @UseInterceptors(new ResponseInterceptor(UserResponseDto))
  async getUser(@Param(&#39;id&#39;) userId: string) {
    await this.userService.getUser(userId);
  }
}
</code></pre>

<p>실행해 보면... 아까랑 같은 결과가 나옵니다. 왠지 여전히 <code>secret</code> 필드를 넣어서 보내 주고 있는 거죠.</p>

<h2 id="expose-사용하기" id="expose-사용하기"><code>@Expose()</code> 사용하기</h2>

<p>이런 문제가 생기는 원인은 <code>plainToInstance</code>가 변환 대상이 되는 클래스에 없는 필드가 plain object에 있다고 해서 삭제하지는 않기 때문입니다.</p>

<p>특정 필드를 변환 과정에서 삭제하려면 그 필드를 <code>exclude</code>해야 합니다. <code>class-transformer</code>는 기본적으로 클래스나 필드에 달 수 있는 <code>@Exclude()</code>와 <code>@Expose()</code> 데코레이터를 제공하고, <code>plainToInstance</code>의 설정 기본값은 <code>@Exclude()</code>가 붙어 있지 않은 모든 필드를 노출하는 거죠.</p>

<p>이 기본 설정을 그대로 둔 상태로 <code>User</code> 클래스의 <code>secret</code>에 <code>@Exclude()</code>를 적용하는 방법도 있겠지만, <code>secret</code>을 반환해야 하는 API를 따로 만들 일이 있을지도 모르니 그리 마음에 들지 않습니다. 그러니 <code>plainToInstance</code>의 <code>strategy</code>를 기본값인 <code>exposeAll</code> 대신 <code>excludeAll</code>로 바꿔 주고, 각 DTO 클래스에서 어떤 필드를 클라이언트에게 노출할지 <code>@Expose()</code>로 알려 주는 게 좋겠어요.</p>

<pre><code class="language-typescript">// response.interceptor.ts
// ...
@Injectable()
export class ResponseInterceptor&lt;T extends object&gt;
  implements NestInterceptor&lt;any, T&gt;
{
  constructor(private readonly dto: new () =&gt; T) {}

  intercept(context: ExecutionContext, next: CallHandler) {
    // ...
        const transformedData = plainToInstance(
          this.dto,
          instanceToPlain(data),
          {
            // 변환 시 @Expose()가 붙어 있지 않은 모든 필드를 삭제합니다.
            strategy: &#39;excludeAll&#39;,
            // nested 객체도 변환을 적용합니다.
            enableImplicitConversion: true,
          },
        )
// ...
</code></pre>

<pre><code class="language-typescript">// user-response.dto.ts
import { PickType } from &#39;@nestjs/swagger&#39;
import { Expose } from &#39;class-transformer&#39;
import { User } from &#39;src/entities/user.entity&#39;

export class UserResponseDto extends PickType(User, [
  &#39;id&#39;,
  &#39;name&#39;,
  &#39;email&#39;,
] as const) {
  @Expose()
  id: string;
  @Expose()
  name: string;
  @Expose()
  email: string;
}
</code></pre>

<p>여기까지 하고 나니 계획대로 <code>userRepository</code>에서 반환된 <code>User</code> 객체를 컨트롤러에서 그대로 반환해도 클라이언트에는 <code>id</code>, <code>name</code>, <code>email</code>만 전송되게 되었습니다. 하지만 이번엔 <code>UserResponseDto</code>가 너무 길어져서 처음이 더 나을 지경이네요.</p>

<p>그래서 정해진 필드 몇 개만 모아서 <code>@Expose()</code>할 수 있는 데코레이터를 하나 더 만들었습니다.</p>

<h2 id="exposefields-데코레이터" id="exposefields-데코레이터"><code>@ExposeFields()</code> 데코레이터</h2>

<p>간단하게 필드 이름의 목록을 인수로 넣으면 그 필드에 <code>@Expose()</code>를 붙여 주는 데코레이터입니다.</p>

<pre><code>// expose-fields.decorator.ts
import { Expose } from &#39;class-transformer&#39;

export default function ExposeFields&lt;
  T extends object,
  // 필드 이름이 아닌 값을 넣지 못하도록 keyof T를 extend해 줍니다.
  // 단순히 keyof T를 사용하면 숫자 인덱스 접근자도 포함하는데,
  // PropertyDecorator는 (target: Object, propertyKey: string | symbol) =&gt; void로 정의되기 때문에
  // 필드에 Expose()를 적용하는 과정에서 타입이 충돌합니다.
  K extends keyof T &amp; (string | symbol),
&gt;(fields: readonly K[]) {
  return function (constructor: new (...args: any[]) =&gt; T) {
    fields.forEach((field) =&gt; {
      Expose()(constructor.prototype, field)
    })
  }
}
</code></pre>

<p>그러면 이제 <code>UserResponseDto</code>를 훨씬 간결하게 만들 수 있죠.</p>

<pre><code class="language-typescript">// user-response.dto.ts
import { PickType } from &#39;@nestjs/swagger&#39;
import { ExposeFields } from &#39;src/decorators/expose-fields.decorator&#39;
import { User } from &#39;src/entities/user.entity&#39;

@ExposeFields([&#39;id&#39;, &#39;name&#39;, &#39;email&#39;] as const)
export class UserResponseDto extends PickType(User, [
  &#39;id&#39;,
  &#39;name&#39;,
  &#39;email&#39;,
] as const) {}
</code></pre>

<p>사실 이제는 <code>PickType</code>을 쓰지 않고 바로 <code>User</code>를 extend해도 알아서 <code>id, name, field</code>만 걸러져서 응답 데이터로 전송됩니다! 이젠 DTO에 <code>@ExposeFields</code>를 달고 요청 핸들러에 <code>@UseInterceptors</code>를 달기만 하면 서비스 구현체에서 <code>User</code>를 바로 반환하더라도 문제없어요. 다만 그렇게 하면 Swagger 문서에 나오는 필드와 실제 반환되는 필드가 서로 달라질테니 저는 <code>PickType</code>을 그대로 두기로 했습니다.</p>

<pre><code class="language-typescript">// user-response.dto.ts
import { PickType } from &#39;@nestjs/swagger&#39;
import { ExposeFields } from &#39;src/decorators/expose-fields.decorator&#39;
import { User } from &#39;src/entities/user.entity&#39;

@ExposeFields([&#39;id&#39;, &#39;name&#39;, &#39;email&#39;] as const)
export class UserResponseDto extends User {}
</code></pre>



<hr>

<h3 id="dani-soohan-park-heartade" id="dani-soohan-park-heartade">Dani Soohan Park (@heartade)</h3>

<p>Follow this blog at Fediverse:
<a href="https://blog.heartade.dev/heartade"><a href="https://blog.heartade.dev/@/heartade@blog.heartade.dev" class="u-url mention">@<span>heartade@blog.heartade.dev</span></a></a></p>

<p>Follow my shorter shoutouts at Fediverse:
<a href="https://social.silicon.moe/@heartade"><a href="https://blog.heartade.dev/@/heartade@social.silicon.moe" class="u-url mention">@<span>heartade@social.silicon.moe</span></a></a></p>

<p>Follow me at Bluesky:
<a href="https://bsky.app/profile/did:plc:e4a32z23pazq5dxnucj6wpee">@heartade.dev</a></p>
]]></content:encoded>
      <guid>https://blog.heartade.dev/nestjseseo-eungdab-gaegcee-class-transform-jeogyonghagi</guid>
      <pubDate>Fri, 21 Feb 2025 21:35:57 +0000</pubDate>
    </item>
    <item>
      <title>NestJS: typeerror: do not know how to serialize a bigint</title>
      <link>https://blog.heartade.dev/nestjs-typeerror-do-not-know-how-to-serialize-a-bigint</link>
      <description>&lt;![CDATA[표지 사진&#xA;&#xA;Postgres의 BIGSERIAL 타입을 Prisma로 가져오면 BigInt 타입이 됩니다. 이걸 #NestJS 컨트롤러에서 그대로 반환하면 이런 오류를 보게 되죠.&#xA;&#xA;typeerror: do not know how to serialize a bigint&#xA;&#xA;JSON.stringify() 함수에서 왠지 bigint 타입을 지원하지 않기 때문에 발생하는 문제라고 하는데, bigint를 반환하기 전에 string 같은 걸로 바꿔 주면 간단히 해결되겠지만 그렇다고 해서 bigint가 들어가는 모든 모델에 대해 타입 변환 함수 같은 걸 작성해 주기는 귀찮죠.&#xA;&#xA;그런고로 오늘의 #뚝딱뚝딱 은 응답 DTO의 모든 bigint를 알아서 string으로 바꿔 주는 방법입니다. 더 나은 해결책이 있을 것 같기는 한데, 저는 이렇게 해결했어요.&#xA;&#xA;!--more--&#xA;&#xA;Interceptor가 뭐예요?&#xA;&#xA;인터셉터는 NestJS 애플리케이션에서 요청과 응답을 가로채거나 변형할 수 있도록 하는 클래스로, @Injectable() 데코레이터를 사용해야 하며 NestInterceptor 인터페이스를 구현해야 합니다.&#xA;&#xA;NestInterceptor 인터페이스를 구현하려면 intercept(context: ExecutionContext, next: CallHandler) 함수를 구현해야 하는데, 우리는 CallHandler만 쓸 거예요. CallHandler.handle()을 써서 루트 핸들러를 호출할 수 있고, 그 결과를 pipe()로 조작할 수 있습니다. 그러니까 우리는 루트 핸들러가 실행되어 나온 결과에 bigint가 있으면 모조리 string으로 바꿔 주는 인터셉터를 구현하면 되는 거죠.&#xA;&#xA;ExecutionContext에 대해서는 문서를 참조합시다.&#xA;&#xA;bigint 잡기&#xA;&#xA;그냥 객체의 모든 키를 재귀적으로 순회하면서 typeof data[key] === &#39;bigint&#39;면 data[key] = data[key].toString()을 실행해 줍시다.&#xA;&#xA;convertBigIntToString(data: any): any {&#xA;// 배열 타입이라면...&#xA;  if (Array.isArray(data)) {&#xA;// 배열 안의 모든 요소에 대해 실행&#xA;    return data.map((item) =  this.convertBigIntToString(item));&#xA;  } else if (data !== null &amp;&amp; typeof data === &#39;object&#39;) {&#xA;// 오브젝트 안의 모든 필드에 대해...&#xA;    Object.keys(data).forEach((key) =  {&#xA;// 필드의 타입이 bigint라면...&#xA;      if (typeof data[key] === &#39;bigint&#39;) {&#xA;// string으로 바꿔 주기&#xA;        data[key] = data[key].toString();&#xA;// 필드의 타입이 object라면...&#xA;      } else if (typeof data[key] === &#39;object&#39;) {&#xA;// nested object에 대해 같은 작업 반복&#xA;        convertBigIntToString(data[key]);&#xA;      }&#xA;    });&#xA;  }&#xA;  return data;&#xA;}&#xA;&#xA;인터셉터 구현하기&#xA;&#xA;next.handle().pipe()에서 pipe()에 들어가는 인자는 rxjs.OperatorFunction인데, rxjs의 Observable을 인자로 받는 함수입니다. 여기서 Observable로는 출력 스트림이 들어갑니다. 자세한 설명은 접어두고, 우리는 핸들러의 결과에 convertBigIntToString()을 실행하면 되니까 rxjs/operators에서 map 함수를 가져옵시다.&#xA;&#xA;import {&#xA;  CallHandler,&#xA;  ExecutionContext,&#xA;  Injectable,&#xA;  NestInterceptor,&#xA;} from &#39;@nestjs/common&#39;;&#xA;import { Observable } from &#39;rxjs&#39;;&#xA;import { map } from &#39;rxjs/operators&#39;;&#xA;&#xA;@Injectable()&#xA;export class BigIntInterceptor implements NestInterceptor {&#xA;  intercept(context: ExecutionContext, next: CallHandler): Observableany {&#xA;    return next.handle().pipe(map((data) =  this.convertBigIntToString(data)));&#xA;  }&#xA;&#xA;  private convertBigIntToString(data: any): any {&#xA;    if (Array.isArray(data)) {&#xA;      return data.map((item) =  this.convertBigIntToString(item));&#xA;    } else if (data !== null &amp;&amp; typeof data === &#39;object&#39;) {&#xA;      Object.keys(data).forEach((key) =  {&#xA;        if (typeof data[key] === &#39;bigint&#39;) {&#xA;          data[key] = data[key].toString();&#xA;        } else if (typeof data[key] === &#39;object&#39;) {&#xA;          this.convertBigIntToString(data[key]);&#xA;        }&#xA;      });&#xA;    }&#xA;    return data;&#xA;  }&#xA;}&#xA;&#xA;인터셉터 적용하기&#xA;&#xA;@UseInterceptors() 데코레이터를 사용해서 컨트롤러나 메소드에 Interceptor를 적용할 수 있습니다.&#xA;&#xA;// hello.dto.ts&#xA;// bigint가 request에 있는 경우 ValidationPipe({transform: true})를 사용하기만 해도 됩니다.&#xA;export class HelloRequestDto {&#xA;  @ApiProperty()&#xA;  hello: bigint;&#xA;}&#xA;&#xA;// bigint가 response에 있는 경우 JSON.serialize()에서 bigint를 처리하지 못하므로 인터셉터가 필요합니다.&#xA;export class HelloResponseDto {&#xA;  @ApiProperty()&#xA;  goodbye: bigint;&#xA;}&#xA;&#xA;// hello.controller.ts&#xA;@Get(&#39;hello&#39;)&#xA;@UseInterceptors(BigIntInterceptor)&#xA;helloController(&#xA;  @Query() body: HelloRequestDto&#xA;): HelloResponseDto {&#xA;  return ({ goodbye: body.hello })&#xA;}&#xA;&#xA;app.useGlobalInterceptors()를 쓰면 전역 범위에 적용할 수도 있습니다.&#xA;&#xA;// main.ts&#xA;async function bootstrap() {&#xA;  / ... /&#xA;  app.useGlobalInterceptors(&#xA;    new BigIntInterceptor(),&#xA;  );&#xA;  / ... /&#xA;}&#xA;&#xA;이제 bigint가 포함된 요청을 전송하면 결과가 string으로 변환되어 날아오는 것을 확인할 수 있습니다.&#xA;&#xA;!--more--&#xD;&#xA;---&#xD;&#xA;Dani Soohan Park (@heartade)&#xD;&#xA;&#xD;&#xA;Follow this blog at Fediverse:&#xD;&#xA;@heartade@blog.heartade.dev&#xD;&#xA;&#xD;&#xA;Follow my shorter shoutouts at Fediverse:&#xD;&#xA;@heartade@social.silicon.moe&#xD;&#xA;&#xD;&#xA;Follow me at Bluesky:&#xD;&#xA;@heartade.dev]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://blog.heartade.dev/img/posts/nestjs-serialize-bigint.png" alt="표지 사진"></p>

<p>Postgres의 <code>BIGSERIAL</code> 타입을 Prisma로 가져오면 <code>BigInt</code> 타입이 됩니다. 이걸 <a href="https://blog.heartade.dev/tag:NestJS" class="hashtag"><span>#</span><span class="p-category">NestJS</span></a> 컨트롤러에서 그대로 반환하면 이런 오류를 보게 되죠.</p>

<pre><code>typeerror: do not know how to serialize a bigint
</code></pre>

<p><code>JSON.stringify()</code> 함수에서 왠지 <code>bigint</code> 타입을 지원하지 않기 때문에 발생하는 문제라고 하는데, <code>bigint</code>를 반환하기 전에 <code>string</code> 같은 걸로 바꿔 주면 간단히 해결되겠지만 그렇다고 해서 <code>bigint</code>가 들어가는 모든 모델에 대해 타입 변환 함수 같은 걸 작성해 주기는 귀찮죠.</p>

<p>그런고로 오늘의 <a href="https://blog.heartade.dev/tag:%EB%9A%9D%EB%94%B1%EB%9A%9D%EB%94%B1" class="hashtag"><span>#</span><span class="p-category">뚝딱뚝딱</span></a> 은 응답 DTO의 모든 <code>bigint</code>를 알아서 <code>string</code>으로 바꿔 주는 방법입니다. 더 나은 해결책이 있을 것 같기는 한데, 저는 이렇게 해결했어요.</p>



<h2 id="interceptor가-뭐예요" id="interceptor가-뭐예요">Interceptor가 뭐예요?</h2>

<p>인터셉터는 NestJS 애플리케이션에서 요청과 응답을 가로채거나 변형할 수 있도록 하는 클래스로, <code>@Injectable()</code> 데코레이터를 사용해야 하며 <code>NestInterceptor</code> 인터페이스를 구현해야 합니다.</p>

<p><code>NestInterceptor</code> 인터페이스를 구현하려면 <code>intercept(context: ExecutionContext, next: CallHandler)</code> 함수를 구현해야 하는데, 우리는 <code>CallHandler</code>만 쓸 거예요. <code>CallHandler.handle()</code>을 써서 루트 핸들러를 호출할 수 있고, 그 결과를 <code>pipe()</code>로 조작할 수 있습니다. 그러니까 우리는 루트 핸들러가 실행되어 나온 결과에 <code>bigint</code>가 있으면 모조리 <code>string</code>으로 바꿔 주는 인터셉터를 구현하면 되는 거죠.</p>

<p><code>ExecutionContext</code>에 대해서는 <a href="https://docs.nestjs.com/fundamentals/execution-context">문서를 참조합시다</a>.</p>

<h2 id="bigint-잡기" id="bigint-잡기"><code>bigint</code> 잡기</h2>

<p>그냥 객체의 모든 키를 재귀적으로 순회하면서 <code>typeof data[key] === &#39;bigint&#39;</code>면 <code>data[key] = data[key].toString()</code>을 실행해 줍시다.</p>

<pre><code class="language-typescript">convertBigIntToString(data: any): any {
// 배열 타입이라면...
  if (Array.isArray(data)) {
// 배열 안의 모든 요소에 대해 실행
    return data.map((item) =&gt; this.convertBigIntToString(item));
  } else if (data !== null &amp;&amp; typeof data === &#39;object&#39;) {
// 오브젝트 안의 모든 필드에 대해...
    Object.keys(data).forEach((key) =&gt; {
// 필드의 타입이 bigint라면...
      if (typeof data[key] === &#39;bigint&#39;) {
// string으로 바꿔 주기
        data[key] = data[key].toString();
// 필드의 타입이 object라면...
      } else if (typeof data[key] === &#39;object&#39;) {
// nested object에 대해 같은 작업 반복
        convertBigIntToString(data[key]);
      }
    });
  }
  return data;
}
</code></pre>

<h2 id="인터셉터-구현하기" id="인터셉터-구현하기">인터셉터 구현하기</h2>

<p><code>next.handle().pipe()</code>에서 <code>pipe()</code>에 들어가는 인자는 <code>rxjs.OperatorFunction</code>인데, <code>rxjs</code>의 <code>Observable</code>을 인자로 받는 함수입니다. 여기서 <code>Observable</code>로는 출력 스트림이 들어갑니다. 자세한 설명은 접어두고, 우리는 핸들러의 결과에 <code>convertBigIntToString()</code>을 실행하면 되니까 <code>rxjs/operators</code>에서 <code>map</code> 함수를 가져옵시다.</p>

<pre><code class="language-typescript">import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from &#39;@nestjs/common&#39;;
import { Observable } from &#39;rxjs&#39;;
import { map } from &#39;rxjs/operators&#39;;

@Injectable()
export class BigIntInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable&lt;any&gt; {
    return next.handle().pipe(map((data) =&gt; this.convertBigIntToString(data)));
  }

  private convertBigIntToString(data: any): any {
    if (Array.isArray(data)) {
      return data.map((item) =&gt; this.convertBigIntToString(item));
    } else if (data !== null &amp;&amp; typeof data === &#39;object&#39;) {
      Object.keys(data).forEach((key) =&gt; {
        if (typeof data[key] === &#39;bigint&#39;) {
          data[key] = data[key].toString();
        } else if (typeof data[key] === &#39;object&#39;) {
          this.convertBigIntToString(data[key]);
        }
      });
    }
    return data;
  }
}
</code></pre>

<h2 id="인터셉터-적용하기" id="인터셉터-적용하기">인터셉터 적용하기</h2>

<p><code>@UseInterceptors()</code> 데코레이터를 사용해서 컨트롤러나 메소드에 Interceptor를 적용할 수 있습니다.</p>

<pre><code class="language-typescript">// hello.dto.ts
// bigint가 request에 있는 경우 ValidationPipe({transform: true})를 사용하기만 해도 됩니다.
export class HelloRequestDto {
  @ApiProperty()
  hello: bigint;
}

// bigint가 response에 있는 경우 JSON.serialize()에서 bigint를 처리하지 못하므로 인터셉터가 필요합니다.
export class HelloResponseDto {
  @ApiProperty()
  goodbye: bigint;
}

// hello.controller.ts
@Get(&#39;hello&#39;)
@UseInterceptors(BigIntInterceptor)
helloController(
  @Query() body: HelloRequestDto
): HelloResponseDto {
  return ({ goodbye: body.hello })
}
</code></pre>

<p><code>app.useGlobalInterceptors()</code>를 쓰면 전역 범위에 적용할 수도 있습니다.</p>

<pre><code class="language-typescript">// main.ts
async function bootstrap() {
  /* ... */
  app.useGlobalInterceptors(
    new BigIntInterceptor(),
  );
  /* ... */
}
</code></pre>

<p>이제 <code>bigint</code>가 포함된 요청을 전송하면 결과가 <code>string</code>으로 변환되어 날아오는 것을 확인할 수 있습니다.</p>



<hr>

<h3 id="dani-soohan-park-heartade" id="dani-soohan-park-heartade">Dani Soohan Park (@heartade)</h3>

<p>Follow this blog at Fediverse:
<a href="https://blog.heartade.dev/heartade"><a href="https://blog.heartade.dev/@/heartade@blog.heartade.dev" class="u-url mention">@<span>heartade@blog.heartade.dev</span></a></a></p>

<p>Follow my shorter shoutouts at Fediverse:
<a href="https://social.silicon.moe/@heartade"><a href="https://blog.heartade.dev/@/heartade@social.silicon.moe" class="u-url mention">@<span>heartade@social.silicon.moe</span></a></a></p>

<p>Follow me at Bluesky:
<a href="https://bsky.app/profile/did:plc:e4a32z23pazq5dxnucj6wpee">@heartade.dev</a></p>
]]></content:encoded>
      <guid>https://blog.heartade.dev/nestjs-typeerror-do-not-know-how-to-serialize-a-bigint</guid>
      <pubDate>Fri, 15 Dec 2023 17:00:17 +0000</pubDate>
    </item>
  </channel>
</rss>