티스토리 뷰

Github Repo

https://github.com/wanted-wecode-subjects/freshcode-subject

개요

  • 회원, 상품 API 만들기

기간

  • 2021.11.04 - 2021.11.06

개발 환경

  • TypeScript
  • NestJS
  • Sqlite3

사용 라이브러리

  • bcrypt
  • passport
  • typeorm
  • class-validator

구현 사항

요구사항

  • 회원가입
  • 로그인 JWT 사용
  • Request시 Header에 Authorization 체크
    • 토큰 없으면 에러 처리
  • 상품 CRUD
    • 상품 추가/수정/삭제는 관리자(admin 권한)만 가능
    • 사용자(user 권한)는 상품 조회만 가능
    • 상품 하나 조회
    • 상품 전체 조회
      • 페이지네이션(페이지당 5개)
    • 에러 핸들링

ERD

ERD(Entity-Relationship Modelling)는 데이터 모델링 분야에서 사용하는 용어로 개체-관계 모델이라 하며,구조화된 데이터를 도식화 한 표현입니다.

ERD cloud

ERD cloud는 DB 모델링 도구로 ERD 작성을 도와줍니다.
이번 프로젝트에서 팀원의 소개로 처음 사용해보았습니다.

커밋 메시지 컨벤션

커밋 메시지에 대한 형식을 두어 깔끔하게 작성하거나 다른 사람들이 읽기 쉽게끔 컨벤션을 둘 수 있습니다.
팀원과 상의하여 컨벤션을 정의할 수 있으며, 프로젝트 초기에 이를 작성하는 것이 좋습니다.
보통 일반적으로 첫째 줄에 제목을 적고, 둘째 줄부터 본문으로 취급하여 커밋에 대한 내용을 작성합니다.

예시

  • title convention
    1. feat - 신규 기능 추가
    2. fix - 버그 수정
    3. docs - 문서 수정
    4. style - 코딩 스타일 관련(로직 변경x)
    5. refactor - 코드 리팩터링
    6. test - 테스트 코드 관련
    7. ci - CI/CD 관련
    8. chore - 기타
  • rules
    • 제목 끝에 마침표를 적지 않는다.
    • 제목은 명령문으로 사용한다.
    • 제목은 과거형을 사용하지 않는다.
    • '어떻게' 보다는 '무엇'과 '왜'를 설명한다.
    • 제목은 50자 이내로 작성한다.
    • 본문의 각 줄은 72자 이내로 작성한다.
    • 마지막 줄에는 이슈 id를 작성한다.
      • Resolves: 해당 이슈 id
      • See also: 관련된 이슈 id

클래스 확장

너무나 당연한 얘기지만 상속을 통해 클래스를 확장할 수 있습니다.
이번 프로젝트에서는 CoreEntity 라는 클래스를 두어 엔티티가 공통적으로 가지고 있는 createdAt, updatedAt, deletedAt 같은 컬럼을 갖도록 하고, 다른 엔티티 클래스에서 이를 상속하여 사용할 수 있도록 처리하였습니다.

409 Conflict

ID 중복에 대한 상태코드를 찾던 중 다음 글(https://deveric.tistory.com/62) 찾게 되었습니다.
해당 글에서는 ID 중복에 대해 상태코드로 423, 403, 409 중 하나를 반환하려 하였으며, 그 중 409를 ID 중복에 대한 상태코드로 반환하게 되었습니다.
위의 각 상태코드에 대한 설명은 다음과 같습니다.

  • 423(Locked): 메소드의 잠금을 의미
  • 403(Forbidden): 인가(authorization) 실패 상태 코드
  • 409(Conflict): 리소스와 충돌이 발생하여 사용자가 이를 반영할 수 있도록 유도

이로 미루어 볼 때, 423은 가입 이후 해당 리소스(ID)에 대한 잠금과는 무관하여 제외되어야 하고, 403은 ID 중복보다는 데이터 유효성이나 인가 실패에 사용하므로 제외되어야 합니다. 마지막으로 409는 리소스에 대한 충돌이므로 ID라는 PK 자원을 점유한 것에 대한 충돌이기 때문에 적합하다고 볼 수 있습니다.

@nestjs/config

@nestjs/config를 사용하여 환경변수를 가져올 수 있습니다.
외부에서 정의된 환경변수는 전역으로 선언된 process.env를 통해 확인할 수 있습니다. 또한 .env 파일에 정의된 key-value 값을 가져와서 사용할 수도 있습니다. .env 파일 안의 데이터 형태는 key=value 형태로 정의되어야 합니다.
이를 위해선 @nestjs/config 패키지를 설치하고, AppModule에서 ConfigModule을 import 합니다.

app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [ConfigModule.forRoot()],
})
export class AppModule {}

envFilePath

ConfigModule에 envFilePath를 두어 .env 파일의 경로를 직접 정의할 수 있습니다.

ConfigModule.forRoot({
  envFilePath: ['.env.development.local', '.env.development'],
});

custom configuration files

custom configuration file은 config 객체를 반환하는 팩토리 함수를 내보냅니다.
환경에 따라 적절한 변수 설정이 필요할 때 사용할 수 있습니다.

예시

config/configuration.ts

export default () => ({
  port: parseInt(process.env.PORT, 10) || 3000,
  database: {
    host: process.env.DATABASE_HOST,
    port: parseInt(process.env.DATABASE_PORT, 10) || 5432
  }
});

app.module.ts

import configuration from './config/configuration';

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [configuration],
    }),
  ],
})
export class AppModule {}

출처: https://docs.nestjs.kr/techniques/configuration

env에서 가져온 값을 JwtModule에서 사용하기

일반적인 방법으로 env에서 값을 가져와 JwtModule의 secret으로 설정하면 에러가 발생합니다.

@Module({
    imports: [
       JwtModule.register({
          secretOrPrivateKey: process.env.SECRET
       })
    ]
})

이는 JwtModule이 인스턴스화 될 때 아직 env에서 해당 값을 가져오지 못해 발생하는 이슈이며, 이를 위해 JwtModule의 registerAsync를 사용하여 해결할 수 있습니다.

JwtModule.registerAsync({
    imports: [ConfigModule],
    useFactory: async (configService: ConfigService) => ({
      secretOrPrivateKey: configService.jwtSecret,
    }),
    inject: [ConfigService],
}),

출처: https://stackoverflow.com/questions/55673424/nestjs-unable-to-read-env-variables-in-module-files-but-able-in-service-files

bcrypt mock

테스트 코드에서 bcrypt 함수를 사용하는 경우가 있었습니다. 그러나 해당 함수에 대한 mock이 없어 테스트에 실패하였습니다.
이를 해결하기 위해 아래와 같이 코드를 작성하면 bcrypt.compare에 대한 mock을 생성할 수 있습니다.

const bcryptCompare = jest.fn().mockResolvedValue(true);
(bcrypt.compare as jest.Mock) = bcryptCompare;

출처: const bcryptCompare = jest.fn().mockResolvedValue(true);
(bcrypt.compare as jest.Mock) = bcryptCompare;

role guard

Role에 대한 데코레이터를 생성하여 보호하고자 하는 메소드에 데코레이터를 적용하였으나, user가 빈 값으로 출력되는 등의 정상적으로 동작하지 않는 이슈가 있었습니다.
이는 @UseGuards(JwtAuthGuard, RoleGuard) 같이 가드를 사용하지 않아서 발생한 이슈이며, 해당 데코레이터를 적용한 결과 context.switchToHttp().getRequest()의 user 값을 잘 가져오는 것을 확인하였습니다.

@UseGuards(JwtAuthGuard, RolesGuard)
@Role(UserRole.admin)
...
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) {
      return true;
    }
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

출처: https://docs.nestjs.kr/security/authorization

회고

이번 회고는 5F(Fact, Feeling, Feedback, Finding, Future action)에 의거하여 작성해보았습니다.
(출처: https://codechasseur.tistory.com/102)

Fact

  • JwtAuthGuard를 구현하였으나 필요한 코드에서 적용되지 않음
  • RoleGuard 데코레이터 적용 실패로 인해 데코레이터를 쓰지 않는 방법으로 구현

Feeling

  • 기능 구현 이후에 팀원들에게 제대로 전파되지 않았고, 문서화도 제대로 되지 않아 이러한 상황이 발생하였기에 많이 아쉬웠다.
  • 비효율적인 방법으로 기능을 구현하여 아쉬웠다. 그나마 방법을 알게 되어 다음에는 적용할 수 있을 것이기에 다행이라고 생각한다.

Finding

  • 기능 구현을 할 때마다 미팅을 주최하여 팀원들에게 전파하기에는 시간이 낭비되고 비효율적인 방법으로 보인다. 앞서 말한 것과 같이 문서화를 잘해서 구현한 기능을 잘 사용하도록 유도하는 것이 중요하다고 생각한다.
  • 검색 능력이 중요하다고 생각한다. 키워드를 잘 조합해서 원하는 데이터를 찾을 수 있도록 해야겠다.

Future action

  • PR을 올릴 때 문서화를 자세히 한다.
Comments