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 요청을 보내도록 작동한다.
![](https://blog.kakaocdn.net/dn/dMpHox/btsc2qrst1V/0FBcSHmd0kihKAylKOHhO0/img.png)
하지만, 모달창 내에서 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() (0) | 2023.03.27 |
TIL : 리팩토링, 모듈화로 재사용성, 유지보수성 높이기(nestJS, EJS) (0) | 2023.03.24 |
TIL : NestJS, TypeORM, EJS 검색 결과 갯수 구하기 (0) | 2023.03.22 |
댓글