개발 학습일지(TIL)

TIL : 트러블슈팅, NestJS 인터셉터 사용으로 리팩토링 (유저 로그인 정보에 따른 버튼 처리)

Veams 2023. 3. 31.

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 코드

            <ul class="navlist1">
                <% if (global.buttonUserId ) {%>
                    <li class="list1"><a href="/userpage/<%= global.buttonUserId %>">My page</a></li>
                    <% } else { %>
                        <% } %>
            </ul>
            <div class="header_button">
                <% if (global.buttonUserId ) {%>
                    <button onclick="logout()" class="myButton">Log out</button>
                    <% } else { %>
                        <button onclick="location='/sign'" class="myButton">Log in</button>
                        <% } %>
            </div>

 

바뀐 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

 

댓글