NestJS에서 tRPC를 사용할 때의 Controller 사용

11t518s·2024년 7월 30일
0

서버 개발할 때 보통 MVC패턴을 주로 사용합니다.

NestJS에서도 이와 비슷한 패턴을 사용하게 됩니다.

그런데 tRPC를 사용하게되면 이러한 @Controller라는 데코레이터를 제대로 사용할 수 없는 문제가 생기게 됩니다.

왜냐면 tRPC의 HTTP 프로토콜을 따르지만, request와 response의 구현체는 라이브러리에 모두 맡겨버리는 형태이기 때문입니다.

tRPC의 예시를 바탕으로 설명하면 아래와 같이 서버코드를 구현해두고

const t = initTRPC.create();
 
const router = t.router;
const publicProcedure = t.procedure;
 
const appRouter = router({
  greeting: publicProcedure
    .input(z.object({ name: z.string() }))
    .query((opts) => {
      const { input } = opts;
               
const input: {
    name: string;
}
 
      return `Hello ${input.name}` as const;
  }),
});
 
export type AppRouter = typeof appRouter;

아래와 같은 방식으로 프론트에서 서버를 호출하는게 보통이기 때문입니다.

const trpc = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000',
    }),
  ],
});
 
const res = await trpc.greeting.query({ name: 'John' });

즉 @Controller데코레이터는 t.router 내부에서 사용할 수 없기도하고, t.router을 배재한다고 하면 tRPC를 사용하는 이유가 없기 때문입니다.

그래서 tRPC에 NestJS를 얹으려고 한다면 Controller 데코레이터를 포기하고 t.router를 더 잘사용하기 위해 코드를 변경해야합니다.

NestJS에서 tRPC 열어주기

먼저 NestJS에서 tRPC기반의 코드를 받아줄 수 있도록 진입지점을 열어줘야합니다.

이 부분은 main.ts에서 trpc도 apply시키는 방식으로 구현해둡니다.

async function bootstrap() {
  const app = await NestFactory.create(AppModule)

  const trpc = app.get(TrpcRouter)
  trpc.applyMiddleware(app)

  await app.listen(8080)
}

bootstrap()

물론 위의 방식은 DI를 정말 잘 사용하고 있는 부분은 아니지만 부트스트래핑 해주는 부분에서 middleware를 적용시키는 의미에서 사용해줍니다.

그리고 바탕으로 한번 더 이해하면, NestJS자체의 HTTP 요청(@Controller를 사용한)도 원래 의도대로 동작하고, tRPC를 사용한 HTTP 요청도 그대로 사용할 수 있다는 걸 알고있습니다.

아래는 위에서 사용된 trpc/trpc.router의 구현체와, trpc.router를 구현하기 위한 tRPC를 init하는 trpc.service의 코드입니다.

// trpc.service
@Injectable()
export class TrpcService {
  trpc = initTRPC.context<typeof this.createContext>().create()
  procedure = this.trpc.procedure
  router = this.trpc.router
}
// trpc.router
@Injectable()
export class TrpcRouter {
  constructor(
    private readonly trpcService: TrpcService,
    private readonly userController: UserController,
    private readonly anohterController: AnotherController,
  ) {}

  appRouter = this.trpcService.router({
    user: this.userController.router,
    another: this.anohterController.router,
  })

  async applyMiddleware(app: INestApplication) {
    app.use(
      `/trpc`,
      trpcExpress.createExpressMiddleware({
        router: this.appRouter,
        createContext: this.trpcService.createContext as any,
        onError(opts) {
          const { error, type, path, input, ctx, req } = opts
          console.error('Error:', error)
        },
      }),
    )
  }
}

export type AppRouter = TrpcRouter['appRouter']

위의 코드를 보면 applyMiddleware를 통해 tRPC의 구현체를 사용할 수 있는데 이를 위해서는 router를 사용해야합니다.

Controller의 역할이 결국 routing해주는 역할이기도 하고, tRPC에서도 말하고자 하는 바가 이와 동일하기 때문에, tRPC아래서 NestJS의 Controller는 tRPC의 Router를 만들어주는 역할이 돼야합니다.

이 과정에서 Auth를 다루는 코드나, 더 좋은 타입 추론을 위한 코드를 추가할 수 있는데 이는 다음에 한번 더 글을 작성해보겠습니다.

tRPC Controller

@Injectable()
export class UserController {
  constructor(
    private readonly trpcService: TrpcService,
  ) {}

  router = this.trpcService.router({
    hello: this.trpcService.procedure
      .input(
        z.object({
          name: z.string().optional(),
        }),
      )
      .query(({ input }) => {
        const { name } = input
        
        return {
          greeting: `Hello ${name ? name : `Bongsu`}`,
        }
      }),
  })
}

그래서 tRPC아래의 NestJS에서의 Controller는 위와 같은 코드가 나오게 됩니다.

router를 정의해주는데 이는 trpcService.router를 통해 열어줍니다.

이때 사용한 trpcService.router는 trpc.router에서 사용한 것과 동일하게 사용되는데, 프론트에서 더 편하게 api를 사용할 수 있도록 매핑시켜주기 위한 과정으로 사용됩니다. 즉 용도와 필요에 따라 trpcService.router로 뎁스를 만들어줄 수 있습니다.

Controller이후의 Service나 Repository는 이전에 사용하는 것과 동일하게 사용할 수 있는데, Controller는 위와 같이 정의해야 tRPC에 맞게 사용할 수 있습니다.

이렇게 코드를 구현하면

const trpc = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000',
    }),
  ],
});
 
const res = await trpc.user.hello.query({ name: 'hello tRPC' });

처럼 사용할 수 있게 됩니다.

위에서 trpcService.router를 중복해서 사용한 덕분에 user.hello로 관심사에 맞는 api를 나눠줄 수 있고, 이를 통해 기존의 Controller와 유사한 효과를 낼 수 있습니다.

뿐만 아니라 나중에 tRPC를 버리고 일반 HTTP 혹은 다른 프로토콜로 전환할 때도 Controller의 코드만 변환하여 사용할 수 있습니다.

profile
사람들에게 행복을 주는 서비스를 만들고 싶은 개발자입니다!

0개의 댓글