🔧 숫자형 쿼리 파라미터 유효성 검증하기
해당 PR 보러가기
문제
- Query Parameter든 Path Parameter든 들어올 땐 string 타입으로 들어온다.
- 현재 프로젝트에서, number 타입의 쿼리 파라미터로
pageCount
, page
가 있다. - 저 변수들이 서비스 로직에선 산술 연산자와 함께 암시적 형변환이 이루어지고 있어 동작하는데 문제는 없었지만, 타입을 정확히 하고,
NaN
처리를 해주지 않아 추가해야 한다.
시도
1
2
3
4
5
6
7
| @IsNotEmpty()
@Transform(({ value }) => Number(value))
readonly pageCount: number = 10;
@IsNotEmpty()
@Transform(({ value }) => Number(value))
readonly page: number = 0;
|
class-transformer
의 Transform
데코레이터를 활용해 number로 타입을 변환한다.- 형 변환에는
parseInt
, Number
, +
단항 연산자 붙이기 등 많은 방법이 있지만 명시적으로 Number
를 사용했다.
1
2
3
4
5
6
7
| async findAll(hashtag: string, queries: PostingQueryDto): Promise<PostingResponseDto[]> {
if (isNaN(queries.pageCount) || isNaN(queries.page)) {
throw new BadRequestException();
}
const rawPostings = await this.getRawPostings(hashtag, queries);
...
|
- 그리고 서비스 로직 안에서
NaN
인 경우에 대한 예외 처리를 추가했다.
새로운 문제
- 위 방법은 DTO에서 기본값을 설정하고 있는데, 해당 변수에 아무것도 입력되지 않아 기본값(number)가 들어갈 때 타입 문제가 있었다.
- DTO에서의 기본값을 string 타입으로 (
'10'
, '0'
) 바꾸는 방법도 있었지만 논리적으로 맞지 않는다고 생각했다. - 또
NaN
처리를 서비스 로직에서 하고 있는데, 지금까지 데이터 유효성 검증은 컨트롤러단에서 모두 했던 것 같아 그다지 효율적인 방식이라는 생각은 들지 않았다.
시도 ✅
1
2
3
4
5
6
7
| @IsNotEmpty()
@IsNumberString()
readonly pageCount: number = 10;
@IsNotEmpty()
@IsNumberString()
readonly page: number = 0;
|
- DTO에서 해당하는 변수들에
@IsNumberString()
데코레이터를 사용하여 별다른 코드 추가 없이 간결하게 해결하였다! - DTO에서 모든 유효성 검증을 처리해 코드 일관성을 유지할 수 있다.
- number로 실질적인 타입 변환까지 되진 않지만, 이게 코드를 쓰는데 크게 작용할 것 같진 않다. 아주 굿~
💾 User 정보가 담긴 Request 타입 처리하기
해당 PR 보러가기
문제
1
2
3
4
5
6
| @UseGuards(AuthGuard('jwt'))
@Get()
async findAll(@Req() req: any, @Query() queries: PostingQueryDto): Promise<PostingResponseDto[]> {
const hashtag = queries.hashtag || req.user.accountName;
return await this.postingsService.findAll(hashtag, queries);
}
|
- 보통 컨트롤러에서 Request 객체에 접근이 필요하면,
express
의 Request
를 import해 타입 명시를 해주었다. - 그러나 이 코드는, 인가 과정을 거치기 때문에 들어오는
req
객체에 User
객체도 포함되어 있을 것이다. 그래서 Request { ..., user: User {} }
이런 식의 커스텀 타입이 필요하다.- 우리가 궁극적으로 필요한 건
User
객체의 accountName
이다. - 해당 과정은
src/auth/jwt.strategy.ts
에서 수행한다.
첫 번째 시도
1
2
3
| interface UserRequest extends Request {
user: UserResponseDto;
}
|
- 다른 팀원분께서 정의해놓으신 인터페이스를 갖다 쓰는 방법
- 채택되지 않은 이유:
export
키워드 붙여도 되나… 싶어서
두 번째 시도 ✅
1
2
3
4
5
6
| @Get()
@ApiAcceptedResponse({ type: StatisticResponseDto })
getStatistic(@Query() statisticQuery: StatisticQueryDto, @Req() req: Request): Promise<StatisticResponseDto> {
const userName = (req.user as User).accountName;
return this.statisticService.getStatistics(statisticQuery, userName);
}
|
- 지난 코드 리뷰 시간에 통계 쪽에서도
accountName
이 필요했던 게 기억나서, 다른 팀원분의 코드를 훔쳐보았다ㅎㅎ as
키워드를 사용해 타입을 우회하고 있다.- 시도 1과 비교해 뭐가 더 나은 방법인지, 스타일의 문제인지 모르겠으나 이 방법을 쓰면 현재 내 상황에서 다른 코드를 더 고치지 않아도 되어서 채택했다.
- 지금 보니 만약
req.user
가 null
이나 undefined
값이면 accountName
에 접근하는 과정에서 에러가 날 것이다. 당시엔 별 이슈가 없었나 보다.
또 다른 방법
1
2
3
4
5
6
7
8
| import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const UserInfo = createParamDecorator(
(data: any, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user || null;
},
);
|
보통은 이렇게 커스텀 데코레이터를 만들어 변수에 붙이는 식으로 사용했다. 중복 코드를 최소화할 수 있어 좋은 방법인 것 같다.
♻️ QueryBuilder대신 TypeORM API 활용하기
문제
1
2
3
4
5
6
7
| const posting = await this.postingRepository
.createQueryBuilder('posting')
.leftJoinAndSelect('posting.postingHashtags', 'postingHashtag')
.leftJoinAndSelect('postingHashtag.hashtag', 'hashtag')
.where('posting.id = :id', { id })
.getOne();
...
|
queryBuilder
를 사용한 원래 코드 (상세 조회 API)- 팀 컨벤션대로, Repository API로 구현할 수 있으면
queryBuilder
지양하기.
시도 ✅
1
2
3
4
| const posting = await this.postingRepository.findOne({
where: { id },
relations: ['postingHashtags', 'postingHashtags.hashtag'],
});
|
- Repository API
relations
옵션을 사용해 수정했다. - 가독성 ㄹㅈㄷ로 개선
- 조건별 쿼리 빌드가 필요한 목록 조회 API는 기존대로
queryBuilder
를 사용하기로 결정했다.