드디어 API 작성의 대장정이 마무리가 된다..

그리고 간만에 기분 좋게 글을 쓴다.. 테스트는 성공적이었다!! 

정말, 이 맛에 개발하는구나 싶었다. 머신러닝 전공이라, 개발보다는 머신러닝을 먼저 접했는데 

처음 가동하던 머신러닝 모델이 잘 돌아갈 때 느꼈던 희열을 다시금 느낄 수 있었다...

이 맛에 개발하는구나..? 개발하면서 스트레스 받을 일이 한 두 번이 아닐 것 같은데, 막상 잘 테스트 되는 것을 보니까 그동안 쌓였던 스트레스가 싹 씻겨 내려가는 기분... 

거두절미하고 테스트 결과 공개!

 

 @GET 엔드포인트로 잘 생성된 것 확인!

 

처음에 false 나와서, 엥 맞는 양식인데 왜?? 싶었는데

알고 보니 맞는 양식이면 false고 틀리면 true로 나오게 설정했던 것이다..ㅋㅋ 이런 멍청..

뭐 어찌되었건 맞게 동작한다는 얘기니까 그런 건 아무래도 좋다...!

양식 틀리면 true! 역시 맞게 잘 작동하는 것 확인!

이제 이 코드를 올린 다음에, 협업하시는 분과 코드 리뷰 시간을 가진 다음, 그 결과에 맞게 적절히 리팩토링 하려고 한다!

그래... 의존성 주입... 분명 자바에도 이런거 있었던거같은데...

해당 오류는 NestJS의 의존성 주입(Dependency Injection) 시스템에서 UserController 클래스에 필요한 의존성을 해결할 수 없다는 것을 나타냅니다. 구체적으로는 EmailService가 ApiRestModule 컨텍스트에서 사용 가능하지 않은 상태입니다.

이러한 오류를 해결하기 위해 다음과 같은 점검 사항들을 확인해 보세요:

  1. ApiRestModule이 올바른 NestJS 모듈인지 확인하세요: ApiRestModule 클래스가 정상적인 NestJS 모듈로 작성되어 있는지 확인합니다. @Module() 데코레이터를 사용하여 올바르게 설정되었고, 필요한 프로바이더(Provider)와 의존성 등록이 이루어져 있는지 다시 한번 검토합니다.
  2. EmailService가 현재의 ApiRestModule에 포함되어 있는지 확인하세요: EmailService가 현재의 ApiRestModule에서 제공자(Provider)로 등록되어 있는지 확인합니다. 즉, providers 배열 또는 exports 배열 내에 해당 서비스(EmailService)가 포함되어 있어야 합니다.
  3. 별도의 @Module에서 EmailService를 내보내고 현재의 ApiRestModule에서 가져오고 있는지 확인하세요: 만약 EmailService가 별도의 모듈(EmailModule)에서 정의되고 내보내진 경우, 해당 모듈(EmailModule)이 현재의 ApiRestModule 내부에서 임포트(import)되었는지 다시 한번 확인합니다.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOneOptions } from 'typeorm';
import { User } from '../../domains/entities/User.entity';

@Injectable()
export class EmailService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}

validateEmailFormat(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+.[^\s@]+$/;
return emailRegex.test(email);
}

async checkDuplicateEmail(email: string): Promise<boolean> {
const options: FindOneOptions<User> = {
where: {
userEmail: email,
},
};

const existingUser = await this.usersRepository.findOne(options);
return !!existingUser;
}
}

EmailService라는 NestJS 서비스 클래스입니다. 이 클래스는 이메일 관련 기능을 수행하며, 주로 사용자 이메일의 유효성 검사와 중복 여부 확인을 담당합니다.

여기서 몇 가지 주요 포인트를 설명해 드리겠습니다:

  1. @Injectable(): EmailService 클래스에 @Injectable() 데코레이터가 지정되어 있습니다. 이를 통해 해당 클래스가 NestJS의 의존성 주입(Dependency Injection) 시스템에서 관리되는 서비스임을 나타냅니다.
  2. @InjectRepository(User): TypeORM의 Repository<User> 타입의 인스턴스를 주입받기 위해 @InjectRepository() 데코레이터가 사용되었습니다. 이를 통해 User 엔티티에 대한 Repository 객체(usersRepository)가 생성되고, 해당 객체를 통해 데이터베이스 조작이 가능해집니다.
  3. validateEmailFormat(email: string): boolean: 입력된 이메일 문자열(email)이 유효한 형식인지 확인하는 메서드입니다. 정규식(emailRegex)을 사용하여 유효성 검사를 수행하고, 결과로 boolean 값을 반환합니다.
  4. async checkDuplicateEmail(email: string): Promise<boolean>: 입력된 이메일 문자열(email)이 이미 데이터베이스에 존재하는 중복된 이메일인지 확인하는 메서드입니다. TypeORM의 findOne() 메서드를 사용하여 해당 이메일과 일치하는 사용자(User) 엔티티를 찾아옵니다. 찾아온 결과가 존재한다면 중복된 이메일로 간주하고 true 값을 반환합니다.

위 코드에서는 NestJS와 TypeORM을 활용하여 EmailService 클래스가 데이터베이스와 상호작용하며, 유효성 검사 및 중복 여부 확인과 같은 기능을 수행합니다.

 

@Module({
  imports: [PassportModule],
  providers: [EmailService], // EmailService를 providers 배열로 이동
  controllers: [AuthController, UserController, MatchController, BillController, PlaceController],
})
export class ApiRestModule {}

컨트롤러야 그냥 컨트롤러 정의된거 때려 넣으면 되는데,

imports, providers 이거 구분 은근 헷갈려서 잘 살펴서 넣어야 한다. 안그럼

[Nest] 25564 - 2023. 10. 18. 오후 10:25:01 ERROR [ExceptionHandler] Classes annotated with @Injectable(), @Catch(), and @Controller() decorators must not appear in the "imports" array of a module. Please remove "EmailService" (including forwarded occurrences, if any) from all of the "imports" arrays.

Scope [AppModule -> ApiRestModule]

이렇게 오류난다... 여기서 삽질이 한 4~5번 정도 있었는데, 여기까지 고치고 나니 문득 드는 생각...

'근데 유저 리포지토리는...?'

후... 급하게 추가..

@Module({
  imports: [PassportModule]
  providers: [EmailService, UserRepository], // UserRepository 추가
  controllers: [AuthController, UserController, MatchController, BillController, PlaceController],
})
export class ApiRestModule {}

이러면 이제 동작은 되는데, 저장하는데서 오류가 난다...

그렇게 삽질하길 몇 번 반복... 드디어 동작하고, 저장도 완벽하게 되는 올바른 코드는...

 

@Module({
  imports: [TypeOrmModule.forFeature([User]), PassportModule],
  providers: [EmailService],
  controllers: [AuthController, UserController, MatchController, BillController, PlaceController],
})
export class ApiRestModule {}

이거였다고 한다.. 애초에 내가 쓰던 모듈이  TypeOrmModule 이었는데 이걸 import해주지 않았고, 

유저 리포지토리라고 해서 냅다 유저 리포지토리 때려 넣는게 아니라..ㅋㅋ forFeature([User])를 써야 한다는 걸 알았다.

큰 깨달음을 얻고 가는 것 같다. 이로써 마지막으로 테스트만 해보면 끝...!

제발 테스트야 잘 되어줘,,,!

src/modules/api.rest/user.controller.ts:12:30 - error TS2306: File '대충 파일 주소' is not a module.

주어진 오류 메시지는 TypeScript 컴파일러가 C:/Users/LG/Livey_api/src/modules/api.rest/email.service.ts 파일을 모듈로 인식하지 못한다는 것을 나타냅니다. 이는 해당 파일이 모듈로 내보내기(exports)가 제대로 설정되지 않았거나, 잘못된 형식으로 import되었을 가능성이 있습니다.

해당 오류를 해결하기 위해 다음과 같은 단계를 따라갈 수 있습니다:

  1. email.service.ts 파일 확인: 먼저 C:/Users/LG/Livey_api/src/modules/api.rest/email.service.ts 파일의 내용을 확인합니다. 해당 파일이 모듈로 정의되어 있는지 확인하고, 올바른 형식으로 작성되었는지 다시 한번 검토합니다.
  2. 모듈 내보내기 설정: email.service.ts 파일에서 해당 서비스 클래스(EmailService)를 export하는 부분이 있는지 확인합니다. 예를 들어, 아래와 같은 코드가 있어야 합니다:
  3. typescript
    export class EmailService {
      // ...
    }
    
  4. import 구문 확인: UserController 클래스에서 EmailService를 import하는 부분을 검토합니다. 아마도 다음과 같은 코드일 것입니다:이때 경로('./email.service')가 올바른지 다시 한번 확인하세요.
  5. typescript
    import { EmailService } from './email.service';
    
  6. tsconfig.json 설정: 프로젝트의 tsconfig.json 파일에 "moduleResolution" 옵션이 올바르게 설정되어 있는지 확인합니다. 일반적으로 "moduleResolution" 값은 "node"나 "classic" 중 하나여야 합니다.

위 단계들을 따라가면서 오류 원인을 찾아 수정할 수 있습니다. TypeScript 컴파일러에서 발생한 이 오류는 주로 모듈 정의와 import/export 관련 문제에 의해 발생하므로, 해당 부분들에 집중하여 검토하면 도움이 될 것입니다.

 

라고 하길래...

{
  "compilerOptions": {
    // 다른 설정들...
    "moduleResolution": "node"
  }
}
"moduleResolution" 옵션은 모듈 해석 방식을 지정하는 옵션입니다. 
TypeScript는 두 가지 주요한 모듈 해석 방식을 제공합니다:

"node": 이 옵션은 Node.js 환경에서 모듈을 해석하는 방식으로,
Node.js의 내장 모듈 해석 알고리즘을 사용합니다. 일반적으로 Node.js 기반 프로젝트에서 사용됩니다.
"classic": 이 옵션은 예전 버전의 JavaScript 모듈 시스템과 호환되는 모듈 해석 방식입니다. 
이는 브라우저 환경이나 구형 JavaScript 프로젝트에서 사용될 수 있습니다.

일단 이거 먼저 추가해주고...

Error: Nest can't resolve dependencies of the UserController (Symbol(UserUseCase), ?). Please make sure that the argument EmailService at index [1] is available in the ApiRestModule context.

킹받는  새로운 오류 하나가 또 뜨길래...

Potential solutions:

  • Is ApiRestModule a valid NestJS module?
  • If EmailService is a provider, is it part of the current ApiRestModule?
  • If EmailService is exported from a separate @Module, is that module imported within ApiRestModule?

NestJS의 의존성 주입(Dependency Injection) 시스템에서 UserController 클래스에 필요한 의존성을 해결할 수 없다는 것을 나타냅니다. 구체적으로는 EmailService가 ApiRestModule 컨텍스트에서 사용 가능하지 않은 상태입니다.

이러한 오류를 해결하기 위해 다음과 같은 점검 사항들을 확인해 보세요:

  1. ApiRestModule이 올바른 NestJS 모듈인지 확인하세요: ApiRestModule 클래스가 정상적인 NestJS 모듈로 작성되어 있는지 확인합니다. @Module() 데코레이터를 사용하여 올바르게 설정되었고, 필요한 프로바이더(Provider)와 의존성 등록이 이루어져 있는지 다시 한번 검토합니다.
  2. EmailService가 현재의 ApiRestModule에 포함되어 있는지 확인하세요: EmailService가 현재의 ApiRestModule에서 제공자(Provider)로 등록되어 있는지 확인합니다. 즉, providers 배열 또는 exports 배열 내에 해당 서비스(EmailService)가 포함되어 있어야 합니다.
  3. 별도의 @Module에서 EmailService를 내보내고 현재의 ApiRestModule에서 가져오고 있는지 확인하세요: 만약 EmailService가 별도의 모듈(EmailModule)에서 정의되고 내보내진 경우, 해당 모듈(EmailModule)이 현재의 ApiRestModule 내부에서 임포트(import)되었는지 다시 한번 확인합니다.

이때는 몰랐다... API만들면서 가장 빡치는 순간이 다가오고 있다는 것을..

Swagger UI에서 요청 바디(Request Body)를 입력하고 실행하는 방법은 다음과 같습니다:

  1. Swagger UI 페이지에서 원하는 API 엔드포인트와 요청 메소드를 선택합니다.
  2. 해당 엔드포인트의 "Request Body" 섹션을 찾습니다. 이 섹션은 대개 "Parameters" 또는 "Request Payload" 등의 이름으로 표시됩니다.
  3. 요청 바디의 스키마(Schema)를 확인하고 필요한 필드와 값에 대한 정보를 파악합니다.
  4. 요청 바디 입력란을 찾아 값을 입력합니다. 일반적으로 JSON 형식으로 데이터를 입력하며, 필요한 필드와 값을 적절히 설정해야 합니다.
  5. 입력이 완료되면 "Execute" 또는 "Try it out" 버튼을 클릭하여 API 호출을 실행합니다.
  6. 실행 결과가 나타나며, 응답 코드(HTTP 상태 코드), 응답 본문(JSON 형식) 및 기타 응답 정보가 제공됩니다.

따라서, Swagger UI에서 Request Body 입력란을 사용하여 원하는 데이터를 제공하고, 실행 버튼을 클릭하여 API 호출을 수행할 수 있습니다. 이렇게 하면 해당 API의 유효성 검사 테스트가 진행됩니다.

 

음.. 그래 UI 맘에 들고, 다 좋은데, 막상 실행해보면,

{"statusCode":404,"message":"Cannot GET /api","error":"Not Found"} 왤까?

 

주어진 오류 메시지인 "Cannot GET /api"는 요청한 엔드포인트가 존재하지 않음을 나타냅니다. 이는 Nest.js 애플리케이션에서 해당 경로에 대한 라우트가 정의되지 않았거나, 잘못된 경로를 요청했을 가능성이 있습니다.

해결하기 위해 다음 단계를 따를 수 있습니다:

  1. 라우트 확인: Nest.js 애플리케이션에서 /api 경로에 대한 적절한 라우트가 정의되어 있는지 확인하세요. main.ts 또는 해당 프로젝트의 라우팅 파일에서 /api 경로에 대한 핸들러 함수나 컨트롤러 메서드를 찾아보세요.
  2. 올바른 경로 사용: 요청하는 엔드포인트의 경로가 올바른지 확인하세요. Nest.js 애플리케이션에서 /api 외부에 다른 접두사나 다른 경로 구조를 사용하는 경우, 그에 맞게 요청을 수정해야 합니다.
  3. HTTP 메서드 확인: API 테스트 도구(Swagger, Postman 등)를 사용하여 정확한 HTTP 메서드(GET, POST 등)와 함께 /api 엔드포인트에 요청을 보내세요.
  4. 서버 실행 상태 확인: Nest.js 서버가 실행 중인지 확인하세요. 포트 충돌 문제 없이 서버가 제대로 실행되고 있는지 확인하고, 필요하다면 서버를 재시작하세요.테스트 결과가 제대로 작동하지 않고 잘못된 형식에 대한 반응이 오지 않는다면 몇 가지 가능한 원인과 해결 방법을 살펴볼 수 있습니다.

위 절차를 따라서, 어찌저찌 고치긴 했다... 그런데 이번엔 API 호출이 말썽이다 ㅡㅡ;;

테스트 결과가 제대로 작동하지 않고 잘못된 형식에 대한 반응이 오지 않는다면 몇 가지 가능한 원인과 해결 방법을 살펴볼 수 있습니다:

  1. 요청 바디 형식 확인: 먼저, 요청 바디의 형식과 필드를 정확하게 확인해야 합니다. Swagger UI에서 제공하는 스키마(Schema)를 기반으로 요청 바디를 구성해야 합니다. 필수 필드, 데이터 타입, 유효성 검사 규칙 등을 올바르게 설정해야 합니다.
  2. 오류 메시지 확인: 잘못된 형식의 요청을 보냈을 때 Swagger UI에서 오류 메시지나 경고 사항이 표시되어야 합니다. 이를 통해 어떤 부분이 잘못되었는지 파악할 수 있습니다. 따라서 실행 결과에 오류 메시지가 표시되는지 확인하고 해당 내용을 분석합니다.
  3. 서버 로그 확인: 만약 Swagger UI에서는 문제가 없어 보이는데도 API 호출 결과가 예상과 다르다면, 서버 측 로그를 확인해야 할 수도 있습니다. 서버 로그에서 API 호출 시 발생하는 에러나 예외 정보를 찾아보고 문제 해결에 도움을 받을 수 있습니다.
  4. 다른 도구 사용: Swagger UI 외에도 다른 API 테스트 도구(예: Postman)를 사용하여 같은 요청을 보내보세요. 이렇게 함으로써 Swagger UI 자체의 문제인지 아니면 API 구현 자체의 문제인지 판단할 수 있습니다.
  5. 엔드포인트 및 버전 호환성 확인: API의 엔드포인트와 버전 호환성도 주의해야 합니다. Swagger 스펙 파일 또는 URL이 정확한 버전 및 엔드포인트 정보와 일치하는지 다시 한번 확인하세요.
  6. 라우팅 설정 확인: NestJS의 라우팅 설정을 확인하여 /users/validate-email 경로가 올바르게 매핑되어 있는지 확인하세요. UserController 클래스에 @Controller('users') 데코레이터가 지정되어 있으므로, /users/validate-email 경로에 대한 GET 요청을 처리할 수 있어야 합니다.
  7. validateEmail() 메서드 확인: validateEmail() 메서드는 주어진 이메일을 검증하고 유효성 여부를 반환하는 역할을 합니다. 이 메서드 내에서 필요한 로직(이메일 형식 검사, 중복 이메일 검사 등)이 정확하게 구현되었는지 다시 한번 확인해 보세요.
  8. 응답 반환: validateEmail() 메서드에서 유효성 여부에 따라 적절한 응답 값을 반환해야 합니다. 현재 코드에서는 Promise<boolean> 타입으로 유효성 여부를 반환하고 있습니다. 그러나 클라이언트에게 실제 응답을 보내기 위해서는 해당 boolean 값을 HTTP 응답으로 변환하여 전송해야 합니다.
  9. Swagger 문서화: Swagger 문서화와 관련된 부분도 점검해 보세요. @Get('validate-email') 엔드포인트에 대한 Swagger 데코레이터(@ApiTags, @ApiResponse, 등)가 올바르게 설정되었는지 다시 한번 확인합니다.

무슨 확인해야 할 게 이렇게 많은지..;;

얘는 아무래도 코드 문제인것같아서 코드를 다시한번 살펴보았다.

@Get('validate-email')
async validateEmail(@Query('email') email: string): Promise<boolean> {
const isValidFormat = this.emailService.validateEmailFormat(email);
if (!isValidFormat) {
return false;

내가 테스트하려는 API 엔드포인트는 바로 이것. 이메일 유효성 검사를 GET으로 요청하였다.

NestJS를 사용한 UserController 클래스입니다. 
이 컨트롤러는 사용자 관련 API 엔드포인트를 정의하고 있습니다. 
각 엔드포인트에는 Swagger 데코레이터가 사용되어 API 문서화가 이루어지고 있습니다.

여기서 몇 가지 주요 포인트를 설명해 드리겠습니다:

@ApiTags('users'): Swagger 문서에서 해당 컨트롤러에 대한 태그를 지정합니다. 
여기서는 'users'라는 태그로 분류됩니다.
@Post('signup'), @Post('profile-image'), @Get('validate-email'): 
각각 회원 가입, 프로필 이미지 업로드, 이메일 유효성 검사와 관련된 API 엔드포인트입니다.
@ApiResponse({ type: SignupResponse }): 회원 가입 API의 응답 스키마로 SignupResponse 
클래스를 사용한다는 것을 나타냅니다.
async signup(@Body() body: SignUpRequest): 
회원 가입 API에서 요청 바디의 형식으로 SignUpRequest 클래스를 사용하고 있습니다.
async uploadProfileImage(@CtxUser() user: UserContext, @UploadedFile() image: Express.Multer.File): 
프로필 이미지 업로드 API에서 CtxUser 데코레이터를 사용하여 인증된 사용자 정보(UserContext)를 가져오고, 
UploadedFile 데코레이터를 통해 업로드된 파일(image)을 받아옵니다.
async validateEmail(@Query('email') email: string): Promise<boolean>: 
이메일 유효성 검사 API에서 쿼리스트링으로 전달받은 이메일 값을 검증하고 결과(boolean)를 반환합니다.
위 코드에서는 Swagger 데코레이터와 NestJS의 기능들을 활용하여 API 문서화 및 요청 처리 로직을 
구현하고 있습니다.

삽질기 시즌 2는 계속된다...

고진감래라고, 힘겹게 완성한 API가 잘 동작해주기만 하면 그동안의 스트레스가 싹 날아갈 것만 같다.

하지만 테스트하는 순간까지 고진감래는 커녕 고진고래(...)인 상태가 지속되었다.

먼저, API 명세서 작성을 하였다.(사실 지난번 코드에 명시된 바 있다) 일단 여기까지는 순조로웠다..

 

  1. 테스트 환경 설정: 테스트하기 전에 Nest.js 프로젝트가 실행 가능한 상태인지 확인해야 합니다. 필요한 종속성이 설치되었는지 확인하고, 프로젝트를 실행할 수 있는 상태인지 확인하세요.
  2. API 엔드포인트 확인: 이메일 유효성을 확인하는 API의 엔드포인트 경로와 HTTP 메서드를 알아야 합니다. 일반적으로 /api/email/validate 또는 유사한 경로일 수 있습니다. 해당 엔드포인트의 요청 방식(GET, POST 등)을 알아두세요.
  3. API 요청 보내기: 선택한 API 테스트 도구(예: Postman, cURL, Insomnia 등) 를 사용하여 해당 엔드포인트에 요청을 보낼 수 있습니다.
    • GET 요청의 경우:
    •  
      GET http://localhost:3000/api/email/validate?email=test@example.com
      
    • POST 요청의 경우:
    •  
      POST http://localhost:3000/api/email/validate
      Content-Type: application/json
      
      {
        "email": "test@example.com"
      }
      
    위 예시에서 http://localhost:3000은 Nest.js 서버가 실행 중인 로컬 호스트 및 포트 번호입니다. 실제로 사용하는 호스트 및 포트 번호에 맞게 수정하세요.
  4. 응답 분석: 서버에서 반환된 응답을 분석하여 이메일 유효성 검사 결과를 확인할 수 있습니다. 일반적으로 성공 또는 실패 여부와 함께 추가 정보가 포함됩니다.

위 단계들을 따라 진행하면서 이메일 유효성 검사 API를 테스트해보세요. 필요에 따라 API 동작과 관련된 추가적인 정보나 코드 스니펫을 제공해주시면 보다 구체적으로 도움을 드릴 수 있습니다.

(*스니펫: 작은 코드 조각이라는 뜻으로, 재사용 가능한 소스 코드, 기계어, 텍스트의 작은 부분을 일컫는 프로그래밍 용어이다. 사용자가 루틴 편집 조작 중 반복 타이핑을 회피할 수 있게 도와준다.)

라는데... 우리 프로젝트는 Swagger 라는 혁신적인(?) UI가 탑재된, Web API 문서화를 위한 프레임워크를 사용 중이었기 때문에 전혀 위와 같은 과정이 필요 없다! 지금까지는 Swagger만큼 가독성 좋고 편한 프레임워크는 못 본 것 같다.

 

Nest.js 프로젝트에 Swagger를 사용하여 API 문서화를 했다면, Swagger UI를 통해 API를 테스트할 수 있습니다.

Swagger UI는 프로젝트의 엔드포인트와 해당 엔드포인트에 대한 요청을 시각적으로 제공하며, 테스트하기 쉽게 만들어줍니다.

아래는 Swagger UI를 사용하여 API를 테스트하는 방법입니다:

  1. Nest.js 서버 실행: 먼저, Nest.js 프로젝트가 실행 중이어야 합니다. 필요한 종속성이 설치되었는지 확인하고, npm run start 또는 yarn start 명령을 사용하여 서버를 실행하세요.
  2. Swagger UI 접속: 웹 브라우저에서 다음 URL을 입력하여 Swagger UI에 접속합니다:위 예시에서 http://localhost:3000은 Nest.js 서버가 실행 중인 로컬 호스트 및 포트 번호입니다. 실제로 사용하는 호스트 및 포트 번호에 맞게 수정하세요.
    http://localhost:3000/api
    
  3. API 테스트: Swagger UI에서 제공되는 인터페이스를 사용하여 이메일 유효성 검사 API의 요청을 구성하고 보낼 수 있습니다. 필요한 매개변수(예: 이메일 주소)와 함께 요청을 작성하고 "Try it out" 버튼을 클릭하세요.
  4. 응답 확인: 서버에서 반환된 응답은 Swagger UI에서 시각적으로 표시됩니다. 성공 또는 실패 여부와 함께 추가 정보가 제공될 것입니다.

Swagger UI를 사용하여 Nest.js API를 직접 테스트할 수 있으므로, 별도의 도구 없이도 간편하게 작업할 수 있습니다.

 

이런 식이다. 이 얼마나 직관적이고 간결한가... 난 앞으로도 스웨거를 애용하게 될 듯 하다.

오랜만의 삽질기가 아닌 회고이다 ㅋㅋ

 

일반적으로 "컨트롤러"는 웹 애플리케이션에서 클라이언트의 요청을 처리하고 응답을 반환하는 역할을 담당한다.

NestJS 프레임워크에서는 컨트롤러를 사용하여 API 엔드포인트를 정의하고 해당 엔드포인트에 대한 로직을 구현한다.

따라서, EmailService 클래스가 API와 상호작용하는 부분을 담당한다면 해당 클래스는 일종의 "서비스"로 볼 수 있다.

그리고 이 서비스를 사용하는 컨트롤러에서 실제 API 엔드포인트를 정의하고 EmailService를 호출하여 비즈니스 로직을 처리할 수 있다.

고 해서... 이메일 컨트롤러를 따로 만들었는데 그냥 유저 컨트롤러에 통합하기로 결정하면서,

import { Controller, Post, Body } from '@nestjs/common';
import { EmailService } from './email.service';

@Controller('email')
export class EmailController {
  constructor(private readonly emailService: EmailService) {}

  @Post('/validate')
  validateEmail(@Body('email') email: string): boolean {
    return this.emailService.validateEmailFormat(email);
  }

  @Post('/check-duplicate')
  async checkDuplicateEmail(@Body('email') email: string): Promise<boolean> {
    return this.emailService.checkDuplicateEmail(email);
  }
}
//위의 코드에서 @Controller() 데코레이터로 '/email' 경로에 대한 컨트롤러를 정의하였습니다. 
그리고 validateEmail()와 checkDuplicateEmail() 메서드는 각각 /validate와 /check-duplicate 경로에 
대한 POST 요청 핸들러입니다. 이 핸들러들은 클라이언트로부터 전달된 이메일 값을 받아서 
EmailService의 메서드를 호출하여 비즈니스 로직을 처리하고 결과를 반환합니다.

따라서, 위 코드에서 EmailController는 API 관점에서 볼 때 "컨트롤러" 역할을 하며, 
실제 비즈니스 로직은 EmailService 클래스 내에 구현되어 있습니다.

이걸 없애버리고,

import { Controller, Post, Body } from '@nestjs/common';
import { User } from '../../domains/entities/User.entity';
import { UserService } from './user.service';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  // ... 다른 메서드들 ...

  @Post('/validate-email')
  validateEmail(@Body('email') email: string): boolean {
    return this.userService.validateEmailFormat(email);
  }
}
//@Controller() 데코레이터로 '/users' 경로에 대한 컨트롤러를 정의하였습니다. 
그리고 validateEmail() 메서드는 /validate-email 경로에 대한 POST 요청 핸들러입니다. 
클라이언트로부터 전달된 이메일 값을 받아서 UserService의 validateEmailFormat() 메서드를
호출하여 이메일 유효성 검사를 수행하고 결과를 반환합니다.

위와 같이 유저 컨트롤러에 이메일 유효성 검사 API 엔드포인트를 정의해주었다. 만약 다른 기능과 관련된 API 엔드포인트가 많거나 복잡해진다면, 관련 기능들을 별도의 컨트롤러(EmailController)로 분리하는 것도 고려해볼 수 있겠으나, 유효성 검사의 특성 상 그리 복잡해 질 확률은 상당히 낮기 때문에 과감하게 컨트롤러 통합을 감행하였다.

 

그리고 어쩌다보니 NestJS의 의존성 주입에 대해 확실하게 이해한 것 같아 정리해보려고 한다.

NestJS에서의 "의존성 주입(Dependency Injection)"은 클래스 또는 컴포넌트가 필요로 하는 의존성을 외부에서 제공받는 개념을 말한다(다른 프레임워크도 마찬가지이긴 하다). 이를 통해 코드의 유연성과 재사용성을 높일 수 있다.

NestJS에서 의존성 주입은 @Injectable() 데코레이터와 constructor를 사용하여 구현된다.

@Injectable() 데코레이터를 클래스에 적용하면 해당 클래스는 의존성 주입 가능한 클래스로 간주되며,

해당 클래스의 생성자(constructor)에 필요한 의존성을 매개변수로 선언하면 NestJS가 자동으로 해당 의존성을 주입한다.

예를 들어, 다음과 같이 EmailService가 UserService에 의존하는 경우를 생각해보자.

import { Injectable } from '@nestjs/common';
import { UserService } from './user.service';

@Injectable()
export class EmailService {
  constructor(private readonly userService: UserService) {}

  // 이메일 서비스의 로직 구현
}
//위 코드에서 EmailService는 생성자에 UserService를 필요로 하며, 해당 의존성은 자동으로 주입됩니다.
이렇게 함으로서 EmailService 내부에서 UserService 인스턴스에 접근하여 사용할 수 있습니다.

따라서, NestJS에서 "의존성 주입"은 외부 모듈, 서비스, 컴포넌트 등 다른 클래스나 객체들과 협력하고 
상호작용하기 위해 필요한 의존성을 자동으로 제공받는 것입니다. 
이는 스프링 프레임워크에서도 비슷한 개념으로 사용되지만 구체적인 구현 방식과 문법은 조금 다를 수 있습니다.

Argument of type 'FindOptions<User>' is not assignable to parameter of type 'FindOneOptions<User>'. Types of property 'comment' are incompatible. Type 'unknown' is not assignable to type 'string | undefined'.

 

원인은 FindOptions<User>FindOneOptions<User>의 하위 타입이 아니기 때문에 발생했으며, FindOneOptions<User> 타입은 findOne() 메서드에 전달되는 옵션 객체의 타입이란다.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOneOptions } from 'typeorm';
import { User } from '../../domains/entities/User.entity';

@Injectable()
export class EmailService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  validateEmailFormat(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }

  async checkDuplicateEmail(email: string): Promise<boolean> {
    const options: FindOneOptions<User> = {
      where: {
        email: email,
      },
    };

    const existingUser = await this.usersRepository.findOne(options);
    return !!existingUser;
  }
}
//거의 다 똑같고 위의 import 셋째줄에서 FindOptions를 FindOneOptions로 바꿔주면 끝이다.

checkDuplicateEmail() 메서드 내에서 검색 조건으로 이메일 값을 사용하도록 수정한 것이다. FindOneOptions<User> 타입을 사용하여 검색 옵션 객체를 정의하고, 해당 객체를 findOne() 메서드에 전달하여 중복된 이메일을 확인한다.

 

그리고 위처럼 고치니,

Object literal may only specify known properties, and 'email' does not exist in type 'FindOptionsWhere<User> | FindOptionsWhere<User>[]'.

The expected type comes from property 'where' which is declared here on type 'FindOneOptions<User>

두 개의 오류가 동시에...ㅋㅋㅋ 차분하게 수정해보자.

 

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOneOptions } from 'typeorm';
import { User } from '../../domains/entities/User.entity';

@Injectable()
export class EmailService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  validateEmailFormat(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }

  async checkDuplicateEmail(email: string): Promise<boolean> {
    const options: FindOneOptions<User> = {
      where: {
        email: email,
      },
    };

    const existingUser = await this.usersRepository.findOne(options);
    return !!existingUser;
  }
}

?? 뭐가 다른지 모르겠다. 첫번째 코드랑 두번째 코드랑 다른게 없는데 1번째는 오류가 2개나 나고 2번째는 멀쩡하다 왜??

맞왜틀?? 오류의 원인은 FindOneOptions<User>where 속성에 정의되지 않은 속성인 'email'을 사용하려고 하기 때문에 발생합니다. 이 오류를 해결하기 위해서는 where 속성을 올바르게 정의해야 한다고 한다. 1번도 올바르게 정의 됐는데?? 진짜 이해 할 수가 없다..

 

5편에서 계속..

import { ApiProperty } from '@nestjs/swagger';
import { UserGender, UserType } from '../../../commons/enums';
import { SignUpCommand } from '../../../domains/ports/in/command/user.command';

export class SignUpRequest extends SignUpCommand {
  /**
   * 유저 가입 이메일
   * @example testUser@livey.kr
   */
  @ApiProperty()
  userEmail: string;

  /**
   * @example testPW
   */
  @ApiProperty()
  userPW: string;

  /**
   * @example tester
   */
  @ApiProperty()
  userName: string;

  /**
   *
   */
  userGender: UserGender;

  userType: UserType;

  /**
   * @example 010-1234-5678
   */
  @ApiProperty()
  userPhoneNum: string;

  
/**
* 이메일 유효성 검사 API 추가 예시 
*/
@IsEmail() // <- 여기에 이메일 유효성을 확인하는 데코레이터를 추가합니다.
@ApiProperty({
    example: '테스트용 이메일',
    description: '유저 가입 이메일',
})
userEmail:string;
  

  
/**
* 여기까지 예시입니다. 나머지 코드는 그대로 사용하시면 됩니다.
위의 코드에서 SignUpRequest 클래스의 userEmail 프로퍼티에
@IsEmail() 데코레이터를 추가하여 이메일의 유효성을 검사할 수 있습니다. 
해당 데코레이터는 NestJS에서 제공하는 class-validator 패키지의 일부입니다.

또한, @ApiProperty() 데코레이터를 사용하여 Swagger 문서화에 필요한 정보를 추가할 수 있습니다. 
예를 들어, example 속성을 사용하여 예시 값을 제공하고, description 속성을 사용하여 
프로퍼티에 대한 설명을 추가할 수 있습니다.

추가된 코드 부분은 주석으로 표시되어 있으며,
위와 같은 방식으로 기존 코드에 이메일 유효성 검사 API를 추가할 수 있습니다.
*/

}

생략된 내용이 굉장히 많은데, 아무래도 프로젝트 내용 상 철저히 보안이 유지되어야 하기 때문에 코드 공유가 어려워서, 예시 코드 내용 올린 점.. 양해 바란다.

아무튼 대충 위와 같이 유저 리퀘스트 DTO에 이메일 유효성 검사 API를 추가해주고, 실행하려는데..

 

Object literal may only specify known properties, and 'email' does not exist in type 'FindOptionsWhere<User> | FindOptionsWhere<User>[]'.

하.. 이번엔 또 뭐가..

알고 보니 FindOptionsWhere<User> 타입에 'email' 속성이 존재하지 않기 때문에 발생했다. 이 오류를 해결하기 위해서는 FindOptionsWhere<User> 타입에 'email' 속성을 추가해야 한다.

import { User } from '../../domains/entities/User.entity';
import { FindConditions } from 'typeorm';

// ...

const conditions: FindConditions<User> = {
  email: 'testUser@livey.kr',
};

// 사용 예시:
const user = await userRepository.findOne(conditions);

//위의 코드에서는 FindConditions<User>를 사용하여 검색 조건을 정의하고 있습니다.
해당 타입은 TypeORM에서 제공하는 타입으로, email과 같은 필드로 검색 조건을 지정할 수 있도록 합니다.

userRepository.findOne() 메서드를 호출할 때 검색 조건으로 conditions 객체를 전달하여 
사용할 수 있습니다. 이렇게 하면 원하는 이메일 값을 가진 유저를 찾을 수 있습니다.

프로젝트의 실제 구조와 TypeORM 설정에 따라 코드가 달라질 수 있으므로, 
필요한 경우 해당 프로젝트의 구조와 설정에 맞게 코드를 수정하셔야 합니다.

처음에 이렇게 했다가, 유저 레포지토리가 있길래 

import { User } from '../../domains/entities/User.entity';
import { FindConditions, Repository } from 'typeorm';

class UserRepository extends Repository<User> {
  // ...

  async findUserByEmail(email: string): Promise<User | undefined> {
    const conditions: FindConditions<User> = {
      email: email,
    };

    return this.findOne(conditions);
  }

  // ...
}
//위의 코드에서는 findUserByEmail() 메서드를 추가하여 이메일로 유저를 조회하고 있습니다.
해당 메서드 내에서 검색 조건으로 주어진 이메일 값을 사용하고 있습니다.

위와 같이 바꿔주었다.

 

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOptions } from 'typeorm';
import { User } from '../../domains/entities/User.entity';

@Injectable()
export class EmailService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  validateEmailFormat(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }

  async checkDuplicateEmail(email: string): Promise<boolean> {
    const options: FindOptions<User> = {
      where: {
        email: email,
      },
    };

    const existingUser = await this.usersRepository.findOne(options);
    return !!existingUser;
  }
}
//위의 코드는 checkDuplicateEmail() 메서드 내에서 
검색 조건으로 이메일 값을 사용하도록 수정한 것입니다. 
FindOptions<User> 타입을 사용하여 검색 조건 객체를 정의하고, 
해당 객체를 findOne() 메서드에 전달하여 중복된 이메일을 확인합니다.
**typeorm에서는 FindConditions 대신 FindOptions를 사용해야 합니다.
typeorm에서는 FindOptions를 사용하여 검색 조건을 정의합니다**

이렇게 바꿨는데, 또 오류가 발생했다... 나머지는 4편에서.

아마 한 6~7편까지 나올 듯 하다..ㅋㅋ

import { ApiProperty } from '@nestjs/swagger';
import { UserGender, UserType } from '../../../commons/enums';
import { SignUpCommand } from '../../../domains/ports/in/command/user.command';

export class SignUpRequest extends SignUpCommand {
  /**
   * 유저 가입 이메일
   * @example testUser@livey.kr
   */
  @ApiProperty()
  userEmail: string;

  /**
   * @example testPW
   */
  @ApiProperty()
  userPW: string;

  /**
   * @example tester
   */
  @ApiProperty()
  userName: string;

  /**
   *
   */
  userGender: UserGender;

  userType: UserType;

  /**
   * @example 010-1234-5678
   */
  @ApiProperty()
  userPhoneNum: string;

  
/**
* 이메일 유효성 검사 API 추가 예시 
*/
@IsEmail() // <- 여기에 이메일 유효성을 확인하는 데코레이터를 추가합니다.
@ApiProperty({
    example: '테스트용 이메일',
    description: '유저 가입 이메일',
})
userEmail:string;
  

  
/**
* 여기까지 예시입니다. 나머지 코드는 그대로 사용하시면 됩니다.
*/

}

대충 위와 같이 유저 리퀘스트 DTO에 이메일 유효성 검사 API를 추가했고..

 

개발을 할 때 간과한 것이 있었다. 상대 경로라는 것이다.

상대 경로는 현재 파일의 위치를 기준으로 연결하려는 파일의 상대적인 경로로 나타낸 것을 의미한다. 상대 경로는 주소나 프로젝트 디렉토리 위치가 바뀌어도 내부 구조만 그대로라면 수정없이 그대로 사용할 수 있다는 장점을 가지고 있다. 그러나 자기 자신이 기준이기 때문에 자기 자신의 위치가 바뀌는 것에 취약하다는 단점이 있다(이거 때문에 컨트롤러 통합할때 애먹었다.. ㅂㄷㅂㄷ)
상대 경로는 보통 다음과 같이 명시한 것들을 의미한다.

./src/compnents/Counter.js
../../img/logo.jpg

기호의 의미는,

/ root
./ 현재 위치
../ 상위 경로

/만 사용되면 root, 즉 가장 토대가 되는 경로가 선택된다.
./는 현재 위치를 나타낸다. 현재 위치 ./는 보통 생략.
../는 상위 경로를 나타낸다. 상위 경로는 현재 폴더가 속한 폴더를 가리킨다.

이걸 몰라서,

import { User } from './src/entities/User.entity';
import { User } from './entities/User.entity';
import { User } from '../entities/User.entity';
import { User } from '../../entities/User.entity';
import { User } from '../../domains/entities/User.entity';

장장 5번의 삽질을 거쳐서야 드디어 오류가 멈췄다.. 후 ㅂㄷㅂㄷ!!!

여기서 끝이 아니다... 삽질기는 계속된다 ㅋㅋ

정말 말 많고 탈 많던 회고가 될 것으로 생각된다.

우선 처음에는 이메일 컨트롤러를 만들었다. 아래와 같이

import { Controller, Get, Query } from '@nestjs/common';
import { EmailService } from './email.service';

@Controller('email')
export class EmailController {
  constructor(private readonly emailService: EmailService) {}

  @Get('validate')
  async validateEmail(@Query('email') email: string): Promise<boolean> {
    const isValidFormat = this.emailService.validateEmailFormat(email);
    if (!isValidFormat) {
      return false;
    }

    const isDuplicate = await this.emailService.checkDuplicateEmail(email);
    if (isDuplicate) {
      return false;
    }

    return true;
  }
}
//위의 코드에서 validateEmail 메서드는 email 쿼리 파라미터로 전달받은 이메일 주소의 유효성을 검사합니다.
먼저 validateEmailFormat 메서드로 이메일 주소의 형식이 올바른지 확인하고, 
그 다음에 checkDuplicateEmail 메서드로 중복 여부를 확인합니다. 모든 검사가 통과되면 true를 반환하고, 
그렇지 않으면 false를 반환합니다.

 

다음으로, 해당 컨트롤러에서 사용할 서비스 클래스인 EmailService도 생성해주었다.

이 서비스 클래스에서 실제로 유효성 검사와 중복 여부 확인 로직을 구현한다.

import { Injectable } from '@nestjs/common';

@Injectable()
export class EmailService {
  validateEmailFormat(email: string): boolean {
    // 여기에 이메일 형식 검증 로직을 구현하세요.
    // 예시: 정규식을 사용하여 형식이 맞는지 확인하는 방법
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }

  async checkDuplicateEmail(email: string): Promise<boolean> {
    // 여기에 중복 여부 확인 로직을 구현하세요.
    // 예시: 데이터베이스에서 이미 등록된 이메일인지 조회하는 방법
    const existingUser = await User.findOne({ where: { email } });
    
     return !!existingUser; // 이미 등록된 경우 true 반환
   }
}

위의 코드에서는 각각 validateEmailFormat 메서드와 checkDuplicateEmail 메서드가 있다.

첫 번째 메서드(validateEmailFormat)는 정규식을 사용하여 입력된 이메일 주소가 올바른 형식인지 판별하고,

두 번째 메서드(checkDuplicateEmail)는 데이터베이스나 다른 저장소에서 이미 등록된 이메일인지 조회하여 중복 여부를 판별한다.

마지막으로, 해당 컨트롤러와 서비스 클래스를 Nest.js 앱 모듈에 등록해 주면 API 제작이 끝난다

import { Module } from '@nestjs/common';
import { EmailController } from './email.controller';
import { EmailService } from './email.service';

@Module({
  controllers: [EmailController],
  providers: [EmailService],
})
export class AppModule {}
// 대략 이런 식으로

그런데, 알고 보니 유저 컨트롤러에 이미 회원가입(당연히 이메일 관련 내용도 포함) 로직이 있던 것이다. 

이럴 경우, 이메일 컨트롤러를 만들 필요 없이 유저 컨트롤러에 추가적으로 로직을 구현하면 되는 일이었다. 서비스가 늘어날수록 컨트롤러도 다양해질 것인데, 그럴 경우를 미리 대비해서 조금이라도 컨트롤러의 개수를 줄이려고 노력해보았다. 

그래서 결국 이메일 컨트롤러를 지우고 유저 컨트롤러에 병합을 시도했다.

문제는 다음부터였다...ㅂㄷㅂㄷ

+ Recent posts