NestJS를 기반으로 한 인증 시스템을 구현하며, 회원가입과 로그인 기능을 개발한 과정을 기술 블로그 형식으로 자세히 정리합니다. 이 글에서는 강의에서 제공한 AppModule, AuthModule, AuthService, AuthController 등의 소스코드를 기반으로 전체 흐름과 각 구성 요소의 역할을 설명합니다.
1. 프로젝트 구조 및 환경 설정
1-1. 환경 설정
AppModule에서는 프로젝트 전역 설정과 모듈 구성을 담당합니다.
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        REFRESH_TOKEN_SECRET: Joi.string().required(),
        ACCESS_TOKEN_SECRET: Joi.string().required(),
        HTTP_PORT: Joi.number().required(),
        DB_URL: Joi.string().required(),
      }),
    }),
    TypeOrmModule.forRootAsync({
      useFactory: (configService: ConfigService) => ({
        type: 'postgres',
        url: configService.getOrThrow('DB_URL'),
        autoLoadEntities: true,
        synchronize: true,
      }),
      inject: [ConfigService],
    }),
    UserModule,
    AuthModule,
  ],
})
export class AppModule {}
ConfigModule로 환경 변수를 정의하고, Joi를 통해 스키마 유효성 검증을 수행합니다.
TypeOrmModule로 PostgreSQL 연결 및 엔티티 자동 로딩을 설정합니다.
2. Auth 모듈 구성
2-1. AuthModule
@Module({
  imports: [
    JwtModule.register({}),
    TypeOrmModule.forFeature([User]),
    UserModule,
  ],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}
JwtModule을 임포트하여 JWT 발급 기능을 사용할 수 있도록 준비합니다.
UserModule, User 엔티티를 함께 불러와 의존성을 주입받습니다.
3. AuthController: API 엔드포인트 구현
@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}
  @Post('register')
  @UsePipes(ValidationPipe)
  registerUser(@Authorization() token: string, @Body() registerDto: RegisterDto) {
    if (token === null) throw new UnauthorizedException('토큰을 입력해주세요!');
    return this.authService.register(token, registerDto);
  }
  @Post('login')
  @UsePipes(ValidationPipe)
  loginUser(@Authorization() token: string) {
    if (token === null) throw new UnauthorizedException('토큰을 입력해주세요!');
    return this.authService.login(token);
  }
}
register와 login 엔드포인트는 Authorization 헤더를 통해 전달받은 token을 사용합니다.
@Authorization() 데코레이터를 통해 토큰을 추출합니다. (별도 구현 필요)
등록과 로그인은 AuthService에 위임되어 처리됩니다.
4. AuthService: 핵심 비즈니스 로직
4-1. 회원가입
async register(rawToken: string, registerDto: any) {
  const { email, password } = this.parseBasicToken(rawToken);
  return this.userService.create({ ...registerDto, email, password });
}
Basic 방식으로 인코딩된 토큰을 디코딩하여 이메일과 비밀번호를 파싱합니다.
registerDto의 나머지 정보와 함께 유저 생성 요청을 보냅니다.
4-2. 로그인
async login(rawToken: string) {
  const { email, password } = this.parseBasicToken(rawToken);
  const user = await this.authenticate(email, password);
  return {
    refreshToken: await this.issueToken(user, true),
    accessToken: await this.issueToken(user, false),
  };
}
이메일/비밀번호 인증 후, access token과 refresh token을 각각 발급하여 반환합니다.
4-3. 토큰 파싱
parseBasicToken(rawToken: string) {
  const basicSplit = rawToken.split(' ');
  if (basicSplit.length !== 2 || basicSplit[0].toLowerCase() !== 'basic') {
    throw new BadRequestException('토큰 포맷이 잘못됐습니다!');
  }
  const decoded = Buffer.from(basicSplit[1], 'base64').toString('utf-8');
  const [email, password] = decoded.split(':');
  if (!email || !password) {
    throw new BadRequestException('토큰 포맷이 잘못됐습니다!');
  }
  return { email, password };
}
클라이언트에서 base64로 인코딩한 email:password 형태의 토큰을 디코딩합니다.
4-4. 사용자 인증
async authenticate(email: string, password: string) {
  const user = await this.userRepository.findOne({
    where: { email },
    select: { id: true, email: true, password: true },
  });
  if (!user || !(await bcrypt.compare(password, user.password))) {
    throw new BadRequestException('잘못된 로그인 정보입니다!');
  }
  return user;
}
4-5. JWT 발급
async issueToken(user: any, isRefreshToken: boolean) {
  const secret = isRefreshToken
    ? this.configService.getOrThrow<string>('REFRESH_TOKEN_SECRET')
    : this.configService.getOrThrow<string>('ACCESS_TOKEN_SECRET');
  return this.jwtService.signAsync(
    {
      sub: user.id,
      role: user.role,
      type: isRefreshToken ? 'refresh' : 'access',
    },
    {
      secret,
      expiresIn: '3600h',
    },
  );
}
JWT 페이로드에는 사용자 ID, 권한(role), 토큰 타입 정보를 담아 생성합니다.
5. 마무리
인증 방식은 Bearer가 아닌 Basic 기반 토큰을 사용하여 이메일/비밀번호를 전달받는 독특한 구조입니다.
NestJS의 ConfigModule, JwtModule, TypeOrmModule, ValidationPipe, Exception 처리 방식 등 실무에서 자주 쓰이는 기능들을 실습할 수 있었습니다.
단순하지만 강력한 인증 로직을 통해 NestJS 구조의 장점을 잘 활용한 예제였습니다.













댓글 ( 0)  
댓글 남기기