NestJS, TypeORM, EJS 환경이다.

문제상황 : 네비게이션바의 Ajax 코드 에러
- 기존에 위 네비게이션바는 Ajax로 구현되어 있었다. 로그인정보 유무에 따라서 MyPage 버튼이 생기거나, Login 버튼에 변화가 생기도록 구현된 상태이다.
- 문제는 작업시간이 길어지며 코드가 복잡해지자, 클라이언트 사이드에서 유저가 로그인 및 로그아웃 버튼을 동작시킬 때마다, 401 오류가 계속 발생했다. 파악해보니 아래 코드블록의 38번째 const userId = obj.value 코드가 제대로 작동되지 않고 있었다.
- 나는 이 방식이 로그인 정보에 따른 변경이 필요한 적절한 응답을 못해주고 있다고 이해했고, 해결할 필요성을 느꼈다.
기존 코드 예시 : 네비게이션바 로그인 및 로그아웃 버튼 변경을 위해 JS 파일 내에 Ajax로 구현됨

시도 : 일부 해결, 기존의 Ajax 코드를 EJS파일에서 처리하는 방식으로 변경
- 나는 일단 Ajax로 구현된 위 코드부터 바꿀 필요성을 느꼈고, EJS 파일에서 네비게이션바 버튼 변경을 처리하는 방법으로 바꾸었다.
- 기존 코드는 localStorage에서 로그인 정보를 확인하는데, 클라이언트 단에서 로그인 정보를 다루는 것은 서버에서 다루는 것보다 보안상 불리할 것이라 생각했다. 그래서 서버사이드 렌더링 방식인 EJS에서 로그인 버튼 상태 업데이트를 다루기로 했다.
- 다음은 변경한 header.ejs 코드 일부. 이제 모든 EJS 파일 렌더링시에 buttonUserId를 꼭 필요로 한다.
</div>
<ul class="navlist1">
<% if (buttonUserId ) {%>
<li class="list1"><a href="/userpage/<%= buttonUserId %>">My page</a></li>
<% } else { %>
<% } %>
</ul>
<div class="header_button">
<% if (buttonUserId ) {%>
<button onclick="logout()" class="myButton">Log out</button>
<% } else { %>
<button onclick="location='/sign'" class="myButton">Log in</button>
<% } %>
</div>
</div>
</div>
</header>
- 각 페이지로 접속시 EJS로 구현된 페이지가 렌더링 되며, 유저의 로그인 정보가 담겨있는지 확인하도록 구성을 바꾸었다.
- 이제, 페이지 렌더링을 위한 API 요청에 대해서는 로그인 정보인 buttonUserId을 반환값으로 컨트롤러에서 응답해야 한다고 생각했다.
변경된 컨트롤러 단의 코드 일부)
- 자세히 살펴보면 모든 페이지 렌더링의 GET요청 API 마다 아래 코드가 반복 선언이 되고 있다.
@Post("/clubspost")
@UseGuards(AuthGuard())
async createClub(@Body() data: CreateClubDto, @Req() req) {
const userId = req.user;
let buttonUserId = null;
if (req.user) {
buttonUserId = req.user
}
const post = await this.clubService.createClub(
userId,
data.title,
data.content,
data.maxMembers,
data.category,
);
return {post, buttonUserId};
}
@Post("/:id")
@UseGuards(OptionalAuthGuard)
async createApp(
@Param("id") id: number,
@Body() data: CreateAppDto,
@Req() req,
) {
let buttonUserId = null;
if (req.user) {
buttonUserId = req.user
}
const userId = req.user;
const createNew = await this.clubService.createApp(
id,
userId,
data.application,
data.isAccepted,
);
return {createNew, buttonUserId};
}
@Get("/clubs/:id")
@UseGuards(OptionalAuthGuard)
async updateclub(
@Param("id") id: number,
@Res() res: Response,
@Req() req,
) {
let buttonUserId = null;
if (req.user) {
buttonUserId = req.user
}
const detail = await this.clubService.getClubById(id);
const nowPost = detail.nowPost
return res.render("clubupdate.ejs", { nowPost, detail, buttonUserId });
}
let buttonUserId = null;
if (req.user) {
buttonUserId = req.user
}
- 기존 Ajax 코드에서 EJS 파일에서 처리하는 것으로 변경하자 네비게이션에 있는 버튼 상태 변경도 좀 더 부드럽게 이루어졌다.
- 이렇게, 최초 발생했던 문제는 해결되었다.
코드 변경 후 새 오류 인지
이제, 새로운 오류가 발생했다. 모임 게시글에는 유저가 모임에 신청할 수 있도록하는 버튼이 있다. 역시 Ajax로 구현되어있다. 이 버튼을 작동시키면 1차적으로 모달창이 뜨며, 그 모달창 내의 신청사유와 함께 작동시키는 신청 버튼은 POST 요청을 보내도록 작동한다.

하지만, 모달창 내에서 submit! 버튼을 눌러 POST요청을 할 때마다 변경된 header.ejs에서 반복적으로 buttonUserId is not defined이라는오류가 계속 발생했다.
- 나는 응답-요청에 관련한 문제로 파악했기에, 응답-요청을 다르게 처리하면 해결이 되지 않을까 아이디어가 떠올랐다.
새 오류에 대한 해결 시도 1 : 새로운 API 작성?
- 현재는 각 페이지 렌더링에 필요한 GET 요청 시에 buttonUserId를 함께 응답해주고 있는데, 모달창 구현을 담당했던 동료가 말하길, 새로운 API를 작성하면 해결할 수도 있겠다고 응답했다.
- 동료의 제안대로라면, 현재 모달창이 4개 이기에 이에 따른 API를 추가 작성하면 될 것이다.
- 하지만 이 경우, 유지보수성에 문제가 있다. 앞으로 우리 웹페이지에 모달창과 관련된 기능이 늘수록 API를 추가 작성을 해야 할 것이 때문이다.
- 더 좋은 방법은 없을까 고민했다. API 추가 작성 없이 buttonUserId을 보내주는 일괄적으로 무언가를 처리할 수 있는 방법을 고민했다.
그래서 Nest.js를 학습하면서 배웠던 인터셉터 혹은 미들웨어로 탐색하기 시작했다.
새 오류에 대한 해결 시도 2 : 미들웨어 처리?

미들웨어는 전역 미들웨어를 사용할 경우, 앞서 반복적으로 작성했던 코드를 미들웨어에서 선언하여, 클라이언트의 요청이 발생하면 모든 API에 접근하기 전에 이를 일괄적으로 처리할 수 있다.
let buttonUserId = null;
if (req.user) {
buttonUserId = req.user
}
- 미들웨어를 사용하면 위 선언 부분에 대한 반복적인 코드를 줄일 수는 있었다.
- 그러나, return 값에 응답을 위하여 buttonUserId을 일일이 작성해야 하기에 유지 보수에 대한 불편함은 사라지지 않는다.
- 가장 중요한 Ajax 통신을 원활하게 하기 위해 모달창에 대한 API를 작성해야 하는 일은 여전히 발생한다.
새 오류에 대한 해결 시도 3 : 해법! '인터셉터' 사용

nestjs에서 인터셉터(interceptor)는 클라이언트의 request와 response가 발생할 때, 미리 어떤 처리를 하고, 또 그에 대한 반환도 처리할 수 있게 만든다. 즉 클라이언트의 요청과 응답을 가로채서 변형을 가할 수 있는 컴포넌트이다. 그래서 각각의 요청과 응답을 맞춤형으로 처리할 수 있게 된다.
그래서 인터셉터 파일을 만들어 구성했다. 이후 구성한 인터셉터를 app.module에 선언해 주면 된다.
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class ButtonUserIdInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const buttonUserId = request.user ? request.user : null;
// 전역 변수로 설정
global.buttonUserId = buttonUserId;
return next.handle();
}
}
- 이제 인터셉터에서는 HTTP 요청 객체를 통해 로그인한 유저 정보(request.user)를 가져와서 buttonUserId 변수에 할당하고, global.buttonUserId 전역 변수에 할당한다.
- 이를 통해 해당 인터셉터를 거치는 모든 요청에 대해 global.buttonUserId 변수를 처리할 수 있게 된다.
let buttonUserId = null;
if (req.user) {
buttonUserId = req.user
}
- 인터셉터가 존재함으로써 각 요청을 처리하기 전에 이전에 반복 선언했던 위 코드는 필요 없어졌다. 코드가독성도 늘고, 유지보수에도 더 도움이 된다.
- EJS 페이지 렌더링시 필요한 buttonUserId 요청은, 이제 global.buttonUserId를 수정하여 사용하면 된다.
바뀐 header.ejs 코드
바뀐 controller 코드 일부)
@Post("/clubspost")
@UseGuards(AuthGuard())
async createClub(@Body() data: CreateClubDto, @Req() req) {
const userId = req.user;
const post = await this.clubService.createClub(
userId,
data.title,
data.content,
data.maxMembers,
data.category,
);
return post
}
@Post("/:id")
@UseGuards(OptionalAuthGuard)
async createApp(
@Param("id") id: number,
@Body() data: CreateAppDto,
@Req() req,
) {
const userId = req.user;
const createNew = await this.clubService.createApp(
id,
userId,
data.application,
data.isAccepted,
);
return createNew
}
@Get("/clubs/:id")
@UseGuards(OptionalAuthGuard)
async updateclub(
@Param("id") id: number,
@Res() res: Response,
@Req() req,
) {
const detail = await this.clubService.getClubById(id);
const nowPost = detail.nowPost
return res.render("clubupdate.ejs", { nowPost, detail});
}
알게 된 점.
nestjs에서 인터셉터(interceptor)는 클라이언트의 request와 response가 발생할 때, 미리 어떤 처리를 하고, 또 그에 대한 반환도 처리할 수 있게 만든다. 반복적으로 선언하고, 반환할 일이 있다면, 클라이언트의 요청과 응답을 가로채서 변형을 가할 수 있는 일은 인터셉터로 효과적으로 처리하자.
참고
https://jakekwak.gitbook.io/nestjs/overview/interceptors
Interceptors - nestjs
핸들러 호출을 완전히 막고 대신 다른 값을 반환하려는 몇가지 이유가 있습니다. 명백한 예는 응답 시간을 개선하기 위해 캐시를 구현하는 것입니다. 캐시에서 응답을 반환하는 간단한 캐시 인...
jakekwak.gitbook.io
'개발 학습일지(TIL)' 카테고리의 다른 글
TIL : Next.js 13 Provider 문제 : 다크모드가 안 되는 이유 (1) | 2023.05.19 |
---|---|
TIL : Next.js, useState와 useEffect 이용하여 댓글 실시간 업데이트 (0) | 2023.05.12 |
TIL : typeORM 외래키 없이 조인. getRawMany() vs getMany() (1) | 2023.03.27 |
TIL : 리팩토링, 모듈화로 재사용성, 유지보수성 높이기(nestJS, EJS) (0) | 2023.03.24 |
TIL : NestJS, TypeORM, EJS 검색 결과 갯수 구하기 (0) | 2023.03.22 |
댓글