@User() 데코레이터 CSR vs SSR 동작 비교

에옹이다아옹·2025년 2월 26일
1
post-thumbnail

NestJS로 BFF(Backend for Frondend)를 사용하고 있는데 로그인 여부에 따라 같은 API라도 응답값을 걸러서 보여줘야 하는 상황이 있다.

상품상세 조회 API를 호출할 때 가격정보나 옵션정보 재고수량은 비로그인 시에는 노출시키지 않아야 하는데 @User()를 통해 그동안 BFF에서 판별해왔다.

이번에 공유하기 기능을 상품상세 및 이벤트 상세 페이지에서 추가하게 되면서 og 태그로 이미지와 상품 이름을 인식시키기 위해 SSR을 적용하게 되었는데

@User()로 웹에서는 적용이 되었지만 모바일의 경우에는 호출 로직이 동일함에도 잘 동작하지 않았다.


이 과정에서 새로운 사실들을 알게 되었는데

우선 처음에는 @Req()를 Controller에서 대신 넘겨서
@Req() request: Request를 서비스에서 활용해보면 어떨까 하고 request를 콘솔로 찍어보니 웹으로 접근했을 때(모바일에서 안됐던 이유는 뒤에 설명) 로그인 후에는 request.user에 로그인한 내 정보가 담겨있었다.

정보를 좀 찾아보니
passport-jwt를 사용하면 JWT 인증이 성공할 경우 request.user에 JWT의 payload가 채워진다는 것이다!!!

전체적인 동작 원리는 다음과 같다.

AuthGuard('jwt')와 JwtStrategy의 동작 원리

  • 클라이언트가 Authorization: Bearer JWT 헤더를 포함해서 요청
  • AuthGuard('jwt')를 적용한 핸들러(컨트롤러의 라우트 핸들러)가 실행
  • AuthGuard('jwt')가 실행되면서 JWT를 추출하고 검증
  • 내부적으로 JWT를 검증하는 역할을 하는 JwtStrategy가 실행되면서 validate(payload) 메서드가 호출됨
  • validate(payload)에서 JWT의 payload를 request.user에 채움

=> 컨트롤러의 핸들러에서 @User()를 사용하면 request.user에 접근 가능


그런데 applyDecorators(...,UseGuards(JwtAuthGuard, RoleGuard, JwtRefreshAuthGuard))가 Auth 데코레이터에 설정되어 있지만 정작 내가 쓰는 컨트롤러들에는 또 @Auth() 데코레이터가 붙여져 사용되고 있지 않는데 어떻게 작동하는건지 궁금했다.

또 GPT 선생의 도움을 받아 알아보니 middleware를 잘 살펴보라고 해서 app.moudle.ts를 보니

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer): any {
    consumer
      .apply(LoggerMiddleware, CookieValidationMiddleware, JwtMiddleware, TokenValidationMiddleware)
      .forRoutes({ path: '*', method: RequestMethod.ALL });
  }
}

라고 명시되어 있었다.

어떤 경로에서 들어오든지, 모든 HTTP 메서드(GET, POST, PUT, DELETE, 등)에 대해 미들웨어가 적용된다는 의미였다.


그럼 남은 의문은 왜 웹에선 되고 모바일에선 SSR시 회원 정보를 불러올 수 없을까?였다.

메인, 상품상세, 이벤트 상세 페이지를 제외하고는 react-query로 CSR 환경에서 api를 호출하는데 이때는 브라우저에서 자동으로 쿠키가 포함되지만 getServerSideProps를 사용하게 되면

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { req } = context;

  // API 요청 시 쿠키 포함
  const res = await fetch('http://localhost:3000/api/users/me', {
    headers: {
      Cookie: req.headers.cookie || '', // 쿠키에 JWT가 있어야 함
    },
  });

  const user = await res.json();

  return {
    props: { user },
  };
};

다음과 같이 쿠키값을 반!드!시! 전달해줘야 한다고 한다.

하지만 웹에서는 저렇게 전달하고 있지 않은데 어떻게 가능한건지 또 궁금해졌다.

한참을 삽질하다가 _app.tsxgetInitialProps에서

App.getInitialProps = async ({ Component, ctx }: AppContext) => {
  let pageProps = {};
  if (Component.getInitialProps) {
    pageProps = await Component.getInitialProps(ctx);
  }

  const cookie = ctx.req?.headers.cookie;

  if (cookie) {
    http.defaults.headers.cookie = cookie;
  }

  ...

  pageProps = { ...pageProps};

  return { pageProps };
};

export default App;

getInitialProps에서는 서버 측에서 최초로 실행될 때 쿠키 값을 읽고 http.defaults.headers.cookie에 세팅한다는 사실을 알게 되었다.

이 작업은 서버 측에서만 한 번 실행되고 그 다음부터는 서버 측에서 추가적인 API 요청을 보낼 때 그 쿠키가 자동으로 포함되므로, http.defaults.headers.cookie를 다시 설정할 필요가 없다고 한다.

모바일의 _app.tsx에서는 http.defaults.headers.cookie를 설정하는 부분이 누락되어 있어서

직접적으로 SSR을 진행하는 페이지에서 쿠키값을 전달하지 않는다면 실행되지 않았던 것이다!!


@User() 데코레이터의 동작을 CSR, SSR에서 각각 비교해보면 다음과 같다.

CSR (Client Side Rendering)SSR (Server Side Rendering)
JWT 전달 방식브라우저가 자동으로 쿠키 포함getServerSideProps에서 수동으로 쿠키 포함 필요
@User() 값로그인 시 request.user가 항상 존재쿠키가 없으면 request.user === undefined
문제 발생 가능성없음 (쿠키 자동 포함)SSR에서 쿠키 없이 요청하면 undefined
해결 방법@User() 사용 가능getServerSideProps에서 쿠키 포함해야 @User() 가능
예외 처리AuthGuard에서 자동 처리undefined 가능성 고려 필요

쿠키값이 담겨지는 것을 확인하고 나서는 @Req() request: Request를 서비스에 전달하던 로직을 다시 @User()로 변경하였다.

가독성이 좋고 NestJS 스타일을 유지하는 장점이 있기 때문이다.

@Get('/:productId')
  getProduct(
    @Param('productId') productId: string,
    @User() user?: IUser
  ): Promise<ProductResponse> {
    return this.productsService.findById(productId, user);
  }

최종적으로는 이렇게 변경하였고 잘 동작하는 것을 확인하였다!

권한 처리를 하려다 보니 NestJS의 @User() 데코레이터 및 SSR 동작 시 유의할 점을 함께 공부하게 되었는데 아주 의미있었다..!!

profile
숲(구조)을 보는 개발자

1개의 댓글

comment-user-thumbnail
2025년 2월 26일

설명 맛집이네용
좋은 글 읽고 갑니다~

답글 달기

관련 채용 정보