kakasoo

Nestia에서 typeof, namespace를 쓰면 안 됩니다 본문

프로그래밍/NestJS

Nestia에서 typeof, namespace를 쓰면 안 됩니다

카카수(kakasoo) 2023. 4. 9. 15:42
반응형

왜 안 되는데요?

Nestia는 NestJS로 작성된 백엔드 코드를 읽고 TypeScript compiler를 이용해서 프론트에서 사용 가능한 SDK를 만든다.

그래서 지금 사용하면 곤란한 TypeScript keyword들이 있는데, 하나는 typeof고 하나는 namespace이다.

typeof의 경우 타입의 이름을 추론해낼 수 없게 되는 문제가 있다.

namespace는 한 함수가 동일 네임스페이스로부터 두 개 이상의 내부 인터페이스, 타입을 가져올 때 문제가 될 수 있다.

이유는 import 시 두 타입이 하나의 인터페이스로부터 추론된다는 것을 컴파일러가 인지하기 어렵기 때문에 하나만 갖고 오게 되서다.

 

https://kscodebase.tistory.com/663

 

모든 타입이 추론되는 API 만들기 ( feat. Nestia )

이 글은 다음의 내용을 다룬다. 프론트 개발자들이 눈치채면 곤란한 백엔드 이야기 백엔드 개발자가 프로그래밍 언어가 Node.js 라서 얻을 수 있는 이점 Nestia 라이브러리를 이용한 Swagger 문서 및 S

kscodebase.tistory.com

따라서 내가 이전에 쓴 글에 있던 것처럼 에러를 추론하는 방법은 SDK가 부적절하게 생성될 위험이 있다.

 

그럼 어떻게 해야 하는데요?

export const ERROR: Record<string, ErrorResponse> = {
  ALREADY_CREATED_EMAIL: { result: false, code: 4001, data: '이미 생성된 이메일입니다.' },
  NO_AUTH_TOKEN: { result: false, code: 4002, data: '인증이 필요합니다.' },
  IS_SAME_POSITION: { result: false, code: 4003, data: '이미지의 정렬 값이 동일한 경우가 존재합니다.' },
  CANNOT_FINDONE_ARTICLE: { result: false, code: 4004, data: '게시글을 찾지 못했습니다.' },
  SELECT_MORE_THAN_ONE_BODY_IMAGE: { result: false, code: 4005, data: '적어도 1장 이상의 이미지를 골라야 합니다.' },
  NOT_FOUND_ARTICLE_TO_COMMENT: { result: false, code: 4006, data: '댓글을 작성할 게시글을 찾지 못했습니다.' },
  TOO_MANY_REPORTED_ARTICLE: { result: false, code: 4007, data: '신고가 접수된 게시글이라 댓글 작성이 불가능합니다.' },
  ALREADY_FOLLOW_USER: { result: false, code: 4008, data: '이미 좋아요를 누른 유저입니다!' },
  CANNOT_FIND_ONE_DESIGNER_TO_FOLLOW: { result: false, code: 4009, data: '팔로우할 유저를 찾지 못했습니다.' },
  STILL_UNFOLLOW_USER: { result: false, code: 4010, data: '아직 팔로우한 적 없는 유저에요!' },
  CANNOT_FIND_ONE_DESIGNER_TO_UNFOLLOW: { result: false, code: 4011, data: '언팔로우할 유저를 찾지 못했습니다.' },
  CANNOT_FIND_ONE_REPLY_COMMENT: { result: false, code: 4012, data: '답글을 달 댓글을 찾지 못했어요.' },
  ALREADY_CREATED_PHONE_NUMBER: { result: false, code: 4013, data: '이미 생성된 전화번호입니다.' },
  ARLEADY_REPORTED_ARTICLE: { result: false, code: 4014, data: '이미 신고한 게시글입니다.' },
  IS_NOT_WRITER_OF_THIS_ARTICLE: { result: false, code: 4015, data: '이 게시글의 작성자만이 수정할 수 있습니다.' },
  CANNOT_FIND_ONE_COMMENT: { result: false, code: 4016, data: '해당 댓글을 찾지 못했습니다.' },
  CANNOT_FOLLOW_MYSELF: { result: false, code: 4017, data: '설마 자기 자신을 팔로우하려고 했어요?!' },
} as const;

왜냐하면 내가 만든 에러는 이러한 형태의 객체였고, 여기서 typeof ERROR.ALREADY_CREATED_EMAIL 형식을 취했기 때문이다.

이걸 변환해서 네임스페이스를 만들고, 네임스페이스 안에다가 인터페이스들을 정의할 수도 있다.

하지만 네임스페이스로 바꾸게 될 경우에도 SDK의 경로가 제대로 추적되지 않는 문제가 발생한다.

따라서 모듈을 만들기 위해서 네임스페이스를 만들기보다, 아예 모듈로 구분지을 것들을 하나의 파일로 모아 관리하는 것이 낫다.

 

// business-error.ts

export const ERROR: Record<string, ErrorResponse> = {
  // 생략
} as const;

파일 명을 ERROR로 퉁치는 대신에, 에러를 구조를 만들 수 있도록 분리해주자.

추후 에러를 런타임에서 관리할 때를 대비해서 index.ts로 모아서 export 해주는 등 구조 관리를 해주는 것도 좋다.

하지만 타입을 관리할 때는 무조건 파일 단위로 인터페이스를 나눠서 경로를 가지게 하지 말자.

 

export declare namespace NAMESPACE {
  interface INTERFACE {
    name: string;
  }
}

그 다음으로는 namespace나 typeof 키워드들을 모두 제거하는 일이 필요하다.

이런 형태의 네임스페이스를 쓰게 될 경우, 그 네임스페이스로부터 내부 인터페이스를 추론할 수는 있다.

이 경우에는 NAMESPACE.ITERFACE 와 같이 점 연산자로 표기하게 되는데, 이 상태에서는 아무런 문제가 없다.

하지만 하나의 함수의 SDK를 추론해낼 때, 동일한 네임스페이스로부터 두 개 이상의 인터페이스를 추론할 때는 에러가 발생할 수 있다.

 

결론만 말하면...

export interface ERROR {
  type: string;
  result: false;
  code: number;
  data: string;
}

export const isBusinessErrorGuard = (obj: any): obj is ERROR & { type: 'business' } => {
  if (isErrorGuard(obj)) {
    if (obj.type === 'business') {
      return true;
    }
  }
  return false;
};

export const isErrorGuard = (obj: any): obj is ERROR => {
  if (obj.result === false) {
    return true;
  }
  return false;
};

export interface ALREADY_CREATED_EMAIL extends ERROR {
  type: 'business';
  result: false;
  code: 4001;
  data: '이미 생성된 이메일입니다.';
}

 

1. 에러를 구조적으로 나누기 위해 네임스페이스를 사용하는 것은 금지.

2. 에러를 만들고 타입스크립트를 사용하는 것도 금지.

3. 따라서 에러 타입을 정의한 후, 그 에러 타입에 맞는 에러들을 정의하고, 내부 프로퍼티로 구조를 결정할 type 프로퍼티 정의

4. 에러를 인터페이스로 쓰게 될 경우, 이전처럼 에러를 리턴하거나 그런 행위가 불가능해지는데?

 

/**
 * @summary 230129 - 게시글 작성 / 임시저장 기능이 추가되어야 한다. (incompleted)
 * @tag articles
 
  * @param userId 글을 쓰고자 하는 작성자의 id
  * @param createArticleDto 게시글의 정보
  * @returns
  */
@TypedRoute.Post()
public async writeArticle(
  @UserId() userId: number,
  @TypedBody() createArticleDto: CreateArticleDto,
): Promise<TryCatch<ArticleType.DetailArticle, CANNOT_FINDONE_ARTICLE | IS_SAME_POSITION>> {
  const savedArticle = await this.articlesService.write(userId, createArticleDto);
  if (isErrorGuard(savedArticle)) {
    return savedArticle;
  }

  const article = await this.articlesService.getOneDetailArticle(userId, savedArticle.id);
  if (!article) {
    return typia.random<CANNOT_FINDONE_ARTICLE>();
  }
  return createResponseForm(article);
}

만들어진 에러 타입은 모든 내부 값들이 상수로 정의되어 있기 때문에 typia.random<T>()을 이용해서 리턴해주면 된다.

해당 타입에 속하는 랜덤한 객체를 만들어주는 메서드인데, 여기서 상수로만 정의되어 있기 때문에 무조건 동일한 값이 추론된다.

즉, 인터페이스를 값처럼 다룰 수 있다.

반응형