23년 3월 24일 TIL & 개발노트 (Typescript에서 문자열 상수, 그리고 코드 리팩토링)
개발하며 얻은 TIL
흑우집합소 개발하며 얻은 내용 및 노션에 작성한 내용을 정리한 포스팅이다.
오늘 개발하면서 배운 내용은 아래와 같다.
- 예외처리의 미흡함 (예외처리 부분)
- 문자열 상수(String Literal) - Typescript Object 키 문제
- 반복적 코드를 줄이는 리팩토링
각 항목은 아래의 내용을 참고하자.
1. 예외처리 부분
오늘도 개발을 하던 도중 코드가 이상한 부분을 찾게 되었다.
백엔드 측의 에러 핸들링 부분이었다.
if (exception instanceof AuthException) {
//토큰 문제가 발생한 경우
if (
exception.errorCode === '8-1' ||
exception.errorCode === '8-2' ||
exception.errorCode === '8-3' ||
exception.errorCode === '8-4' ||
exception.errorCode === '8-5'
) {
const decodeData = this.decodeJwtOutboundPort.accessJwtDecode(request.header(HEADER_JWT));
//로그아웃 처리
await this.logoutAccountInboundPort.excute({
email: decodeData.email,
joinType: decodeData.joinType,
});
response.setHeader(HEADER_JWT, '');
}
}
Error Handler 부분의 일부다.
여기서 저기 const decodeData 부분의 로직을 보면 request의 헤더로부터 jwt를 가져오는 부분이 있다.
만약 헤더에 jwt를 안가지고 올 경우 어떻게 될까?
또 버그를 양산하는 모양새가 된다.
물론 jwtStrategy에서 검증을 하겠지만, 혹시 AuthExceptionHandler를 strategy 없이 사용하는 곳에서 발생했다면?
그래서 이 부분은 검사를 한번 더 하는 방향으로 로직을 수정했다.
if (exception instanceof AuthException) {
//토큰 문제가 발생한 경우
if (
exception.errorCode === '8-1' ||
exception.errorCode === '8-2' ||
exception.errorCode === '8-3' ||
exception.errorCode === '8-4' ||
exception.errorCode === '8-5'
) {
//만약 JWT가 헤더 안에 있다면...
if (request.header(HEADER_JWT)) {
const decodeData = this.decodeJwtOutboundPort.accessJwtDecode(request.header(HEADER_JWT));
//로그아웃 처리
await this.logoutAccountInboundPort.excute({
email: decodeData.email,
joinType: decodeData.joinType,
});
}
response.setHeader(HEADER_JWT, '');
}
}
이렇게 한번 보호를 함으로써 예외를 막을 수 있었다.
2. Typescript Object 키 문제
개발을 하던 도중 아래와 같은 코드를 작성했다.
const F_HEADER_BKEY = 'test_key' as const;
const defaultH: { [key: string]: string } = {
'key-str': 'value-str',
'F_HEADER_BKEY': '111aaa',
};
console.log('show value = ', defaultH);
이렇게 작성할 경우 결과는 아래와 같아 나온다.
[LOG]: "show value = ", {
"key-str": "value-str",
"F_HEADER_BKEY": "111aaa"
}
내가 원하는 그림은 F-HEADER_BKEY의 값이 저 상수 이름 값이 아닌,
test_key 상수의 값이 들어가길 원했다.
이 문제를 해결하려면 아래와 같이 작성한다.
const F_HEADER_BKEY = 'test_key' as const
const defaultH: { [key: string]: string } = {
'key-str': 'value-str',
}
defaultH[F_HEADER_BKEY] = "aaa"
//결과
[LOG]: "show value = ", {
"key-str": "value-str",
"test_key": "aaa"
}
맨 처음처럼 나타나는 이유는 내가 선언한 defaultH의 경우,
타입이 다음과 같이 { [key: string]: string } 되어 있다.
즉 key는 string으로 되어 있기에 string 자체로 인식을 해버린다.
하지만 두 번째 제대로 나오는 부분에서는 Object형에 접근하기 위한 값으로 사용한 것이,
문자열(string)이 아닌 **상수 문자열(string literal)**를 사용했기에 적용이 된다.
그리고 하나 더 찝어서 이야기 하면 아래와 같은 코드도 동작이 안될 것이다.
defaultH.F_HEADER_BKEY = "aaa"
//결과
[LOG]: "show value = ", {
"key-str": "value-str",
"test_key": "aaa",
"F_HEADER_BKEY": "a111"
}
이거는 간단한 것이 object에 .(Dot)으로 접근 시 해당 키를 이용해 접근하는 방식이다.
하지만 여기도 F-HEADER_BKEY를 문자열 자체로 받아들인다.
이게 문자열 상수 값으로 받아들여지지 않고, 그냥 문자열 자체로 받아들인단 의미다.
그래서 혹시 선언된 객체형에 키를 동적으로 쓰고 싶다면 [] 형식을 이용해서 접근하는게 좋다.
3. 반복적 코드를 줄이는 리팩토링
백엔드 측을 개발하면서 다음과 같은 코드가 있었다.
(아래 코드는 예시를 위해 사용하여 일부 변수 및 내용이 코드 문법에 어긋날 수 있습니다.)
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'Jwt') {
constructor(
@Inject(VERIFY_JWT_OUTBOUND_PORT)
private readonly verifyjwtOutboundPort: VerifyJwtOutboundPort,
@Inject(DECODE_JWT_OUTBOUND_PORT)
private readonly decodJwtOutboundPort: DecodeJwtOutboundPort,
@Inject(CREATE_JWT_OUTBOUND_PORT)
private readonly createJwtOutboundPort: CreateJwtOutboundPort,
@Inject(FIND_ACCOUNT_EMAIL_JOINTYPE_STATUS_OUTBOUND_PORT)
private readonly findAccountEmailJoinTypeOutboundPort: FindAccountEmainJoinTypeStatusOutboundPort,
@Inject(LOGOUT_ACCOUNT_INBOUND_PORT)
private readonly logoutAccountInboundPort: LogoutAccountInboundPort,
) {
super();
}
async validate(req: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>, options?: any) {
const res = req.res;
const jwt = req.header(HEADER_JWT);
/**
* JWT가 존재하지 않는 경우 에러 처리
*/
if (!jwt) {
throw new AuthException(AuthJwtErrorCode.jwtNotFound());
}
const account: Account = await this.getAccountInJwt(jwt, JWT_TYPE.ACCESS_TOKEN);
//...Some work
}
//JWT 내에서 계정 정보를 가져오는 함수
private async getAccountInJwt(jwt: string, tokenType: JwtType): Promise<Account> {
//Access가 만료된 경우 리프레시 토큰도 체크해줘야 한다.
const jwtData = this.decodJwtOutboundPort.accessJwtDecode(jwt);
//계정 정보를 가져온다.
return await this.findAccountEmailJoinTypeOutboundPort.excute({
u_email: jwtData.email,
ua_joinType: jwtData.joinType,
uStatus: ACCOUNT_STATUS_TYPE.ACTIVE,
uAccAuth: jwtData.auth,
});
}
}
근데 코드를 작성하다보니...getAccountInJwt 함수의 경우 다른 곳에서도 비슷한 방식으로 사용되고 있었다.
쉽게말해 중복적으로 코드가 작성되고 있었다.
이 부분은 Inbound Port 성향의 서비스 로직으로 분리할 수 있었다.
난 Port & Adaptor 아키텍쳐 방식을 사용할 때 In과 Out을 저번에 받은 강의 내요을 토대로 사용한다.
In 성향은 외부 세계에서 요청이 오는 경우로 나누고,
Out은 백엔드 내에서 처리하여 인프라 쪽과 연계되는 경우로 나눈다.
지금처럼 Jwt를 받아서 그 안에 담겨있는 정보를 요청해야 하는 경우,
이 성향은 Inbound 성향이라 할 수 있다.
설령 내부에서 호출한다 해도 jwt라는 속성 자체가 외부에서 오기 때문이다.
물론 인프라 측의 DB 로직을 한번 타긴 하는데, 난 이런 경우 처음 접근하는 변수를 기준으로 잡는다.
그래서 이 부분은 Inbound Port로 만들었다.
//Inbound Port 정의
import { JwtType } from 'src/domain/auth/types/jwt.enums';
export const GET_ACCOUNT_IN_JWT_INBOUND_PORT = 'Get_Account_In_Jwt_Inbound_Port' as const;
export type GetAccountInJwtInputDto = {
jwt: string;
jwtType: JwtType;
};
export type GetAccountInJwtOutputDto = {};
export interface GetAccountInJwtInboundPort {
excute(params: GetAccountInJwtInputDto);
}
//인바운드 인터페이스 구현체 서비스
export class GetAccountInJwtService implements GetAccountInJwtInboundPort {
constructor(
@Inject(DECODE_JWT_OUTBOUND_PORT)
private readonly decodJwtOutboundPort: DecodeJwtOutboundPort,
@Inject(FIND_ACCOUNT_EMAIL_JOINTYPE_STATUS_OUTBOUND_PORT)
private readonly findAccountEmailJoinTypeOutboundPort: FindAccountEmainJoinTypeStatusOutboundPort,
) {}
async excute(params: GetAccountInJwtInputDto) {
//Access가 만료된 경우 리프레시 토큰도 체크해줘야 한다.
const jwtData = this.decodJwtOutboundPort.accessJwtDecode(params.jwt);
//계정 정보를 가져온다.
return await this.findAccountEmailJoinTypeOutboundPort.excute({
u_email: jwtData.email,
ua_joinType: jwtData.joinType,
uStatus: ACCOUNT_STATUS_TYPE.ACTIVE,
uAccAuth: jwtData.auth,
});
}
}
위와 같이 별도 Inbound Service로 분리할 경우 아래와 같이 간단하게 작성할 수 있었다.
constructor(@Inject()) {} //여기서 아래의 getAccountInJwtService 인터페이스 주입을 해준다.
//Get Account
const account: Account = await this.getAccountInJwtService.excute({ jwt, jwtType: JWT_TYPE.ACCESS_TOKEN });
//기존 코드
//const account: Account = await this.getAccountInJwt(jwt, JWT_TYPE.ACCESS_TOKEN);
이렇게 함으로써 다른 영역에서도 해당 로직을 재사용할 수 있게 되었다.
정리
개발을 하면서 계속 더 나은 코드를 작성하는 건 뭐랄까...
완벽할 수 없지만, 완벽에 가까워지도록 하는 형태가 된다고 해야 할까?
그런 의미로 리팩토링은 참 재미있는 것 같다.