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의 동작 원리
=> 컨트롤러의 핸들러에서 @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.tsx의 getInitialProps에서
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 동작 시 유의할 점을 함께 공부하게 되었는데 아주 의미있었다..!!
설명 맛집이네용
좋은 글 읽고 갑니다~